네트워크 프로토콜을 사용한 보안

클라이언트-서버 암호화 상호작용은 전송 계층 보안(TLS)을 사용하여 앱의 데이터를 보호합니다.

이 문서에서는 보안 네트워크 프로토콜 권장사항 및 공개 키 인프라(PKI) 고려사항과 관련된 권장사항을 설명합니다. 자세한 내용은 Android 보안 개요권한 개요를 참고하세요.

개념

TLS 인증서가 있는 서버에는 공개 키 및 일치하는 비공개 키가 있습니다. 서버는 TLS 핸드셰이크 중에 공개 키 암호화를 사용하여 인증서에 서명합니다.

단순한 핸드셰이크는 서버가 인증서의 비공개 키를 알고 있다는 것만 증명합니다. 이 문제를 해결하려면 클라이언트가 여러 인증서를 신뢰하도록 하세요. 서버의 인증서가 클라이언트 측의 신뢰할 수 있는 인증서 세트에 나타나지 않으면 서버를 신뢰할 수 없습니다.

하지만 서버에서 키 순환을 사용하여 인증서의 공개 키를 새 키로 변경할 수도 있습니다. 서버 구성을 변경하려면 클라이언트 앱을 업데이트해야 합니다. 서버가 웹브라우저나 이메일 앱과 같은 서드 파티 웹 서비스라면 클라이언트 앱을 업데이트할 시기를 파악하기가 더 어렵습니다.

서버는 일반적으로 인증 기관(CA) 인증서를 사용하여 인증서를 발급하므로 시간이 지남에 따라 클라이언트 측 구성이 더 안정적으로 유지됩니다. CA는 비공개 키를 사용하여 서버 인증서에 서명합니다. 그러면 클라이언트는 서버에 플랫폼에 알려진 CA 인증서가 있는지 확인할 수 있습니다.

신뢰할 수 있는 CA는 일반적으로 호스트 플랫폼에 나열됩니다. Android 8.0(API 수준 26)에는 각 버전에서 업데이트되고 기기 간에 변경되지 않는 100개 이상의 CA가 포함되어 있습니다.

클라이언트 앱에는 서버를 확인하는 메커니즘이 필요합니다. CA가 여러 서버에 인증서를 제공하기 때문입니다. CA의 인증서는 gmail.com과 같은 특정 이름이나 *.google.com과 같은 와일드 카드를 사용하여 서버를 식별합니다.

웹사이트의 서버 인증서 정보를 보려면 openssl 도구의 s_client 명령어를 사용하여 포트 번호를 전달하세요. 기본적으로 HTTPS는 포트 443을 사용합니다.

이 명령어는 openssl s_client 출력을 openssl x509로 전송하며 이는 X.509 표준으로 인증서 정보 형식을 지정합니다. 이 명령어는 주제(서버 이름)와 발급기관(CA)을 요청합니다.

openssl s_client -connect WEBSITE-URL:443 | \
  openssl x509 -noout -subject -issuer

HTTPS 예

잘 알려진 CA에서 발급한 인증서를 보유한 웹 서버가 있다고 가정하면 다음 코드와 같이 보안 요청을 할 수 있습니다.

Kotlin

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

Java

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

HTTP 요청을 맞춤설정하려면 HttpURLConnection으로 전송합니다. Android HttpURLConnection 문서에는 요청 및 응답 헤더 처리, 콘텐츠 게시, 쿠키 관리, 프록시 사용, 응답 캐싱 등에 관한 예가 포함되어 있습니다. Android 프레임워크는 이러한 API를 사용하여 인증서와 호스트 이름을 확인합니다.

가능하면 이러한 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가 누락되어 있습니다.

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

알 수 없는 인증 기관

SSLHandshakeException은 시스템이 CA를 신뢰하지 않기 때문에 발생합니다. 이는 Android가 신뢰하지 않는 새 CA의 인증서를 받았거나 앱이 CA가 없는 이전 버전에서 작동하기 때문일 수 있습니다. 비공개이기 때문에 CA는 거의 알 수 없습니다. 대개의 경우 CA는 알 수가 없는데 그 이유는 공개 CA가 아니라 자체 사용을 위해 정부, 회사 또는 교육 기관 같은 조직에서 발급하는 비공개 CA이기 때문입니다.

앱의 코드를 변경할 필요 없이 맞춤 CA를 신뢰하려면 네트워크 보안 구성을 변경하세요.

주의: 상당수의 웹사이트에서는 아무 작업도 하지 않는 TrustManager를 설치하는 잘못된 대안을 제시합니다. 이렇게 하면 공용 Wi-Fi 핫스팟을 사용할 때 사용자가 공격에 취약해집니다. 그 이유는 공격자가 DNS 트릭을 사용하여, 내 서버인 것처럼 가장한 프록시를 통해 사용자의 트래픽을 전송할 수 있기 때문입니다. 또한, 공격자는 비밀번호와 기타 개인 정보를 기록할 수 있습니다. 이는 공격자가 인증서를 생성할 수 있고 인증서가 신뢰할 수 있는 소스에서 발급한 것인지 검증하는 TrustManager 없이는 이러한 유형의 공격을 차단할 수 없기 때문에 작동합니다. 일시적으로라도 절대 이 방법을 따르면 안 됩니다. 대신 앱이 서버 인증서 발급기관을 신뢰하도록 합니다.

자체 서명 서버 인증서

둘째, 서버가 자체 CA가 되는 자체 서명 인증서로 인해 SSLHandshakeException이 발생할 수 있습니다. 이는 알 수 없는 인증 기관과 유사하므로 자체 서명 인증서를 신뢰하도록 애플리케이션의 네트워크 보안 구성을 수정하세요.

누락된 중간 인증 기관

셋째, SSLHandshakeException은 중간 CA의 누락으로 인해 발생합니다. 공개 CA는 서버 인증서에 거의 서명하지 않습니다. 대신 루트 CA가 중간 CA에 서명합니다.

손상 위험을 줄이기 위해 CA는 루트 CA를 오프라인으로 유지합니다. 하지만 Android와 같은 운영체제는 일반적으로 루트 CA만 직접 신뢰하므로 중간 CA가 서명한 서버 인증서와 루트 CA를 인식하는 인증서 인증 도구 사이에는 약간의 신뢰 격차가 있습니다.

이러한 신뢰 격차를 없애기 위해 서버는 TLS 핸드셰이크 중에 중간 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 LLC/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는 일반 웹 서버에서 이 작업을 실행하는 방법에 관한 안내를 제공합니다.

SSLSocket 직접 사용에 관한 경고

지금까지 예에서는 HttpsURLConnection을 사용하는 HTTPS에 초점을 맞추었습니다. 때로 앱은 HTTPS와는 별도로 TLS를 사용해야 합니다. 예를 들어 이메일 앱은 SMTP, POP3 또는 IMAP이라는 TLS 변형을 사용할 수 있습니다. 이러한 경우 앱은 HttpsURLConnection이 내부적으로 하는 것과 거의 동일한 방식으로 SSLSocket을 직접 사용할 수 있습니다.

인증서 확인 문제를 다루기 위해 지금까지 설명한 기법은 SSLSocket에도 적용됩니다. 실제로 맞춤 TrustManager를 사용할 때 HttpsURLConnection에 전달되는 것은 SSLSocketFactory입니다. 따라서 맞춤 TrustManagerSSLSocket와 함께 사용해야 하는 경우 동일한 단계를 따라 SSLSocketFactory를 사용하여 SSLSocket을 만듭니다.

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

차단된 CA

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

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

앱을 특정 인증서로 제한

주의: 앱에 유효하다고 간주되는 인증서를 이전에 승인한 인증서로 제한하는 관행인 인증서 고정은 Android 앱에 권장되지 않습니다. 향후 서버 구성 변경(예: 다른 CA로 변경)으로 인해 고정된 인증서가 있는 앱이 클라이언트 소프트웨어 업데이트를 수신하지 않고는 서버에 연결할 수 없게 됩니다.

지정한 인증서만 허용하도록 앱을 제한하려면 완전히 제어할 수 있는 하나 이상의 키를 비롯하여 여러 백업 핀과 호환성 문제를 방지하기 위한 충분히 짧은 만료 기간을 포함하는 것이 중요합니다. 네트워크 보안 구성은 이러한 기능을 통해 고정을 제공합니다.

클라이언트 인증서

이 문서에서는 TLS를 사용하여 서버와의 통신을 보호하는 방법을 중점적으로 설명합니다. TLS는 서버가 클라이언트의 ID를 검증할 수 있는 클라이언트 인증서 개념도 지원합니다. 이 문서의 범위를 벗어나기는 하지만 관련된 기법은 맞춤 TrustManager 지정과 유사합니다.

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

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

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

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

Nogotofail은 Android, iOS, Linux, Windows, ChromeOS, macOS는 물론 인터넷에 연결하는 데 사용하는 모든 기기에서 작동합니다. 클라이언트는 Android와 Linux에서 설정을 구성하고 알림을 받는 데 사용할 수 있으며, 공격 엔진 자체는 라우터, VPN 서버 또는 프록시로 배포할 수 있습니다.

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

SSL 및 TLS 업데이트

Android 10

Chrome과 같은 일부 브라우저에서는 TLS 서버가 TLS 핸드셰이크의 일부로 인증서 요청 메시지를 보낼 때 인증서를 선택할 수 있습니다. Android 10부터 KeyChain 객체는 KeyChain.choosePrivateKeyAlias()를 호출하여 사용자에게 인증서 선택 메시지를 표시할 때 발급기관과 키 사양 매개변수를 준수합니다. 특히 이 메시지에는 서버 사양을 준수하지 않는 선택사항이 포함되지 않습니다.

서버 사양과 일치하는 인증서가 없거나 기기에 인증서가 설치되지 않은 경우와 같이 사용자가 선택할 수 있는 사용 가능한 인증서가 없는 경우 인증서 선택 메시지가 아예 표시되지 않습니다.

또한 Android 10 이상에서는 키 또는 CA 인증서를 KeyChain 객체로 가져오기 위해 기기 화면 잠금이 필요하지 않습니다.

TLS 1.3 기본 사용 설정

Android 10 이상에서는 모든 TLS 연결에서 TLS 1.3이 기본적으로 사용 설정됩니다. 다음은 TLS 1.3 구현에 관한 몇 가지 중요한 세부정보입니다.

  • TLS 1.3 암호화 스위트는 맞춤설정할 수 없습니다. TLS 1.3을 사용 설정하면 지원되는 TLS 1.3 암호화 스위트가 항상 사용 설정됩니다. setEnabledCipherSuites()를 호출하여 이를 사용 중지하려는 시도는 모두 무시됩니다.
  • TLS 1.3이 협상되면 세션이 세션 캐시에 추가되기 전에 HandshakeCompletedListener 객체가 호출됩니다. TLS 1.2 및 기타 이전 버전에서는 세션이 세션 캐시에 추가된 후에 이러한 객체가 호출됩니다.
  • SSLEngine 인스턴스가 이전 버전의 Android에서 SSLHandshakeException을 발생시키는 일부 상황에서 이러한 인스턴스는 Android 10 이상에서 대신 SSLProtocolException을 발생시킵니다.
  • 0-RTT 모드는 지원되지 않습니다.

원하는 경우 SSLContext.getInstance("TLSv1.2")를 호출하여 TLS 1.3이 사용 중지된 SSLContext를 가져올 수 있습니다. 또한 적절한 객체에서 setEnabledProtocols()를 호출하여 연결별로 프로토콜 버전을 사용 설정 또는 사용 중지할 수 있습니다.

SHA-1로 서명된 인증서가 TLS에서 신뢰되지 않음

Android 10에서 SHA-1 해시 알고리즘을 사용하는 인증서는 TLS 연결에서 신뢰되지 않습니다. 루트 CA는 2016년 이후 이러한 인증서를 발급하지 않았으며 Chrome 또는 기타 주요 브라우저에서 더 이상 신뢰되지 않습니다.

SHA-1을 사용하여 인증서를 제공하는 사이트에 연결하려는 경우 연결 시도가 실패합니다.

KeyChain 동작 변경사항 및 개선사항

Chrome과 같은 일부 브라우저에서는 TLS 서버가 TLS 핸드셰이크의 일부로 인증서 요청 메시지를 보낼 때 인증서를 선택할 수 있습니다. Android 10부터 KeyChain 객체는 KeyChain.choosePrivateKeyAlias()를 호출하여 사용자에게 인증서 선택 메시지를 표시할 때 발급기관과 키 사양 매개변수를 준수합니다. 특히 이 메시지에는 서버 사양을 준수하지 않는 선택사항이 포함되지 않습니다.

서버 사양과 일치하는 인증서가 없거나 기기에 인증서가 설치되지 않은 경우와 같이 사용자가 선택할 수 있는 사용 가능한 인증서가 없는 경우 인증서 선택 메시지가 아예 표시되지 않습니다.

또한 Android 10 이상에서는 키 또는 CA 인증서를 KeyChain 객체로 가져오기 위해 기기 화면 잠금이 필요하지 않습니다.

기타 TLS 및 암호화 변경사항

Android 10에 적용되는 TLS 및 암호화 라이브러리에서 사소한 몇 가지 사항이 변경되었습니다.

  • AES/GCM/NoPadding 및 ChaCha20/Poly1305/NoPadding 암호화는 getOutputSize()에서 더욱 정확한 버퍼 크기를 반환합니다.
  • TLS_FALLBACK_SCSV 암호화 스위트는 최대 프로토콜이 TLS 1.2 이상인 연결 시도에서 생략됩니다. TLS 서버 구현이 향상되었으므로 TLS 외부 대체를 시도하지 말고, 대신 TLS 버전 협상에 의존하는 것이 좋습니다.
  • ChaCha20-Poly1305는 ChaCha20/Poly1305/NoPadding의 별칭입니다.
  • 끝에 점이 있는 호스트 이름은 유효한 SNI 호스트 이름으로 간주되지 않습니다.
  • 인증서 응답용 서명 키를 선택할 때 CertificateRequest의 supported_signature_algorithms 확장이 존중됩니다.
  • TLS에서 RSA-PSS와 함께 불투명한 서명 키(예: Android 키 저장소의 서명 키)를 사용할 수 있습니다.

HTTPS 연결 변경

Android 10을 실행하는 앱이 setSSLSocketFactory()에 null을 전달하면 IllegalArgumentException이 발생합니다. 이전 버전에서는 setSSLSocketFactory()에 null을 전달하는 것이 현재의 기본 팩토리를 전달하는 것과 동일했습니다.

Android 11

SSL 소켓은 기본적으로 Conscrypt SSL 엔진을 사용함

Android의 기본 SSLSocket 구현은 Conscrypt에 기반합니다. Android 11부터 이 구현은 Conscrypt의 SSLEngine에 기반하여 내부적으로 빌드됩니다.