HTTP1.x でデータを送信する場合、送信される内容はすべて平文であり、クライアントもサーバーも相手の身元を確認できません。既存の問題は次のとおりです。
(1) 平文による通信は盗聴される可能性がある
TCP/IP プロトコル スイートの動作メカニズムによれば、インターネットのあらゆる場所で通信内容が盗聴される危険性があります。 HTTPプロトコル自体には暗号化機能はなく、送信されるデータはすべて平文です。たとえ通信が暗号化されていても、通信内容が覗き見されてしまうのは、暗号化されていない通信と同じです。これは、通信が暗号化されている場合、メッセージ情報の意味を解読することは不可能である可能性がありますが、暗号化されたメッセージ自体は表示されることを意味します。
(2) 通信相手の本人確認を怠った場合、偽装が行われる可能性があります
HTTPプロトコル通信では、通信相手を確認する処理がないため、誰でもリクエストを開始できます。また、サーバーはリクエストを受信すれば、相手が誰であってもレスポンスを返します。したがって、通信相手が確認できない場合には、次のような危険が潜んでいます。リクエストを送信したWebサーバーが、真の意図に沿ったレスポンスを返してくるサーバーであるかどうかを判断することはできません。偽装された Web サーバーである可能性があります;
いわゆる完全性とは、情報の正確さを指します。完全性を証明できないということは、通常、情報が正確であるかどうかを判断できないことを意味します。 HTTPプロトコルでは通信メッセージの完全性を証明することができず、リクエストやレスポンスを送信してから相手が受信するまでの間は、リクエストやレスポンスの内容が改ざんされているかどうかを知る方法がありません。
たとえば、ある Web サイトからコンテンツをダウンロードする場合、クライアントがダウンロードしたファイルとサーバーに保存されているファイルが一致しているかどうかを判断することはできません。ファイルの内容が送信中に他の内容に改ざんされた可能性があります。本当に内容が変わったとしても、受け手であるクライアントはそれを知りません。このように、送信中にリクエストやレスポンスが攻撃者に傍受され、内容が改ざんされる攻撃を中間者攻撃(MITM)といいます。
(4) 安全な HTTP バージョンが持つべきいくつかの特性
上記の問題により、以下の機能を提供できるHTTPセキュリティ技術が必要となります。
(1) サーバー認証 (クライアントは、偽のサーバーではなく本物のサーバーと通信していることを知っています);
(2) クライアント認証 (サーバーは、偽のクライアントではなく本物のクライアントと通信していることを認識します);
(3) 整合性 (クライアントとサーバーのデータは変更されません);
(4) 暗号化 (クライアントとサーバー間の会話はプライベートなので、盗聴される心配はありません);
(5) 効率 (ローエンドのクライアントやサーバーで使用できるほど高速に実行されるアルゴリズム);
(6) 汎用性 (基本的にすべてのクライアントとサーバーがこれらのプロトコルをサポートします);
2. HTTPS の主要テクノロジーこのような需要を背景に、HTTPS テクノロジーが誕生しました。 HTTPS プロトコルの主な機能は基本的に TLS/SSL プロトコルに依存しており、認証、情報暗号化、完全性検証機能を提供し、HTTP のセキュリティ問題を解決できます。このセクションでは、HTTPS プロトコルのいくつかの重要な技術点に焦点を当てます。
(1) 暗号化技術
暗号化アルゴリズムは一般に 2 つのタイプに分類されます:
対称暗号化: 暗号化と復号化のキーは同じです。 DES アルゴリズムで表されます;
非対称暗号化: 暗号化と復号化のキーは異なります。 RSA アルゴリズムで表されます;
対称暗号化は非常に強力で、通常は解読できません。ただし、クライアントとサーバー間の各セッションが固定された同じキーで暗号化されている場合、キーを生成して安全に保存できないという大きな問題があります。復号化には大きなセキュリティリスクが伴うはずです。
非対称鍵交換アルゴリズムが登場する前、対称暗号化に関する大きな問題は、鍵を安全に生成および保管する方法がわからないことでした。非対称鍵交換プロセスは主にこの問題を解決し、鍵の生成と使用をより安全にすることを目的としています。しかし、HTTPS のパフォーマンスと速度を大幅に低下させる「犯人」でもあります。
HTTPS は、対称暗号化と非対称暗号化の両方を使用するハイブリッド暗号化メカニズムを使用しており、鍵交換プロセスでは非対称暗号化を使用し、後続の通信およびメッセージ交換段階では対称暗号化を使用します。
(2) 認証 – 公開鍵の正当性を証明する証明書
非対称暗号化の最大の問題の 1 つは、公開キー自体が本物の公開キーであることを証明できないことです。たとえば、公開鍵暗号を使用して特定のサーバーとの通信を確立する準備をする場合、受け取った公開鍵が本来期待されていたサーバーが発行した公開鍵であることをどのように証明するか。おそらく、公開キーの送信中に、実際の公開キーが攻撃者によって置き換えられた可能性があります。
公開鍵の信頼性が検証されない場合、少なくとも次の 2 つの問題が発生します。中間者攻撃と情報拒否です。
上記の問題を解決するには、デジタル証明書認証局 (CA、Certificate Authority) およびその関連機関が発行した公開鍵証明書を使用できます。
CA を使用するための具体的なプロセスは次のとおりです:
(1) サーバー運営者は、デジタル証明書認証局 (CA) に公開鍵を申請します。
(2) CA は、組織が存在するかどうか、企業が合法であるかどうか、ドメイン名の所有権を持っているかどうかなど、オンライン、オフライン、およびその他の手段を通じて申請者によって提供された情報の信頼性を検証します。(3) 情報がレビューされて合格した場合、CA は適用された公開キーにデジタル署名し、署名された公開キーを配布し、公開キーを公開キー証明書に入れてバインドします。証明書には、申請者の公開鍵、申請者の組織情報および個人情報、発行局 CA の情報、有効期間、証明書のシリアル番号などの情報が平文で含まれ、署名も含まれています。署名生成アルゴリズム: 最初にハッシュを使用します。列関数は公開平文情報の情報ダイジェストを計算し、次に CA の秘密キーを使用して情報ダイジェストを暗号化し、暗号文が署名です。
(4) クライアントは、HTTPS ハンドシェイク フェーズ中にサーバーにリクエストを送信し、サーバーに証明書ファイルを返すように要求します。
(5) クライアントは、証明書内の関連する平文情報を読み取り、同じハッシュ関数を使用して情報ダイジェストを計算し、対応する CA の公開キーを使用して署名データを復号化し、証明書の情報ダイジェストを比較します。それらが一致していれば、証明書の正当性、つまり公開キーが正当であることを確認できます。
(6) 次に、クライアントは、ドメイン名情報、有効期間、および証明書に関連するその他の情報を検証します。(7) クライアントには信頼できる CA 証明書情報 (公開鍵を含む) が組み込まれていますが、CA が信頼されていない場合は、その CA に対応する証明書が見つからず、証明書も信頼できるものであると判断されます。違法。
このプロセスでは、いくつかの点に注意してください:
(1) 証明書を申請するときに秘密キーを提供する必要はなく、秘密キーはサーバーによってのみ管理されることが保証されます。
(2) 証明書の有効性は依然として非対称暗号化アルゴリズムに依存しており、証明書は主にサーバー情報と署名を追加します。(3) 組み込み CA に対応する証明書はルート証明書と呼ばれ、発行者と利用者が同一であり、自分自身で署名したものを自己署名証明書といいます。
(4) 証明書 = 公開鍵の申請者および発行者情報の署名;
3.HTTPS プロトコルの原則
(1) HTTPSの歴史
HTTPS プロトコルの歴史:
(2) プロトコルの実装
巨視的に見ると、TLS はレコード プロトコルによって実装されます。レコード プロトコルは、トランスポート接続を介してすべての低レベル メッセージを交換する役割を担っており、暗号化用に構成できます。すべての TLS レコードは短いヘッダーで始まります。ヘッダーには、レコードの内容のタイプ (またはサブプロトコル)、プロトコルのバージョン、および長さが含まれます。次の図に示すように、メッセージ データはヘッダーの後に続きます。TLS マスター仕様では、4 つのコア サブプロトコルが定義されています:
ハンドシェイク プロトコル;
ハンドシェイクは、TLS プロトコルの最も洗練された部分です。このプロセス中に、通信当事者は接続パラメータをネゴシエートし、ID 認証を完了します。使用する機能に応じて、通常、プロセス全体で 6 ~ 10 個のメッセージの交換が必要になります。構成およびサポートされているプロトコル拡張に応じて、交換プロセスにはさまざまなバリエーションが存在する場合があります。次の 3 つのプロセスが使用されていることがよく見られます:
(1) 完全なハンドシェイク、サーバーの認証 (一方向認証、最も一般的);
このセクションでは、QQ メールボックスのログイン プロセスを例として、パケットをキャプチャすることによる一方向検証のハンドシェイク プロセスを分析します。一方向検証の完全なハンドシェイク プロセスは次のとおりです。
主に 4 つのステップに分かれます:
(1) それぞれがサポートする機能を交換し、必要な接続パラメータに同意します;
ハンドシェイク プロセスでは、ClientHello が最初のメッセージです。このメッセージは、クライアントの機能と設定をサーバーに伝えます。クライアントによってサポートされる SSL の指定されたバージョンと暗号スイートのリスト (使用される暗号化アルゴリズム、キーの長さなど) が含まれます。
2.ServerHello
ServerHello メッセージは、サーバーによって選択された接続パラメータをクライアントに送り返します。このメッセージの構造は ClientHello に似ていますが、各フィールドにオプションが 1 つだけ含まれている点が異なります。サーバーの暗号化コンポーネントの内容と圧縮方法は、受信したクライアント暗号化コンポーネントからすべてフィルターで除外されます。
3.証明書
サーバーは、公開キー証明書を含む証明書メッセージを送信します。サーバーは、送信する証明書が選択されたアルゴリズム スイートと一致していることを確認する必要があります。ただし、すべてのパッケージで認証が使用されるわけではなく、すべての認証方法で証明書が必要なわけではないため、証明書メッセージはオプションです。
4.ServerKeyExchange
ServerKeyExchange メッセージの目的は、キー交換のための追加データを運ぶことです。メッセージの内容は、ネゴシエーション アルゴリズム スイートごとに異なります。一部のシナリオでは、サーバーは何も送信する必要がなく、このようなシナリオでは ServerKeyExchange メッセージを送信する必要はありません。
5.ServerHelloDone
ServerHelloDone メッセージは、サーバーが予期されたすべてのハンドシェイク メッセージの送信を完了したことを示します。この後、サーバーはクライアントがメッセージを送信するのを待ちます。 6.ClientKeyExchange ClientKeyExchange メッセージには、キー交換のためにクライアントから提供されたすべての情報が含まれます。このメッセージはネゴシエートされた暗号スイートの影響を受け、その内容はネゴシエートされた暗号スイートによって異なります。 7.ChangeCipherSpec ChangeCipherSpec メッセージは、送信者が接続パラメータを生成するのに十分な情報を取得し、暗号化キーを生成し、暗号化モードに切り替えることを示します。クライアントとサーバーの両方は、条件が整ったときにこのメッセージを送信します。注: ChangeCipherSpec はハンドシェイク メッセージには属しておらず、メッセージが 1 つだけある別のプロトコルであり、そのサブプロトコルとして実装されています。 8.完了 Finished メッセージは、ハンドシェイクが完了したことを意味します。メッセージの内容は暗号化されるため、双方がハンドシェイク全体の整合性を検証するために必要なデータを安全に交換できます。クライアントとサーバーの両方は、条件が整ったときにこのメッセージを送信します。 (5) 双方向認証のハンドシェイクプロセス より高いセキュリティ要件が必要な一部のシナリオでは、双方向検証が必要になる場合があります。完全な双方向検証プロセスは次のとおりです: 一方向検証プロセスと比較すると、双方向検証には CertificateRequest と CertificateVerify という 2 つの追加メッセージがあることがわかります。プロセスの残りの部分はほぼ同じです。 1.証明書リクエスト 証明書リクエストは TLS によって指定されたオプション機能であり、クライアントの ID を認証するためにサーバーによって使用されます。クライアントに証明書の送信を求めるサーバーによって実装され、サーバーは ServerKeyExchange の直後に CertificateRequest メッセージを送信する必要があります。 メッセージの構造は次のとおりです: 識別名で表される、受け入れる認証局のリストを送信することを選択できます。 2.証明書検証 クライアント認証が必要な場合、クライアントは CertificateVerify メッセージを送信して、クライアント証明書の秘密キーを実際に所有していることを証明します。このメッセージは、クライアント証明書に署名機能がある場合にのみ送信されます。 CertificateVerify は ClientKeyExchange の直後に続く必要があります。メッセージの構造は次のとおりです: (6) アプリケーションデータプロトコル アプリケーション データ プロトコルはアプリケーション メッセージを伝送します。TLS の観点から見ると、これらはデータ バッファーです。レコード層は、現在の接続セキュリティ パラメーターを使用して、これらのメッセージをパッケージ化し、デフラグし、暗号化します。下図に示すように、送信データが暗号化されていることがわかります。 (7) アラートプロトコル アラームの目的は、単純な通知メカニズムを通じて異常な通信状態をピアに通知することです。通常、これは close_notify 例外を運びます。これは、接続が閉じられたときにエラーを報告するために使用されます。アラートは非常にシンプルで、フィールドは (1) 服务器证书验证错误 这是最常见的一种问题,通常会抛出如下类型的异常: 出现此类错误通常可能由以下的三种原因导致: 当服务器的CA不被系统信任时,就会发生 SSLHandshakeException。可能是购买的CA证书比较新,Android系统还未信任,也可能是服务器使用的是自签名证书(这个在测试阶段经常遇到)。 解决此类问题常见的做法是:指定HttpsURLConnection信任特定的CA集合。在本文的第5部分代码实现模块,会详细的讲解如何让Android应用信任自签名证书集合或者跳过证书校验的环节。 (2) 域名验证失败 SSL连接有两个关键环节。首先是验证证书是否来自值得信任的来源,其次确保正在通信的服务器提供正确的证书。如果没有提供,通常会看到类似于下面的错误: 出现此类问题的原因通常是由于服务器证书中配置的域名和客户端请求的域名不一致所导致的。 有两种解决方案: (1) 重新生成服务器的证书,用真实的域名信息; (2) 自定义HostnameVerifier,在握手期间,如果URL的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。可以通过自定义HostnameVerifier实现一个白名单的功能。 代码如下: (3) 客户端证书验证 SSL支持服务端通过验证客户端的证书来确认客户端的身份。这种技术与TrustManager的特性相似。本文将在第5部分代码实现模块,讲解如何让Android应用支持客户端证书验证的方式。 (4) Android上TLS版本兼容问题 之前在接口联调的过程中,测试那边反馈过一个问题是在Android 4.4以下的系统出现HTTPS请求不成功而在4.4以上的系统上却正常的问题。相应的错误如下: 按照官方文档的描述,Android系统对SSL协议的版本支持如下: 也就是说,按官方的文档显示,在API 16+以上,TLS1.1和TLS1.2是默认开启的。但是实际上在API 20+以上才默认开启,4.4以下的版本是无法使用TLS1.1和TLS 1.2的,这也是Android系统的一个bug。 参照stackoverflow上的一些方式,比较好的一种解决方案如下: 对于4.4以下的系统,使用自定义的TLSSocketFactory,开启对TLS1.1和TLS1.2的支持,核心代码: 本部分主要基于第四部分提出的Android应用中使用HTTPS遇到的一些常见的问题,给出一个比较系统的解决方案。 (1) 整体结构 不管是使用自签名证书,还是采取客户端身份验证,核心都是创建一个自己的KeyStore,然后使用这个KeyStore创建一个自定义的SSLContext。整体类图如下: 类图中的MySSLContext可以应用在HttpURLConnection的方式与服务端连接的过程中: 核心是通过httpsURLConnection.setSSLSocketFactory使用自定义的校验逻辑。整体设计上使用策略模式决定采用哪种验证机制: (2) 单向验证并自定义信任的证书集合 在App中,把服务端证书放到资源文件下(通常是asset目录下,因为证书对于每一个用户来说都是相同的,并且也不会经常发生改变),但是也可以放在设备的外部存储上。 serverCertificateNames中定义了App所信任的证书名称(这些证书文件必须要放在指定的文件路径下,并其要保证名称相同),而后就可以加载服务端证书链到keystore,通过获取到的可信任并带有服务端证书的keystore,就可以用它来初始化自定义的SSLContext了: (3) 跳过证书校验过程 和上面的过程类似,只不过这里提供的TrustManager不需要提供信任的证书集合,默认接受任意客户端证书即可: 而后构造相应的SSLContext:
HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// 设置接受的域名集合
if (hostname.equals(...)) {
return true;
}
}
};
HttpsURLConnection.setDefaultHostnameVerifier(DO_NOT_VERIFY);
03-09 09:21:38.427: W/System.err(2496): javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0xb7fa0620: Failure in SSL library, usually a protocol error
03-09 09:21:38.427: W/System.err(2496): error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:741 0xa90e6990:0x00000000)
SSLSocketFactory noSSLv3Factory;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
noSSLv3Factory = new TLSSocketFactory(mSSLContext.getSSLSocket().getSocketFactory());
} else {
noSSLv3Factory = mSSLContext.getSSLSocket().getSocketFactory();
}
public class TLSSocketFactory extends SSLSocketFactory {
private SSLSocketFactory internalSSLSocketFactory;
public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, null, null);
internalSSLSocketFactory = context.getSocketFactory();
}
public TLSSocketFactory(SSLSocketFactory delegate) throws KeyManagementException, NoSuchAlgorithmException {
internalSSLSocketFactory = delegate;
}
......
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
}
// 开启对TLS1.1和TLS1.2的支持
private Socket enableTLSOnSocket(Socket socket) {
if(socket != null && (socket instanceof SSLSocket)) {
((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.1", "TLSv1.2"});
}
return socket;
}
}
if (JarConfig.__self_signed_https) {
SSLContextByTrustAll mSSLContextByTrustAll = new SSLContextByTrustAll();
MySSLContext mSSLContext = new MySSLContext(mSSLContextByTrustAll);
SSLSocketFactory noSSLv3Factory;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
noSSLv3Factory = new TLSSocketFactory(mSSLContext.getSSLSocket().getSocketFactory());
} else {
noSSLv3Factory = mSSLContext.getSSLSocket().getSocketFactory();
}
httpsURLConnection.setSSLSocketFactory(noSSLv3Factory);
httpsURLConnection.setHostnameVerifier(MY_DOMAIN_VERIFY);
}else {
httpsURLConnection.setSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault());
httpsURLConnection.setHostnameVerifier(DO_NOT_VERIFY);
}
public class SSLContextWithServer implements GetSSLSocket {
// 在这里进行服务器正式的名称的配置
private String[] serverCertificateNames = {"serverCertificateNames1" ,"serverCertificateNames2"};
@Override
public SSLContext getSSLSocket() {
String[] caCertString = new String[serverCertificateNames.length];
for(int i = 0 ; i < serverCertificateNames.length ; i++) {
try {
caCertString[i] = readCaCert(serverCertificateNames[i]);
} catch(Exception e) {
}
}
SSLContext mSSLContext = null;
try {
mSSLContext = SSLContextFactory.getInstance().makeContextWithServer(caCertString);
} catch(Exception e) {
}
return mSSLContext;
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
originalX509TrustManager.checkServerTrusted(chain, authType);
} catch(CertificateException originalException) {
try {
X509Certificate[] reorderedChain = reorderCertificateChain(chain);
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
CertificateFactory factory = CertificateFactory.getInstance("X509");
CertPath certPath = factory.generateCertPath(Arrays.asList(reorderedChain));
PKIXParameters params = new PKIXParameters(trustStore);
params.setRevocationEnabled(false);
validator.validate(certPath, params);
} catch(Exception ex) {
throw originalException;
}
}
}
public class AcceptAllTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
//do nothing,接受任意客户端证书
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
//do nothing,接受任意服务端证书
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public SSLContext makeContextToTrustAll() throws Exception {
AcceptAllTrustManager tm = new AcceptAllTrustManager();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { tm }, null);
return sslContext;
}
以上がHTTPS の原理と Android でのその使用を分析するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。