[原]OpenSSL SSL连接初始化部分解析


SSL_CTX_new: 分配并初始化SSL_CTX结构,
1 很重要的就是load cipher_list
2 设置ssl_session timeout时间默认为7200秒
3 初始化client_CA STACK
4 初始化EVP_MD, rsa_md5, md5, sha1
5 初始化ex_data.
6 初始化comp_methods

重点解析load cipher_list, 因为在替换engine时, 要指定我们支持的算法;

关键函数: ssl_create_cipher_list

预设EVP_CIPHER算法
ssl_cipher_methods - 全局空数组, 元素类型: EVP_CIPHER *, 容量:9
0, 1, 2 3 4, 7, 8 - 只用到了7个
7 - SN_aes_128_cbc
8 - SN_aes_256_cbc

预设EVP_MD算法
ssl_digest_methods - 全局空数组, 元素类型: EVP_MD *, 容量:2
0 - sm_md5
1 - SN_sha1(fmcpc supported)

所有evp_cipher和evp_md是放在一个hash列表中, 根据名称检索得到的

根据一个static int init_ciphers全局标志, 来决定是否load_cipher_list
其过程用CRYPTO_w_lock进行lock, 线程安全

完了后, 会调用: ssl_cipher_get_disabled
首先是硬设置一些被disable的算法:
默认编译下:
10000000101000
SSL_kFZA - 0x8 | SSL_kKRB5 - 0x20 |SSL_aKRB5 - 0x2000
然后检测ssl_cipher_methods和ssl_digest_methods表中, 哪些指针为空, 如果为
空, 将该算法对应的disable mask位设置为1
SSL_eFZA - 0x100000
最终mask == 100000010000000101000 - 0x00102028
如果我们强行改ssl代码, 应该可以首先disable其他算法, 只保留
AES-SHA1, 但这种方法视乎不妥.

然后获取num_of_ciphers, 这个num_of_ciphers非常简单, 就是一个全局的
sizeof(ssl3_ciphers)/sizeof(SSL_CIPHER)
ssl3_ciphers在s3_lib.c中定义, 全局, 定义时初始化. 原属为SSL_CIPHER
SSL_CIPHER 的每个算法指定了:
密钥交换算法, 非对称算法, 对称算法及模式, hash算法, 如:
TLS1_TXT_RSA_WITH_AES_128_SHA/TLS1_TXT_DH_RSA_WITH_AES_128_SHA
加密强度, used(前面是disable)掩码, 算法位数等;
这里得到的num_of_cipher长度为0x48(72)个.

然后将预定义的SSL_CIPHERS从全局数组中取出来, 和disable_mask按位比较, 去掉不适用的算法.
算法被放到一个CIPHER_ORDER结构中, 有个active标志, 应该特别注意, 这个标志初始化被置为0
后面还要根据rulestr, strength, mask来决定, 哪些算法应该保留, 保留的被放到cipherstack中
这个过程看起来比较复杂, 没有仔细解读.

这里还应该注意一个宏: KSSL_DEBUG, 如果在nt_dll.mak中定义该宏, 应该所有算法会被打印出来.
这是OpenSSL作者自己定义的宏.

实际上, 被CIPHER_ORDER数组的算法有0x3c(60)个(很多了). CIPHER_ORDER内部有个prev和next
在加入完成后, 按照顺序排成一个双向链表, 并返回头和尾的指针.

最后有1B(27)个算法被选中, 与我们dump出来的server_cipher_list的个数相同. 算法列表:
DHE-RSA-AES256-SHA
DHE-DSS-AES256-SHA
AES256-SHA <<<<<<<<<<< 调试后发现使用的这个算法.
EDH-RSA-DES-CBC3-SHA
EDH-DSS-DES-CBC3-SHA
DES-CBC3-SHA
DHE-RSA-AES128-SHA
DHE-DSS-AES128-SHA
AES128-SHA  >>>>>>>>>>>>> 因为加密卡只支持这两种算法, 所以, 这里应该用这个.
IDEA-CBC-SHA
DHE-DSS-RC4-SHA
RC4-SHA
RC4-MD5
EXP1024-DHE-DSS-DES-CBC-SHA
EXP1024-DES-CBC-SHA
EXP1024-RC2-CBC-MD5
EDH-RSA-DES-CBC-SHA
EDH-DSS-DES-CBC-SHA
DES-CBC-SHA
EXP1024-DHE-DSS-RC4-SHA
EXP1024-RC4-SHA
EXP1024-RC4-MD5
EXP-EDH-RSA-DES-CBC-SHA
EXP-EDH-DSS-DES-CBC-SHA
EXP-DES-CBC-SHA
EXP-RC2-CBC-MD5
EXP-RC4-MD5

SHA - 是指用SHA1
AES128 - 使用的模式是CBC
这里有两种算法必须支持:
NID_md5 和 NID_sha1必须支持, 不能屏蔽掉, 否则会报错.


/////////////////////////////////////////////
接下来是使用client证书
//int SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type)
SSL_CTX_use_certificate_file(ctx, CLIENTCERT, SSL_FILETYPE_PEM)
返回值, <= 0表示错误.
过程:
1 读取client证书, 证书存放格式: pem (der编码的BASE64表示)
  PEM_read_bio_X509, 解析为X509格式
2 SSL_CTX_use_certificate
  2.1 ssl_set_cert
  证书中, pub_key存放位置: X509::cert_info::key, 格式:X509_PUBKEY
  可以使用函数: X509_PUBKEY_get获得, 返回EVP_KEY, 比较关键了.
  (key = x->cert_info->key)
  type=OBJ_obj2nid(key->algor->algorithm); type:6, 对应NID_rsaEncryption
  key->algor->algorithm是NID_rsaEncryption对应的OBJECT DER编码
  然后调用EVP_PKEY_new, 这些都调不到engine中. go on -->

  der = key->public_key->data存放的是rsa public key的der编码.
  length = key->public_key->length
  调用: d2i_PublicKey(type, &ret, &p, (long)j), ret为EVP_KEY类型
 
  ret->save_type=type; // ret类型EVP_KEY
  ret->type=EVP_PKEY_type(type);
  这里直接ret->pkey.rsa=d2i_RSAPublicKey(NULL, der,length);
 
  // 从这里看, 已经得到EVP_KEY了, 但并没有调用与Engine相关的函数

  然后将得到EVP_KEY指针存放在x->cert_info->key->pkey中, 并返回该指针
  这里我们记录下整个x509结构的内容:
    -    x    0x019e8cb8
    -    cert_info    0x019e8d30
    +    version    0x019e7b30
    +    serialNumber    0x019e8d70
    +    signature    0x007c9798
    +    issuer    0x019e8d98
    +    validity    0x007c4070
    +    subject    0x019e7980
    -    key    0x019e79d0
    +    algor    0x019e79f8
    +    public_key    0x019e7a18
    -    pkey    0x019e8ee8
        type    0x00000006
        save_type    0x00000006
        references    0x00000002
    -    pkey    {...}
    +    ptr    0x019e8f18 ""
    -    rsa    0x019e8f18
        pad    0x00000000
        version    0x00000000
    +    meth    0x0063e958 rsa_pkcs1_eay_meth
        engine    0x00000000
    +    n    0x019e9008
    +    e    0x019e90d8
    +    d    0x00000000
    +    p    0x00000000
    +    q    0x00000000
    +    dmp1    0x00000000
    +    dmq1    0x00000000
    +    iqmp    0x00000000
    -    ex_data    {...}
    +    sk    0x00000000
        dummy    0xbaadf00d
        references    0x00000001
        flags    0x00000006
    +    _method_mod_n    0x00000000
    +    _method_mod_p    0x00000000
    +    _method_mod_q    0x00000000
    +    bignum_data    0x00000000 ""
        blinding    0x00000000
        mt_blinding    0x00000000
    +    dsa    0x019e8f18
    +    dh    0x019e8f18
        ec    0x019e8f18
        save_parameters    0x00000001
    +    attributes    0x00000000
    +    issuerUID    0x00000000
    +    subjectUID    0x00000000
    +    extensions    0x019ea208
    +    sig_alg    0x019e7a40
    +    signature    0x019e7a60
        valid    0x00000000
        references    0x00000001
    +    name    0x019ea868 "/C=CN/ST=Chongqing/O=YZ/OU=YZ/CN=sslsocketclient/emailAddress=sslsocketclient@yunzhen.com"
    +    ex_data    {...}
        ex_pathlen    0xffffffff
        ex_pcpathlen    0x00000000
        ex_flags    0x00000000
        ex_kusage    0x00000000
        ex_xkusage    0x00000000
        ex_nscert    0x00000000
    +    skid    0x00000000
    +    akid    0x00000000
        policy_cache    0x00000000
    +    sha1_hash    0x019e8cfc ""
    +    aux    0x00000000
    可以看到, evp_pkey中的rsa的结构, 只有n, e, 没有私钥的部分. 合理, 构造只具有公钥部分的rsa完全没问题.
   
    然后会检测证书中是否包含有私钥, 如果有私钥, 会将私钥部分拷贝到x->cert_info->key->pkey中.
    在使用硬件加密卡时, 证书存放在加密卡的存储器中, 证书一般不包含私钥, key pair另外存放在保密区域.
    所以, 这里不会出现这种情况. 这里说一下https通信, 我们在生成测试证书来搭建https通信时, 会
    将私钥捆绑在个人证书上, 然后导入人证书存放区域, 靠密码保护, 与此不同. (没有研究使用usb key的情况)
    因为ms的证书管理器没有单独导入key pair的地方(可以运行certmgr来看一下, 确实没有单独管理私钥的地方).
    https通信时, 一般需要用到P12格式的个人证书, 这个P12个人证书是将私钥与X509证书捆绑在一起的.
    openssl参考命令: openssl pkcs12 -export -clcerts -in crunch.cer -inkey crunchkey.pem -out crunch.p12

    然后会执行EVP_KEY_free, 把我吓一跳, 怎么会就free了呢, 才想起前面在返回EVP_PKEY前, EVP_PKEY
    的引用计数被加了一次. 原来这里只是减少引用计数而已;

    然后将cert->pkeys[i == 0].x509 = 传入的x509证书
    最后将cert->key指向&pkeys[i == 0].
    对了, 这个i是ssl_cert_type获得的结果, 如果是rsa证书, i为0, dsa对应2, ecc对应5, 其他的有错误

    ////////////////////////////////////////////

    接下来是需要调用到SSL_CTX_use_PrivateKey, 前面看到因为在加载证书时, 因为证书上没有捆绑私钥, 所以
    这里要单独执行SSL_CTX_use_PrivateKey.
    首先是调用PEM_read_bio_PrivateKey读取私钥证书. 然后再调用SSL_CTX_use_PrivateKey.
    key可能是PEM格式的, 也可能是直接存为DER格式的, 都可以, 只是读取key文件的函数要变一下;
    SSL_CTX_use_PrivateKey(SSL_CTX*, EVP_KEY*);
    SSL_CTX_use_PrivateKey内部主要调用ssl_set_pkey(CERT*, EVP_KEY*);
    在调用X509_get_pubkey, 获得的是前面提到的cert->pkes[i== 0].x509->cert_info->key->pkey
    然后调用EVP_PKEY_copy_parameters, 这个函数误导我了, 以为这里就是拷贝私钥部分, 只有当私钥type类型
    为EVP_PKEY_EC或EVP_PKEY_DSA时, 这个函数才拷贝私钥部分; RSA类型的在后面.
   
    读取的privatekey实际上是一个keypair, 直接将其赋值给了cert->pkeys[i == 0].privatekey.
    而在校验pubkey和privatekey时, 实际上是比较keypair中的公钥部分与pubkey中的公钥部分是否相等.

OK, SSL连接的初始化工作已经做完了. 下面开始socket连接, 接下来就是SSL的握手部分了.

原文地址:https://www.cnblogs.com/crunchyou/p/2872722.html