HTTP1.x在传输数据时,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份,存在的问题如下:
其实这些问题不仅在HTTP上出现,其他未加密的协议中也会存在这类问题。
按TCP/IP协议族的工作机制,互联网上的任何角落都存在通信内容被窃听的风险。而HTTP协议本身不具备加密的功能,所传输的都是明文。即使已经经过过加密处理的通信,也会被窥视到通信内容,这点和未加密的通信是相同的。只是说如果通信经过加密,就有可能让人无法破解报文信息的含义,但加密处理后的报文信息本身还是会被看到的。
在HTTP协议通信时,由于不存在确认通信方的处理步骤,因此任何人都可以发起请求。另外,服务器只要接收到请求,不管对方是谁都会返回一个响应。因此不确认通信方,存在以下隐患:
所谓完整性是指信息的准确度。若无法证明其完整性,通常也就意味着无法判断信息是否准确。HTTP协议无法证明通信的报文完整性,在请求或响应送出之后直到对方接收之前的这段时间内,即使请求或响应的内容遭到篡改,也没有办法获悉。
比如,从某个Web网站下载内容,是无法确定客户端下载的文件和服务器上存放的文件是否前后一致的。文件内容在传输途中可能已经被篡改为其他的内容。即使内容真的已改变,作为接收方的客户端也是觉察不到的。像这样,请求或响应在传输途中,遭攻击者拦截并篡改内容的攻击称为中间人攻击(Man-in-the-Middle attack,MITM)。
由于上述的几个问题,需要一种能够提供如下功能的HTTP安全技术:
1 .服务器认证(客户端知道它们是在与真正的而不是伪造的服务器通话);
2 .客户端认证(服务器知道它们是在与真正的而不是伪造的客户端通话);
3 .完整性(客户端和服务器的数据不会被修改);
4 .加密(客户端和服务器的对话是私密的,无需担心被窃听);
5 .效率(一个运行的足够快的算法,以便低端的客户端和服务器使用);
6 .普适性(基本上所有的客户端和服务器都支持这些协议);
在这样的需求背景下,HTTPS技术诞生了。HTTPS协议的主要功能基本都依赖于TLS/SSL协议,提供了身份验证、信息加密和完整性校验的功能,可以解决HTTP存在的安全问题。本节就重点探讨一下HTTPS协议的几个关键技术点。
加密算法一般分为两种:
对称加密强度非常高,一般破解不了,但存在一个很大的问题就是无法安全地生成和保管密钥,假如客户端和服务器之间每次会话都使用固定的、相同的密钥加密和解密,肯定存在很大的安全隐患。
在非对称密钥交换算法出现以前,对称加密一个很大的问题就是不知道如何安全生成和保管密钥。非对称密钥交换过程主要就是为了解决这个问题,使密钥的生成和使用更加安全。但同时也是HTTPS性能和速度严重降低的“罪魁祸首”。
HTTPS采用对称加密和非对称加密两者并用的混合加密机制,在交换密钥环节使用非对称加密方式,之后的建立通信交换报文阶段则使用对称加密方式。
非对称加密最大的一个问题,就是无法证明公钥本身就是货真价实的公钥。比如,正准备和某台服务器建立公开密钥加密方式下的通信时,如何证明收到的公开密钥就是原本预想的那台服务器发行的公开密钥。或许在公开密钥传输途中,真正的公开密钥已经被攻击者替换掉了。
如果不验证公钥的可靠性,至少会存在如下的两个问题:中间人攻击和信息抵赖。
为了解决上述问题,可以使用由数字证书认证机构(CA,Certificate Authority)和其相关机关颁发的公开密钥证书。
CA使用具体的流程如下:
证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构CA的信息、有效时间、证书序列号等信息的明文,同时包含一个签名;
签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用CA的私钥对信息摘要进行加密,密文即签名;
在这个过程注意几点:
HTTPS协议历史简介:
宏观上,TLS以记录协议(record protocol)实现。记录协议负责在传输连接上交换所有的底层消息,并可以配置加密。每一条TLS记录以一个短标头起始。标头包含记录内容的类型(或子协议)、协议版本和长度。消息数据紧跟在标头之后,如下图所示:
TLS的主规格说明书定义了四个核心子协议:
握手是TLS协议中最精密复杂的部分。在这个过程中,通信双方协商连接参数,并且完成身份验证。根据使用的功能的不同,整个过程通常需要交换6~10条消息。根据配置和支持的协议扩展的不同,交换过程可能有许多变种。在使用中经常可以观察到以下三种流程:
本节以QQ邮箱的登录过程为例,通过抓包来对单向验证的握手流程进行分析。单向验证的一次完整的握手流程如下所示:
主要分为四个步骤:
1.ClientHello
在握手流程中,ClientHello是第一条消息。这条消息将客户端的功能和首选项传送给服务器。包含客户端支持的SSL的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)。
2.ServerHello
ServerHello消息将服务器选择的连接参数传送回客户端。这个消息的结构与ClientHello类似,只是每个字段只包含一个选项。服务器的加密组件内容以及压缩方法等都是从接收到的客户端加密组件内筛选出来的。
3.Certificate
之后服务器发送Certificate报文,报文中包含公开密钥证书,服务器必须保证它发送的证书与选择的算法套件一致。不过Certificate消息是可选的,因为并非所有套件都使用身份验证,也并非所有身份验证方法都需要证书。
4.ServerKeyExchange
ServerKeyExchange消息的目的是携带密钥交换的额外数据。消息内容对于不同的协商算法套件都会存在差异。在某些场景中,服务器不需要发送任何内容,在这些场景中就不需要发送ServerKeyExchange消息。
5.ServerHelloDone
ServerHelloDone消息表明服务器已经将所有预计的握手消息发送完毕。在此之后,服务器会等待客户端发送消息。
6.ClientKeyExchange
ClientKeyExchange消息携带客户端为密钥交换提供的所有信息。这个消息受协商的密码套件的影响,内容随着不同的协商密码套件而不同。
7.ChangeCipherSpec
ChangeCipherSpec消息表明发送端已取得用以生成连接参数的足够信息,已生成加密密钥,并且将切换到加密模式。客户端和服务器在条件成熟时都会发送这个消息。注意:ChangeCipherSpec不属于握手消息,它是另一种协议,只有一条消息,作为它的子协议进行实现。
8.Finished
Finished消息意味着握手已经完成。消息内容将加密,以便双方可以安全地交换验证整个握手完整性所需的数据。客户端和服务器在条件成熟时都会发送这个消息。
在一些对安全性要求更高的场景下,可能会出现双向验证的需求。完整的双向验证流程如下:
可以看到,同单向验证流程相比,双向验证多了如下两条消息:CertificateRequest与CertificateVerify,其余流程大致相同。
1.Certificate Request
=Certificate Request是TLS规定的一个可选功能,用于服务器认证客户端的身份。通过服务器要求客户端发送一个证书实现,服务器应该在ServerKeyExchange之后立即发送CertificateRequest消息。
消息结构如下:
enum {
rsa_sign(1), dss_sign(2), rsa_fixed_dh(3),dss_fixed_dh(4),
rsa_ephemeral_dh_RESERVED(5),dss_ephemeral_dh_RESERVED(6),
fortezza_dms_RESERVED(20),
ecdsa_sign(64), rsa_fixed_ecdh(65),
ecdsa_fixed_ecdh(66),
(255)
} ClientCertificateType;
opaque DistinguishedName<1..2^16-1>;struct {
ClientCertificateType certificate_types<1..2^8-1>;
SignatureAndHashAlgorithm
supported_signature_algorithms<2^16-1>;
DistinguishedName certificate_authorities<0..2^16-1>;
} CertificateRequest;
复制
可以选择发送一份自己接受的证书颁发机构列表,这些机构都用其可分辨名称来表示.
2.CertificateVerify
当需要做客户端认证时,客户端发送CertificateVerify消息,来证明自己确实拥有客户端证书的私钥。这条消息仅仅在客户端证书有签名能力的情况下发送。CertificateVerify必须紧跟在ClientKeyExchange之后。消息结构如下:
struct {
Signature handshake_messages_signature;
} CertificateVerify;
复制
应用数据协议携带着应用消息,只以TLS的角度考虑的话,这些就是数据缓冲区。记录层使用当前连接安全参数对这些消息进行打包、碎片整理和加密。如下图所示,可以看到传输的数据已经是经过加密之后的了。
警报的目的是以简单的通知机制告知对端通信出现异常状况。它通常会携带close_notify异常,在连接关闭时使用,报告错误。警报非常简单,只有两个字段:
struct {
AlertLevel level;
AlertDescription description;
} Alert;
AlertLevel字段:表示警报的严重程度;
AlertDescription:直接表示警报代码;
复制
这是最常见的一种问题,通常会抛出如下类型的异常:
出现此类错误通常可能由以下的三种原因导致:
当服务器的CA不被系统信任时,就会发生 SSLHandshakeException。可能是购买的CA证书比较新,Android系统还未信任,也可能是服务器使用的是自签名证书(这个在测试阶段经常遇到)。
解决此类问题常见的做法是:指定HttpsURLConnection信任特定的CA集合。在本文的第5部分代码实现模块,会详细的讲解如何让Android应用信任自签名证书集合或者跳过证书校验的环节。
SSL连接有两个关键环节。首先是验证证书是否来自值得信任的来源,其次确保正在通信的服务器提供正确的证书。如果没有提供,通常会看到类似于下面的错误:
出现此类问题的原因通常是由于服务器证书中配置的域名和客户端请求的域名不一致所导致的。
有两种解决方案:
代码如下:
HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// 设置接受的域名集合
if (hostname.equals(...)) {
return true;
}
}
};
HttpsURLConnection.setDefaultHostnameVerifier(DO_NOT_VERIFY);
复制
SSL支持服务端通过验证客户端的证书来确认客户端的身份。这种技术与TrustManager的特性相似。本文将在第5部分代码实现模块,讲解如何让Android应用支持客户端证书验证的方式。
之前在接口联调的过程中,测试那边反馈过一个问题是在Android 4.4以下的系统出现HTTPS请求不成功而在4.4以上的系统上却正常的问题。相应的错误如下:
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)
复制
按照官方文档的描述,Android系统对SSL协议的版本支持如下:
Name Supported (API Levels) Default 10+ SSL 10–TBD SSLv3 10–TBD TLS 1+ TLSv1 10+ TLSv1.1 16+ TLSv1.2 16+
Name |
Supported (API Levels) |
---|---|
Default |
10+ |
SSL |
10–TBD |
SSLv3 |
10–TBD |
TLS |
1+ |
TLSv1 |
10+ |
TLSv1.1 |
16+ |
Default |
10+ |
TLSv1.2 |
16+ |
也就是说,按官方的文档显示,在API 16+以上,TLS1.1和TLS1.2是默认开启的。但是实际上在API 20+以上才默认开启,4.4以下的版本是无法使用TLS1.1和TLS 1.2的,这也是Android系统的一个bug。
参照stackoverflow上的一些方式,比较好的一种解决方案如下:
SSLSocketFactory noSSLv3Factory;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
noSSLv3Factory = new TLSSocketFactory(mSSLContext.getSSLSocket().getSocketFactory());
} else {
noSSLv3Factory = mSSLContext.getSSLSocket().getSocketFactory();
}
复制
对于4.4以下的系统,使用自定义的TLSSocketFactory,开启对TLS1.1和TLS1.2的支持,核心代码:
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;
}
}
复制
本部分主要基于第四部分提出的Android应用中使用HTTPS遇到的一些常见的问题,给出一个比较系统的解决方案。
不管是使用自签名证书,还是采取客户端身份验证,核心都是创建一个自己的KeyStore,然后使用这个KeyStore创建一个自定义的SSLContext。整体类图如下:
类图中的MySSLContext可以应用在HTTPUrlConnection的方式与服务端连接的过程中:
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);
}
复制
核心是通过httpsURLConnection.setSSLSocketFactory使用自定义的校验逻辑。整体设计上使用策略模式决定采用哪种验证机制:
在App中,把服务端证书放到资源文件下(通常是asset目录下,因为证书对于每一个用户来说都是相同的,并且也不会经常发生改变),但是也可以放在设备的外部存储上。
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;
}
复制
serverCertificateNames中定义了App所信任的证书名称(这些证书文件必须要放在指定的文件路径下,并其要保证名称相同),而后就可以加载服务端证书链到keystore,通过获取到的可信任并带有服务端证书的keystore,就可以用它来初始化自定义的SSLContext了:
@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;
}
}
}
复制
和上面的过程类似,只不过这里提供的TrustManager不需要提供信任的证书集合,默认接受任意客户端证书即可:
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;
}
复制
而后构造相应的SSLContext:
public SSLContext makeContextToTrustAll() throws Exception {
AcceptAllTrustManager tm = new AcceptAllTrustManager();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { tm }, null);
return sslContext;
}
复制