PHP 로 공인인증서 서명하기

PHP로 yessign 공인인증서를 서명하는 과정을 정리하고자 합니다.
뭐 리눅스 기반의 PHP에서 공인인증서 서명방식이 쓸일이 있을지는 모르겠습니다.
우선 정리 차원이니 기록하도록 하지요.

PHP용 Seed 클래스 완료후 공인인증서 서명 방식을 알기위해 검색을 했습니다.
http://blog.kangwoo.kr/49 剛宇 님께서 정리를 아주 잘 해 놓으셨습니다.
공인인증서에 대한 자세한 설명으로 정말 감사의 뜻을 전합니다.
이분이야말로 고수이신것 같습니다.
이블로그 도움으로 한단계씩 PHP로 진행해 갔습니다.

기본적으로(?) 공인인증서는 공인인증서파일과 개인키 파일로로 나뉘어집니다.
파일구조는 기본적으로 signCert.der, signPri.key 두개의 파일이 존재하며

PC에 공인인증서를 저장한 경우 대부분
C:\Program Files\NPKI\yessign\USER 폴더에
자신의 이름 + 인증기관 정보명으로 폴더명이 존재합니다.(yessign 기준)

signCert.der 파일엔 명 암호화알고리즘, 암호화버젼, 인증기관의 정보등.. 공인인증서에 대한 정보가 저장되어 있으며
signPri.key 파일엔 개인키가 저장되어 있습니다. 이파일에 저장된 개인키는 국내 암호화 알고리즘 SEED가 적용되어 저장되어 있습니다.

이 seed 암호화 알고리즘은 국내에서만 이용하고 있으며 국내에서 인증서를 사용할때 이 Seed 암호화 알고리즘을 사용하기 위해서 윈도우에 내장되어 있지 않은
Seed 를 배포하기 위해 ActiveX를 사용해야만 했습니다.
(외국 은행사이트만 살펴보더라도 https 만 사용하는데 유독 우리나라에서만 공인인증서를 이용하고 있습니다.)
ex) http://online.citibank.com

공인인증서 서명과정을 간략히 정리하면
# 개인키와 메시지 내용을 이용해 서명값 생성
1. signPri.key 파일에서 개인키를 얻는다.
2. 서명할 메시지내용과 개인키를 이용해 서명을한다.

# 공개키와 만들어진 서명값과 메시지내용을 이용해 서명되는지 확인한다.
1. signCert.der 파일에서 공개키를 얻는다.
2. 서명값과 메시지내용과 공개키를 이용해 서명이 되는지 확인한다.

참 간단하죠? 하지만 개인키가 문제입니다.
剛宇 님께서 언급 하셨던 것처럼 공개키는 날로 먹었는데 개인키는 역시나 PHP에서도 만만치 않았습니다.
대부분 직접 구현해야만 했습니다.

하나씩 개인키 받아오는것부터 진행해 보겠습니다. 아래 이미지는
전자서명인증관리센터 > 기술규격 > 공인전자서명인증체계기술규격 > 암호 알고리즘 > 암호 알고리즘 규격[v1.21] PDF 파일에서 확인할 수 있습니다.
(http://www.rootca.or.kr/kor/standard/standard01B.jsp)

 

위 이미지에서 나와있듯이 개인키 파일을 로드하여 SEED 암호화 알고리즘으로 복호화 하는 과정이 필요합니다.
복호화 하기위해서는 몇가지 과정이 거치는데 순서를 나열하면

1. 개인키(signPri.key) 파일을 로드한다.
2. 로드된 개인키 데이터에서 솔트와 아이터레이션 카운트 값을 구한다.
3. 구해진 솔트와 아이터레이션 카운트 값과 공인인증서 암호를 이용하여 PBKDF1 추출키(DK)를 구한다.
4. 구해진 DK를 이용해 암호화키와 초기 벡터를 구한다.
5. 구해진 암호화키와 초기백터를 이용해 암호화된 개인키를 CBC 블럭 암호화 알고리즘으로 복호화 한다.
6. 다음 블럭부터는 CBC를 이용해 복호화 한다.
7. 복호화된 데이터 마지막에 적용된 PKCS#5 Padding 을 제거한다.
8. Padding 제거한 복호화된 값을 ASN.1 방법으로 분석한다.
9. 분석된 결과값중 OCTET STRING 값을 리턴하여 개인키값을 구한다.
10. 인증서 서명은 RSA로 서명하기 때문에 포멧을 RSA로 변경한다.

이런 과정을 거처 개인키를 구할수 있습니다. 좀 복잡한 과정을 거치더군요.
고수가 아니라…. 이걸 구하기 위해 2주는 고생한거 같습니다.

1. 개인키(signPri.key) 파일을 로드한다.

2. 로드된 개인키 데이터에서 솔트와 아이터레이션 카운트 값을 구한다.

signPri.key 파일은 PKCS#8 규정에 의해 저장되어 있다.

PKCS#8 란? : Private­Key Information Syntax Standard PKCS#8은 비밀키 정보를 위한 구문을 정의한다.
이 내부 구조는 ASN.1(Abstract Syntax Notation One)이라는 것인데, 복잡한 데이터 구조를 내부 구현에 독립적으로 추상화하여 표현하기 위한 표준 구문법이라고 한다 (剛宇 님 발췌)

결국 signPri.key 파일내용을 ASN.1 분석해야만 한다.
剛宇님께서 알려주신 이 페이지에서 기반으로 함수 하나를 만들었다.
http://en.wikipedia.org/wiki/Basic_Encoding_Rules
홈페이지에서 나와 있는데로 구현해 보았다. 이 함수는 완벽한 구현은 아니고 필요한 부분만 구현하는데 급급한 소스이다.

 

여기서 사용한 외부 클래스가 있다. 바로 Math_BigInteger 클래스이다.

이클래스는 pear 홈페이지에서 제공되고 있으며
아래 링크를 따라가면 다운로드 받거나 설치할수 있다.
http://pear.php.net/package/Math_BigInteger

기본적으로 컴퓨터는 아주 큰수를 표현이 불가능하다. 예를 들면 47의 150승 같은 숫자들은
표현자체가 불가능하다. 왜냐하면 OS는 32bit나 64bit 이기때문에 이보다 큰수는 표현이 불가능하다는것이다.
이문제를 해결하기 위해 Math_BigInteger 같은 수학공식이 들어간 클래스를 이용한다.
(본문 하단에 첨부하였다.)

3. 구해진 솔트와 아이터레이션 카운트 값과 공인인증서 암호를 이용하여 PBKDF1 추출키(DK)를 구한다.

PBKDF1 구현방식은 아래 사이트에 잘 나와 있다.
http://www.faqs.org/rfcs/rfc2898.html 

RFC 2898 – PKCS #5: Password-Based Cryptography Specification Ve

나와있는대로 설명하면
해쉬 함수로 차례로 공인인증서 비밀번호와 솔트를 업데이트하고
해쉬값을 구한다음 구한 해쉬값을 다시 업데트하고 해쉬값을 구한다.
이과정을 아이터레이션 값만큼 반복을 한다.

PHP로 구현하면 다음과 같다.

4. 구해진 DK를 이용해 암호화키와 초기 벡터를 구한다.

암호화키값(K)은 DK의 앞 16자리이며
초기벡터값(IV)은 DK의 뒤 4자리 값을 sha1 암호화한 값을 이용한다.

이부분에서 이상하게도 $temp4Bytes 값을 sha1암호화 했더니 이상한값이 나왔다.
이유는 알수 없었다 $temp4Bytes 값이 분명히 hex 로 출력했을때 같은값이었으나
이값이 sha1($temp4Bytes, true) 값이 전혀 다르게 나왔다.

이상한건 무의미한 아래 작업을 거치면 정확한 값을 얻게되었다.
for($i=0;$i이부분은 고수분들의 도움이 필요하다.

5,6,7. 구해진 암호화키와 초기백터를 이용해 암호화된 개인키를 CBC 블럭 암호화 알고리즘으로 복호화 하고 최종 PKCS#5Padding 제거한다.

실제 암호화된 개인키를 복호화 하는 작업이다.
이 과정에서는 SEED-CBC-PKCS5Padding 과정을 통해 복호화 한다.

PHP 용 SEED 클래스는 지난 강좌에 공개되어 있으니 지난강좌를 참고하시면 됩니다.
http://cena.co.kr/mibany/14523

CBC란?
. 이전의 암호문과 현재의 평문 블럭을 XOR(exclusive or)한 후 그 결과를 암호화하여 암호문을 생성
. 초기값(Initial Value)가 필요. IV는 비밀로 할 필요는 없음.      
. 마지막 블럭의 데이타가 블럭보다 작을 경우 패딩을 한다(주로 PKCS5 Padding)

http://blog.acronym.co.kr/92

복호화하는 과정을 설명을 하자면

암호화된 데이터를 16byte씩 블럭화 하여 SEED 알고리즘으로 복호화 하고
복호화된 데이터를 CBC 처리 즉 벡터와 XOR연산 작업을 통해 평문블럭을 얻는 과정이다.
제일 마지막 패딩작업은 블럭단위로 암호화 복호화 처리하기때문에 부족한 블럭은 패딩으로
처리되어 있기때문에 패딩을 제거하는 작업이 필요하다.
CBC는 PKCS5 방식으로 패딩 처리되어 있기 때문에 PKCS5 로 제거한다.

PKCS5란?
블럭단위로 처리할때 문자열이 블럭단위보다 작을경우 부족한 수많큼 블럭을 채워준다.
단 채울때 부족한 블럭의 수의 값으로 모두 채워준다.
16byte라고 한다면

ex) a  b  c  d  e  f g  h  i  j  k  l  m  n  2  2
ex) a  b  c  d  e  f g  h  i  j  k  l  m  3  3  3
ex) a  b  c  d  e  f g  9  9  9  9  9  9  9  9  9

이런식으로 채워지는 방식이다.

8, 9. Padding 제거한 복호화된 값을 ASN.1 방법으로 분석하여 개인키값을 구한다.

복화된 데이터도 PKCS#8 기준으로 규정되어 있다
위에서 구현한 함수 PKCS8EncodeKeySpec 함수로 ASN.1 으로 분석한다.

$PKCS8[1] 에 개인키가 들어 있다.

10. 인증서 서명은 RSA로 서명하기 때문에 포멧을 RSA로 변경한다.

제목대로 RSA로 서명하기때문에 포멧을 RSA로 변경한다.

RSA포멧방식은
format + base64(개인키) + format
방식으로 되어 있다.

RSA로 변환하는 함수로 구현해 보았다.

지금까지 공인인증서 개인키 파일을 가지고 최종 개인키를 가져오는 부분을 마무리 했습니다.
여기까지 따라오신분은 수고하셨습니다. 제일 어려운 부분을 마무리 하신거 같습니다.

이제 값을 구한 개인키와 메시지 내용을 이용해 서명하고
서명한 값이 진짜 서명한 값이지 확인하는 작업만이 남았습니다.

다음은 개인키와 메시지를 이용해 서명하도록 하겠습니다. 테스트 소스이니 참고하시기 바랍니다.

$CryptoUtils 클래스는 지금까지 위에 공개한 함수를
CryptoUtils 란 클래스로 묶어두었으며
getPrivateKey 함수는 위에서 처리한 모든걸 적용하고 개인키를 RSA로 리턴되도록 묶어둔 함수입니다.

getPrivateKey 함수로 개인키를 구하고 구한 개인키 RSA를
openssl_get_privatekey 에 인자값으로 사용합니다.
여기서 리턴된 리소스 값을 openssl_sign함수에 3번째 인자값으로 넣고
메시지 내용과 리턴받을 $signature 변수를 세팅하여
$signature 변수에 서명값을 받아 냅니다.

CryptoUtils 클래느는 아래에 첨부파일로 두었으니 다운받으시고 테스트 하시면 됩니다.

서버 환경이 UTF-8 이 아니라 EUC-KR 인경우
다음과 같은작업을 하시고
$text = mb_convert_encoding(“서명테스트”, “UTF-8”, “EUC-KR”);
UTF-8 환경이라면 하지 않아도 됩니다.

결과

개인키 :
—–BEGIN RSA PRIVATE KEY—–
MIICXAIBAAK…생략…Bd/n9uEBiUX2xp
…생략…
oib3xfyAR…생략…6goBbznaCc=
—–END RSA PRIVATE KEY—–

데이터 : ec849cebaa85ed858cec8aa4ed8ab8

서명키 : 37edc171a5af7…생략…ddcc66d23f3ba0187a18c

개인키와 서명값이 잘 나온거 같습니다.

이제 마지막으로 서명된 값과 메시지 내용 그리고 공개키를 받아 정상적으로 서명된 메시지 내용인지 확인하는 작업입니다.

공개키를 구하는건 정말 간단합니다.
개인키 구하는거에 비해 좀 어이 없을 정도로 간단합니다.

이미 PHP함수로 제공중인 openssl_get_publickey 함수를 이용해 공개키를 구합니다.

구한 공개키를 open_verify 함수에 대입하고 $text, $signature 변수 또한 위에서 입력한 값을 그대로 대입하시면됩니다.

결과는 다음과 같이 나와야 정상입니다.

공개키 :
—–BEGIN PUBLIC KEY—–
MIGfMA0GCSqGSIb3…생략…sbEnom6HysSm7Jq5RM
…생략…
xv5qzOQ…생략…IDAQAB
—–END PUBLIC KEY—–

서명 확인 결과 : 성공

어떻습니까? 개인키작업만 빼면 인증서 서명하는과정이 참 쉽죠?
수많은 삽질로 어려움이 많았지만 삽질해보는 과정에서 좀더 이거저거를 배울수 있엇습니다.
아직도 배움에 길은 멀고도 험한가 봅니다.

이로써 공인인증서를 PHP로 최종 서명하는 과정을 잘 마쳤습니다.
매우 많이 어렵고 헷갈리고 모르는 용어도 많았으며 버그가 있을수도 있습니다.
비록 언어 전달능력이 부족한터라 설명이 부족했지만 여러분에게 조금이나마 도움이 되었으면 하는 바램입니다.

첨부 파일
class.seed.php 30.3KB
class.crypto.utils.php 7.3KB
class.biginteger.php 55.7KB
test.php 1.6KB

전체 소스 파일

mibany

]]>

도큐멘트 에 올린 글

댓글 남기기