Certificate Pinning은 서버의 인증서 또는 공개 키를 고정하는 메커니즘으로, 애플리케이션이 미리 알려진 지문을 사용하여 HTTPS 연결을 확인합니다. CA를 통한 표준 신뢰 체인과 달리, 핀닝은 손상된 인증 기관이라도 도메인에 대한 위조 인증서를 발급할 수 없도록 보장합니다. OWASP MSTG(2025)에 따르면, Certificate Pinning은 L2 보호 수준 애플리케이션을 위한 필수 통제 목록에 포함됩니다. 구현에는 코드에 인증서 해시를 저장하고 각 요청 시 확인하는 과정이 포함됩니다.
주요 포인트
Certificate Pinning은 애플리케이션이 신뢰하는 인증서의 지문을 저장하고 HTTPS 연결을 설정하기 위한 유일한 기준으로 사용하는 보안 기술입니다. 표준 TLS 모델에서 클라이언트는 서버의 인증서가 신뢰할 수 있는 루트 CA에 의해 서명되었는지 확인합니다 — 시스템에 사전 설치된 수백 개의 인증 기관 중 하나입니다. Certificate Pinning은 이 체인을 직접 확인으로 대체합니다: 인증서는 저장된 샘플과 일치하거나 예상된 공개 키를 포함해야 합니다.
표준 모델의 문제는 CA 침해 사건 — DigiNotar(2011), Comodo(2011), TrustCor(2022) — 이후 명확해졌습니다. CA가 도메인에 대해 위조 인증서를 발급하면 브라우저나 애플리케이션이 이를 유효한 것으로 수락합니다. Certificate Pinning은 이 공격을 방지합니다: 완벽하게 서명된 위조 인증서라도 그 지문이 애플리케이션에 고정된 것과 일치하지 않아 거부됩니다.
핀닝이라는 용어는 핀(pin) — ‘고정 장치’에서 유래했습니다. 개발자가 신뢰하는 인증서를 고정하면, 그로부터의 이탈은 연결을 차단합니다. Mitre CWE-295의 연구에 따르면, 부적절한 인증서 검증은 모바일 애플리케이션에서 가장 위험한 보안 오류 상위 10위 안에 계속해서 포함되며, Certificate Pinning은 이를 방지하는 직접적인 방법입니다.
원래 Certificate Pinning은 RFC 7469에서 표준화된 HPKP(HTTP Public Key Pinning) 메커니즘을 통해 브라우저에서 사용되었습니다. 개발자는 예상 키의 해시가 포함된 HTTP Public-Key-Pins 헤더를 보내고, 브라우저는 지정된 기간 동안 이를 저장했습니다. 그러나 HPKP는 위험한 것으로 판명되었습니다: 단 하나의 구성 오류로 사이트가 수개월 동안 차단될 수 있었습니다. 2018년 Chrome은 HPKP 지원을 중단했으며, 현재 표준은 클라이언트 측 구현 — 모바일 애플리케이션 또는 브라우저 확장 프로그램 내부 — 이 되었습니다.
Certificate Pinning 프로세스는 지문 계산, 연결 확인, 오류 처리의 세 가지 주요 단계로 구성됩니다. 준비 단계에서 개발자는 프로덕션 서버의 인증서 또는 공개 키의 SHA-256 해시를 얻습니다. GDPR 및 PCI DSS 준수 애플리케이션의 경우 체인의 중간 CA 지문도 고정해야 합니다.
각 HTTPS 요청 시, 애플리케이션은 TLS 인증 콜백을 가로채고 서버의 인증서를 추출하여 SHA-256 해시를 계산합니다. 이 해시는 저장된 신뢰할 수 있는 지문 목록과 비교됩니다. 일치하는 항목이 있으면 연결이 계속됩니다. 없으면 애플리케이션은 연결을 종료하고 공격자에게 구현 세부 정보를 노출하지 않고 오류를 보고해야 합니다.
fun validateCertificate(certificate: X509Certificate,
expectedHash: String): Boolean {
val digest = MessageDigest.getInstance("SHA-256")
val hash = Base64.encodeToString(
digest.digest(certificate.publicKey.getEncoded()),
Base64.DEFAULT
).trim()
return hash == expectedHash
}
이 함수는 서버로부터 X509Certificate 객체와 예상 해시를 받습니다. 먼저 인증서의 공개 키를 추출하고 SHA-256 해시를 계산한 후 Base64로 인코딩합니다. 결과는 예상 지문과 비교됩니다. 프로덕션에서는 로테이션을 지원하기 위해 2~3개의 지문 배열에 대한 검증을 추가해야 합니다.
핀닝을 구현할 때 어떤 암호화 개체를 고정할지 선택해야 합니다. Certificate Pinning은 X.509 인증서 자체 — 일련 번호, 유효 기간 및 전체 체인 — 에 바인딩됩니다. Public Key Pinning은 인증서 내의 공개 키만 고정하고 다른 필드는 무시합니다. 이 선택은 운영 비용에 큰 영향을 미칩니다.
| 기준 | Certificate Pinning | Public Key Pinning |
|---|---|---|
| 고정 대상 | 전체 X.509 인증서 | RSA/ECDSA 공개 키 |
| 로테이션 | 재발행 시마다 업데이트 필요 | 동일 키로 인증서 갱신 시 변경 없음 |
| 보안 | 최대 정밀 바인딩 | 세부 사항에 덜 민감 |
| 유연성 | 낮음 — 인증서는 1~2년마다 변경 | 높음 — 키는 5~10년 사용 가능 |
| 권장 | 업데이트가 통제된 중요 시스템용 | 대부분의 모바일 애플리케이션 및 API용 |
Public Key Pinning은 대부분의 프로젝트에서 선호되는 선택입니다. 인증서 재발행 시 서버의 공개 키는 일반적으로 변경되지 않습니다 — 회사는 단순히 이전 키를 새 인증서로 서명합니다. 즉, 키 쌍이 변경되지 않은 경우 인증서 변경 후 애플리케이션 업데이트가 필요하지 않습니다. 반면 Certificate Pinning은 개발자가 서버와 클라이언트 코드를 모두 완전히 제어하는 시나리오(예: 엄격한 업데이트 주기를 가진 엔터프라이즈 애플리케이션)에서 권장됩니다.
TOFU는 Certificate Pinning을 미리 구성하지 않고 서버에 대한 첫 연결 시 인증서를 기억하는 전략입니다. 이 접근 방식은 미리 어떤 서버에 연결할지 모르는 애플리케이션에 편리합니다. 단점은 초기 공격에 대한 취약성입니다: 첫 번째 연결이 가로채지면 위조 인증서가 신뢰할 수 있는 것으로 수락됩니다. TOFU는 SSH 연결 및 일부 P2P 프로토콜에서 사용됩니다.
두 플랫폼 모두에서 Certificate Pinning은 네트워크 스택 수준에서 TLS 연결을 가로채서 구현됩니다. iOS에서는 URLSession 델리게이트 또는 Alamofire ServerTrustManager가 사용됩니다. Android에서는 OkHttp CertificatePinner가 선호되는 방법으로, 인기 있는 HTTP 클라이언트에 내장되어 있으며 각 도메인에 대해 여러 지문을 구성할 수 있습니다.
func validate(serverTrust: SecTrust,
pinnedHash: String) -> Bool {
guard let certificates = SecTrustCopyCertificateChain(serverTrust)
as? [SecCertificate] else { return false }
for certificate in certificates {
let data = SecCertificateCopyData(certificate)
var hash = Data(repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
CC_SHA256($0.baseAddress,
CC_LONG(data.count), &hash)
}
if hash.base64EncodedString() == pinnedHash {
return true
}
}
return false
}
Swift 함수에서는 serverTrust에서 인증서 체인을 추출하고, 각 인증서에 대해 SHA-256 해시를 계산한 후 결과를 예상 값과 비교합니다. 체인의 모든 인증서를 반복하면 중간 CA 수준에서 핀닝을 구현할 수 있습니다 — 중간 인증서가 일치하면 연결이 수락됩니다. 이는 리프 인증서 로테이션 시 유연성을 제공합니다.
애플리케이션이 OkHttp를 사용하지 않는 경우, 사용자 정의 X509TrustManager를 통해 Certificate Pinning을 구현할 수 있습니다. 이 방법은 더 많은 코드가 필요하지만 검증 프로세스를 완전히 제어할 수 있습니다. TrustManager는 checkServerTrusted 메서드를 재정의하며, 개발자가 수동으로 서버 인증서를 검증하고 신뢰 여부를 결정합니다. OkHttp 라이브러리를 사용할 수 없는 특정 시나리오에서만 권장됩니다.
가장 흔한 실수는 백업 핀이 없는 것입니다. 개발자가 단일 인증서 지문만 포함하고, 그것이 만료되면 사용자가 대규모로 연결을 잃습니다. 최소 허용 구성은 두 개의 지문입니다: 현재 인증서와 백업용입니다. 이상적으로는 세 개: 현재, 백업, 그리고 폴백으로 루트 CA 지문입니다.
두 번째 실수는 코드에 핀을 일반 텍스트로 저장하는 것입니다. APK나 IPA에 접근할 수 있는 공격자는 지문을 쉽게 추출하여 교체할 수 있습니다. 해시 난독화가 권장됩니다: 문자열을 여러 부분으로 나누고 암호화된 리소스에 저장하거나 런타임에 계산합니다. Android에서는 문자열 상수 난독화가 포함된 ProGuard가 효과적입니다.
세 번째 실수는 개발 인증서 수준에서 핀닝하는 것입니다. 개발용과 프로덕션용 인증서는 일반적으로 다르지만, 개발자는 릴리스 빌드 시 핀 전환을 자주 잊어버립니다. 결과적으로 프로덕션 애플리케이션이 서버에 연결할 수 없게 됩니다. 해결책은 BuildConfig 또는 플레이버별 리소스를 통해 디버그와 릴리스에 대해 별도의 핀 구성을 사용하는 것입니다.
자주 묻는 질문
SSL Pinning은 SSL/TLS 인증서에 바인딩하는 일반 용어입니다. Certificate Pinning은 공개 키뿐만 아니라 X.509 인증서 자체를 고정하는 구체적인 구현입니다. 차이점은 바인딩 대상에 있습니다: 인증서 vs 키.
ProGuard(Android)를 통한 난독화와 함께 리소스에 해시를 저장하거나 Keychain(iOS)을 통해 암호화하여 저장하는 것이 권장됩니다. 암호화 없이 strings.xml 또는 Info.plist에 일반 텍스트로 핀을 저장하지 마세요.
서버의 인증서가 변경될 때마다. 현재 인증서 만료 3~6개월 전에 새 지문을 백업 핀으로 추가하고 로테이션 후 이전 것을 제거하는 것이 권장됩니다. 최소 하나의 백업 핀이 필수입니다.
네, 조건부 컴파일을 통해 가능합니다: 디버그 빌드에서는 핀닝이 비활성화되고 릴리스 빌드에서는 활성화됩니다. 전환에는 Android의 BuildConfig.DEBUG 또는 iOS의 #if DEBUG를 사용하세요. 사용자가 접근 가능한 런타임 플래그를 통해 절대 수행하지 마세요.
즉시 새 지문이 포함된 애플리케이션 업데이트를 출시하고 스토어에 게시하세요. 강제 업데이트 메커니즘을 사용합니다. 백업 핀에 예비 CA의 지문이 포함된 경우, 다른 인증서가 있는 다른 도메인으로 일시적으로 전환할 수 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.