(Android/OkHttp) OkHttp SSL Pinning 구현


1. SSL Pinning이란?

기본 HTTPS는 기기가 신뢰하는 모든 CA가 발급한 인증서를 받아들입니다. 공격자가 사용자 기기에 악성 CA를 설치하거나 CA가 침해되면 중간자 공격(MITM) 이 가능해집니다.

SSL Pinning은 “이 서버는 이 공개키(인증서)일 때만 신뢰한다”고 앱에 고정해 이를 막습니다.

일반 HTTPS:  기기가 신뢰하는 CA가 서명했으면 통과
Pinning:     서버 인증서의 핀이 앱에 심어둔 값과 같아야 통과

2. 핀 값 추출

OkHttp는 인증서의 공개키 SHA-256 해시를 핀으로 사용합니다.

openssl s_client -servername api.example.com -connect api.example.com:443 </dev/null 2>/dev/null \
 | openssl x509 -pubkey -noout \
 | openssl pkey -pubin -outform der \
 | openssl dgst -sha256 -binary \
 | openssl enc -base64
# 출력 예: k7Hn...Qw=  →  "sha256/k7Hn...Qw="

인증서 전체가 아니라 공개키를 고정하므로, 같은 키로 인증서를 갱신(재발급)해도 핀이 유지됩니다.


3. CertificatePinner 적용

val pinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/k7Hn...현재서버키핀...=")
    .add("api.example.com", "sha256/Zq9P...백업키핀...=")      // 백업 핀
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(pinner)
    .build()

핀이 맞지 않으면 SSLPeerUnverifiedException이 발생하며 요청이 차단됩니다.

try {
    api.getUser()
} catch (e: SSLPeerUnverifiedException) {
    // 인증서 불일치 — 공격 가능성 또는 핀 만료. 사용자에게 안전하지 않은 연결을 알림
}

4. 가장 중요한 주의 — 백업 핀과 갱신 계획

핀을 고정하면 서버 인증서의 키가 바뀌는 순간 앱이 서버에 접속하지 못합니다. 앱 업데이트 전에는 복구할 수 없습니다.

❌ 현재 서버 키 하나만 핀 → 키 교체 시 모든 사용자의 앱이 장애
✅ 현재 키 + 백업 키(미리 준비한 다음 키)를 함께 핀
✅ 또는 서버 리프 인증서 대신 중간(Intermediate) CA 키를 핀 (갱신 영향 감소)
대상 장점 단점
리프(서버) 인증서 키 가장 엄격 갱신 시 키가 바뀌면 장애 위험
중간 CA 키 서버 인증서 갱신에 안전 해당 CA가 발급한 다른 인증서도 허용

5. Network Security Config 방식 (선언형)

코드 없이 XML로도 설정할 수 있습니다. OkHttp뿐 아니라 시스템 전체 HTTPS에 적용됩니다.

<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2027-06-01">
            <pin digest="SHA-256">k7Hn...현재서버키핀...=</pin>
            <pin digest="SHA-256">Zq9P...백업키핀...=</pin>
        </pin-set>
    </domain-config>
</network-security-config>
<application android:networkSecurityConfig="@xml/network_security_config" ... />

expiration을 지정하면 그 날짜 이후에는 핀 검증을 건너뛰어 갱신 누락 시의 전면 장애를 완화합니다.


6. 테스트와 디버그 빌드

val client = OkHttpClient.Builder().apply {
    if (!BuildConfig.DEBUG) certificatePinner(pinner)
}.build()

7. SSL Pinning의 한계


8. 정리



Related Posts