presentation
presentation

[번역] SSL Pinning

September 05, 2019 • ☕️ 4 min read
study

원글: Explain SSL Pinning with simple codes - Zhang QiChuan

HTTPS 연결에는 두 가지 요소가 있는데, 핸드셰이킹 시 서버가 제시하는 유효한 증명서(certificate)와 데이터 전달 시 데이터 암호화에 사용되는 암호화 스위트(Cipher suite)이다. 증명서는 반드시 필요한 요소이고, 이는 서버 스스로를 증명하는 역할을 한다. 유효한 증명서란 Certificate Authorities(CA) 중 하나에 의해 서명된 경우를 말하며, 클라이언트는 서버가 유효한 증명서를 제시할 때만 이를 신뢰하고 통신을 할 것이다. (CA는 클라이언트 쪽에 미리 설치된다)

공격자는 이런 방식을 악용할 수 있는데, 클라이언트 쪽에 악의적인 CA 증명서를 심어, 공격자가 서명한 어떤 증명서든 신뢰하게 만들 수 있고, 심지어 CA를 무력화 할 수 있다. 그러므로 서버가 클라이언트에게 제시하는 증명서는 서버의 진위성을 판단하기에 부족할 수 있으며, 잠재적인 중간자 공격에 대한 취약성을 갖고 있다.

SSL Pinning은 클라이언트 측에서 사용하는 기법으로, SSL 핸드셰이킹 이후에도 서버의 증명서를 다시 확인하여 중간자 공격을 피할 수 있다. 개발자는 개발 시 클라이언트 측에 신뢰할 수 있는 증명서들을 저장(embed or pin)하고, 이는 이후 실제 통신 과정에서 서버가 제공하는 증명서와 비교하는 데 사용된다. 만약 이 과정에서 미리 저장된 증명서서버가 제시한 증명서가 일치하지 않는다면, 연결은 중단될 것이고, 유저의 정보가 서버로 전송되지 않을 것이다. 이런 강제로 인해 클라이언트는 원래의 서버와 통신할 수 있도록 보장된다.

그러나 SSL Pinning에 대해서 각별히 주의해야 할 경우가 있는데, 클라이언트 측에 미리 저장된 증명서가 만기되거나, 서버 측에서 새로운 증명서를 갖게 되어 클라이언트 측의 저장된 증명서와 다르게 될 경우이다. 이 때, 클라이언트는 더 이상 서버를 믿을 수 없고, 연결을 끊게 되며, 클라이언트 어플리케이션은 그냥 벽돌이 된다(고장난다). 그러므로 이런 상황을 피하기 위해서 릴리즈 이전에 클라이언트 어플리케이션에 미래의 증명서를 저장하는 것(pinning)을 권장한다.

클라이언트 어플리케이션에서 SSL Pinning을 하는 방식엔 주로 두 가지가 있다. 하나는 완전한 증명서를 저장하는 것이고 하나는 해쉬된 공개키(hashed public key)를 저장하는 것이다. 해쉬된 공개키를 저장하는 접근법은 더욱 선호되는데, 하나의 개인키(private key)가 업데이트할 증명서를 사인하는 데 사용될 수 있고, 이에 따라 새로운 증명서를 만들 때 새로운 해쉬된 공개키를 만들 필요가 없고, 앱이 벽돌이 될 위험을 낮출 수 있다.

Codes In Action

나는 www.google.com과 통신하는 간단한 안드로이드 어플리케이션을 만들고, Charles proxy를 중간자 공격 서버로 사용할 것이다. 그리고 SSL Pinning이 어떻게 중간자 공격을 막을 수 있는지 보자.

다음 코드는 교육의 목적으로 만들어졌으며, production에서 사용하라는 뜻이 아니다. 부디 okhttp 같은 유명한 라이브러리를 사용하여 더욱 성숙한 해결법을 사용하는 걸 고려해라.

  // Read the pinned certificate from local (i.e., assets folder)
  val inputStream = context.assets.open("google.crt")
  val pinnedCertificate = CertificateFactory.getInstance("X.509")
      .generateCertificate(inputStream)

  // Create a request to www.google.com
  val url = URL("https://www.google.com")
  val httpsUrlConnection = url.openConnection() as HttpsURLConnection

  // Establish the connection
  httpsUrlConnection.connect()

  // Check the certificates and see if one of the server certificates 
  // matches the pinned certificate
  if (httpsUrlConnection.serverCertificates.contains(pinnedCertificate)) {
      // Open stream
      httpsUrlConnection.inputStream
      Log.d("Pinning", "Server certificates validation successful")
  } else {
      Log.d("Pinning", "Server certificates validation failed")
      throw SSLException("Server certificates validation failed for google.com")
  }

위의 코드는 딱 보면 알 수 있다. HttpsUrlConnection로부터 받은 서버 증명서는 로컬에 pinned 증명서에 의해 체크되고, 증명서가 일치하면 connection input stream은 열리고, 아니면 SSLException이 발생할 것이다.

나는 www.google.com 증명서를 다운받기 위해 openssl command를 사용하고, 이 증명서는 assets 폴더에 저장한다. 그냥 이 코드를 실행하면 Server certificates validation successful 메시지를 로그에서 확인할 수 있다.

이제 Charles를 실행하고, Charles 루트 증명서(root certificate)를 테스트할 기기에 설치한 후, 기기의 proxy 세팅을 Charles service의 IP로 설정한다. Charles 루트 증명서가 신뢰된 CA로서 설치된 상태로, Charles는 이제 중간자가 된다. 그리고 Charles에 의해 서명된 모든 증명서는 클라이언트 시스템에 의해 기본적으로 신뢰받게 된다 (Android 7.0 버전 이상을 제외, 이에 대한 자세한 정보는 여기로). 그러나 우리는 미리 www.google.com의 증명서를 pin 해놨기 때문에, 이 경우 서버의 증명서를 확인하는 과정은 실패하게 된다.

Charles proxy를 실행한 상태로 테스트 어플리케이션을 실행하면, 예상한대로 로그에서 SSLException과 에러메시지 Server certificates validation failed for google.com를 보게 될 것이다.

Further reading

이 포스트의 목적은 SSL Pinning 기법에 대한 입문 레벨의 정보를 제공하는 것이며, 페이팔 엔지니어링 팀에서 처음 제안한 문서를 읽을 수도 있고, 자바/안드로이드 상에서의 효과적이지 않은 SSL Pinning에 의한 버그에 대해서도 배울 수 있다.

참고자료