[ 사설인증서를 이용하여 상호 인증서 검증 통신 하는 방법 ]

 

1. Java KeyStore 에 사설인증서 RootCA를 저장한다. 

2. Java 에서 KeyStore & TrustManager 객체를 생성 후 소스 레벨에서 등록한다. 

 

지금 작업할 내용은 Java 소스 레벨에서 사설인증서를 이용한 상호인증서 검증 (이하 MTLS) 통신 소스 이다. 

 

[ 참고 자료 : Openssl 로 사설인증서 만들기. ]

 

* 준비물

-- RSA & SHA256 사설인증서 생성.

1. 사설인증서 RootCA ( 테스트로 생성 : TestRootCA.pem )

2. 사설인증서 CertCA  ( 테스트로 생성한 Client 인증서 : TestClientCert.pem )

3. 사설인증서 CertCA 의 개인키 ( TestClientCert.pem 파일 생성을 위하 만든 TestClientPrikey.der )

(* 참고: Java 에서는 .pem 형식의 key 는 인식하지 못한다. 때문에 .der 형식의 개인키를 생성 )

 

자 이제 개발 작업을 시작하자.

Java HttpsUrlConnection 통신 시 사설인증서를 적용하기 위해 다음과 같은 객체가 필요하다.

1. javax.net.ssl.KeyManager : 용도~~

2. javax.net.ssl.TrustManager : 용도 ~~

3. javax.net.ssl.HostnameVerifier : 용도 ~~

 

 

[ 인증서 파일을 Certificate 객체로 리턴 함수 ]

- loadCertificate( .... )

/*****************************
[ Title : 인증서 파일을 읽어 Certificate 객체 리턴 ]
- certPath : 생성된 TestClientCert.pem 파일 경로
*****************************/
public Certificate loadCertificate( String certPath )
{
    Collection<? extends Certificate> collCert = null;
    FileInputStream                        fisCert   = null;
    Certificate                               rtnCert   = null;

    try
    {
        fisCert = new FileInputStream( certPath );

        collCert = Certificate.getInstance( "X509" ).generateCertificates( fisCert );

        rtnCert = collCert.iterator().next();
    }
    catch( Exception e)
    {
        e.printStackTrace();
    }
    finally
    {
        if( fisCert != null ) try { fisCert.close(); } catch ( IOException ex ) {}
    }
    
    return rtnCert;
}

 

[ PrivateKey 객체 추출 (Key 파일은 decrypt Key 이어야함) ]

- loadDecryptPrivateKey( .... )

/*****************************
[ Title : 사설인증서 키 TestClientPrikey.der 의 PrivateKey 추출 ]
- certKeyPath : 생성된 TestClientPrikey.der 파일 경로 ( Decrypt Key 이어야 함 )
*****************************/
public PrivateKey loadDecryptPrivateKey( String certKeyPath )
{
    PrivateKey priKey = null;

    FileInputStream fisKey = null;
    DataInputStream disKey = null;
    ByteArrayInputStream bisKey = null;

    try
    {
        fisKey = new FileInputStream( certKeyPath );
        disKey = new DataInputStream( fisKey );
        byte[] btKeyData = new byte[ disKey.available() ];

        disKey.readFully( btKeyData );
        disKey.close();

        bisKey = new ByteArrayInputStream( btKeydata );
        byte[] baKey = new byte[ bisKey.available() ];

        bisKey.read( baKey, 0, bisKey.available() );
        bisKey.close();

        PKCS8EncodedKeySpec pkcs8Spec = new PKCS8EncodedKeySpec( baKey );
        KeyFactory keyFac = KeyFactory.getInstance("RSA");

        priKey = keyFac.generatePrivate( pkcs8Spec);        
    }
    catch( Exception e )
    {
        e.printStackTrace();
        return null;
    }
    finally
    {
        if( bisKey != null ) try{ bisKey.close(); } catch( Exception e) {}
        if( disKey != null ) try{ disKey.close(); } catch( Exception e) {}
        if( fisKey != null ) try{ fisKey.close(); } catch( Exception e) {}
    }

    return priKey;
}

 

[ KeyManager 을 리턴하는 함수 ]

- getKeyManager( ..... )


/*****************************
[ Title : 사설인증서를 KeyManager 에 등록 ]
- certPath: 생성된 TestClientCert.pem 파일 경로
- certKeyPath : 생성된 TestClientPrikey.der 파일 경로
- certPasswd  TestClientPrikey.der 의 비밀번호 (ex: 1234 )
*****************************/
public KeyManager[] getKeyManager( String certPath, String certKeyPath , String certPasswd )
{
    KeyManager[] keyMgr = null;

    try
    {
        KeyStore ks = KeyStore.getInstance( "JKS" );

        ks.load( null );

        // 위에서 생성했던 loadCertificate 함수
        Certificate clientCert= loadCertificate( certPath );

       // 개인키 파일에서 PrivateKey 객체 추출
        PrivateKey keyPri = loadDecryptPrivateKey( certKeyPath, certPasswd );

        ks.setCertificateEntry( "ClientCERT", clientCert );
        ks.setKeyEntry( "ClientCertKey", keyPri, certPasswd.toCharArray(), new Certificate[]{clientCert} );

        KeyManagerFactory kmFactory = KeyManagerFactory.getInstance( "SunX509" );
        kmFactory.init( ks, certPasswd.toCharArray() );

        keyMgr = kmFactory.getKeyManagers();
    }
    catch( KeyStoreException e)
    {
        e.printStackTrace();
    }
    catch( NoSuchAlgorithmException e)
    {
        e.printStackTrace();
    }
    catch( CertificationException e)
    {
        e.printStackTrace();
    }
    catch( IOException e)
    {
        e.printStackTrace();
    }
    catch( UnrecoverableKeyException e)
    {
        e.printStackTrace();
    }

    return keyManager;
}

 

[ TrustManager 리턴 함수 ]

- getTrustManager( .... )

/*****************************
[ Title : 사설인증서 RootCA를 TrustManager 에 등록 ]
- rootCaPath : 생성된 TestRootCA.pem.pem 파일 경로
*****************************/
public TrustManager[] getTrustManager ( String rootCaPath )
{
    TrustManager[] trustMgr = null;

    try
    {
        KeyStore ksJKS = KeyStore.getInstance("JKS");

        Certificate certCA = loadCertificate( rootCaPath );

        KeyStore ksTrust = KeyStore.getInstance ( KeyStore.getDefaultType() );

        // changeit : java keystore 의 Default password
        ksTrust.load( new FileInputStream( System.getProperty("java.home") + "/lib/security/cacerts"), "changeit".toCharArray();

        ksJKS.setCertificateEntry( "TestRootCA" certCA );
        ksTrust.setCertificateEntry( "TestRootCA", certCA );

        TrustManagerFactory trustFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm());

        trustFactory.init( ksTrust );
 
        trustMgr = trustFactory.getTrustManagers();

    }
    catch (KeyStoreException e)
    {
        e.printStackTrace();
    }
    catch (NoSuchAlgorithmException e)
    {
        e.printStackTrace();
    }
    catch (CertificateException e)
    {
        e.printStackTrace();
    }
    catch (IOException e)
    {
        e.printStackTrace();
    }

    return trustMgr;
}

 

위와 같이 사설인증서를 이용한 상호 검증 TLS 통신 ( 이하 MTLS ) 를 위한 통신함수를 만든다.

[ MTLS 통신 함수 ]

- mtlsConnTest( .... )

/*****************************
[ Title : MTLS 연동 테스트 ]
- targetURL: MTLS 통신 대상 도메인
- sendData : 전달할 데이터

*****************************/
public String mtlsConnTest( String targetURL, String sendData )
{
    String rootCaPath = "...."; // 사설인증서 ROOT CA 파일 경로.
    String certCaPath  = "...."; // 사설인증서 Client 인증서 파일 경로. 
    String certCaPriKeyPath = "..."; // 사설인증서 Client 인증서 개인키 파일 경로. 
    String certCaPriKeyPasswd = "...."; // 사설인증서 Client 인증서 개인키 암호

    // KeyManager 생성
    KeyManager[] keyMgr = getKeyManager( certCaPath, certCaPriKeyPath, certCaPriKeyPasswd );

    // TrustManager 생성
    TrustManager[] trustMgr = getTrustManager( rootCaPath );

    // Read data 변수 선언
    OutputStreamWriter osw = null;
    OutputStream os = null;

    // Connection 객체 선언
    URL url = null;
    HttpsURLConnection conn = null;

    // Return 변수 선언
    StringBuilder sb = new StringBuilder();

    try
    {
        // TLS 1.2 통신만 허용
        SSLContext sslCtx = SSLContext.getInstance( "TLSv1.2" );
        sslCtx.init( keyMgr, trustMgr, null );

        HostnameVerifier hostVerifier = new HostnameVerifier()
        {
            @Override
            public boolean verify( String hostname, SSLSession session)
            {
                try
                {
                    // ROOT CA 검증
                    X509Certificate cert = (X509Certificate) session.getPeerCertificates()[0];
                   
                    // 인증서 유효기간 검증
                    cert.checkValidity();                    

                   /***************************************
                   - 인증서 전체 내용확인 ( RootCA / Chain (branch) / Cert (leaf)
                   int idx = 0;

                   for( Certificate tmpCert : session.getPeerCertificates();
                   {
                       X509Certificate xCert = (X509Certificate) tmpCert;

                       // SubjectAlternativeName 확인 ( 인증서 마다 값이 없을 수 있음 )
                       Collection<List<?>> colSubAltName = xCert.getSubjectAlternativeNames();

                       if( colSubAltName != null )
                       {
                           for( List<?> tmp : colSubAltName )
                           {
                               System.out.println( "[" + idx + "] colSubAltName : " + tmp );
                           }
                       }
                       
                       // SubjectDN-CN ( 인증서 DN 값에 따라 정규식 변경 필요 )
                       Pattern pattern = Pattern.compile( "(?<=CN=)([^,]+)" );
                       Matcher matcher = pattern.matcher( xCert.getSubjectDN().toString() );

                       if( matcher.find() ) System.out.println( "[" + idx + "] matcher.group(1) : " + matcher.group(1) );

                       // 인증서 Subject, Issuer 정보 확인
                      System.out.println( "[" + idx + "] SubjectND : " + xCert.getSubjectDN() ); // 소유자 정보
                      System.out.println( "[" + idx + "] IssuerDN : " + xCert.getIssuerDN() );     // 발행자 정보
                      System.out.println( "[" + idx + "] SigAlgName : " + xCert.SigAlgName() ); // 서명 알고리즘
                      System.out.println( "[" + idx + "] SerialNum : " + xCert.getSerialNumber() );
                      System.out.println( "[" + idx + "] SubX500Principal : " + xCert.SubjectX500Principal.getName() );
                      System.out.println( "[" + idx + "] Type : " + xCert.getType() );

                   }


                    ***************************************/
                }
                catch (SSLPeerUnverifiedException e)
                {
                    e.printStackTrace();
                    return false;
                }
                catch (CertificateExpiredException e)
                {
                    e.printStackTrace();
                    return false;

                }
                catch (CertificateNotYetValidException e)
                {
                    e.printStackTrace();
                    return false;

                }

                return true;                
            }
        };    // HostnameVerifier hostVerifier

        HttpsURLConnection.setDefaultSSLSocketFactory( sslCtx.getSocketFactory() );
        HttpsURLConnection.setDefaultHostnameVerifier( hostVerifier );

        url = new URL( targetURL )
        conn = (HttpsURLConnection) url.openConnection();

        // Set header
        conn.setRequestMethod( "POST" );
        conn.setRequestProperty( "Accept-Charset", "UTF-8" );
        conn.setRequestProperty( "Content-Type", "application/json" );
        conn.setConnectionTimeout( 2000 );
        conn.setReadTimeout ( 5000 );
        conn.setUseCaches( false );
        conn.setDoInput( true );
        conn.setDoOutput ( true );

        osw = new OutputStreamWriter( conn.getOutputStream(), "UTF-8" );
        osw.write( sendData );
        osw.flush();
        osw.close();

        BufferedReader br = null;
        String line = "";
        
        if( conn.getResponseCode() == 200 )
        {
            br = new BufferedReader( new InputStreamReader( conn.getInputStream(), "UTF-8") );

            while( (line = br.readLine()) != null )
            {
                if( !line.trim().equals( "" ) sb.append( line );
            }

            br.close();
        }

    }
    catch( NoSuchAlgorithmException e)
    {
        e.printStackTrace();
    }
    catch( KeyManagementException e)
    {
        e.printStackTrace();
    }
    catch( MalformedURLException e)
    {
        e.printStackTrace();
    }

    catch( IOException e)
    {
        e.printStackTrace();
    }
    finally
    {
        conn.disconnect();
    }

    sb.toString();
}

위 처럼 연결하면 된다.

 

* 추가적으로 PrivateKey 값이 Encrytpt 되어있을 경우, PrivateKey 값을 얻어오는 함수이다. 

  위 예재는 .der ( Encrypt 되어 있지 않는 Key ) 이다. 

[ PrivateKey 객체 추출 Encrypt PrivateKey file ]

- loadEncryptPrivateKey( .... )

/*****************************
[ Title : 사설인증서 키 TestClientPrikey.der 의 PrivateKey 추출 ]
- certKeyPath : 생성된 TestClientPrikey.der 파일 경로 ( Encrypt Key 이어야 함 )
- certKeyPasswd : 생성된 TestClientPrikey.der 비밀번호

*****************************/
public PrivateKey loadEncryptPrivateKEy( String certKeyPath, String certKeyPasswd )
{
    PrivateKey priKey = null;

    FileInputStream fisKey = null;
    DataInputStream disKey = null;
    ByteArrayInputStream bisKey = null;

    try
    {
        fisKey = new FileInputStream( certKeyPath );
        disKey = new DataInputStream( fisKey );
        byte[] btKeyData = new byte[ disKey.available() ];

        disKey.readFully( btKeyData );
        disKey.close();

        bisKey = new ByteArrayInputStream( btKeydata );
        byte[] baKey = new byte[ bisKey.available() ];

        bisKey.read( baKey, 0, bisKey.available() );
        bisKey.close();

        /************************************************************************
          암호화된 개인키 파일에서 PrivateKey 생성 
        ************************************************************************/
        EncryptedPrivateKeyInfo encKeyInfo = new EncryptedPrivateKeyInfo( baKey );
        String                         encKeyAlg = encKeyInfo.getAlgName();                    

        SecretKeyFactory          skFactory   = SecretKeyFactory.getInstance( encKeyAlg  );

        PBEKeySpec                pbeKeySpec = new PBEKeySpec( certKeyPasswd.toCharArray() );
        SecretKey                   sk              = skFactory.generateSecret( pbeKeySpec );

        Cipher                       cipher        = Cipher.getInstance( encKeyAlg );
        cipher.init( Cipher.DECRYPT_MODE, sk, encKeyInfo.getAlgParameters() ); 

        PKCS8EncodedKeySpec pkcs8Spec    = encKeyInfo.getKeySpec( cipher );
        KeyFactory keyFac                         = KeyFactory.getInstance("RSA");

        priKey = keyFac.generatePrivate( pkcs8Spec );        
    }
    catch( Exception e )
    {
        e.printStackTrace();
        return null;
    }
    finally
    {
        if( bisKey != null ) try{ bisKey.close(); } catch( Exception e) {}
        if( disKey != null ) try{ disKey.close(); } catch( Exception e) {}
        if( fisKey != null ) try{ fisKey.close(); } catch( Exception e) {}
    }

    return priKey;
}

 

'Development > Security (보안)' 카테고리의 다른 글

MTLS 란 무엇인가? 통신과정은 ?  (0) 2020.10.27
SSL 통신원리  (0) 2020.10.14
SSL 이란?  (0) 2020.09.22
X.509 인증서  (0) 2020.09.18
JAVA 사설인증서 MTLS 통신 ( Server )  (0) 2020.09.16

+ Recent posts