数字签名与证书

在上一篇文章《非对称密钥沉思系列(2):聊聊RSA与数字签名》中,我们聊了关于数字签名的概念,对比了数字签名与MAC、哈希值之间的关系。

在这篇文章中,我们聊聊数字签名在身份认证中的使用场景。

数字证书,网络世界的身份证

很多网站或应用,在发布到互联网之前,都需要申请一份证书,以证明此网站是合法的。

对于数字证书,我们可以认为它是一种网络世界的身份证,一般它是由权威机构中心发行、能提供在互联网上进行身份验证的一种权威性电子文档,人们可以在互联网交往中用它来证明自己的身份和识别对方的身份。

比如我们在访问https://www.baidu.com时,在浏览器地址栏左上角可以看到一个🔐形状的图案,点击就可以查看这个网站的证书信息:

image.png

Clipboard_Screenshot_1766572001

在使用浏览器访问网站的过程中,浏览器可以抽象理解为第三方的认证机构,https://www.baidu.com可以理解为被颁发证书的机构,而颁发此证书的机构,我们可以将其称之为证书颁发机构,在现实世界中一般统称为CA

证书就如身份证,证明"该公钥对应该网站"。

证书的组成

一般来说证书包含以下内容:

  • 证书的发布机构
  • 证书的有效期
  • 公钥
  • 证书所有者(Subject)
  • 签名所使用的算法
  • 指纹以及指纹算法等
  • 证书信息
  • 被颁发证书机构的公钥

证书信息

被颁发证书机构的公钥

证书模型的简单抽象

在这篇文章中,我们不对证书的每个字段详细展开说明。

这篇文章围绕证书与身份证明的关键问题而展开讨论,因此我们把证书简单的进行如下抽象:

抽象模型

抽象模型

这里我们假设证书包含一下几个简单信息:

  • 颁发者信息
  • 被颁发者信息
  • 被颁发者公钥
  • 其他的附加信息

并且在这篇文章中,我们忽略证书的信任链,仅抽象出三个角色:

  • 证书颁发机构
  • 被颁发证书的机构
  • 第三方验证方

证书颁发机构

证书颁发机构

证书颁发机构,其公钥可以被所有用户或机构获取。

这里我们默认,证书颁发机构是独立且权威的。

被颁发证书的机构或应用

被颁发证书的机构或应用

被颁发证书的机构或应用,其也是面向所有互联网用户提供服务的,他的公钥也是可以被所有用户任意获取的。

并且被颁发证书的机构,他所提供的服务也是可以被所有用户自由访问的,因此,任意访问该应用的用户,都是其证书的校验者。

简单的证书应用模拟

还是基于RSA的模拟实现

在这篇文章中,我们还是基于经典的RSA非对称秘钥算法进行实验。

在开始证书的模拟实现之前,我们先回顾下RSA签名的特性。

密钥生成

我们定义了如下的RSA密钥对生成逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def generate_rsa_keypair(key_size: int = 2048, exponent: int = 65537) -> Tuple[RSAPublicKey, RSAPrivateKey]:
    """参考文档: https://www.cnblogs.com/caidi/p/14794952.html"""
    private_key = rsa.generate_private_key(
        public_exponent=exponent,
        key_size=key_size,
        backend=default_backend()
    )
    public_key = private_key.public_key()
    # print("pubkey: e={}, n={}".format(public_key.public_numbers().e, public_key.public_numbers().n))
    # print("prikey: d={}, n={}".format(private_key.private_numbers().d, private_key.public_key().public_numbers().n))
    return public_key, private_key

默认密钥大小为2048比特,也就是256字节。

密钥长度特性

RSA的密钥长度,无论从公钥还是私钥中都可以获取到,其以比特长度来进行表达。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def test_key_size(self):
    """
    同一对公私钥,其密钥长度是一致的
    无论是公钥加密后的密文数据长度,还是私钥签名后的签名数据长度,都与密钥长度保持一致,都是密钥长度个字节数
    密钥长度的大小,与n值需要的比特数是一样的
    """
    pub_key, pri_key = rsa_base.generate_rsa_keypair()
    print("pub_key key size:{}, pri_key key size:{}".format(
        pub_key.key_size, pri_key.key_size))
    self.assertEqual(pub_key.key_size, pri_key.key_size)
    self.assertEqual(pub_key.key_size, pub_key.public_numbers().n.bit_length())
    self.assertEqual(pri_key.key_size, pub_key.public_numbers().n.bit_length())

密文长度特性

密钥对的大小决定后,其无论加密还是签名的密文分组长度也就决定了

由于RSA也是分组加密模式,因此,每个分组的长度与密钥的长度保持一致。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def test_rsa_cipher_size(self):
    pub_key, pri_key = rsa_base.generate_rsa_keypair()
    plain: bytes = b"hello,world"
    
    cipher = pub_key.encrypt(
        plaintext=plain,
        padding=padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA256()),
            algorithm=hashes.SHA256(),
            label=None
        )
    )
    self.assertEqual(len(cipher), pub_key.key_size // 8)
    
    signature = pri_key.sign(
        data=plain,
        padding=padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        algorithm=hashes.SHA256()
    )
    self.assertEqual(len(signature), pub_key.key_size // 8)

在明文长度不超过分组长度时,其密文长度和签名值长度保持为256字节。

关于RSA加密时明文最长长度,可以参考签名的文章:《非对称密钥沉思系列(1):RSA专题之PKCSv1.5填充模式下的选择性密文攻击概述》中的推理。

证书的模拟生成

证书内容的定义

在前面我们提到过,我们对证书内容进行简单的抽象,并不完全复原证书原本的各种字段。

我们的证书现在被抽象为:

抽象的证书内容

在实现上我们定义以下逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
SUBJECT_KEY: str = 'subject'
AWARDED_PUB_KEY: str = 'awarded_pub_key'
ISSUER_NAME_KEY: str = 'issuer_name'
HASH_ALG_KEY: str = 'hash_alg'
HASH_NAME_KEY: str = 'hash_name'
HASH_BLOCK_SIZE_KEY: str = 'block_size'
HASH_DIGEST_SIZE_KEY: str = 'digest_size'
SIGNATURE_KEY: str = 'signature'

def mock_serialize_cert_content(subject: str, awarded_pub_key: RSAPublicKey, 
                               issuer_name: str, hash_alg: hashes.HashAlgorithm) -> bytes:
    """
    首先将证书内容结构化为json对象,然后将此json对象编码为字节流
    Args:
        hash_alg: 签名使用的hash算法
        subject: 证书的主题
        awarded_pub_key: 被颁发者的公钥
        issuer_name: 颁发机构
    Returns:
        json对象编码为字节流返回
    """
    # 对公钥做base64编码
    meta_data: Dict[str, str] = {
        SUBJECT_KEY: subject,
        AWARDED_PUB_KEY: pub_key_base64_encode(awarded_pub_key),
        ISSUER_NAME_KEY: issuer_name,
        HASH_ALG_KEY: json.dumps({
            HASH_NAME_KEY: hash_alg.name,
            HASH_BLOCK_SIZE_KEY: hash_alg.block_size,
            HASH_DIGEST_SIZE_KEY: hash_alg.digest_size
        })
    }
    # print("cert raw meta data:{}".format(meta_data))
    meta_data_bytes = json.dumps(meta_data).encode(encoding='utf-8')
    return meta_data_bytes

def mock_deserialize_cert_content(raw_data: bytes) -> Dict[str, str]:
    raw_str = raw_data.decode(encoding='utf-8')  # 转换为string
    raw_dict: Dict[str, str] = json.loads(raw_str)  # 反序列化为json对象
    return raw_dict

其中需要单独说明的是,关于被颁发者公钥的编码,这里我们自己定义为,其是公钥数据按照PEM格式进行序列化后再进行base64编码的数据,其实现可以定义为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def pub_key_base64_encode(pub_key: RSAPublicKey) -> str:
    p_bytes = pub_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    b64_bytes = base64.b64encode(p_bytes)
    return b64_bytes.decode(encoding='utf-8')

def pub_key_base64_decode(pub_key_b64: str) -> RSAPublicKey:
    pub_key_raw_bytes = base64.b64decode(pub_key_b64)
    public_key = serialization.load_pem_public_key(
        pub_key_raw_bytes, backend=default_backend())
    return public_key

证书签发的模拟

证书签发

证书签发,是基于证书内容进行签名的过程。

我们将其模拟为以下实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def mock_create_cert(subject: str, awarded_pub_key: RSAPublicKey, 
                    issuer_pri_key: RSAPrivateKey, issuer_name: str, 
                    hash_alg: hashes.HashAlgorithm) -> bytes:
    """
    模拟颁发证书,这里的证书是字节形式的,后面可以根据不同的格式需要进行编码
    Args:
        hash_alg: 签名使用的hash算法
        subject: 证书的主题
        awarded_pub_key: 被颁发者的公钥
        issuer_pri_key: 颁发者的私钥
        issuer_name: 颁发机构
    Returns:
        证书信息,由证书内容 + 签名组成
    """
    meta_data_bytes = mock_serialize_cert_content(
        subject, awarded_pub_key, issuer_name, hash_alg)
    # print("meta data:{}".format(str(meta_data_bytes)))
    signature = rsa_sign.sign_raw_message(
        issuer_pri_key, meta_data_bytes, hash_alg)
    # print("signature:{}".format(signature))
    return meta_data_bytes + signature

这里可以看到最终生成的证书内容,其实是原始证书内容 + 签名值的简单拼装:

模拟的证书内容

证书颁发机构的模拟实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class MockIssuer(object):
    """
    模拟的证书颁发机构
    用于给下游机构颁发证书
    证书颁发机构有自己的公私钥对
    """
    def __init__(self, this_issuer_name: str, 
                 this_issuer_hash_alg: hashes.HashAlgorithm = hashes.SHA256()):
        self.__root_issuer_pub_key, self.__root_issuer_pri_key = rsa_base.generate_rsa_keypair()
        self.__issuer_name = this_issuer_name
        self.__issuer_hash = this_issuer_hash_alg

    @property
    def pub_key(self) -> RSAPublicKey:
        return self.__root_issuer_pub_key

    @property
    def issuer_name(self) -> str:
        return self.__issuer_name

    @property
    def issuer_hash(self) -> hashes.HashAlgorithm:
        return self.__issuer_hash

    def mock_issue_cert(self, subject: str, awarded_pub_key: RSAPublicKey) -> bytes:
        """
        为下游主体颁发证书
        Args:
            subject: 证书主题,可以与被颁发者有关,颁发者自行填写
            awarded_pub_key: 被颁发者的公钥
        Returns:
            证书信息,字节流形式,未编码
        """
        return mock_create_cert(subject, awarded_pub_key, 
                               self.__root_issuer_pri_key, 
                               self.__issuer_name, self.__issuer_hash)

被颁发证书机构的模拟

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class MockAwarded(object):
    """
    模拟被颁发证书的主体
    这个机构将会获取来自于证书颁发机构的证书信息
    同时它将会为第三方验证者签名数据供第三方验证者验证,以此证明自己确实持有私钥
    """
    def __init__(self, awarded_name: str):
        # # 被颁发主体自己的公钥和私钥
        self.__awarded_pub_key, self.__awarded_pri_key = rsa_base.generate_rsa_keypair()
        self.__awarded_name = awarded_name  # 被颁发主体的名称
        self.__awarded_cert = bytes([0x00 for _ in range(256)])  # 由证书颁发机构颁发的证书
        self.__default_hash_alg = hashes.SHA256()

    @property
    def name(self):
        return self.__awarded_name

    @property
    def pub_key(self) -> RSAPublicKey:
        return self.__awarded_pub_key

    @property
    def awarded_cert(self) -> bytes:
        return self.__awarded_cert

    @awarded_cert.setter
    def awarded_cert(self, cert: bytes):
        self.__awarded_cert = cert

    def sign_data_for_verifier(self, data: bytes) -> Dict[str, Any]:
        """
        为第三方验证者签名数据,此签名数据将会被第三方验证者验证
        Args:
            data: 第三方验证这提供的随机数据
        Returns:
            返回验证的数据,格式为字典结构:
            {
                'signature': b'1234567890',  # 字节流数据
                'hash_alg': hashes.SHA256()  # hashes.HashAlgorithm类型
            }
        """
        signature = rsa_sign.sign_raw_message(
            self.__awarded_pri_key, data, self.__default_hash_alg)
        return {
            SIGNATURE_KEY: signature,
            HASH_ALG_KEY: self.__default_hash_alg
        }

证书验证的几个关键问题

在完成证书的颁发后,对证书与证书持有者的身份校验,是证书应用的关键。

在前面的逻辑中,我们已经完成以下几个步骤:

验证证书前的铺垫

验证证书前的铺垫

  • 证书颁发机构公开自己的公钥
  • 被颁发证书的机构公开自己的公钥
  • 证书颁发机构向被颁发机构颁发证书

关键问题:如何验证证书是合法的?

证书合法性验证

证书合法性验证

证书是否合法,是整个证书验证中最基础也是最核心的一个环节。

我们都知道,签名时,使用的是私钥,而验签使用的是公钥。

前面的铺垫中我们已经明确,证书颁发机构的公钥是公开的,任何人可以获取,因此,任意一个验证者,只要获取了证书颁发机构的公钥,就可以对证书本身进行验签,已验证此证书是否是被合法的机构签发的。

关键问题:如何验证证书是被正确持有的?

证书与持有者合法性验证

证书与持有者合法性验证

在验证了证书是被合法签发的之后,我们还有一个需要确认的点,那就是,这个证书,到底是不是被颁发给持有证书的这个人的。

因为,证书是可以被盗用的。虽然证书本身是合法签发的,但是被非法的人持有,也是有可能的

万幸我们的证书中包含了被颁发者的公钥,以及其他的一些信息。

因此,在验证证书时,我们需要解析证书的内容,并对部分字段单独与持有者提供的信息做校验。

在本篇中对证书内容进行拆分解析的逻辑可以实现为如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def mock_parse_cert(cert: bytes, key_len: int = 256) -> Tuple[Dict[str, Any], bytes]:
    """
    解析证书,将证书的签名值与证书内容解析出来
    Args:
        key_len: 用于签名的私钥长度,这个长度决定了签名值的长度
        cert:证书数据
    Returns:
        返回证书内容与数字签名值
    """
    if len(cert) < key_len:
        raise ValueError("invalid cert data, length invalid")
    split_index = len(cert) - key_len
    signature = cert[split_index:len(cert)]
    raw_data = cert[:split_index]
    try:
        cert_dict = mock_deserialize_cert_content(raw_data)
    except BaseException as e:
        print(e)
    else:
        return cert_dict, signature

关键问题:如何验证证书持有者是合法的?

持有者身份合法性验证

持有者身份合法性验证

证书可以被盗用,公钥其实也可以被盗用。

因此在证明了证书本身是合法的,并且证书的确是被颁发给某把公钥之后,我们还需要验证:当前被验证的这个人,是否持有公钥对应的私钥

第三方验证者的模拟实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class MockVerifier(object):
    """
    模拟第三方验证者,
    它在获取到某个主体的证书时,将会验证此证书是否的确是由颁发机构颁发的
    在验证了证书的真实性后,他还会要求证书获得者进行数据签名,然后由第三方验证者来验证签名
    """
    def __init__(self, awarded_pub_key: RSAPublicKey, 
                 awarded_sign_func: Callable[[bytes], Dict[str, Any]], 
                 issuer_name: str, issuer_pub_key: RSAPublicKey, 
                 issuer_hash_alg: hashes.HashAlgorithm):
        # 第三方验证者自己的公钥和私钥
        self.__verifier_pub_key, self.__verifier_pri_key = rsa_base.generate_rsa_keypair()
        
        # 第三方验证者在实例化时已经注册了颁发机构的:公钥、名称、哈希算法
        self.__issuer_pub_key = issuer_pub_key
        self.__issuer_name = issuer_name
        self.__issuer_hash_alg = issuer_hash_alg
        
        # 第三方验证者在实例化时已经注册了被验证方的公钥与签名函数指针
        self.__awarded_pub_key = awarded_pub_key
        self.__awarded_sign_func = awarded_sign_func

    def __verify_cert_signature(self, verified_cert_data: bytes, signature: bytes) -> bool:
        """
        验证证书与签名是否是一致的
        如果是一致的,则只说明证书本身是真实的,但是持有证书的人不一定是真实的
        Args:
            verified_cert_data: 待验证的明文数据
            signature: 待验证的签名值
        Returns:
            验签是否成功
        """
        return rsa_sign.verify_raw_message(
            pub_key=self.__issuer_pub_key,
            message=verified_cert_data,
            signature=signature,
            hash_alg=self.__issuer_hash_alg
        )

    def __verify_cert_content(self, cert_dict: Dict[str, Any]) -> bool:
        """
        验证证书内容与预期是否一致
        如果证书颁发机构与证书公钥都是一致的,只能说明证书是与此公钥绑定的
        但是,持有这个公钥的人不一定真的拥有私钥,因此还需要让其用私钥进行签名,然后验证方进行验签
        Args:
            cert_dict: 原始证书内容
        Returns:
            证书内容与期望是否相符
        """
        if cert_dict[ISSUER_NAME_KEY] != self.__issuer_name:
            """验证证书中的颁发机构名称与验证者持有的颁发机构名称是否一致"""
            print("issuer name not match:{} vs {}".format(
                cert_dict[ISSUER_NAME_KEY], self.__issuer_name))
            return False
            
        if cert_dict[AWARDED_PUB_KEY] != pub_key_base64_encode(self.__awarded_pub_key):
            """验证证书中的被颁发者公钥与验证者接受到的公钥是否一致"""
            print("awarded pub key not match")
            return False
            
        return True

    def verify_cert(self, cert_data: bytes) -> bool:
        """
        验证证书是否由颁发者颁发的
        验证证书是否由颁发者颁发给授予者的
        """
        try:
            cert_dict, signature = mock_parse_cert(cert_data, self.__issuer_pub_key.key_size // 8)
        except ValueError as e:
            # 如果证书解析失败则直接报错
            print(e)
            return False
        else:
            # 证书解析成功的话,则验证签名
            # cert_dict[HASH_ALG_KEY] = json.loads(cert_dict[HASH_ALG_KEY])
            verified_cert_data = json.dumps(cert_dict).encode(encoding='utf-8')
            if not self.__verify_cert_signature(verified_cert_data=verified_cert_data, signature=signature):
                return False
                
            if not self.__verify_cert_content(cert_dict=cert_dict):
                return False
                
            return True

    def verify_awarded(self) -> bool:
        """
        Returns:
            验证方生成随机数,让被验证方签名,然后使用被验证注册的公钥验签
        """
        random_data: bytes = bytes([random.randint(1, 255) for _ in range(64)])
        sign_dict = self.__awarded_sign_func(random_data)
        signature: bytes = sign_dict[SIGNATURE_KEY]
        hash_alg: hashes.HashAlgorithm = sign_dict[HASH_ALG_KEY]
        return rsa_sign.verify_raw_message(
            self.__awarded_pub_key, random_data, signature, hash_alg)

验证流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if __name__ == '__main__':
    issuer = MockIssuer('test_issuer', hashes.SHA256())
    awarded = MockAwarded('test_awarded')
    awarded.awarded_cert = issuer.mock_issue_cert('cert for {}'.format(awarded.name), awarded.pub_key)
    
    verifier = MockVerifier(awarded.pub_key, awarded.sign_data_for_verifier,
                           issuer.issuer_name, issuer.pub_key, issuer.issuer_hash)
    
    if verifier.verify_cert(awarded.awarded_cert) and verifier.verify_awarded():
        print("certification verified success!!!")

验证成功

总结

证书就如身份证,证明"公钥与某种身份的绑定"。

基于非对称秘钥的身份验证流程,是我们整个网络应用安全的基础。