인증서 피닝(Certificate Pinning)은 클라이언트가 서버 인증서나 공개키를 사전에 하드코딩해 중간자 공격(MITM)을 방지하는 기술이다.
피닝 방식 비교
| 방식 | 대상 | 유연성 | 보안 강도 |
|---|
| 인증서 피닝 | 전체 인증서 | 낮음 (갱신 시 재배포) | 높음 |
| 공개키 피닝 | 공개키 해시 | 중간 (인증서 갱신해도 유지) | 높음 |
| CA 피닝 | 발급 CA | 높음 | 낮음 |
HTTP Public Key Pinning (HPKP) - 레거시
# 서버 응답 헤더 (현재 브라우저에서 폐기됨)
Public-Key-Pins:
pin-sha256="base64_of_sha256_of_pubkey_1";
pin-sha256="base64_of_sha256_of_pubkey_2";
max-age=2592000;
includeSubDomains
Android 인증서 피닝 (OkHttp)
kotlin
// 공개키 해시 계산
// openssl s_client -connect api.example.com:443 | // openssl x509 -pubkey -noout | // openssl pkey -pubin -outform der | // openssl dgst -sha256 -binary | base64
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // 현재 키
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=" // 백업 키
)
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
iOS 인증서 피닝 (URLSession)
swift
class PinningDelegate: NSObject, URLSessionDelegate {
let pinnedHash = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverCert = challenge.protectionSpace
.serverTrust.flatMap({ $0.certificateChain.first }) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// 공개키 추출 후 SHA256 해시 비교
let publicKey = SecCertificateCopyKey(serverCert)!
let keyData = SecKeyCopyExternalRepresentation(publicKey, nil)! as Data
let hash = SHA256.hash(data: keyData).base64EncodedString()
if hash == pinnedHash {
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
주의사항
위험: 백업 핀 미등록 시 키 교체/침해 시 서비스 불가
권장:
- 최소 2개 핀 등록 (현재 키 + 백업 키)
- 키 교체 주기 계획 수립
- 모바일 앱 강제 업데이트 절차 확립
- Expect-CT 헤더 병행 사용
관련 문서