# JWT 서명 검증 및 인증서 관리 ## 1. 전체 구조 ``` ┌─────────────────────────────────────────────────────────────────┐ │ Authorization Server (elink-oauth) │ │ elink-oauth-dev.jks │ │ ├── 개인키 (RSA Private Key) ← JWT 서명 생성용 │ │ └── 공개키 인증서 (Certificate) │ └─────────────────────────────────────────────────────────────────┘ │ 공개키만 추출 ▼ ┌─────────────────────────────────────────────────────────────────┐ │ API Gateway (ApiAuthFilter) │ │ elink-oauth-dev.pub │ │ └── 공개키만 (RSA Public Key) ← JWT 서명 검증용만 │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 2. JKS vs PUB 파일 차이점 | 항목 | `.jks` (Java KeyStore) | `.pub` (Public Key) | |------|----------------------|---------------------| | 형식 | 바이너리 (Java 전용) | PEM 텍스트 (`-----BEGIN PUBLIC KEY-----`) | | 포함 내용 | 개인키 + 공개키 인증서 | 공개키만 | | 보호 | KeyStore 비밀번호 + Key 비밀번호 | 없음 (공개 가능) | | 사용처 | AuthorizationServerConfig - **토큰 서명** | ApiAuthFilter - **토큰 검증** | | 코드 로딩 | `KeyStoreKeyFactory` → `KeyPair` | `X509EncodedKeySpec` → `RSAPublicKey` | | 보안 등급 | **극비** (노출 시 토큰 위조 가능) | 공개 배포 가능 | ### 코드에서의 사용 위치 ```java // AuthorizationServerConfig.java - JKS로 JWT 서명 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(keystore, keystorePassword.toCharArray()); KeyPair keyPair = keyStoreKeyFactory.getKeyPair(keyAlias, keyPassword.toCharArray()); converter.setKeyPair(keyPair); // 개인키로 JWT 서명 // ApiAuthFilter.java - PUB로 JWT 검증 X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); jwsVerifier = new RSASSAVerifier((RSAPublicKey) keyFactory.generatePublic(keySpec)); // 공개키로 검증만 ``` --- ## 3. JWT 서명 검증 상세 동작 (`signedJWT.verify(jwsVerifier)`) ### JWT 구조 ``` eyJhbGciOiJSUzI1NiJ9 . eyJzdWIiOiJ1c2VyMSIsImV4cCI6...} . MEUCIQDx... Header Payload Signature (Base64url) (Base64url) (Base64url) ``` ### 서명 생성 과정 (Authorization 서버 측) ``` [Header].[Payload] ↓ SHA-256 해싱 ↓ digest (32 bytes) ↓ RSA 개인키로 암호화 (PKCS#1 v1.5 패딩) ↓ Signature (Base64url 인코딩) ``` ### 서명 검증 과정 (ApiAuthFilter 측) ``` JWT 수신 ↓ ① Header + Payload 분리 "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIs..." ↓ ② Signature 파트를 Base64url 디코딩 → 바이너리 서명값 확보 ↓ ③ RSA 공개키로 서명값 복호화 ← RSASSAVerifier (Nimbus JOSE) → digest_from_signature 추출 ↓ ④ ①의 "Header.Payload" 문자열을 SHA-256 해싱 → digest_from_message 계산 ↓ ⑤ digest_from_signature == digest_from_message ? → true : 서명 유효 (개인키 소유자가 서명한 것 확인) → false : 서명 불일치 → Token invalid 예외 ``` ### Nimbus JOSE 라이브러리 내부 흐름 ```java // RSASSAVerifier.verify() 내부 public boolean verify(JWSHeader header, byte[] signingInput, Base64URL signature) { String jcaAlg = "SHA256withRSA"; // RS256 알고리즘 Signature verifier = Signature.getInstance(jcaAlg); verifier.initVerify(publicKey); // RSA 공개키 초기화 verifier.update(signingInput); // [Header].[Payload] 바이트 입력 return verifier.verify(signature.decode()); // Signature 바이트와 대조 검증 } ``` ### 보안 원리 (RSA 비대칭 키) | 시도 | 결과 | |------|------| | Payload 변조 후 전송 | digest 불일치 → 검증 실패 | | 개인키 없이 서명 위조 | RSA 수학적으로 불가능 | | 공개키로 새 서명 생성 | 공개키로는 암호화 불가 | | 만료된 토큰 재사용 | exp 체크에서 차단 | ### ApiAuthFilter 검증 흐름 ``` 클라이언트 요청 ↓ Bearer 토큰 추출 (Authorization 헤더 or access_token 파라미터) ↓ SignedJWT.parse(token) ← JWT 파싱 ↓ 만료시간(exp) 확인 ↓ signedJWT.verify(jwsVerifier) ← RSA 공개키로 서명 검증 ↓ Scope 검증 (oob/public이면 패스, 아니면 API별 허용 scope 확인) ↓ client_id 확인 (헤더 vs 토큰 payload 비교) ↓ 등록된 APP 및 API 접근 권한 확인 ``` --- ## 4. Self-Signed 인증서 생성 방법 ### 사전 요구사항 - **JDK** (keytool 포함) - **OpenSSL** ### 1단계: JKS 파일 생성 ```bash keytool -genkeypair \ -alias elink-oauth \ -keyalg RSA \ -keysize 2048 \ -sigalg SHA256withRSA \ -validity 3650 \ -keystore elink-oauth-dev.jks \ -storepass elink1234 \ -keypass elink1234 \ -dname "CN=elink-oauth, OU=eActive, O=eActive, L=Seoul, ST=Seoul, C=KR" ``` > 옵션 설명 > - `-alias` : 키 별칭 (코드의 `certification.keyAlias` 설정값과 일치해야 함) > - `-keysize 2048` : RSA 키 길이 (최소 2048 권장) > - `-validity 3650` : 유효기간 (일 단위, 3650 = 10년) > - `-storepass` : KeyStore 전체 비밀번호 (`certification.keystorePassword`) > - `-keypass` : 개별 키 비밀번호 (`certification.keyPassword`) ### 2단계: JKS에서 공개키(.pub) 추출 **방법 A: keytool + openssl 조합** ```bash # JKS → 인증서(CER) 추출 keytool -exportcert \ -alias elink-oauth \ -keystore elink-oauth-dev.jks \ -storepass elink1234 \ -file elink-oauth-dev.cer # CER에서 공개키 추출 → PEM 형식(.pub) openssl x509 \ -inform DER \ -in elink-oauth-dev.cer \ -pubkey -noout \ -out elink-oauth-dev.pub ``` **방법 B: JKS → PKCS12 변환 후 openssl** ```bash # JKS → PKCS12 변환 keytool -importkeystore \ -srckeystore elink-oauth-dev.jks \ -destkeystore elink-oauth-dev.p12 \ -deststoretype PKCS12 \ -srcalias elink-oauth \ -srcstorepass elink1234 \ -deststorepass elink1234 # PKCS12에서 공개키 추출 openssl pkcs12 \ -in elink-oauth-dev.p12 \ -passin pass:elink1234 \ -nokeys \ -out temp-cert.pem openssl x509 \ -in temp-cert.pem \ -pubkey -noout \ -out elink-oauth-dev.pub ``` ### 3단계: 생성 결과 확인 ```bash # JKS 내용 확인 keytool -list -v \ -keystore elink-oauth-dev.jks \ -storepass elink1234 # 공개키 내용 확인 cat elink-oauth-dev.pub # 결과: # -----BEGIN PUBLIC KEY----- # MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... # -----END PUBLIC KEY----- ``` --- ## 5. 파일 배치 ``` Authorization Server (elink-oauth) └── src/main/resources/certificate/ └── elink-oauth-dev.jks ← 개인키 포함, 절대 외부 노출 금지 API Gateway (elink-online-common) └── src/main/resources/certificate/ └── elink-oauth-dev.pub ← 공개키만, 배포 가능 ``` ## 6. 설정 파일 (`OAuthServer` PropGroup) | 프로퍼티 키 | 설명 | 사용 위치 | |------------|------|----------| | `certification.keystorePath` | JKS 파일 경로 | AuthorizationServerConfig | | `certification.keystorePassword` | KeyStore 비밀번호 | AuthorizationServerConfig | | `certification.keyAlias` | 키 별칭 | AuthorizationServerConfig | | `certification.keyPassword` | 키 비밀번호 | AuthorizationServerConfig | | `certification.publicKeyPath` | PUB 파일 경로 | ApiAuthFilter |