본문 바로가기

Java

공인인증서 개인키를 PrivateKey 로 추출하기

반응형


공인인증서로 로그인하기 위하여 공인인증서 파일의 내용을 활용하여 전자서명하려면 개인키(signPri.key)를 자바의 PrivateKey 객체로 변환해서 사용해야 한다.

하지만 우리나라 공인인증서의 개인키는 KISA에서 권고하는 SEED 방식으로 암호화 되어 있기 때문에 일반적인 방법으로는 개인키를 PrivateKey 객체로 얻을 수 없다.

더구나 개인키의 종류(개인/법인)나 발급기관에 따라서 암호화 되어 있는 데이터를 풀 수 있는 키를 얻는 방법이 다르다.

그럼 signPri.key의 구조를 확인하여 이런 수수깨끼를 풀어보자.

인증서는 ASN.1 이라는 구조로 저장되어 있는데 자바의 ASN1InputStream으로 개인키의 내용을 읽어서 확인할 수 있지만 guidumpasn-ng.exe 파일을 사용하면 보다 쉽게 구조를 파악할 수 있다.

guidumpasn-ng.exe 파일을 사용해서 설치 후 signPri.key 파일을 열어보면 아래와 같은 구조가 나온다.

참고로 가지고 있는 인증서에 따라서 아래와 다른 내용이 나올 수 있다.

File: 인증서 저장경로\인증서DN
Time: 11:28:57, 08/22/2019
---------------------------------------------------------------------
   0 30 1342: SEQUENCE {
   4 30   72:   SEQUENCE {
   6 06    9:     OBJECT IDENTIFIER pkcs5PBES2 (1 2 840 113549 1 5 13)
  17 30   59:     SEQUENCE {
  19 30   27:       SEQUENCE {
  21 06    9:         OBJECT IDENTIFIER pkcs5PBKDF2 (1 2 840 113549 1 5 12)
  32 30   14:         SEQUENCE {
  34 04    8:           OCTET STRING
            :             E9 1B 3A 5C 44 74 DF 16
  44 02    2:           INTEGER 1024
            :           }
            :         }
  48 30   28:       SEQUENCE {
  50 06    8:         OBJECT IDENTIFIER '1 2 410 200004 1 4'
  60 04   16:         OCTET STRING
            :           A7 B7 48 17 5F E7 F8 9A 12 6A 21 47 AF 0A 86 1F
            :         }
            :       }
            :     }
  78 04 1264:   OCTET STRING
            :     E2 B9 1A 19 6B 03 5A 09 1C E3 5B E8 D8 D7 14 18
            :     59 0E BC E5 67 F6 25 92 20 E5 25 7A 3F AA 8A 83
            :     83 63 5C 6A E4 75 06 13 7A 77 A2 07 C2 DA 40 01
            :     06 E0 B2 8F 4E A6 D5 90 37 CC CE EB 0F 16 BD 24
            :     65 7B 67 A3 ED 0E 18 41 74 DA 11 B4 42 21 61 01
            :     FB 32 4D 45 6A 5E B4 B9 64 2C F0 B1 2C 2C F8 33
            :     63 8C 8D 79 BB 3F FF E0 02 D8 7C E4 09 AE 0B CE
            :     FD 0E C1 BD F7 8C 1A 63 D6 01 29 CB 1C 25 FA FF
            :             [ Another 1136 bytes skipped ]
            :   }

뭔가 이쁘게 나오긴 한거 같은데 그냥 봐서는 잘 모르겠다.

얼핏보면 Root SEQUENCE를 기준으로 첫번째는 뭔가에 대한 설명같고, 두번째는 데이터 같다.

이런 구조가 뭘까 싶어 구글링을 해보니 https://tools.ietf.org/html/rfc5208 에서 Encrypted Private-Key Information Syntax를 확인할 수 있었다.

EncryptedPrivateKeyInfo ::= SEQUENCE {
encryptionAlgorithm  EncryptionAlgorithmIdentifier,
encryptedData        EncryptedData }

EncryptionAlgorithmIdentifier ::= AlgorithmIdentifier
EncryptedData ::= OCTET STRING

역시나 첫번째는 암호화 알고리즘에 대한 정보를 가지고 있고, 두번째는 암호화된 데이터가 맞는거 같다. 아마 암호화된 데이터를 알고리즘 정보를 활용해서 복호화를 하면 개인키가 나오는 거 같다.

이번엔 알고리즘 파트의 첫번째로 나오는 1 2 840 113549 1 5 13가 뭘까 검색해보자.

그랬더니 http://oid-info.com/get/1.2.840.113549.1.5.13 사이트에서 이 내용이 Password-Based Encryption Scheme 2 (PBES2) 라는 것을 알 수 있었다.

1.2.840.113549.1.5.13가 PKCS5 형식의 PBES2 라는 내용란다. 그럼 1 2 840 113549 1 5 12는 뭘까.

역시나 http://oid-info.com/get/1.2.840.113549.1.5.12 에서 검색하니 Password-Based Key Derivation Function 2 (PBKDF2) 라고 한다.

PBES2에 대해 검색하면 https://tools.ietf.org/html/rfc2898#appendix-A.2 를 찾을 수 있는데 여기서 PBKDF2에 대한 내용 중 눈에 띄는게 있었다.

A.2   PBKDF2

   The object identifier id-PBKDF2 identifies the PBKDF2 key derivation
   function (Section 5.2).

   id-PBKDF2 OBJECT IDENTIFIER ::= {pkcs-5 12}

   The parameters field associated with this OID in an
   AlgorithmIdentifier shall have type PBKDF2-params:

   PBKDF2-params ::= SEQUENCE {
       salt CHOICE {
           specified OCTET STRING,
           otherSource AlgorithmIdentifier {{PBKDF2-SaltSources}}
       },
       iterationCount INTEGER (1..MAX),
       keyLength INTEGER (1..MAX) OPTIONAL,
       prf AlgorithmIdentifier {{PBKDF2-PRFs}} DEFAULT
       algid-hmacWithSHA1 }

PBKDF2에 대한 내용 밑으로 개인키 암호화에 사용된 salt 값과 iterationCount, keyLength가 들어가 있는 구조라고 한다.

우리가 분석한 ASN.1 구조를 대입해보면 OCTET STRING 값이 salt가 되고, INTEGER이 iterationCount가 되는데 테스트로 사용한 인증서의 ASN.1 내용에는 keyLength는 없는거 같다.

  21 06    9:         OBJECT IDENTIFIER pkcs5PBKDF2 (1 2 840 113549 1 5 12)
  32 30   14:         SEQUENCE {
  34 04    8:           OCTET STRING
            :             E9 1B 3A 5C 44 74 DF 16
  44 02    2:           INTEGER 1024
            :           }

아마 여기까지가 암호화 키에 대한 내용 같다.

1 2 410 200004 1 4는 뭘까 찾아보자.

이번에도 http://oid-info.com/cgi-bin/display?oid=1+2+410+200004+1+4&action=display 에서 검색하니 "SEED" encryption algorithm (Cipher Block Chaining (CBC) mode) 라고 한다.

드디어 어떤 방식으로 암호화가 되었는지 정보가 나왔다.

SEED는 우리나라 KISA에서 나온 암호화 알고리즘이고 https://www.rootca.or.kr/kor/standard/standard01B.jsp 에 암호 알고리즘에 보면 SEED 암호화는 암호화 키와 초기 벡터(initial vector)를 가지고 하는거 같다.

앞서 암호화 키에 대한 정보를 찾을 수 있었는데 OCTET STRING 부분이 초기 백터(initial vector) 값인 것 같다.

이제 모든 퍼즐의 조각이 모인거 같다.

개인키는 키가 어떻게 암호화 했는지에 대한 정보와 암호화된 키로 나눠져 있었다.

어떻게 암호화 했는지에 대한 정보에는 암호화 키에 대한 정보와 실질적으로 어떤 알고리즘으로 암호화 했는지, IV 값 등을 가지고 있었다.

우리는 이 정보를 바탕으로 암호화된 개인키를 풀어서 사용하면 되는 것이였다.

이번에는 다른 인증서의 구조를 확인해보자.

File:인증서 저장경로\인증서DN
Time: 11:00:15, 01/22/2020
---------------------------------------------------------------------
   0 30 1296: SEQUENCE {
   4 30   26:   SEQUENCE {
   6 06    8:     OBJECT IDENTIFIER '1 2 410 200004 1 15'
  16 30   14:     SEQUENCE {
  18 04    8:       OCTET STRING
            :         FA 32 6A E8 41 0C 53 81
  28 02    2:       INTEGER 1024
            :       }
            :     }
  32 04 1264:   OCTET STRING
            :     BA 3A 1C A0 BF 8D 97 49 5D CE FD A3 76 FD 12 24
            :     66 77 4D 6D A3 F8 47 43 11 E9 AE E9 6D 2A 72 11
            :     EE A4 A0 AF 5A 9D C8 97 9A 53 F8 F2 36 4D 02 64
            :     CD BB 2D 88 8F 20 6A 1D 99 1E AB 33 18 F6 B3 34
            :     2E BD AE FD F7 65 B4 C8 B4 0E 25 0A 5B 90 CF E5
            :     1D 85 00 FD B3 7F F7 6F E6 09 51 2C 38 39 22 B6
            :     A9 2C C7 0A 70 47 38 32 EF 06 6F 49 1D 6C AD DC
            :     AD B7 C2 3D EB 9A 51 04 12 CF 6C 68 CF 62 83 14
            :             [ Another 1136 bytes skipped ]
            :   }

첫번째로 살펴봤던 내용에 비해서 뭔가 심플하다.

역시나 알고리즘 파트의 첫번째로 나오는 1 2 410 200004 1 15가 뭘까 검색해보자.

그랬더니 http://oid-info.com/get/1.2.410.200004.1.15 사이트에서 이 내용이 Key Generation with SHA1 and Encryption with SEED CBC mode 라는 것을 알 수 있었다.

역시나 앞서 살펴본 PBES2 내용과 다른 내용이라는 것을 알 수 있다.

PBES2와는 다르게 Salt와 Iteration Count(IC)에 대한 정보는 존재하지만 나머지 Initial Vector(IV)에 대한 정보는 없었다.

아마 https://www.rootca.or.kr/kor/standard/standard01B.jsp 의 암호 알고리즘 항목 중 6. SEED 블록 암호 알고리즘을 위한 암호화 키(K) 및 초기 벡터(IV) 생성의 내용으로 직접 IV 값을 만들어야 하는 것 같다.

얼추 seedCBCWithSHA1 방식으로 된 개인키 구조도 알 것 같다.

이제 모았던 퍼즐로 코드를 작성해보자.

public static PrivateKey readPrivateKey(String filePath, String passwd) throws Exception {

    byte[] decryptedKey = null;

    byte[] encodedKey = FileUtils.readBinary(filePath);

    org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = null;
    try (ByteArrayInputStream bIn = new ByteArrayInputStream(encodedKey);
            ASN1InputStream aIn = new ASN1InputStream(bIn);) {

        ASN1Sequence asn1Sequence = (ASN1Sequence) aIn.readObject();

        AlgorithmIdentifier algId = AlgorithmIdentifier.getInstance(asn1Sequence.getObjectAt(0));
        ASN1OctetString data = ASN1OctetString.getInstance(asn1Sequence.getObjectAt(1));

        encryptedPrivateKeyInfo = new org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo(algId, data.getEncoded());

        String privateKeyAlgName = encryptedPrivateKeyInfo.getEncryptionAlgorithm().getAlgorithm().getId();

	if ("1.2.840.113549.1.5.13".equals(privateKeyAlgName)) { // pkcs5PBES2
		
	    // --------------------------------
	    // 개인키 암호화 정보에서 Salt, Iteration Count(IC), Initial Vector(IV)를 가져오는 로직
	    // --------------------------------
	    ASN1Sequence asn1Sequence2 = (ASN1Sequence)algId.getParameters();
	    ASN1Sequence asn1Sequence3 = (ASN1Sequence)asn1Sequence2.getObjectAt(0);
	    // PBKDF2 Key derivation algorithm
	    ASN1Sequence asn1Sequence33 = (ASN1Sequence)asn1Sequence3.getObjectAt(1);
	    // Salt 값
	    DEROctetString derOctetStringSalt = (DEROctetString)asn1Sequence33.getObjectAt(0);
	    // Iteration Count(IC)
	    ASN1Integer asn1IntegerIC = (ASN1Integer)asn1Sequence33.getObjectAt(1);
			
	    ASN1Sequence asn1Sequence4 = (ASN1Sequence)asn1Sequence2.getObjectAt(1);
	    // Initial Vector(IV)
	    DEROctetString derOctetStringIV = (DEROctetString)asn1Sequence4.getObjectAt(1);
			
	    // --------------------------------
	    // 복호화 키 생성
	    // --------------------------------
	    int keySize = 256;
	    PBEParametersGenerator generator = new PKCS5S2ParametersGenerator();
	    generator.init(
		PBEParametersGenerator.PKCS5PasswordToBytes(passwd.toCharArray()),
		derOctetStringSalt.getOctets(),
		asn1IntegerIC.getValue().intValue());
			
	    byte[] iv = derOctetStringIV.getOctets();
			
	    KeyParameter key = (KeyParameter)generator.generateDerivedParameters(keySize);
			
	    // --------------------------------
	    // 복호화 수행
	    // --------------------------------
	    IvParameterSpec ivSpec = new IvParameterSpec(iv);
	    SecretKeySpec secKey = new SecretKeySpec(key.getKey(), "SEED");

	    Cipher cipher = Cipher.getInstance("SEED/CBC/PKCS5Padding", "BC");
	    cipher.init(Cipher.DECRYPT_MODE, secKey, ivSpec);
            decryptedKey = cipher.doFinal(data.getOctets());

	} else { // 1.2.410.200004.1.15 seedCBCWithSHA1
            ASN1Sequence asn1Sequence2 = (ASN1Sequence)algId.getParameters();
	
            // Salt 값
            DEROctetString derOctetStringSalt = (DEROctetString)asn1Sequence2.getObjectAt(0);
		
            // Iteration Count(IC)
            ASN1Integer asn1IntegerIC = (ASN1Integer)asn1Sequence2.getObjectAt(1);
		
	    // --------------------------------
	    // 복호화 키 생성
	    // --------------------------------
            byte[] dk = new byte[20];
            MessageDigest md = MessageDigest.getInstance("SHA1");
            md.update(passwd.getBytes());
            md.update(derOctetStringSalt.getOctets());
            dk = md.digest();
            for (int i = 1; i < asn1IntegerIC.getValue().intValue(); i++) {
		dk = md.digest(dk);
            }
		
            byte[] keyData = new byte[16];
            System.arraycopy(dk, 0, keyData, 0, 16);
            byte[] digestBytes = new byte[4];
            System.arraycopy(dk, 16, digestBytes, 0, 4);
	
            MessageDigest digest = MessageDigest.getInstance("SHA-1");
            digest.reset();
            digest.update(digestBytes);
            byte[] div = digest.digest();
	
	    // --------------------------------
	    // Initial Vector(IV) 생성
	    // --------------------------------
            byte[] iv = new byte[16];
            System.arraycopy(div, 0, iv, 0, 16);
            if ("1.2.410.200004.1.4".equals(privateKeyAlgName)) {
	         iv = "012345678912345".getBytes();
            }
	
            // --------------------------------
            // 복호화 수행
            // --------------------------------
            IvParameterSpec ivSpec = new IvParameterSpec(iv);
            SecretKeySpec secKey = new SecretKeySpec(keyData, "SEED");
		
            Cipher cipher = Cipher.getInstance("SEED/CBC/PKCS5Padding", "BC");
            cipher.init(Cipher.DECRYPT_MODE, secKey, ivSpec);
            decryptedKey = cipher.doFinal(data.getOctets());
	}

    }

    // --------------------------------
    // 복호화된 내용을 PrivateKey 객체로 변환
    // --------------------------------
    PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(decryptedKey);
    KeyFactory kf = KeyFactory.getInstance("RSA", "BC");
    return kf.generatePrivate(ks);
    
}

소스를 설명하면 개인키를 ASN.1 구조로 읽어들이기 위해 ASN1InputStream로 변환한다.

그리고 asn1Sequence 중 첫번째(0)는 암호화 알고리즘 영역, 두번째(1)는 암호화된 개인키 데이터로 분리한다.

암호화 알고리즘 영역에서는 if 문으로 암호화 알고리즘에 따라서 분기처리 할 수 있도록 했다.

서두에도 설명했지만 개인키의 종류(개인/법인)나 발급기관에 따라서 개인키를 암호화 시키는 알고리즘이 PBES2 일 수 있고, seedCBCWithSHA1 일 수 있다.

더 있을 수 있지만 가지고 있는 인증서 종류가 많지 않아서 더 확인을 해 볼 수 없었다.

PBES2 방식은 ASN.1 구조 내에 저장된 salt, ic, iv를 차례대로 얻어온다.

가지고 있는 인증서에서는 keylength가 포함되어 있지 않아서 정확한 값을 알지 못하지만 2012년에 공인인증서 암호체게 고도화 때 1024였던 키 길이를 2048로 늘렸으니 bit를 byte로 환산하여 256을 사용했다.

이후 이 값들을 조합하여 복호화 키를 만들고, SEED 암호화 되어 있는 값을 복호화하면 PKCS8EncodedKeySpec로 변환이 가능한 개인키의 byte[] 값이 나오고, PKCS8EncodedKeySpec를 활용하여 RSA 방식의 PrivateKey를 얻을 수 있다.

seedCBCWithSHA1 방식은 ASN.1 구조 내에 저장된 salt, ic를 차례대로 얻어와 복호화 키를 SHA1으로 만들어 낸다.

이후 복호화 키를 활용하여 iv를 만들어낸 이후 SEED 암호화 되어 있는 값을 복호화하면 PKCS8EncodedKeySpec로 변환이 가능한 개인키의 byte[] 값이 나오고, PKCS8EncodedKeySpec를 활용하여 RSA 방식의 PrivateKey를 얻을 수 있다.

지금까지 과정을 통해 우리나라 공인인증서 파일의 내용을 PrivateKey 객체로 변환하는 방법을 알아 보았다.

참고로 이 방법을 사용하면 개인키에 저장된 idRandomNum 값도 추출할 수 있다.

이 값은 국세청 인증서로 로그인할 때 넘길 필수 값 중에 하나이니 공인/사설 인증기관의 서버 툴킷을 구매하지 않고 직접 개발해야 하는 경우라면 반드시 알아봐야 한다.

 

반응형