반응형

 

출처 : http://blog.kangwoo.kr/49

인터넷 뱅킹을 하시는 분이라면 대부분 공인인증서를 가지고 있다. 이 공인인증서를 가지고 전자서명을 해보도록하자(전혀 쓸데없는 일이긴 하다 ^^;)
필자의 경우 yessign에서 발급한 은행용 공인인증서를 가지고 있는데 그 경로는 C:\NPKI\yessign\USER\아래폴더... 에 위치해 있다.
그 디렉토리에 보면 CaPubs, signCert.der, signPri.key 세 파일이 존재한다.
CaPubs은 무슨 파일인지 잘 모르겠다. signCert.der는 공인 인증서 파일이고, signPri.key는 개인키 파일이다.
(der은 인증서 저장시 바이너르 형태로 저장하기 위한 포맷이고, pem은 문자열로 표현가능한 데이터로 인코딩(BASE64같은..)한 포맷이다.)
한국정보보호진흥원(http://www.rootca.or.kr/kcac.html)의 기술규격을 참조해보면, 현재 사용하는 공인인증서는 RFC3280을 준수하여, 전자서명인증체계에서 사용하는 정수2를 갖는 X.509 v3을 사용하고 있다고 한다.

1. 공개키 가져오기.
- 자바에서 X.590를 지원해주니 간단히 사용해보자.

01 package test.security;
02
03 import java.io.File;
04 import java.io.FileInputStream;
05 import java.io.IOException;
06 import java.security.cert.CertificateFactory;
07 import java.security.cert.X509Certificate;
08
09 public class CertificateTest1 {
10
11 public static void main(String[] args) throws Exception {
12 X509Certificate cert = null;
13 FileInputStream fis = null;
14 try {
15 fis = new FileInputStream(new File("C:/signCert.der"));
16 CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
17 cert = (X509Certificate) certificateFactory.generateCertificate(fis);
18 } finally {
19 if (fis != null) try {fis.close();} catch(IOException ie) {}
20 }
21 System.out.println(cert);
22 System.out.println("-----------------");
23 System.out.println(cert.getPublicKey());
24 }
25 }

실행해보면 아래처럼 인증서에 대한 정보를 볼 수 있을것이다.(보안 관계상 많은 부분을 생략하겠다.)

[
[
Version: V3
Subject: CN=누굴까(RangWoo)0000000000000000, OU=XXX, OU=personalXXX, O=yessign, C=kr
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5

Key: Sun RSA public key, 1024 bits
... 생략 ...
[7]: ObjectId: 1.3.6.1.5.5.7.1.1 Criticality=false
AuthorityInfoAccess [
[accessMethod: 1.3.6.1.5.5.7.48.1
accessLocation: URIName: http://ocsp.yessign.org:4612]
]
... 생략 ...
Sun RSA public key, 1024 bits
modulus: 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
public exponent: 00000

당연히, V3 버젼을 사용하고 서명 알고리즘은 SHA1withRSA을 사용한다. SHA1withRSA 옆에 보면 OID란 놈이 있다.
OID란 Object IDentifier의 약어로서 객체식별체계정도로 이해하면 되겠다. 즉, OID의 값이 1.2.840.113549.1.1.5이면 SHA1withRSA란 의미이다.
http://www.oid-info.com/ 사이트에 가서 1.2.840.113549.1.1.5 값을 입력하면 아래와 같은 값을 얻을 수 있다.

그리고 중간쯤에 ocsp(Online Certificate Status Protocol)라고 실시간으로 인증서 유효성 검증을 할수 있는 정보도 나온다.
좀더 내려가보면 공개키부분이 나오는데, 이놈이 우리가 사용할 부분이다. cert.getPublicKey() 메소드를 이용하면 직접 공개키를 가져올 수 있다.

2. 개인키 가져오기
- 공개키는 거의 날로 먹었는데, 개인키란 놈은 만만하지가 않다.
- 기본적으로(?)는 PKCS#8를 이용해서 개인키를 저장하는데, 국내 공인인증서에 사용하는 개인키 파일는 암호화(?)해서 저장한다.
PKCS#5(Password-Based Cryptography Standard)의 PBKDF1(Password-Based Key Derivation Function), PBES1(Password-Based Encryption Scheme)를 이용한다는 것이다.
여기까지는 별 문제가 없는데, 데이터 암호화를 할때 국내에서만 사용하는 SEED란 블럭암호화 알고리즘를 사용한다는것이다.
즉, 기본적으로 제공이 안되므로 직접 구현을 해야한다.
뭔소리인지 이해가 안가면 한국정보보호진흥원(http://www.rootca.or.kr/)의 암호 알고리즘 규격(KCAC.TS.ENC)를 한번 읽어보자. (사실 읽어봐도 이해가 안가지만... ^^;)
간단히 설명을 하자면, PBES(Password-Based Encryption Scheme) 즉 패스워드 기반의 키 암호화 기법을 사용하겠다는 것이다. 암호화 할때 필요한게 비밀키이다. 이 키는 해당 알고리즘에 맞는 바이트 배열로 보통 사용을 하는데, 이것을 사람이 쉽게 인식할 수 있는 패스워드로 사용하겠다는것이다.
뭐 필자처럼 무식하게 "hello123".getBytes(); 를 사용해서 키로 사용할 수 있지만, 모양새가 안좋아보인다는것이다. 그래서 "hello123" 문자열을 가공해서 멋진(?) 키로 만들어 사용한다는 것이다.
이 가공하는 함수가 PBKDF(Password-Based Key Derivation Function)이다. 그리고 이 함수를 이용해서 비밀키를 생성해서 암호화/복화하는 하는 구조를 PBES라고 한다.
자바에서 기본적으로 "PBEWithMD5AndDES", "PBEWithSHA1AndDESede" 등의 알고리즘을 제공해준다.
Security.getProviders(); 메소드를 이용해서, Provider 정보를 출력해보면 지원하는 알고리즘을 알 수 있다.
01 package test.security;
02
03 import java.security.Provider;
04 import java.security.Security;
05
06 public class ProviderInfo {
07
08 public static void main(String[] args) {
09 Provider[] providers = Security.getProviders();
10 for (int i = 0; i < providers.length; i++) {
11 String name = providers[i].getName();
12 String info = providers[i].getInfo();
13 double version = providers[i].getVersion();
14 System.out.println("--------------------------------------------------");
15 System.out.println("name: " + name);
16 System.out.println("info: " + info);
17 System.out.println("version: " + version);
18
19 for (Object key : providers[i].keySet()) {
20 System.out.println(key + "\t"+ providers[i].getProperty((String)key));
21 }
22 }
23 }
24 }

그런데 불행히도 "PBEWithSHA1AndSeed"같은 알고리즘은 없는거 같다. 어떻게 해야할까? 당연히 삽~을 들어야한다.(아~~ 또 무덤을 파는구나 ㅠㅠ)
일단 파일의 구조를 파악해서 필요한 정보를 읽어와야한다.(ASN. 1으로 인코딩되어있다.)
다행히도 PKCS#8로 정의하고 있는 구조를 읽을 수 있는 EncryptedPrivateKeyInfo 클래스가 존재해서 한결 쉽게 작업을 할 수 있다
EncryptedPrivateKeyInfo 클래스를 사용해서 정보를 읽어오자. 사용하는 알고리즘을 출력해 보자.
01 // 1. 개인키 파일 읽어오기
02 byte[] encodedKey = null;
03 FileInputStream fis = null;
04 ByteArrayOutputStream bos = null;
05 try {
06 fis = new FileInputStream(new File("C:/signPri.key"));
07 bos = new ByteArrayOutputStream();
08 byte[] buffer = new byte[1024];
09 int read = -1;
10 while ((read = fis.read(buffer)) != -1) {
11 bos.write(buffer, 0, read);
12 }
13 encodedKey = bos.toByteArray();
14 } finally {
15 if (bos != null) try {bos.close();} catch(IOException ie) {}
16 if (fis != null) try {fis.close();} catch(IOException ie) {}
17 }
18
19 System.out.println("EncodedKey : " + ByteUtils.toHexString(encodedKey));
20
21 // 2. 개인카 파일 분석하기
22 EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey);
23 System.out.println(encryptedPrivateKeyInfo);
24 System.out.println(encryptedPrivateKeyInfo.getAlgName());

필자의 경우 "1.2.410.200004.1.15"란 값을 얻을 수 있었다. 나머지 파라메터 정보는 불행히도 제공을 안해줘서 직접 처리해야한다.
"1.2.410.200004.1.15" 어디서 많이 본 형식이다. 그렇다. OID이다. 사이트(http://www.oid-info.com/)가서 조회를 해보자.
"Key Generation with SHA1 and Encryption with SEED CBC mode" 란다.

한국정보보호진흥원(http://www.rootca.or.kr/)의 암호 알고리즘 규격(KCAC.TS.ENC)에서도 해당 OID에 대한 정보를 알 수 있다.

즉, 두 번째 방법이라는 것인데, DK의 값을 이용해서 해쉬값을 만든다음 그 값을 IV(초기화 벡터)로 사용하라는 것이다.
여기서 DK란 PBKDF를 사용해서 만든 추출키를 의미한다. 그렇다면 먼저 추출키를 만들어보자.

위의 설명대로 해당 함수를 구현해보자.
salt와 iteration count가 필요하다.
salt는 공인인증서를 발급할때마다 랜덤하게 생성되는것으로, 블특정다수의 사전(Dictionary) 공격을 방지하는 역할을 한다.(21-28바이트 사이의 8바이트를 사용함)
iteration count는 비밀키 생성을 위해 해쉬함수를 몇번 반복할 것인가를 나타낸다. (31-32바이트 사이의 2바이트를 사용함)
1 byte[] salt = new byte[8];
2 System.arraycopy(encodedKey, 20, salt, 0, 8);
3 System.out.println("salt : " + ByteUtils.toHexString(salt));
4 byte[] cBytes = new byte[4];
5 System.arraycopy(encodedKey, 30, cBytes, 2, 2);
6 int iterationCount = ByteUtils.toInt(cBytes);
7 System.out.println("iterationCount : " + ByteUtils.toHexString(cBytes));
8 System.out.println("iterationCount : " + iterationCount);

그럼 PBKDF1을 구현해보자. RFC2898(http://www.ietf.org/rfc/rfc2898.txt)을 보면 아래처럼 설명이 나와있다.
5.1 PBKDF1

   PBKDF1 applies a hash function, which shall be MD2 [6], MD5 [19] or
   SHA-1 [18], to derive keys. The length of the derived key is bounded
   by the length of the hash function output, which is 16 octets for MD2
   and MD5 and 20 octets for SHA-1. PBKDF1 is compatible with the key
   derivation process in PKCS #5 v1.5.

   PBKDF1 is recommended only for compatibility with existing
   applications since the keys it produces may not be large enough for
   some applications.

   PBKDF1 (P, S, c, dkLen)

   Options:        Hash       underlying hash function

   Input:          P          password, an octet string
                   S          salt, an eight-octet string
                   c          iteration count, a positive integer
                   dkLen      intended length in octets of derived key,
                              a positive integer, at most 16 for MD2 or
                              MD5 and 20 for SHA-1

   Output:         DK         derived key, a dkLen-octet string

   Steps:

      1. If dkLen > 16 for MD2 and MD5, or dkLen > 20 for SHA-1, output
         "derived key too long" and stop.

      2. Apply the underlying hash function Hash for c iterations to the
         concatenation of the password P and the salt S, then extract
         the first dkLen octets to produce a derived key DK:

                   T_1 = Hash (P || S) ,
                   T_2 = Hash (T_1) ,
                   ...
                   T_c = Hash (T_{c-1}) ,
                   DK = Tc<0..dkLen-1>

      3. Output the derived key DK.
설명대로 구현해주자. 피곤한 관계상 SHA1을 사용해서 20바이트의 추출키만을 반환하도록 만들었다.
01 public static byte[] pbkdf1(String password, byte[] salt, int iterationCount) throws NoSuchAlgorithmException {
02 byte[] dk = new byte[20];
03 MessageDigest md = MessageDigest.getInstance("SHA1");
04 md.update(password.getBytes());
05 md.update(salt);
06 dk = md.digest();
07 for (int i = 1; i < iterationCount; i++) {
08 dk = md.digest(dk);
09 }
10 return dk;
11 }
12 }

해당 함수를 사용해서 추출키(DK) 초기화 벡터(IV)를 만들어 보자.
01 String password = "password";
02
03 // 추출키(DK) 생성
04 byte[] dk = pbkdf1(password, salt, iterationCount);
05 System.out.println("dk : " + ByteUtils.toHexString(dk));
06
07 // 생성된 추출키(DK)에서 처음 16바이트를 암호화 키(K)로 정의한다.
08 byte[] keyData = new byte[16];
09 System.arraycopy(dk, 0, keyData, 0, 16);
10
11 // 추출키(DK)에서 암호화 키(K)를 제외한 나머지 4바이트를 SHA-1
12 // 으로 해쉬하여 20바이트의 값(DIV)을 생성하고, 그 중 처음 16바이트를 초기
13 // 벡터(IV)로 정의한다.
14 byte[] div = new byte[20];
15 byte[] tmp4Bytes = new byte[4];
16 System.arraycopy(dk, 16, tmp4Bytes, 0, 4);
17 div = SHA1Utils.getHash(tmp4Bytes);
18 System.out.println("div : " + ByteUtils.toHexString(div));
19 byte[] iv = new byte[16];
20 System.arraycopy(div, 0, iv, 0, 16);
21 System.out.println("iv : " + ByteUtils.toHexString(iv));

당연히 password 변수에는 공인인증서 암호를 입력해야한다. 안그러면 에러가 난다.
이제 고지가 눈앞에 보인다. 남은것은 SEED를 이용해서 복화만 하면 되는것이다. SEED 구현 + CBC 운용모드 구현을 직접하려면 정신적인 데미지가 커질 수 있으므로, 만들어놓은것을 가져다 쓰겠다.
Bouncy Castle Crypto APIs(http://www.bouncycastle.org/)를 감사하는 마음으로 가져다 쓰자.
%JAVA_HOME%/jre/lib/ext에 해당 jar파일을 복사한 다음, %JAVA_HOME%/jre/lib/security/java.security 파일에
security.provider.7=org.bouncycastle.jce.provider.BouncyCastleProvider
을 추가해서 사용할 수 있지만, 귀찮은 관계로 그냥(?) 사용하겠다.
1 // 3. SEED로 복호화하기
2 BouncyCastleProvider provider = new BouncyCastleProvider();
3 Cipher cipher = Cipher.getInstance("SEED/CBC/PKCS5Padding", provider);
4 Key key = new SecretKeySpec(keyData, "SEED");
5 cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
6 byte[] output = cipher.doFinal(encryptedPrivateKeyInfo.getEncryptedData());

이젠 해당 데이터로 개인키를 생성만 해주면 된다.
1 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(output);
2 KeyFactory keyFactory = KeyFactory.getInstance("RSA");
3 RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey)keyFactory.generatePrivate(keySpec);
4 System.out.println(privateKey);
패스워드를 일치여부는 PBES에서 정의한 패딩이 존재하는지 여부로 판단한다. 만약 잘못된 패스워드라면
Exception in thread "main" javax.crypto.BadPaddingException: pad block corrupted
같은 에러가 발생할것이다.


그럼 마지막으로 공인인증서의 공개키와 개인키를 가지고 어제 해본 전자서명을 한번 해보자.
001 package test.security;
002
003 import java.io.ByteArrayOutputStream;
004 import java.io.File;
005 import java.io.FileInputStream;
006 import java.io.IOException;
007 import java.security.Key;
008 import java.security.KeyFactory;
009 import java.security.MessageDigest;
010 import java.security.NoSuchAlgorithmException;
011 import java.security.PrivateKey;
012 import java.security.PublicKey;
013 import java.security.Signature;
014 import java.security.cert.CertificateFactory;
015 import java.security.cert.X509Certificate;
016 import java.security.interfaces.RSAPrivateCrtKey;
017 import java.security.spec.PKCS8EncodedKeySpec;
018
019 import javax.crypto.Cipher;
020 import javax.crypto.EncryptedPrivateKeyInfo;
021 import javax.crypto.spec.IvParameterSpec;
022 import javax.crypto.spec.SecretKeySpec;
023
024 import kr.kangwoo.util.ByteUtils;
025
026 import org.bouncycastle.jce.provider.BouncyCastleProvider;
027
028 import com.jarusoft.util.security.SHA1Utils;
029
030 public class CertificateTest {
031
032 public static void main(String[] args) throws Exception {
033 String msg = "하늘에는 달이 없고, 땅에는 바람이 없습니다.\n사람들은 소리가 없고, 나는 마음이 없습니다.\n\n우주는 죽음인가요.\n인생은 잠인가요.";
034 PublicKey publicKey = getPublicKey("C:/signCert.der");
035 PrivateKey privateKey = getPrivateKey("C:/signPri.key");
036
037 // 전자서명하기
038 Signature signatureA = Signature.getInstance("SHA1withRSA");
039 signatureA.initSign(privateKey);
040 signatureA.update(msg.getBytes());
041 byte[] sign = signatureA.sign();
042 System.out.println("signature : " + ByteUtils.toHexString(sign));
043
044 // 전사서명 검증하기
045 String msgB = msg;
046 Signature signatureB = Signature.getInstance("SHA1withRSA");
047 signatureB.initVerify(publicKey);
048 signatureB.update(msgB.getBytes());
049 boolean verifty = signatureB.verify(sign);
050 System.out.println("검증 결과 : " + verifty);
051 }
052
053 public static PublicKey getPublicKey(String file) throws Exception {
054 X509Certificate cert = null;
055 FileInputStream fis = null;
056 try {
057 fis = new FileInputStream(new File(file));
058 CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
059 cert = (X509Certificate) certificateFactory.generateCertificate(fis);
060 } finally {
061 if (fis != null) try {fis.close();} catch(IOException ie) {}
062 }
063 System.out.println(cert.getPublicKey());
064 return cert.getPublicKey();
065 }
066
067 public static PrivateKey getPrivateKey(String file) throws Exception {
068 // 1. 개인키 파일 읽어오기
069 byte[] encodedKey = null;
070 FileInputStream fis = null;
071 ByteArrayOutputStream bos = null;
072 try {
073 fis = new FileInputStream(new File(file));
074 bos = new ByteArrayOutputStream();
075 byte[] buffer = new byte[1024];
076 int read = -1;
077 while ((read = fis.read(buffer)) != -1) {
078 bos.write(buffer, 0, read);
079 }
080 encodedKey = bos.toByteArray();
081 } finally {
082 if (bos != null) try {bos.close();} catch(IOException ie) {}
083 if (fis != null) try {fis.close();} catch(IOException ie) {}
084 }
085
086 System.out.println("EncodedKey : " + ByteUtils.toHexString(encodedKey));
087
088 // 2. 개인카 파일 분석하기
089 EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey);
090 System.out.println(encryptedPrivateKeyInfo);
091 System.out.println(encryptedPrivateKeyInfo.getAlgName());
092
093 byte[] salt = new byte[8];
094 System.arraycopy(encodedKey, 20, salt, 0, 8);
095 System.out.println("salt : " + ByteUtils.toHexString(salt));
096 byte[] cBytes = new byte[4];
097 System.arraycopy(encodedKey, 30, cBytes, 2, 2);
098 int iterationCount = ByteUtils.toInt(cBytes);
099 System.out.println("iterationCount : " + ByteUtils.toHexString(cBytes));
100 System.out.println("iterationCount : " + iterationCount);
101
102
103 String password = "password";
104
105 // 추출키(DK) 생성
106 byte[] dk = pbkdf1(password, salt, iterationCount);
107 System.out.println("dk : " + ByteUtils.toHexString(dk));
108
109 // 생성된 추출키(DK)에서 처음 16바이트를 암호화 키(K)로 정의한다.
110 byte[] keyData = new byte[16];
111 System.arraycopy(dk, 0, keyData, 0, 16);
112
113 // 추출키(DK)에서 암호화 키(K)를 제외한 나머지 4바이트를 SHA-1
114 // 으로 해쉬하여 20바이트의 값(DIV)을 생성하고, 그 중 처음 16바이트를 초기
115 // 벡터(IV)로 정의한다.
116 byte[] div = new byte[20];
117 byte[] tmp4Bytes = new byte[4];
118 System.arraycopy(dk, 16, tmp4Bytes, 0, 4);
119 div = SHA1Utils.getHash(tmp4Bytes);
120 System.out.println("div : " + ByteUtils.toHexString(div));
121 byte[] iv = new byte[16];
122 System.arraycopy(div, 0, iv, 0, 16);
123 System.out.println("iv : " + ByteUtils.toHexString(iv));
124
125 // 3. SEED로 복호화하기
126 BouncyCastleProvider provider = new BouncyCastleProvider();
127 Cipher cipher = Cipher.getInstance("SEED/CBC/PKCS5Padding", provider);
128 Key key = new SecretKeySpec(keyData, "SEED");
129 cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
130 byte[] output = cipher.doFinal(encryptedPrivateKeyInfo.getEncryptedData());
131
132 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(output);
133 KeyFactory keyFactory = KeyFactory.getInstance("RSA");
134 RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey)keyFactory.generatePrivate(keySpec);
135 System.out.println(privateKey);
136 return privateKey;
137
138 }
139
140 public static byte[] pbkdf1(String password, byte[] salt, int iterationCount) throws NoSuchAlgorithmException {
141 byte[] dk = new byte[20]; // 생성이 의미가 없지만 한눈에 알아보라고 20바이트로 초기화
142 MessageDigest md = MessageDigest.getInstance("SHA1");
143 md.update(password.getBytes());
144 md.update(salt);
145 dk = md.digest();
146 for (int i = 1; i < iterationCount; i++) {
147 dk = md.digest(dk);
148 }
149 return dk;
150 }
151 }



반응형

+ Recent posts