SSL Pinning은 애플리케이션이 CA 신뢰 체인에 의존하지 않고 사전에 알려진 지문 또는 인증서를 기준으로 서버 인증서를 검증하는 보안 기술입니다. 표준 검증과 달리, 핀닝은 위조된 루트 인증 기관을 통한 트래픽 가로채기를 방지합니다. OWASP Mobile Security Testing Guide (2025)에 따르면, 이 기술은 MITM 공격으로부터 보호하기 위한 상위 3개 권장 제어 항목에 포함됩니다. 핀닝이 없으면 위조된 루트 인증서를 가진 공격자가 애플리케이션의 모든 HTTPS 트래픽을 복호화할 수 있습니다.
주요 내용
SSL Pinning은 모바일 또는 웹 애플리케이션이 신뢰할 수 있는 서버 인증서 또는 공개 키를 기억하고, 인증서가 저장된 것과 일치하지 않는 모든 연결을 거부하는 보안 메커니즘입니다. 표준 HTTPS 체계에서 클라이언트는 루트 CA까지의 신뢰 체인을 통해 인증서를 검증합니다 — 모든 CA가 모든 도메인에 대한 인증서에 서명할 수 있습니다. SSL Pinning은 이 약점을 제거합니다: 수백 개의 CA를 신뢰하는 대신, 애플리케이션은 특정 인증서 하나만 신뢰합니다.
표준 검증의 문제점은 수백 개의 루트 CA 중 하나가 실수로 또는 강제로 귀하의 도메인에 대한 유효한 인증서를 발급할 수 있다는 것입니다. 자체 루트 인증서를 가진 기업 프록시에 접근할 수 있는 공격자는 브라우저 경고 없이 MITM 공격을 수행할 수 있습니다. SSL Pinning은 이 취약점을 차단합니다: CA가 위조 인증서를 발급하더라도, 지문이 기록된 것과 일치하지 않기 때문에 애플리케이션이 이를 거부합니다.
모바일 애플리케이션에서 SSL Pinning은 특히 중요한데, 장치가 공용 Wi-Fi, 트래픽 검사가 있는 기업 프록시, 감염된 액세스 포인트 등 안전하지 않은 네트워크에서 자주 작동하기 때문입니다. Verizon Mobile Security Index (2025)에 따르면, 모바일 애플리케이션의 데이터 유출 60% 이상이 전송 계층에서의 트래픽 가로채기와 관련되어 있습니다.
모바일 애플리케이션은 인증 토큰, 결제 정보, 사용자 개인 데이터 등 민감한 데이터를 전송합니다. 추가 보호 없이, 장치에서 루트 인증서 교체(예: 기업 프로필 또는 악성 애플리케이션 설치 후)를 통해 HTTPS가 손상될 수 있습니다. SSL Pinning은 장치에 위조된 루트 CA가 설치되어 있더라도 애플리케이션이 자체 화이트리스트에 대해 인증서를 계속 검증하도록 보장합니다.
SSL Pinning 프로세스는 지문 캡처, 연결 시 검증, 오류 처리의 세 단계로 구성됩니다. 개발 중에 엔지니어는 서버 인증서의 SHA-256 지문을 얻고(openssl x509 -fingerprint -sha256) 이를 애플리케이션 코드 또는 구성 파일에 포함시킵니다. 각 HTTPS 요청 시, 애플리케이션은 수신된 인증서의 지문을 계산하여 저장된 것과 비교합니다 — 값이 일치하지 않으면 연결이 종료됩니다.
첫 번째 단계는 빌드 시 핀닝입니다: 개발자가 미리 서버 인증서를 알고 해당 해시를 포함시킵니다. 두 번째 단계는 첫 연결 시 핀닝(TOFU: trust on first use)입니다: 애플리케이션이 첫 번째 요청에서 인증서를 기억하고 이후 모든 요청을 검증하는 데 사용합니다. TOFU는 동적 환경에 편리하지만 첫 번째 공격에 취약합니다 — 첫 번째 연결이 이미 가로채진 경우 위조 인증서가 신뢰할 수 있는 것으로 수락됩니다.
중요한 세부 사항은 백업 핀(backup pins)입니다. 인증서에는 만료일이 있으며, 교체될 때 업데이트되지 않은 애플리케이션은 서버에 대한 연결을 잃게 됩니다. 엔지니어는 2~3개의 추가 지문을 포함합니다 — 예를 들어, 백업 인증서의 지문과 루트 CA의 지문입니다. 기본 인증서가 변경되면 애플리케이션은 백업 핀에 대해 검증하고 연결이 계속 작동합니다.
# SHA-256 인증서 지문 얻기
openssl s_client -connect example.com:443 </dev/null 2>/dev/null | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
base64
핀닝을 구현하는 두 가지 주요 접근 방식이 있습니다: 전체 인증서에 대한 바인딩(인증서 핀닝)과 공개 키에 대한 바인딩(공개 키 핀닝)입니다. 각 접근 방식에는 보안 및 유지 관리에 영향을 미치는 고유한 장점과 제한 사항이 있습니다.
| 유형 | 바인딩 대상 | 유연성 | 보안 |
|---|---|---|---|
| Certificate Pinning | 전체 X.509 인증서 | 낮음 — 인증서 변경 시 업데이트 필요 | 높음 — 정확한 바인딩 |
| Public Key Pinning | 인증서의 공개 키 | 중간 — 키가 새 인증서에 있을 수 있음 | 높음 — 인증서 세부 정보에 덜 민감 |
| Hash Pinning | 인증서 또는 키의 SHA-256 해시 | 높음 — 키 변경 없이 인증서 변경 가능 | 중간 — 해시 강도에 의존 |
인증서 핀닝은 가장 엄격한 방법입니다. 애플리케이션은 신뢰할 수 있는 인증서 또는 해당 SHA-256 지문의 사본을 저장하고 각 HTTPS 연결 시 서버 인증서와 비교합니다. 이 방법은 최대 보안을 제공하지만 로테이션 중에 문제가 발생합니다 — 인증서는 일반적으로 1~2년 동안 유효하며, 이후 강제 애플리케이션 업데이트가 필요합니다. 통제된 업데이트 주기를 가진 중요 시스템에 권장됩니다.
공개 키 핀닝은 더 유연한 접근 방식입니다. 전체 인증서 대신, 애플리케이션은 서버의 RSA 또는 ECDSA 공개 키만 기억합니다. 회사가 동일한 키 쌍을 사용하는 경우 인증서 재발급 시 키가 변경되지 않은 상태로 유지될 수 있습니다. 이는 애플리케이션 업데이트 빈도를 줄입니다. 그러나 키가 손상된 경우 모든 클라이언트에서 연쇄 교체가 필요합니다.
Apple 플랫폼에서 SSL Pinning은 URLSession 델리게이트를 통해 구현됩니다. 개발자는 URLSessionDelegate 프로토콜을 구현하는 클래스를 만들고 didReceive challenge 메서드를 재정의하여 저장된 지문에 대해 수동으로 서버 인증서를 검증합니다. 대안적인 접근 방식은 구성을 단순화하는 ServerTrustManager와 함께 Alamofire를 사용하는 것입니다.
class SSLPinningDelegate: NSObject, URLSessionDelegate {
let pinnedHash = "sha256/Wi24BE7j5qLk0iLvPq6ePEsVRqZ1yW0F6wLg="
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust
else { return completionHandler(.cancelAuthenticationChallenge, nil) }
if validate(serverTrust, pinnedHash) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
예제에서 델리게이트는 URLSession으로부터 인증 요청을 받고, challenge에서 serverTrust를 추출하여 인증서의 SHA-256 지문을 저장된 것과 비교합니다. 지문이 일치하면 연결이 계속되고, 그렇지 않으면 challenge가 거부됩니다. 프로덕션의 경우 여러 백업 핀의 검증과 모니터링을 위한 오류 로깅을 추가하는 것이 좋습니다.
iOS 14부터 Apple은 Info.plist를 통해 Certificate Pinning에 대한 기본 지원을 추가했습니다. 개발자는 NSAppTransportSecurity 키에 NSPinnedDomains 하위 사전과 함께 신뢰할 수 있는 인증서를 지정합니다. 이 접근 방식은 코드 작성이 필요하지 않지만 유연성이 떨어집니다 — 핀을 동적으로 변경하거나 검증 오류를 기록할 수 없습니다.
Android에서 SSL Pinning을 구현하는 세 가지 주요 방법이 있습니다: OkHttp 라이브러리의 CertificatePinner를 통해, XML의 Network Security Config를 통해, HttpsURLConnection의 사용자 지정 검증을 통해입니다. OkHttp는 가장 인기 있고 권장되는 접근 방식으로, Retrofit 및 다른 HTTP 클라이언트에서 사용됩니다.
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com",
"sha256/Wi24BE7j5qLk0iLvPq6ePEsVRqZ1yW0F6wLg=")
.add("api.example.com",
"sha256/FiPq6ePEsVRqZ1yW0F6wLgWi24BE7j5qLk0iL=") // 백업 핀
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
OkHttp 구성에서 개발자는 도메인과 하나 이상의 SHA-256 지문을 지정합니다. 첫 번째 지문에서 OkHttp는 서버 인증서를 지정된 핀과 비교합니다. 일치하지 않으면 클라이언트는 SSLPeerUnverifiedException을 발생시킵니다. 백업 핀은 필수입니다 — 이것이 없으면 인증서가 변경될 때 API 요청이 즉시 실패하기 시작합니다.
Android는 API 24부터 XML 구성을 통해 선언적 Certificate Pinning을 지원합니다. 파일 res/xml/network_security_config.xml에는 도메인 및 해당 지문 목록이 포함됩니다. 이 방법은 정적 구성에 편리하지만 이상 현상 로깅과 함께 TOFU 또는 사용자 지정 검증 로직을 구현할 수 없습니다.
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2027-12-31">
<pin digest="SHA-256">
Wi24BE7j5qLk0iLvPq6ePEsVRqZ1yW0F6wLg=</pin>
<pin digest="SHA-256">
FiPq6ePEsVRqZ1yW0F6wLgWi24BE7j5qLk0iL=</pin>
</pin-set>
</domain-config>
</network-security-config>
SSL Pinning은 모바일 애플리케이션의 보안을 크게 향상시키지만 운영상의 복잡성을 초래합니다. 주요 장점은 루트 CA가 손상된 경우에도 MITM 공격으로부터 보호하는 것입니다. 애플리케이션은 공개 인증 기관의 전체 인프라가 아닌 개발자가 명시적으로 지정한 인증서만 신뢰합니다. 이는 금융 애플리케이션, 메신저 및 민감한 데이터를 다루는 애플리케이션에 특히 중요합니다.
주요 단점은 인증서 로테이션의 복잡성입니다. 인증서가 만료되거나 취소되면, 애플리케이션을 업데이트하지 않은 사용자는 연결을 잃게 됩니다. 이는 백업 핀과 점진적 업데이트 메커니즘을 통해 해결됩니다: 새 애플리케이션은 이전 및 새 인증서를 모두 알고 있으며, 사용자의 전체 업데이트 후 이전 핀이 코드에서 제거됩니다. 최소 2개의 백업 핀을 포함하는 것이 좋습니다 — 하나는 현재 인증서용, 하나는 미래용입니다.
또 다른 절충점은 핀닝을 비활성화하지 않고 트래픽 디버깅(Charles Proxy, Burp Suite)에 공용 프록시를 사용할 수 없다는 것입니다. 이는 개발 중 네트워크 요청 디버깅을 복잡하게 만듭니다. 해결책은 조건부 컴파일입니다: 디버그 빌드에서는 핀닝이 비활성화되고, 릴리스 빌드에서는 활성화됩니다. OWASP는 전환에 BuildConfig.DEBUG 플래그 사용을 권장합니다.
| 측면 | 장점 | 단점 |
|---|---|---|
| 보안 | 위조 CA를 통한 MITM으로부터 보호 | 키 손상 시 복잡성 |
| 유지 관리 | 명시적 신뢰 제어 | 로테이션에 애플리케이션 업데이트 필요 |
| 디버깅 | 올바른 서버에 대한 연결 보장 | 디버깅 프록시 차단 |
자주 묻는 질문
표준 HTTPS 검증은 알려진 루트 CA가 서명한 모든 인증서를 신뢰합니다. SSL Pinning은 특정 인증서 또는 키만 신뢰합니다 — CA가 위조 인증서를 발급하면 애플리케이션이 이를 거부합니다.
인증서는 일반적으로 1~2년 동안 유효합니다. 현재 인증서 만료 3~6개월 전에 핀을 업데이트하고, 새 지문을 백업 핀으로 추가하고, 로테이션 후 이전 것을 제거하는 것이 좋습니다.
네, 하지만 CDN이 에지 서버 간 전환 시 인증서를 변경할 수 있다는 점을 고려해야 합니다. 특정 인증서 대신 공개 키에 고정하고 여러 백업 핀을 사용하는 것이 좋습니다.
연결이 오류와 함께 종료됩니다 — Android에서는 SSLPeerUnverifiedException, iOS에서는 challenge가 .cancelAuthenticationChallenge로 거부됩니다. 애플리케이션은 이 오류를 적절히 처리하고 사용자에게 알려야 합니다.
아니요, 하지만 OWASP는 민감한 데이터를 다루는 애플리케이션(뱅킹, 의료, 기업 시스템)에 이를 권장합니다. 단순한 읽기 전용 애플리케이션의 경우 EV 인증서를 사용한 표준 HTTPS 검증으로 일반적으로 충분합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.