The Android Developer Challenge is back! Submit your idea before December 2.

HTTPS 및 SSL을 사용한 보안

현재 기술적으로 전송 계층 보안(TLS)이라고 알려진 보안 소켓 레이어(SSL)는 클라이언트와 서버 간의 암호화된 통신을 위한 공통 기본 토대입니다. 애플리케이션이 SSL을 잘못 사용하면 악성 개체가 네트워크를 통해 앱 데이터를 가로챌 수 있습니다. 앱에 이러한 문제가 발생하지 않도록 하기 위해 이 도움말에서는 안전한 네트워크 프로토콜을 사용할 때 범하는 일반적인 실수를 다루며 공개키 인프라(PKI) 사용 시 우려되는 사항에 관한 해결 방법을 제시합니다.

Android 보안 개요권한 개요도 참조하세요.

개념

일반적인 SSL 사용 시나리오에서 서버는 일치하는 비공개 키 외에 공개 키를 포함하는 인증서를 사용하여 구성되어 있습니다. SSL 클라이언트와 서버 간의 핸드셰이크 중에 서버는 공개 키 암호화로 인증서에 서명하여 비공개 키를 보유했음을 증명합니다.

하지만 누구나 자체 인증서와 비공개 키를 생성할 수 있기 때문에 단순한 핸드셰이크로는 서버가 인증서의 공개 키와 일치하는 비공개 키를 알고 있다는 것만 증명할 수 있습니다. 이 문제를 해결할 수 있는 방법 중 하나는 클라이언트가 신뢰할 수 있는 하나 이상의 인증서 집합을 갖도록 하는 것입니다. 인증서가 이 집합에 없으면 서버를 신뢰해서는 안 됩니다.

이 단순한 접근방식에는 여러 가지 단점이 있습니다. 서버는 시간이 지날수록 더 강력한 키로 업그레이드되어('키 순환') 인증서의 공개 키를 새로운 키로 대체할 수 있어야 합니다. 하지만 이제는 서버 구성이 변경되었으므로 클라이언트 앱이 업데이트되어야 합니다. 이는 특히 타사 웹 서비스와 같이 앱 개발자가 서버를 제어하지 못하는 경우에 문제가 됩니다. 또한 이 접근 방식은 앱이 웹브라우저나 이메일 앱과 같은 임의의 서버와 통신해야 하는 경우에도 문제가 됩니다.

이러한 단점을 해결하기 위해 서버는 일반적으로 인증 기관(CA)이라는 잘 알려진 발급기관의 인증서를 사용하여 구성됩니다. 일반적으로 호스트 플랫폼에는 이 플랫폼이 신뢰할 수 있으며 잘 알려진 CA의 목록이 포함됩니다. 현재 Android 4.2(Jelly Bean)부터 Android에는 새로운 버전이 출시될 때마다 업데이트되는 100개 이상의 CA가 포함되어 있습니다. 서버와 마찬가지로 CA에는 인증서와 개인 키가 있습니다. 서버의 인증서를 발급할 때 CA는 자체 비공개 키를 사용하여 서버 인증서에 서명합니다. 그러면 클라이언트는 플랫폼에 알려진 CA에서 발급한 인증서가 서버에 있음을 확인할 수 있습니다.

하지만 CA를 사용할 경우 일부 문제가 해결되지만 또 다른 문제가 발생합니다. CA는 여러 서버의 인증서를 발급하기 때문에 자신이 원하는 서버와 통신하고 있는지 확인할 몇 가지 방법이 필요합니다. 이 문제를 해결하기 위해 CA에서 발급한 인증서는 gmail.com과 같은 특정 이름이나 *.google.com과 같이 와일드 카드가 적용된 호스트 집합으로 서버를 식별합니다.

다음 예는 이러한 개념을 더 구체적으로 보여줍니다. 아래 명령줄의 스니펫에서 openssl 도구의 s_client 명령어는 위키백과의 서버 인증서 정보를 봅니다. 이 명령어는 HTTPS의 기본값인 포트 443을 지정합니다. 명령어는 openssl s_client의 결과를 openssl x509에 전송하여 X.509 표준에 따라 인증서 정보 형식을 지정합니다. 구체적으로 명령어에서는 서버 이름 정보를 포함하는 주체와 CA를 식별하는 발급기관을 묻습니다.

    $ openssl s_client -connect wikipedia.org:443 | openssl x509 -noout -subject -issuer
    subject= /serialNumber=sOrr2rKpMVP70Z6E9BT5reY008SJEdYv/C=US/O=*.wikipedia.org/OU=GT03314600/OU=See www.rapidssl.com/resources/cps (c)11/OU=Domain Control Validated - RapidSSL(R)/CN=*.wikipedia.org
    issuer= /C=US/O=GeoTrust, Inc./CN=RapidSSL CA
    

*.wikipedia.org와 일치하는 서버에 대해 RapidSSL CA에서 인증서를 발급했음을 알 수 있습니다.

HTTPS 예

잘 알려진 CA에서 발급한 인증서를 가진 웹 서버가 있다고 가정해 보겠습니다. 이제 다음과 같이 간단한 코드로 안전하게 요청할 수 있습니다.

Kotlin

    val url = URL("https://wikipedia.org")
    val urlConnection: URLConnection = url.openConnection()
    val inputStream: InputStream = urlConnection.getInputStream()
    copyInputStreamToOutputStream(inputStream, System.out)
    

자바

    URL url = new URL("https://wikipedia.org");
    URLConnection urlConnection = url.openConnection();
    InputStream in = urlConnection.getInputStream();
    copyInputStreamToOutputStream(in, System.out);
    

그렇습니다. 정말로 이렇게 간단합니다. HTTP 요청을 조정하려는 경우 HttpURLConnection에 전송하면 됩니다. HttpURLConnection의 Android 문서에서 요청 및 응답 헤더 처리 방법, 콘텐츠 게시, 쿠키 관리, 프록시 사용, 응답 캐싱 등에 관한 더 많은 예를 확인할 수 있습니다. 하지만 인증서와 호스트 이름을 확인하기 위한 세부정보는 Android 프레임워크에서 이러한 API를 통해 처리해 줍니다. 가능하면 이 방법을 사용하는 것이 좋습니다. 하지만 아래와 같이 고려해야 할 다른 사항들이 있습니다.

서버 인증서 확인과 관련된 일반적인 문제

getInputStream()에서 콘텐츠를 수신하는 대신 예외가 발생한다고 가정해 봅시다.

    javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
            at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
            at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
            at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)
            at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)
            at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
            at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
            at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
            at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
            at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)
    

이 예외는 다음과 같은 여러 가지 이유로 인해 발생할 수 있습니다.

  1. 서버 인증서를 발급한 CA를 알 수 없습니다.
  2. CA에서 서버 인증서에 서명한 것이 아니라 자체 서명되었습니다.
  3. 서버 구성에 중개 CA가 누락되었습니다.

다음 섹션에서는 서버 연결을 안전하게 유지하면서 이러한 문제를 해결하는 방법에 관해 논의합니다.

알 수 없는 인증 기관

이 경우에는 시스템에서 신뢰하지 않는 CA를 보유했기 때문에 SSLHandshakeException이 발생합니다. Android에서 아직 신뢰하지 않는 새로운 CA의 인증서를 보유하고 있거나 앱이 CA 없이 오래된 버전을 실행하여 발생할 수 있습니다. 또한 CA가 알려지지 않은 이유는 공개 CA가 아니라 정부, 기업, 교육 기관과 같은 조직에서 자체적으로 사용하기 위해 발급한 비공개 CA이기 때문인 경우가 많습니다.

다행히 특정 CA 집합을 신뢰하도록 HttpsURLConnection에 알릴 수 있습니다. 절차가 다소 복잡할 수 있기 때문에 아래에 InputStream에서 특정 CA를 선택하고 사용하여, KeyStore를 만든 후 다시 KeyStore를 사용하여, TrustManager를 만들고 초기화하는 예를 마련했습니다. 시스템은 하나 이상의 CA가 포함된 KeyStore에서 TrustManager를 생성하고 이를 사용하여 서버 인증서의 유효성을 검사합니다. 이러한 CA는 TrustManager에서 신뢰하는 유일한 CA가 됩니다.

TrustManager가 주어지면 예에서는 새 SSLContext를 초기화하며, 이는 HttpsURLConnection에서 기본 SSLSocketFactory를 재정의하는 데 사용할 수 있는 SSLSocketFactory를 제공합니다. 이렇게 하면 연결 시 인증서 확인을 위해 CA를 사용합니다.

다음은 워싱턴 대학의 조직 CA를 완전히 사용한 예입니다.

Kotlin

    // Load CAs from an InputStream
    // (could be from a resource or ByteArrayInputStream or ...)
    val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
    // From https://www.washington.edu/itconnect/security/ca/load-der.crt
    val caInput: InputStream = BufferedInputStream(FileInputStream("load-der.crt"))
    val ca: X509Certificate = caInput.use {
        cf.generateCertificate(it) as X509Certificate
    }
    System.out.println("ca=" + ca.subjectDN)

    // Create a KeyStore containing our trusted CAs
    val keyStoreType = KeyStore.getDefaultType()
    val keyStore = KeyStore.getInstance(keyStoreType).apply {
        load(null, null)
        setCertificateEntry("ca", ca)
    }

    // Create a TrustManager that trusts the CAs inputStream our KeyStore
    val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm()
    val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
        init(keyStore)
    }

    // Create an SSLContext that uses our TrustManager
    val context: SSLContext = SSLContext.getInstance("TLS").apply {
        init(null, tmf.trustManagers, null)
    }

    // Tell the URLConnection to use a SocketFactory from our SSLContext
    val url = URL("https://certs.cac.washington.edu/CAtest/")
    val urlConnection = url.openConnection() as HttpsURLConnection
    urlConnection.sslSocketFactory = context.socketFactory
    val inputStream: InputStream = urlConnection.inputStream
    copyInputStreamToOutputStream(inputStream, System.out)
    

자바

    // Load CAs from an InputStream
    // (could be from a resource or ByteArrayInputStream or ...)
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    // From https://www.washington.edu/itconnect/security/ca/load-der.crt
    InputStream caInput = new BufferedInputStream(new FileInputStream("load-der.crt"));
    Certificate ca;
    try {
        ca = cf.generateCertificate(caInput);
        System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
    } finally {
        caInput.close();
    }

    // Create a KeyStore containing our trusted CAs
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    keyStore.setCertificateEntry("ca", ca);

    // Create a TrustManager that trusts the CAs in our KeyStore
    String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
    tmf.init(keyStore);

    // Create an SSLContext that uses our TrustManager
    SSLContext context = SSLContext.getInstance("TLS");
    context.init(null, tmf.getTrustManagers(), null);

    // Tell the URLConnection to use a SocketFactory from our SSLContext
    URL url = new URL("https://certs.cac.washington.edu/CAtest/");
    HttpsURLConnection urlConnection =
        (HttpsURLConnection)url.openConnection();
    urlConnection.setSSLSocketFactory(context.getSocketFactory());
    InputStream in = urlConnection.getInputStream();
    copyInputStreamToOutputStream(in, System.out);
    

개발자의 CA에 관해 알고 있는 맞춤 TrustManager를 사용하여 시스템은 신뢰받는 발급기관에서 발급된 서버 인증서임을 확인할 수 있습니다.

주의: 상당수의 웹사이트는 아무런 작업도 하지 않는 TrustManager를 설치하는 잘못된 대안을 제시합니다. 이는 통신을 암호화하지 않는 것과 마찬가지일 수 있습니다. 누구나 DNS 트릭을 사용하여 공용 Wi-Fi 핫스팟에 있는 사용자를 공격하고 내 서버인 것처럼 가장한 체 공격자의 자체 프록시를 통해 사용자 트래픽을 전송할 수 있기 때문입니다. 또한, 공격자는 비밀번호와 기타 개인 정보를 기록할 수 있습니다. 공격자가 인증서를 생성할 수 있으며 실제로 신뢰받는 소스에서 발급한 인증서인지 검증하는 TrustManager 없이 앱이 다른 누군가와 의사소통할 수 있기 때문입니다. 따라서 일시적으로라도 절대 이 방법을 따르면 안 됩니다. 앱이 항상 서버의 인증서 발급기관을 신뢰할 수 있도록 해야 합니다.

자체 서명된 서버 인증서

SSLHandshakeException의 두 번째 사례는 자체 서명된 인증서로 인해 서버가 자체 CA로 작동하게 되어 발생합니다. 이 문제는 알 수 없는 인증 기관의 문제와 유사하기 때문에 이전 섹션과 동일한 접근법을 사용할 수 있습니다.

개발자는 서버 인증서를 직접 신뢰하는 자체 TrustManager를 생성할 수 있습니다. 이렇게 하면 이전에 논의한 것처럼 앱을 인증서에 직접 입력한다는 단점이 있지만 안전하게 작업할 수는 있습니다. 하지만 자체 서명 인증서가 합리적으로 안전한 키를 보유할 수 있도록 주의해야 합니다. 2012년부터 연간 만료되는 지수 65537의 2048비트 RSA 서명이 허용되었습니다. 키 순환 시, 무엇이 적절한지 인증 기관(예: NIST)에서 권장사항을 확인해야 합니다.

누락된 중간 인증 기관

SSLHandshakeException의 세 번째 사례는 중간 CA의 누락으로 인해 발생합니다. 대부분의 공개 CA는 서버 인증서에 직접 서명하지 않습니다. 그 대신 루트 CA라는 메인 CA 인증서를 사용하여 중간 CA에 서명합니다. 이렇게 하면 루트 CA를 오프라인에 저장하여 손상될 위험을 줄일 수 있습니다. 하지만 일반적으로 Android와 같은 운영체제는 루트 CA만 신뢰하기 때문에, 중간 CA로 서명된 서버 인증서와 루트 CA를 알고 있는 인증서 인증 간의 신뢰에 약간의 차이가 있습니다. 이 문제를 해결하려면 서버에서 SSL 핸드셰이크 중에 클라이언트에게 인증서를 전송하지 않고, 신뢰하는 루트 CA에 도달하기 위해 필요한 중간 CA를 통해 서버 CA에서 인증서 체인을 전송해야 합니다.

실제로 어떻게 구현되는지 살펴보기 위해 여기에서는 openssl s_client 명령어로 표시되는 mail.google.com 인증서 체인을 보여줍니다.

    $ openssl s_client -connect mail.google.com:443
    ---
    Certificate chain
     0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/CN=mail.google.com
       i:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
     1 s:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
       i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
    ---
    

여기에서는 서버가 중간 CA인 Thawte SGC CA에서 발급한 mail.google.com의 인증서와 Android에서 신뢰하는 기본 CA인 Verisign CA에서 발급한 Thawte SGC CA의 두 번째 인증서를 전송합니다.

하지만 필요한 중간 CA를 포함하지 않도록 서버를 구성하는 것은 드문 일이 아닙니다. 예를 들어 다음은 Android 브라우저에서 오류의 원인이 될 수 있는 서버와 Android 앱 예외 사항의 예입니다.

    $ openssl s_client -connect egov.uscis.gov:443
    ---
    Certificate chain
     0 s:/C=US/ST=District Of Columbia/L=Washington/O=U.S. Department of Homeland Security/OU=United States Citizenship and Immigration Services/OU=Terms of use at www.verisign.com/rpa (c)05/CN=egov.uscis.gov
       i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)10/CN=VeriSign Class 3 International Server CA - G3
    ---
    

여기서 흥미로운 점은 대부분의 데스크톱에서 이 서버를 방문해도 전혀 알려지지 않은 CA나 자체 서명된 서버 인증서처럼 오류가 발생하지 않는다는 것입니다. 그 이유는 대부분의 데스크톱 브라우저에서 신뢰할 수 있는 중간 CA를 시간의 흐름에 따라 캐시에 저장하기 때문입니다. 브라우저가 어떤 사이트를 방문하여 중간 CA에 대해 인지하면 이후에 포함된 인증서 체인에 더 이상 중간 CA가 필요하지 않게 됩니다.

일부 사이트에서는 리소스를 공급하는 보조 웹 서버를 위해 이 작업을 의도적으로 시행하기도 합니다. 예를 들어 이러한 사이트에는 전체 인증서 체인이 있는 서버가 제공하는 기본 HTML 페이지가 있지만, 이미지, CSS, 자바스크립트 등의 리소스에는 대역폭 절약을 이유로 CA를 포함하지 않는 서버를 이용할 수 있습니다. 이러한 서버는 Android 앱에서 호출하려는 웹 서비스를 없애지 않고 제공할 수 있습니다.

이 문제는 다음 두 가지 방법으로 해결할 수 있습니다.

  • 서버 체인에 중간 CA를 포함하도록 서버를 구성합니다. 대부분 CA는 일반적인 웹 서버에서 이러한 구성 방식을 설명하는 도움말을 제공합니다. 이 방법은 Android 4.2 이상의 기본 Android 브라우저로 사이트에서 작업해야 할 때 유일한 해결방법입니다.
  • 또는 중간 CA를 다른 알 수 없는 CA로 취급하여 직접 신뢰할 수 있도록 TrustManager를 생성합니다. 앞의 두 섹션에서 작업한 방법과 같습니다.

호스트 이름 확인과 관련된 일반적인 문제

이 문서의 첫 부분에서 설명한 것처럼 SSL 연결을 확인하기 위한 두 가지 주요 절차가 있습니다. 첫 번째는 이전 섹션에서 중점적으로 다룬 것처럼 신뢰할 수 있는 소스에서 발급된 인증서임을 확인하는 것입니다. 이 섹션에서 중점적으로 다룰 내용은 두 번째 절차로, 현재 통신하고 있는 서버가 올바른 인증서를 제시하는 것입니다. 제시하지 않으면 다음과 같은 오류가 표시됩니다.

    java.io.IOException: Hostname 'example.com' was not verified
            at libcore.net.http.HttpConnection.verifySecureSocketHostname(HttpConnection.java:223)
            at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:446)
            at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
            at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
            at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
            at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
            at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)
    

그 한 가지 이유는 서버 구성 오류 때문입니다. 서버는 개발자가 연결하려는 서버와 일치하는 주체 또는 주체의 대체 이름 필드가 없는 인증서로 구성되어 있습니다. 한 개의 인증서가 서로 다른 여러 개의 서버에서 사용될 수 있습니다. 예를 들어 openssl s_client -connect google.com:443 | openssl x509 -text가 있는 google.com 인증서를 보면 *.google.com을 지원하는 주체와 *.youtube.com, *.android.com 등에 관한 주체의 대체 이름을 확인할 수 있습니다. 연결하려는 서버 이름이 허용되는 서버로 인증서에 표시되지 않으면 오류가 발생합니다.

이 오류는 다른 이유로 가상 호스팅에서도 발생할 수 있습니다. 두 개 이상의 호스트 이름에 대해 서버를 HTTP와 공유하는 경우 웹 서버는 클라이언트에서 찾고 있는 대상 호스트 이름을 HTTP/1.1 요청으로 알 수 있습니다. 이 방식은 인증서가 HTTP 요청을 확인하기 전에 어떤 인증서가 반환되는지 알아야 하므로 HTTPS에서는 매우 복잡해집니다. 이 문제를 해결하려면 최신 버전의 SSL(특히 TLSv.1.0 이상)이 SNI(Server Name Indication)를 지원하여 SSL 클라이언트가 의도한 호스트 이름을 서버에 지정하여 적절한 인증서가 반환될 수 있도록 허용해야 합니다.

Android 2.3 이후부터는 HttpsURLConnection에서 SNI를 지원하지만, Android 2.2 이하를 지원해야 하는 경우 반환할 서버 인증서가 명확하도록 고유 포트에 대체 가상 호스트를 설정하는 방법을 사용할 수 있습니다.

더 과감하게 가상 호스트의 호스트 이름이 아니라 기본적으로 서버에서 반환하는 호스트 이름을 사용하는 인증자로 HostnameVerifier를 교체하는 대안도 있습니다.

주의: 다른 가상 호스트를 제어할 수 없는 경우 나도 모르게 중간자 공격이 트래픽을 또 다른 서버로 보낼 수 있기 때문에 HostnameVerifier를 교체하는 것은 매우 위험합니다.

그래도 호스트 이름 확인을 재정의하려면 다음 예를 참조하세요. 이 예에서는 단일 URLConnection의 인증자를 호스트 이름이 최소한 앱에서 예상한 위치에 있음을 확인하는 인증자로 교체합니다.

Kotlin

    // Create an HostnameVerifier that hardwires the expected hostname.
    // Note that is different than the URL's hostname:
    // example.com versus example.org
    val hostnameVerifier = HostnameVerifier { _, session ->
        HttpsURLConnection.getDefaultHostnameVerifier().run {
            verify("example.com", session)
        }
    }

    // Tell the URLConnection to use our HostnameVerifier
    val url = URL("https://example.org/")
    val urlConnection = url.openConnection() as HttpsURLConnection
    urlConnection.hostnameVerifier = hostnameVerifier
    val inputStream: InputStream = urlConnection.inputStream
    copyInputStreamToOutputStream(inputStream, System.out)
    

자바

    // Create an HostnameVerifier that hardwires the expected hostname.
    // Note that is different than the URL's hostname:
    // example.com versus example.org
    HostnameVerifier hostnameVerifier = new HostnameVerifier() {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            HostnameVerifier hv =
                HttpsURLConnection.getDefaultHostnameVerifier();
            return hv.verify("example.com", session);
        }
    };

    // Tell the URLConnection to use our HostnameVerifier
    URL url = new URL("https://example.org/");
    HttpsURLConnection urlConnection =
        (HttpsURLConnection)url.openConnection();
    urlConnection.setHostnameVerifier(hostnameVerifier);
    InputStream in = urlConnection.getInputStream();
    copyInputStreamToOutputStream(in, System.out);
    

가상 호스팅으로 인해 호스트 이름 확인을 교체하려는 경우 다른 가상 호스트를 제어할 수 없다면 매우 위험하다는 것을 명심하시기 바랍니다. 이러한 문제를 피할 수 있는 호스팅 정렬 대안을 찾아야 합니다.

SSLSocket 직접 사용에 대한 경고

지금까지 예에서는 HttpsURLConnection을 사용하는 HTTPS에 초점을 맞추었습니다. 때때로 앱은 HTTP와 별개의 SSL을 사용해야 합니다. 예를 들어 이메일 앱은 SMTP, POP3, IMAP와 같은 SSL 변형을 사용합니다. 이러한 경우 앱은 SSLSocket을 직접 사용하려고 하며, 이는 HttpsURLConnection이 내부적으로 사용하는 방식과 상당히 유사합니다.

지금까지 설명한 기술은 SSLSocket에도 적용되는 인증서 확인 문제를 다루고 있습니다. 실제로 맞춤 TrustManager를 사용할 때 HttpsURLConnection에 전달되는 것은 SSLSocketFactory입니다. 따라서 맞춤 TrustManagerSSLSocket과 함께 사용해야 하는 경우 동일한 단계에 따라 SSLSocket을 생성할 수 있는 SSLSocketFactory를 사용하시기 바랍니다.

주의: SSLSocket은 호스트 이름을 확인하지 않습니다. 자체 호스트 이름 확인은 앱에 달려있으며, 이를 위해 선호되는 방법은 예상되는 호스트 이름으로 getDefaultHostnameVerifier()를 호출하는 것입니다. 또한 HostnameVerifier.verify()에서는 오류 시 예외가 발생하지 않으며 대신 개발자가 명시적으로 확인해야 하는 부울 결과를 반환합니다.

다음 예에서는 이를 작업하는 방법을 보여줍니다. SNI 지원 없이 gmail.com 포트 443에 연결하면 mail.google.com의 인증서를 받게 됩니다. 이 사례에서 예상되는 상황이므로 실제로 mail.google.com의 인증서인지 확인하시기 바랍니다.

Kotlin

    // Open SSLSocket directly to gmail.com
    val socket: SSLSocket = SSLSocketFactory.getDefault().run {
        createSocket("gmail.com", 443) as SSLSocket
    }
    val session = socket.session

    // Verify that the certicate hostname is for mail.google.com
    // This is due to lack of SNI support in the current SSLSocket.
    HttpsURLConnection.getDefaultHostnameVerifier().run {
        if (!verify("mail.google.com", session)) {
            throw SSLHandshakeException("Expected mail.google.com, found ${session.peerPrincipal} ")
        }
    }

    // At this point SSLSocket performed certificate verification and
    // we have performed hostname verification, so it is safe to proceed.

    // ... use socket ...
    socket.close()
    

자바

    // Open SSLSocket directly to gmail.com
    SocketFactory sf = SSLSocketFactory.getDefault();
    SSLSocket socket = (SSLSocket) sf.createSocket("gmail.com", 443);
    HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
    SSLSession s = socket.getSession();

    // Verify that the certicate hostname is for mail.google.com
    // This is due to lack of SNI support in the current SSLSocket.
    if (!hv.verify("mail.google.com", s)) {
        throw new SSLHandshakeException("Expected mail.google.com, "
                                        "found " + s.getPeerPrincipal());
    }

    // At this point SSLSocket performed certificate verification and
    // we have performed hostname verification, so it is safe to proceed.

    // ... use socket ...
    socket.close();
    

블랙리스트

SSL은 CA에 의존하여 서버 및 도메인의 제대로 확인된 소유자에게만 인증서를 발급합니다. 드문 경우이지만 CA가 위조되었거나 Comodo 또는 DigiNotar의 경우 보안 규정을 어겨 서버나 도메인 소유자가 아닌 다른 사람에게 호스트 이름의 인증서를 발급할 수 있습니다.

이러한 위험을 완화하기 위해 Android에는 특정 인증서나 전체 CA를 블랙리스트에 포함할 수 있는 기능이 있습니다. 지금까지는 이 목록이 운영체제에 내장되어 있었지만, Android 4.2부터는 향후의 손상에 대처하기 위해 이 목록을 원격으로 업데이트할 수 있습니다.

고정

앱은 '고정'으로 알려진 기술을 사용하여 허위로 발급된 인증서를 강력하게 차단할 수 있습니다. 이 기술에서는 위의 '알려지지 않은 CA' 사례에 제공된 예를 사용하여 앱의 신뢰할 수 있는 CA를 앱의 서버에서 사용되는 것으로 알려진 소수의 CA로만 제한합니다. 이렇게 하면 시스템에 있는 100개 이상의 CA 중 일부가 손상되어 안전한 앱 채널을 위반하는 결과를 방지할 수 있습니다.

클라이언트 인증서

이 도움말에서는 서버와의 통신을 보호하기 위한 SSL 사용자에 초점을 맞추었습니다. 또한 SSL은 서버가 클라이언트의 ID를 확인하도록 허용하는 클라이언트 인증서 개념을 지원합니다. 이 도움말의 범위를 벗어나기는 하지만 관련된 기술은 맞춤 TrustManager 지정과 유사합니다. 맞춤 KeyManager 생성에 관한 설명은 HttpsURLConnection 문서를 참조하세요.

Nogotofail: 네트워크 트래픽 보안 테스트 도구

Nogotofail은 알려진 TLS/SSL 취약점과 구성 오류로부터 앱이 안전한지 쉽게 확인할 수 있는 도구입니다. 이 도구는 네트워크 트래픽이 통과할 것으로 예상되는 모든 기기에서 네트워크 보안 문제를 테스트할 수 있을 만큼 강력하고 자동화되며 확장성이 뛰어납니다.

Nogotofail은 다음과 같은 세 가지 주요 사용 사례에 유용합니다.

  • 버그 및 취약점 찾기
  • 문제 해결 확인 및 성능 저하 감시
  • 어떤 애플리케이션과 기기에서 어떤 트래픽이 발생하는지 이해

Nogotofail은 Android, iOS, Linux, Windows, Chrome OS, OSX 등 인터넷 연결을 위해 사용하는 거의 모든 기기와 호환됩니다. Android 및 Linux에서 설정을 구성하고 알림을 받을 수 있는 간편한 클라이언트뿐 아니라 라우터, VPN 서버 또는 프록시로 배포될 수 있는 자체 공격 엔진도 있습니다.

이 도구는 Nogotofail 오픈소스 프로젝트에서 액세스할 수 있습니다.