https://developer.android.com/training/articles/security-ssl



지금은 기술적으로 TLS(Transport Layer Security)라고 불리는 SSL(Secure Sockets Layer)은 클라이언트와 서버 사이의 암호화된 통신을 위한 기본 구성 블록입니다. 악성 엔터티가 네트워크상의 앱 데이터를 가로챌 수도 있는 잘못된 방식으로 SSL이 애플리케이션에 사용될 가능성도 있습니다. 이러한 상황이 여러분 앱에서 발생하는 것을 막기 위해, 이 글에서는 보안 네트워크 프로토콜을 사용할 때의 일반적인 함정들을 살펴보고, PKI(Public-Key Infrastructure) 사용 시의 더 심각한 문제들을 알아봅니다.

개념

일반적인 SSL 사용 시나리오에서는, 일치하는 개인 키와 공개 키가 포함된 인증서로 서버가 구성됩니다. SSL 클라이언트와 서버 사이의 핸드셰이크의 일환으로, 서버는 공개 키 암호화로 인증서를 서명하여 개인 키가 있음을 입증합니다.

그러나 누구나 자신의 인증서와 개인 키를 생성할 수 있으므로, 단순한 핸드셰이크로는 서버에 대해 아무 것도 입증하지 못하며, 다만 인증서의 공개 키와 일치하는 개인 키가 서버에 있다는 것만 입증할 수 있습니다. 이 문제를 해결하는 한 가지 방법은 클라이언트가 하나 이상의 신뢰할 수 있는 인증서 세트를 갖도록 하는 것입니다. 이 세트 안에 인증서가 없으면, 서버는 신뢰되지 않습니다.

이 단순한 접근방식에는 여러 가지 단점이 있습니다. 서버는 시간의 흐름에 따라 더 강력한 키로 업그레이드할 수 있어야 하며("키 순환"), 이 경우 인증서의 공개 키가 새 키로 대체됩니다. 불행히도, 지금은 근본적인 서버 구성 변경으로 인해 클라이언트 앱이 업데이트되어야 합니다. 이것은 서버가 앱 개발자의 통제를 벗어나는 경우에 특히 문제가 됩니다(예를 들어, 서버가 타사 웹 서비스인 경우). 이 접근방식은 또한 앱이 웹 브라우저나 이메일 앱 등 임의의 서버와 통신해야 하는 경우에 문제가 됩니다.

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

그러나, CA를 사용할 경우 일부 문제가 해결되지만 또 다른 문제가 발생합니다. CA는 여러 서버에 대해 인증서를 발급하기 때문에, 여러분은 자신이 원하는 서버와 통신할 수 있는 방법을 보장해야 합니다. 이 문제를 해결하기 위해, CA가 발급한 인증서는 특정 이름(예: gmail.com) 또는 와일드카드가 포함된 호스트 집합(예: *.google.com)으로 서버를 식별합니다.

다음 예시는 이러한 개념을 더 구체적으로 보여줍니다. 아래 스니펫의 명령줄에서, openssl 도구의 s_client 명령은 Wikipedia의 서버 인증서 정보를 찾습니다. 이 명령은 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가 발급한 인증서가 여러분의 웹 서버에 있다고 가정하면, 다음과 같은 간단한 코드로 보안 요청을 수행할 수 있습니다.

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이 발생합니다. 그 원인은 새 CA로부터 가져온 인증서가 Android에 의해 아직 신뢰되지 않거나, 여러분의 앱이 CA가 없는 구형 버전에서 실행 중이기 때문일 수 있습니다. 대개의 경우 CA는 공개 CA가 아니기 때문에 알 수가 없으며, 개인 CA는 정부, 회사 또는 교육기관 같은 조직들이 자체적인 사용을 위해 발급하는 것입니다.

다행히도, 여러분은 특정한 CA 세트를 신뢰하도록 HttpsURLConnection에게 알릴 수 있습니다. 이 절차는 약간 복잡할 수 있으며, 아래 예시에서는 특정 CA를 InputStream에서 가져와서 이것을 사용하여 KeyStore를 생성한 다음, 다시 이것을 사용하여 TrustManager를 생성하고 초기화합니다. 시스템은 TrustManager를 사용하여 서버에서 인증서의 유효성을 검사하며, 하나 이상의 CA로 KeyStore에서 인증서를 생성합니다. 이들 인증서는 해당 TrustManager가 신뢰하는 유일한 CA가 됩니다.

새 TrustManager가 주어짐에 따라, 이 예시에서는 새 SSLContext를 초기화하며, 이는 HttpsURLConnection에서 기본 SSLSocketFactory를 재정의하는 데 사용할 수 있는 SSLSocketFactory를 제공합니다. 이런 방식으로 연결 시 인증서 유효성 검사를 위해 여러분의 CA가 사용됩니다.

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

// 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 또는 JavaScript와 같은 리소스에 대해서는 서버가 CA를 포함하지 않을 수도 있습니다. 불행히도, 이들 서버는 여러분이 자신의 Android 앱에서 호출하려고 시도 중인 웹 서비스를 제공하는 경우가 가끔씩 있지만 이는 허용되지 않습니다.

이 문제를 해결하는 두 가지 방법이 있습니다.

  • 중간 CA를 서버 체인에 포함하도록 서버를 구성합니다. 대부분의 CA는 이 작업을 모든 일반 웹 서버에서 수행하는 방법에 대한 문서를 제공합니다. 이 방법은 여러분의 사이트가 기본 Android 브라우저(최소 Android 4.2)를 사용해야 하는 경우에 유일한 방법입니다.
  • 또는, 중간 CA를 다른 알 수 없는 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를 사용하는 2개 이상의 호스트 이름에 대해 서버를 공유하는 경우, 웹 서버는 HTTP/1.1 요청으로부터 클라이언트가 찾고 있는 대상 호스트 이름을 알려줄 수 있습니다. 불행히도, HTTPS에서는 이 방법이 복잡합니다. 그 이유는 HTTP 요청을 보기 위해 어떤 인증서를 반환할지를 서버가 알아야 하기 때문입니다. 이 문제를 해결하기 위해, 더 새로운 버전의 SSL(구체적으로 TLSv.1.0 이상)에서는 SNI(Server Name Indication)를 지원합니다. 이를 통해 SSL 클라이언트는 원하는 호스트 이름을 서버에 지정할 수 있으므로, 적절한 인증서를 반환할 수 있습니다.

다행히도, HttpsURLConnection은 Android 2.3 이후로 SNI를 지원합니다. 여러분이 Android 2.2(및 이전 버전)를 지원해야 할 경우 한 가지 해결 방법은 대체 가상 호스트를 고유 포트에 설정하는 것입니다. 그러면 어떤 서버 인증서를 반환할지 명확해집니다.

더 나은 대안은 가상 호스트의 호스트 이름을 사용하지 않는 이름(그러나 기본적으로 서버에 의해 반환된 이름)으로 HostnameVerifier를 바꾸는 것입니다.

주의: 다른 가상 호스트가 여러분의 제어 하에 없는 경우 HostnameVerifier를 바꾸는 것은 매우 위험할 수 있습니다. 그 이유는 여러분도 모르게 중간자(man-in-the-middle) 공격이 트래픽을 또 다른 서버로 보낼 수 있기 때문입니다.

그래도 호스트 이름 확인을 재정의하고자 한다면, 다음은 단일 URLConnection의 확인자를 '최소한 앱에서 예상하는 호스트 이름인지 여부만이라도 확인하는' 확인자로 바꾸는 예시입니다.

// 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입니다. 따라서 SSLSocket과 함께 사용자 지정 TrustManager를 사용해야 하는 경우, 동일한 단계에 따라 해당 SSLSocketFactory를 사용하여 SSLSocket을 생성합니다.

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

다음 예시에서는 이 작업을 수행하는 방법을 보여줍니다. 이 예시에서는 SNI 지원이 없는 gmail.com 포트 443에 연결할 때, 여러분에게 mail.google.com용의 인증서가 전송됨을 보여줍니다. 이 경우 이러한 결과가 예상되므로, 이 인증서가 정말 mail.google.com용인지 확인해야 합니다.

// 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 verificaiton and
// we have performed hostname verification, so it is safe to proceed.

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

블랙리스트

SSL은 적절히 확인된 서버 및 도메인 소유자에게만 인증서를 발급하기 위해 주로 CA에 의존합니다. 아주 드문 경우로 CA가 속임을 당하거나 Comodo 또는 DigiNotar의 경우 CA가 침해되면, 서버 또는 도메인 소유자가 아닌 다른 사람에게 호스트 이름의 인증서가 발급되는 결과를 초래합니다.

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

고정

부정하게 발급된 인증서로부터 스스로를 보호하기 위해 앱이 고정(pinning)이라는 기술을 사용할 수 있습니다. 이 기술에서는 위의 '알 수 없는 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 오픈 소스 프로젝트에서 액세스할 수 있습니다.