[ 사설인증서를 이용하여 상호 인증서 검증 통신 하는 방법 ]
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 |