使用TLS1.2标准协议在TCP上实现安全通信

kipway@outlook.com

2017.9.3, Fix ClientKeyExchange 2023.11.8

TLS协议通过Internet提供隐私通信,旨在防止窃听、篡改和伪造数据,本身是基于Netscape发布的SSL 3.0协议规范。https就是一个基于TLS的具体应用,目前主流的应用版本是TLS1.2,该版本规避了1.0和1.1以来出现的安全隐患,是比较可靠的版本。 本文的目的是介绍在具体实现TLS1.2安全通道过程中处理握手消息时需要避免的坑和有些在协议中描述的不够清楚的地方。

1.需要知道的背景资料

首先要熟读TLS协议原文,TLS1.2的原文为RFC5246,也可以戳这里TLS1.0(RFC 2246)中文版先读一下我翻译的TLS1.0版的中文版。 然后了解一下非对称加密RSA算法、对称加密AES算法、hash散列算法sha1和sha256,这些算法知道原理即可,会正确使用密码学库即可。

2.实现目标

在TCP层之上实现TLS1.2的客户端和服务端,可直接叠加在我们平时使用的网络模型之上,比如Windows的IOCP和Linux的Epoll模型。支持以下密码套件:

TLS_RSA_WITH_AES_128_CBC_SHA256 = { 0x00,0x3C };
TLS_RSA_WITH_AES_256_CBC_SHA256 = { 0x00,0x3D };
TLS_RSA_WITH_AES_128_CBC_SHA = {0x00,0x2F};
TLS_RSA_WITH_AES_256_CBC_SHA = {0x00,0x35};

3.证书和密码学库

使用openssl作为密码学库,使用其中的RSA、AES、SHA和HAMC算法函数。openssl包括3部分:密码学库、TLS协议库和openssl工具。我们不使用其TLS协议库的原因是要掌控底层通信细节,直接在我们常用的网络模型上叠加TLS协议。其实,抛开密码学,TLS协议并不复杂。 到https://www.openssl.org/下载,并在本地编译好静态库备用,本文使用的是1.0.2l长周期支持版。然后使用openssl制作一个自签名根证书和使用此根证书制作一个应用服务证书用于测试,并将证书转成der格式备用。 在Linux在制作自签名证书要方便些,如何制作证书请点击这里(openssl在Linux下编译和自签名证书制作)

4.测试方法和测试环境

先实现一个TLS1.2客户端,直接使用目前主流电商的网站作为测试服务器,比如淘宝,京东等都完美支持TLS1.2协议和本文需求的密码套件。先ping一下这些电商的域名得到IP地址,然后connect到443端口,开始握手。

然后实现服务端,先用调试好的自己的客户端调试,最后使用google chrome和Firefox作为客户端来测试自己的服务器,这时你会发现这两个主流浏览器居然不支持TLS_RSA_WITH_AES_128_CBC_SHA256和TLS_RSA_WITH_AES_256_CBC_SHA256,也就是不支持SHA256的hash摘要,仅支持sha1哈希摘要算法。

5.TLS 记录协议

整个TLS协议是一个分层协议,TLS Record Protocol是在TCP之上,将TCP字节流分成有效承载数据不超过16384(不含头部、压缩和加密增加的附加数据,加上这些不超多16384+2048)长的块。TLS record 协议和旗下的4个协议中传输整数都是大头格式,即big-endian字节顺序的。 Record Protocol有5个字节的头部,第一个字节为旗下的4个子协议:

record协议的报文定义很简单,报头5个字节:1字节子协议号,2字节版本,2字节数据长度:
struct {
    uint8 major;
    uint8 minor;
} ProtocolVersion;
enum {
    change_cipher_spec(20), alert(21), handshake(22),
    application_data(23), (255)
} ContentType;
struct {
    ContentType type;
    ProtocolVersion version;
    uint16 length;
    opaque fragment[TLSPlaintext.length];
} TLSPlaintext;
相同ContentType的多个客户端消息(或者子协议)可以合并到单个记录协议的承载中(fragment字段),或者单个消息可能跨多个记录的fragment字段。简单的举例说,就是几个短的握手消息可以合并到一个record包中,一个长的握手消息可以分成几个record包发送。 注:在测试电商https服务器时发现,每个小于一个record包的握手消息都是单独包装到一个record报文中发送的。

6.TLS握手协议

一定要熟读,理解协议文本,尤其是第4章描述语言(Presentation Language),后面每个消息的格式使用这个描述语言定义。

6.1.握手消息交互流程图

TLS1.2协议上的完整版,如图1,带*的是不同场景的可选部分。图2为常规https模式,本文实现的模式。

client server ClientHello ServerHello Certificate* ServerKeyExchange* CertificateRequest* ServerHelloDone Certificate* ClientKeyExchange CertificateVerify* [ChangeCipherSpec] Finished [ChangeCipherSpec] Finished Application Data Application Data 图1 TLS标准的握手流程 1 2 3 4 5 client server ClientHello ServerHello Certificate ClientKeyExchange [ChangeCipherSpec] Finished [ChangeCipherSpec] Finished Application Data Application Data 图2 常规https网站TLS握手流程 ServerHelloDone 1 2 3 4 5 不含客户端证书(U盾网银才需要),这正式本文要实现的模式。 可以在此模式下升级https为wss(基于https的websocket)协议

6.2.Client hello消息

这是握手开始的第一个消息,由客户端发起,当客户端connect到服务器后,发送此消息。此消息的内容包含客户端支持的最高版本、客户端随机数、会话ID、客户端希望的加密套件列表,客户端希望的压缩方式列表。

把这些信息加上handshake协议头4字节(1字节握手消息类型和3字节长度)变成handshake协议,然后包入record 协议(再加5字节record头部)发送给https服务器。

注意:握手消息的版本信息和record协议层的版本信息不一样,record层的一般都是0x03,0x01表示TLS1.0的record协议版本;而握手消息中的版本此时为协商版本,是指客户端支持的最高TLS版本,这里为TLS1.2即0x03,0x03,如果服务器同意这个版本,则返回的serverhello消息中会给出协商后的版本。

bool mkr_ClientHelloMsg(ec::tArray< unsigned char >*po)
{
    RAND_bytes(_clientrand, sizeof(_clientrand));
    _client_hello.ClearData();
    _client_hello.Add((uint8)tls::handshaketype::hsk_client_hello);  // msg type  1byte
    _client_hello.Add((uint8)0); _client_hello.Add((uint8)0); _client_hello.Add((uint8)0); // msg len  3byte 
    _client_hello.Add((uint8)TLSVER_MAJOR);
    _client_hello.Add((uint8)TLSVER_NINOR);
    _client_hello.Add(_clientrand, 32);// random 32byte 
    _client_hello.Add((uint8)0);    // SessionID = NULL   1byte
    _client_hello.Add((uint8)0); _client_hello.Add((uint8)8); // cipher_suites
    _client_hello.Add((uint8)0); _client_hello.Add((uint8)TLS_RSA_WITH_AES_256_CBC_SHA256);
    _client_hello.Add((uint8)0); _client_hello.Add((uint8)TLS_RSA_WITH_AES_128_CBC_SHA256);
    _client_hello.Add((uint8)0); _client_hello.Add((uint8)TLS_RSA_WITH_AES_256_CBC_SHA);
    _client_hello.Add((uint8)0); _client_hello.Add((uint8)TLS_RSA_WITH_AES_128_CBC_SHA); 
    _client_hello.Add((uint8)1); // compression_methods
    _client_hello.Add((uint8)0);
    *(_client_hello.GetBuf() + 3) = (uint8)(_client_hello.GetSize() - 4);
    return SendToBuf(po, tls::rec_handshake, _client_hello.GetBuf(), _client_hello.GetSize());
}

6.3.Server hello消息

服务器收到clienthello消息后,解析出版本、加密套件、压缩方式,看是否能满足客户端的协商条件,如果不满足,则会发送一个报警消息,握手失败。如果能满足,则填写协商后的版本、服务端随机数、SessionID、加密套件、压缩方式,包装为serverhello消息发送给客户端。 serverhello消息中只有SessionID是向量,其余均为定量数据,比较好解析。这里不再累述。对于服务端来讲,SessionID可以使用原来服务器模型中每个TCP连接的ID来标识每一个客户端。

void MakeServerHello()
{
    RAND_bytes(_serverrand, sizeof(_serverrand));
    _srv_hello.ClearData();
    _srv_hello.Add((uint8)tls::hsk_server_hello);  // msg type  1byte
    _srv_hello.Add((uint8)0); _srv_hello.Add((uint8)0); _srv_hello.Add((uint8)0); // msg len  3byte 
    _srv_hello.Add((uint8)TLSVER_MAJOR);
    _srv_hello.Add((uint8)TLSVER_NINOR);
    _srv_hello.Add(_serverrand, 32);// random 32byte 
    _srv_hello.Add((uint8)4);    // SessionID = 4   1byte
    _srv_hello.Add((uint8)((_ucid >> 24) & 0xFF));
    _srv_hello.Add((uint8)((_ucid >> 16) & 0xFF));
    _srv_hello.Add((uint8)((_ucid >> 8) & 0xFF));
    _srv_hello.Add((uint8)((_ucid >> 0) & 0xFF));
    _srv_hello.Add((uint8)0); _srv_hello.Add((uint8)_cipher_suite & 0xFF); //cipher_suites
    _srv_hello.Add((uint8)0);// compression_methods
    *(_srv_hello.GetBuf() + 3) = (uint8)(_srv_hello.GetSize() - 4);
}

6.4.Server certificate消息

当服务器发送完serverhello消息后,紧接着发送此消息,向客户端发送自己的身份证,即证书,证书中含有自己的RSA public key,并被可信任机构签名背书。目的是让客户端验证服务器可信度,如果是可信任的,客户端使用服务端的public key加密一个PreMasterSecret信息(生成对称加密的前置主信息)发送给服务器。 证书消息定义如下:

opaque ASN.1Cert<1..2^24-1>;
struct {
    ASN.1Cert certificate_list<0..2^24-1>;
} Certificate;
可以看出是一个嵌套变长向量,Certificate是一个X.509v3证书链,链的每一个节点就是一个X.509v3证书。这个证书链不要和PKCS#7证书链格式混淆,没有一点关系。这里的证书链就是一个嵌套可变向量。每个X.509v3证书是用ASN.1二进制格式描述并存放的,就是.der后缀的证书文件内容。 将应用服务的证书放在前面,后面放证书签发方的根证书。一般放两个证书即可,也可以只放应用服务的证书。这里我们的应用证书文件名为kipway.der,自签名根证书为cacert.der(不是权威机构,虽然证书是正确的,chrome和Firefox都不认,需要加入到浏览器可信任列表中)。 注意:由于嵌套可变向量,第一个证书的实际开始内容从握手消息的第11字节开始(4字节消息头,3字节证书链长度,3字节应用证书kipway.der的长度,应用证书kipway.der内容,3字节根证书cacert.der长度,根证书cacert.der的内容)。 客户端解析出证书的public key需要使用到openssl的3个函数:
X509 *d2i_X509(X509 **px, const unsigned char **in, long len);
EVP_PKEY *X509_get_pubkey(X509 *x);
RSA *EVP_PKEY_get1_RSA(EVP_PKEY *pkey);
服务器方组织证书消息的实现代码如下:
void MakeCertificateMsg()
{
    _srv_certificate.ClearData();
    _srv_certificate.Add((uint8)tls::hsk_certificate);
    _srv_certificate.Add((uint8)0); _srv_certificate.Add((uint8)0); _srv_certificate.Add((uint8)0);//1,2,3先填写消息0长度
    uint32 u;
    if (_pcerroot && _cerrootlen)//如果有根证书,先输出应用证书,然后输出根证书。
    {
        u = (uint32)(_cerlen + _cerrootlen + 6);
        _srv_certificate.Add((uint8)((u >> 16) & 0xFF)); 
        _srv_certificate.Add((uint8)((u >> 8) & 0xFF)); 
        _srv_certificate.Add((uint8)(u & 0xFF));//4,5,6 输出证书链长度
        u = (uint32)_cerlen;
        _srv_certificate.Add((uint8)((u >> 16) & 0xFF)); 
        _srv_certificate.Add((uint8)((u >> 8) & 0xFF)); 
        _srv_certificate.Add((uint8)(u & 0xFF));//7,8,9 //输出应用证书长度
        _srv_certificate.Add((const uint8*)_pcer, _cerlen);//输出应用证书
        u = (uint32)_cerrootlen;
        _srv_certificate.Add((uint8)((u >> 16) & 0xFF));
        _srv_certificate.Add((uint8)((u >> 8) & 0xFF));
        _srv_certificate.Add((uint8)(u & 0xFF)); //输出根证书长度
        _srv_certificate.Add((const uint8*)_pcerroot, _cerrootlen);//输出根证书
    }
    else
    {
        u = (uint32)_cerlen + 3;
        _srv_certificate.Add((uint8)((u >> 16) & 0xFF));
        _srv_certificate.Add((uint8)((u >> 8) & 0xFF));
        _srv_certificate.Add((uint8)(u & 0xFF));//4,5,6 输出证书链长度
        u = (uint32)_cerlen;
        _srv_certificate.Add((uint8)((u >> 16) & 0xFF));
        _srv_certificate.Add((uint8)((u >> 8) & 0xFF));
        _srv_certificate.Add((uint8)(u & 0xFF));//7,8,9 //输出应用证书长度
        _srv_certificate.Add((const uint8*)_pcer, _cerlen);//输出应用证书
    }
    u = _srv_certificate.GetSize() - 4;
    *(_srv_certificate.GetBuf() + 1) = (uint8)((u >> 16) & 0xFF);
    *(_srv_certificate.GetBuf() + 2) = (uint8)((u >> 8) & 0xFF);
    *(_srv_certificate.GetBuf() + 3) = (uint8)((u >> 0) & 0xFF);//更改消息正确长度
}
客户端按照上述代码解析出证书,验证证书后,提取出第一个证书证书public key。openssl有验证证书的函数。这里我们自己签发的证书,不被信任。在我们自己的应用中,可以采用将服务的证书复制到客户端,比对就行了。

6.5.ServerHelloDone消息

这个消息最简单,没有内容,整个消息只有4字节,也就是只有握手消息头部。目的是告诉客户端,hello过程完成了。接下来该你验证我的证书和取出我public key做下一步工作了。

unsigned char umsg[4] = { tls::hsk_server_hello_done,0,0,0 };

6.6.ClientKeyExchange消息

再收到服务端的ServerHelloDone消息之后,客户端使用ClientKeyExchange发送一个经服务器public key加密的PreMasterSecret给服务器。PreMasterSecret为两字节版本加46字节随机数组成,由客户端生成。用于产生masterkey和key_block,再由key_block分割出aes密码和hmac种子。

struct {
    ProtocolVersion client_version;
    opaque random[46];
} PreMasterSecret;
struct {
    select (KeyExchangeAlgorithm) {
        case rsa: EncryptedPreMasterSecret;
        case diffie_hellman: ClientDiffieHellmanPublic;
    } exchange_keys;
} ClientKeyExchange;
实际上RSA版的ClientKeyExchange消息体就是使用服务器public key加密的PreMasterSecret,如果使用RSA2048加密,加密后长度为256字节(2048位)。使用RSA public key加密需要用到openssl函数:

int RSA_public_encrypt(int flen, const unsigned char *from,unsigned char *to, RSA *rsa, int padding);
具体实现:

bool mkr_ClientKeyExchange(ec::tArray< unsigned char > *po)
{
    unsigned char premasterkey[48], out[512];
    premasterkey[0] = TLSVER_MAJOR;
    premasterkey[1] = TLSVER_NINOR;
    RAND_bytes(&premasterkey[2], 46); //calculate pre_master_key
    int nbytes = RSA_public_encrypt(48, premasterkey, out, _prsa, RSA_PKCS1_PADDING);
    if (nbytes < 0)
    return false;
    _cli_key_exchange.ClearData();
    _cli_key_exchange.Add((unsigned char)(tls::handshaketype::hsk_client_key_exchange)); // msgtype
    uint32 ulen = nbytes;
    _cli_key_exchange.Add((unsigned char)(((ulen + 2) >> 16) & 0xFF));// 3bytes length
    _cli_key_exchange.Add((unsigned char)(((ulen + 2) >> 8) & 0xFF));
    _cli_key_exchange.Add((unsigned char)((ulen + 2) & 0xFF));
    _cli_key_exchange.Add((unsigned char)((ulen >> 8)& 0xFF)); // 2byte  secrit premasterkey length , public-key-encrypted vector <0..2^16-1>
    _cli_key_exchange.Add((unsigned char)(ulen & 0xFF));
    _cli_key_exchange.Add(out, nbytes);
    return SendToBuf(po, tls::rec_handshake, _cli_key_exchange.GetBuf(), _cli_key_exchange.GetSize());
}

客户端和服务端通过非对称加密成功地隐秘交换了PreMasterSecret,然后使用相同的算法计算出masterkey和key_block,然后分割aes对称加密密码和hmac种子。下面介绍整个计算过程。

6.6.1.计算masterkey和key_block

TLS1.2定义了一个基于HMAC的PRF函数,规定只能使用SHA256作为散列算法。先定义一个数据扩展函数P_hash(secret,data),它使用单个散列函数将一个secret和seed扩展成任意数量的输出:

P_hash(secret, seed) = 
		HMAC_hash(secret, A(1) + seed) +
		HMAC_hash(secret, A(2) + seed) +
		HMAC_hash(secret, A(3) + seed) + ...
其中+表示连接:
A() is defined as:
A(0) = seed
A(i) = HMAC_hash(secret, A(i-1))
再定义PRF:
PRF(secret,label,seed)= P_hash(secret,label + seed)
label是不含结束符0的ASCII字符串。由于TLS1.2强制使用SHA256,最终的结果就是:
PRF(secret,label,seed)= P_sha256(secret,label + seed)
这里需要使用到openssl的HMAC和EVP_sha256函数。为了便于理解,这里给出了一个RPF的实现。
/*!
计算RPF
PRF(secret,label,seed) = P_sha256(secret,label + seed)
\param key [in]  密数secret
\param keylen [in]  密数secret的字节数
\param seed [in] label+seed合并后的数据
\param seedlen [in] label+seed合并后的数据字节数
\param pout [out] 输出区
\param outlen [in] 输出字节数,即需要扩展到的字节数。
*/
static bool prf_sha256(const unsigned char* key, int keylen, const unsigned char* seed, int seedlen, 
                    unsigned char *pout, int outlen)
{
    int nout = 0;
    unsigned int mdlen = 0;
    unsigned char An[32], Aout[32], An_1[32];
    if (!HMAC(EVP_sha256(), key, (int)keylen, seed, seedlen, An_1, &mdlen))
        return false;
    ec::cAp as(32 + seedlen);
    unsigned char *ps = (unsigned char *)as;
    while (nout < outlen)
    {
        memcpy(ps, An_1, 32);
        memcpy(ps + 32, seed, seedlen);
        if (!HMAC(EVP_sha256(), key, (int)keylen, ps, 32 + seedlen, Aout, &mdlen))
            return false;
        if (nout + 32 < outlen)
        {
            memcpy(pout + nout, Aout, 32);
            nout += 32;
        }
        else
        {
            memcpy(pout + nout, Aout, outlen - nout);
            nout = outlen;
            break;
        }
        if (!HMAC(EVP_sha256(), key, (int)keylen, An_1, 32, An, &mdlen))
            return false;
        memcpy(An_1, An, 32);
    }
    return true;
}

计算masterkey,描述语言RPF(PreMasterSecret,"master secret",client_rand+server_rand),具体实现如下代码片段:

bool make_masterkey()
{
    const char* slab = "master secret";
    unsigned char seed[128];
    memcpy(seed, slab, strlen(slab));
    memcpy(&seed[strlen(slab)], _clientrand, 32);
    memcpy(&seed[strlen(slab) + 32], _serverrand, 32);
    return prf_sha256(premasterkey, 48, seed, (int)strlen(slab) + 64, _master_key, 48);
}

计算key_block,描述语言RPF(MasterKey,"key expansion",server_rand+client_rand),具体实现如下代码片段:

bool make_keyblock()
{
    const char *slab = "key expansion";
    unsigned char seed[128];
    memcpy(seed, slab, strlen(slab));
    memcpy(&seed[strlen(slab)], _serverrand, 32);
    memcpy(&seed[strlen(slab) + 32], _clientrand, 32);
    return prf_sha256(_master_key, 48, seed, (int)strlen(slab) + 64, _key_block, 128);                
}

6.6.2.分割密码套件参数

从key_block分出hmac和加密密码,按照如下规则分割:

client_write_MAC_key[mac_key_length]
server_write_MAC_key[mac_key_length]
client_write_key[enc_key_length]
server_write_key[enc_key_length]
当前定义的密码套件需要最多的材料是AES_256_CBC_SHA256。 它需要2 x 32字节的密钥和2 x 32字节的MAC密钥,总共128个字节的密钥材料。这给出本文实现的4种加密算法的分割实现:
void SetCipherParam(unsigned char *pkeyblock, int nsize)
{
    memcpy(_keyblock, pkeyblock, nsize);
    if (_cipher_suite == TLS_RSA_WITH_AES_128_CBC_SHA256)
    {
        memcpy(_key_cwmac, _keyblock, 32);
        memcpy(_key_swmac, &_keyblock[32], 32);
        memcpy(_key_cw, &_keyblock[64], 16);
        memcpy(_key_sw, &_keyblock[80], 16);
    }
    else if (_cipher_suite == TLS_RSA_WITH_AES_256_CBC_SHA256)
    {
        memcpy(_key_cwmac, _keyblock, 32);
        memcpy(_key_swmac, &_keyblock[32], 32);
        memcpy(_key_cw, &_keyblock[64], 32);
        memcpy(_key_sw, &_keyblock[96], 32);
    }
    else if (_cipher_suite == TLS_RSA_WITH_AES_128_CBC_SHA)
    {
        memcpy(_key_cwmac, _keyblock, 20);
        memcpy(_key_swmac, &_keyblock[20], 20);
        memcpy(_key_cw, &_keyblock[40], 16);
        memcpy(_key_sw, &_keyblock[56], 16);
    }
    else if (_cipher_suite == TLS_RSA_WITH_AES_256_CBC_SHA)
    {
        memcpy(_key_cwmac, _keyblock, 20);
        memcpy(_key_swmac, &_keyblock[20], 20);
        memcpy(_key_cw, &_keyblock[40], 32);
        memcpy(_key_sw, &_keyblock[72], 32);
    }
}

6.7.client finished消息

客户端发送完ClientKeyExchange消息之后,双方就可以使用对称加密发送消息了,此时会发送一个客户段握手完成消息。finished消息是第一个使用刚刚协商的算法,密钥和秘密进行保护的消息。 完成消息的收件人必须验证内容是否正确。 一旦一方已经发送了其完成的消息并从其对等体接收并验证了完成的消息,则它可以开始通过连接发送和接收应用程序数据。 在发送finished消息之前还需要发送一个ChangeCipherSpec消息通知对方,接下来的消息开始使用协商好的加密和验证算法。ChangeCipherSpec消息不属于握手协议,而属于专用的record 层下的更改加密规范协议。finished消息是一个12字节的校验数据,采用PRF算法,计算前面所有握手消息信息的校验值。

struct {
opaque verify_data[12];
} Finished;
verify_data:PRF(master_secret, finished_label, Hash(handshake_messages))
finished_label:客户端为"client finished".服务端为"server finished". 
注意这些参与校验的握手消息为不包含record层的头部字节。按照先后顺序,Hash(handshake_messages)在TLS1.2中为SHA256散列值。本文实现的具体为: 客户端校验内容和顺序为:_client_hello,_srv_hello,_srv_certificate,_srv_hellodone,_cli_key_exchange共5个消息。生成校验值的代码片段为:
const char* slab = "client finished";
unsigned char hkhash[48];
memcpy(hkhash, slab, strlen(slab));
ec::tArray< unsigned char > tmp(8192);
tmp.Add(_client_hello.GetBuf(), _client_hello.GetSize());
tmp.Add(_srv_hello.GetBuf(), _srv_hello.GetSize());
tmp.Add(_srv_certificate.GetBuf(), _srv_certificate.GetSize());
tmp.Add(_srv_hellodone.GetBuf(), _srv_hellodone.GetSize());
tmp.Add(_cli_key_exchange.GetBuf(), _cli_key_exchange.GetSize());
unsigned char verfiy[32];
SHA256(tmp.GetBuf(), tmp.GetSize(), &hkhash[strlen(slab)]); //            
if (!prf_sha256(_master_key, 48, hkhash, (int)strlen(slab) + 32, verfiy, 32))//使用verfiy中的前12个字节即可
    return false;
加上握手消息的头部4字节,finished消息为16字节。需要把这16字节按照握手协商的对称加密算法加密打包为record协议发送出去。本文使用的4个密码套件均为aes压缩,差别只在加密位数和校验方式不同。

6.7.1.AES CBC对称加密

AES CBC块加密的数据是这样定义的:

struct {
	opaque IV[SecurityParameters.record_iv_length];
	block-ciphered struct {
		opaque content[TLSCompressed.length];
		opaque MAC[SecurityParameters.mac_length];
		uint8 padding[GenericBlockCipher.padding_length];
		uint8 padding_length;
	};
} GenericBlockCipher;
具体到本文:
    IV:长度为16字节,参与加密和解密计算,自身不被加密。
    MAC:长度为20(SHA1)或32(SHA256)。
    padding:为0-15字节。
    padding_length:1字节,值为padding的长度。
    整个block-ciphered块被补齐到16字节的倍数。
下面是实现的加密一个基本块数据(<=16384)加密打包为record协议的代码。其中AES_set_encrypt_key和AES_cbc_encrypt为openssl函数
/*!
加密打包一个基本数据块为record协议
\param po [out]输出用的动态数组对象指针。
\param type [in] record协议的子协议枚举值
\param sblk [in] 被加密的数据块,小于等于16384
\param size [in] 被加密的数据块的字节数
*/
int MKR_WithAES_BLK(ec::tArray< unsigned char > *po, unsigned char type, const unsigned char* sblk, size_t size)
{
    int i;
    unsigned char* pkeyw = _key_cw, *pkeywmac = _key_cwmac;
    unsigned char IV[AES_BLOCK_SIZE];//rand IV	
    unsigned char srec[1024 * 20];
    unsigned char sv[1024 * 20];
    unsigned char sout_e[1024 * 20];
    unsigned char mac[32];
    if (_bserver) {
        pkeyw = _key_sw;
        pkeywmac = _key_swmac;
    }
    ec::cStream ss(srec, sizeof(srec));
    ss << type;
    ss << (unsigned char)TLSVER_MAJOR;
    ss << (unsigned char)TLSVER_NINOR;
    ss << (unsigned char)0;
    ss << (unsigned char)0;
    RAND_bytes(IV, AES_BLOCK_SIZE);
    ss.write(IV, AES_BLOCK_SIZE);
    if (!caldatahmac(type, _seqno_send, sblk, size, pkeywmac, mac))
        return -1;
    size_t maclen = 32;
    if (_cipher_suite == TLS_RSA_WITH_AES_128_CBC_SHA || _cipher_suite == TLS_RSA_WITH_AES_256_CBC_SHA)
        maclen = 20;
    ec::cStream es(sv, sizeof(sv));
    es.write(sblk, size); //content
    es.write(mac, maclen); //MAC 
    size_t len = es.getpos() + 1;
    if (len % AES_BLOCK_SIZE)
    {
        for (i = 0; i < (int)(AES_BLOCK_SIZE - (len % AES_BLOCK_SIZE)) + 1; i++)//padding and   padding_length  
            es << (char)(AES_BLOCK_SIZE - (len % AES_BLOCK_SIZE));
    }
    else
        es << (char)0; //padding_length
    AES_KEY aes_e;
    int nkeybit = 128;
    if (_cipher_suite == TLS_RSA_WITH_AES_256_CBC_SHA256)
        nkeybit = 256;
    if (AES_set_encrypt_key(pkeyw, nkeybit, &aes_e) < 0)
        return -1;
    AES_cbc_encrypt(sv, sout_e, es.getpos(), &aes_e, IV, AES_ENCRYPT);
    ss.write(sout_e, es.getpos());
    size_t rl = ss.getpos();
    unsigned short uslen = (unsigned short)(es.getpos() + sizeof(IV));
    ss.setpos(3);
    ss << (unsigned char)((uslen >> 8) & 0xFF);
    ss << (unsigned char)(uslen & 0xFF);
    po->Add(srec, rl);
    _seqno_send++;
    return (int)rl;
}
注意:有一个seq_no参与MAC校验计算,在TLS协议中是这样解释的:每个连接状态包含一个序列号,该序列号分别用于读取和写入状态。每当连接状态为活动状态,序列号必须设置为零。序列号为uint64,不得超过2 ^ 64-1。序列号在每条记录之后增加:具体来说,在特定连接状态下发送的第一条记录应使用序列号0。 这里有一个坑:“每当连接状态为活动状态,序列号必须设置为零”,是TCP建立开始后发送第一条cilenthello消息的record为0还是发送第一条加密的record时0?没有解释。经过实验发现,握手协商过程中发送finished消息时(发送第一条加密record)seq_no置0.

其中计算MAC时要根据协商的加密套件定义,本文的具体实现如下,其中HMAC为openssl的函数:

bool caldatahmac(unsigned char type, uint64 seqno, const void* pd, size_t len, unsigned char* pkeymac, 
                    unsigned char *outmac)
{
    int i;
    unsigned char  stmp[1024 * 20];
    ec::cStream es(stmp, sizeof(stmp));
    for (i = 7; i >= 0; i--)
        es << (char)((seqno >> i * 8) & 0xFF);
    es << type; //type
    es << (char)TLSVER_MAJOR; //ver            
    es << (char)TLSVER_NINOR; //ver            
    es << (char)((len >> 8) & 0xFF);
    es << (char)(len & 0xFF); // len
    es.write(pd, len); // data
    unsigned int mdlen = 0;
    if (_cipher_suite == TLS_RSA_WITH_AES_128_CBC_SHA || _cipher_suite == TLS_RSA_WITH_AES_256_CBC_SHA)
        return HMAC(EVP_sha1(), pkeymac, 20, stmp, es.getpos(), outmac, &mdlen) != NULL;
    return HMAC(EVP_sha256(), pkeymac, 32, stmp, es.getpos(), outmac, &mdlen) != NULL;
}
在包装一个函数就可以将任意长的数据打包为多个加密后的record了,然后使用tcp的send发送出了。
bool mk_cipher(ec::tArray< unsigned char > *po, unsigned char rectype, const unsigned char* pdata, size_t size)
{
    int ns = 0;
    size_t us = 0;
    while (us < size)
    {
        if (us + TLS_CBCBLKSIZE < size)
        {
            ns = MKR_WithAES_BLK(po, rectype, pdata + us, TLS_CBCBLKSIZE);
            if (ns < 0)
                return false;
            us += TLS_CBCBLKSIZE;
        }
        else
        {
            ns = MKR_WithAES_BLK(po, rectype, pdata + us, size - us);
            if (ns < 0)
                return false;
            us = size;
            break;
        }
    }
    return true;
}
下面再给出一个record包的解密实现,这样和密码库相关的主要代码就完了。
/*!
解密record并校验
\param pd [in] 完整的record协议
\param len [in] pd长度
\param pout [out]输出缓冲区,不小于pd的长度
\param poutsize[out]实际输出的字节数,成功解密和验证正确后脱壳数据的长度。
\remark 输出为一个不加密的record包。
*/
bool decrypt_record(const unsigned char*pd, size_t len, unsigned char* pout, int *poutsize)
{
    int i;
    unsigned char sout[1024 * 20], iv[AES_BLOCK_SIZE], *pkey = _key_sw, *pkmac = _key_swmac;
    AES_KEY aes_d;
    int nkeybit = 128;
    if (_cipher_suite == TLS_RSA_WITH_AES_256_CBC_SHA256)
        nkeybit = 256;
    if (_bserver)
    {
        pkey = _key_cw;
        pkmac = _key_cwmac;
    }
    memcpy(iv, pd + 5, AES_BLOCK_SIZE);//Decrypt
    if (AES_set_decrypt_key(pkey, nkeybit, &aes_d) < 0)
        return false;
    AES_cbc_encrypt((const unsigned char*)pd + 5 + AES_BLOCK_SIZE, (unsigned char*)sout, 
                len - 5 - AES_BLOCK_SIZE, &aes_d, iv, AES_DECRYPT);
    unsigned int ufsize = sout[len - 5 - AES_BLOCK_SIZE - 1];//verify data MAC
    if (ufsize > 15)
        return false;
    size_t maclen = 32;
    if (_cipher_suite == TLS_RSA_WITH_AES_128_CBC_SHA || _cipher_suite == TLS_RSA_WITH_AES_256_CBC_SHA)
        maclen = 20;
    size_t datasize = len - 5 - AES_BLOCK_SIZE - 1 - ufsize - maclen;
    unsigned char mac[32], macsrv[32];
    memcpy(macsrv, &sout[datasize], maclen);
    if (!caldatahmac(pd[0], _seqno_read, sout, datasize, pkmac, mac))
        return false;
    for (i = 0; i < (int)maclen; i++) {
        if (mac[i] != macsrv[i])
            return false;
    }
    memcpy(pout, pd, 5);
    memcpy(pout + 5, sout, datasize);
    *(pout + 3) = ((datasize >> 8) & 0xFF);
    *(pout + 4) = (datasize & 0xFF);
    *poutsize = (int)datasize + 5;
    _seqno_read++;
    return true;
}

6.8.server finished消息

读到这里,再坚持一下,马上完了。服务器在收到客户端的Finished消息后,解码并验证,计算自己这边的握手消息的verify_data,如果和client发过来的相同,则说明握手成功,这是需要回答client一个finished消息,注意先要发送一条ChangeCipherSpec消息告诉对方后面发送的record包是使用协商后的加密算法加密的。 server finished消息和client finished消息差不多,数据验证中要多一条收到的client finished消息。消息体和PRF算法都一样,只是lab改为"server finished"。下面给出具体代码:

bool mkr_ServerFinished(ec::tArray< unsigned char > *po)
{
    const char* slab = "server finished";
    unsigned char hkhash[48];
    memcpy(hkhash, slab, strlen(slab));
    ec::tArray< unsigned char > tmp(8192);
    tmp.Add(_client_hello.GetBuf(), _client_hello.GetSize());
    tmp.Add(_srv_hello.GetBuf(), _srv_hello.GetSize());
    tmp.Add(_srv_certificate.GetBuf(), _srv_certificate.GetSize());
    tmp.Add(_srv_hellodone.GetBuf(), _srv_hellodone.GetSize());
    tmp.Add(_cli_key_exchange.GetBuf(), _cli_key_exchange.GetSize());
    tmp.Add(_cli_finished.GetBuf(), _cli_finished.GetSize());
    unsigned char verfiy[32], sdata[32];
    SHA256(tmp.GetBuf(), tmp.GetSize(), &hkhash[strlen(slab)]); //            
    if (!prf_sha256(_master_key, 48, hkhash, (int)strlen(slab) + 32, verfiy, 32))
        return false;
    sdata[0] = tls::hsk_finished;
    sdata[1] = 0;
    sdata[2] = 0;
    sdata[3] = 12;
    memcpy(&sdata[4], verfiy, 12);
    _seqno_send = 0;
    _bsendcipher = true;
    return SendToBuf(po, tls::rec_handshake, sdata, 16);//打包为加密后的record包输出到po
}
注意这里还有一个坑,数据校验中的_cli_finished消息,协议中并没有说是接收到的原始加密record包中的数据体还是解密出来的非加密16字节client_finished消息。只有做实验验证了,结果是使用解密后的16字节client_finished消息。

6.9.握手完成

客户端收到server finished消息后,解密验证,如果正确。则握手成功,之后双方就可以使用record协议的Application Data子协议使用协商后的对称加密算法传输加密数据了。完了,以上代码都是验证了,便于阅读者结合协议文档快速理解TLS协议。

7.关于证书

证书有两个作用:

当在互联网上实现面对大量用户提供服务时,是必须花钱去签发一个服务端证书的。但是当我们在局域网实现自己的应用客户端时,客户端明确知道所连的服务器的public key或客户端有服务器自签发证书的正确拷贝,这时就不用去花钱到证书签发机构签发证书了,因为客户端有办法鉴别服务器。 即使使用firefox这样的标准客户端,也可以事先将签发机构自签名证书(我们自己制作的)或服务器证书加入信任列表。具体参考这里(openssl在Linux下编译和自签名证书制作)

8.自己造一个TLS轮子有什么用?

目前主流的web服务器放均支持https,opensll也有TLS协议库,自己再造一个轮子有何用呢?基于以下理由:

整个周末变成了这篇文章,希望对阅读者有一点用处。本文可任意转载,请注明源出处。