# 使用非对称加密的对称密钥分发

一种最朴素的密钥分发思路是这样的 (Merkle, 1979):

Alice 首先生成一个公私钥对{PUa,PRa}\{PU_a,PR_a\},将公钥PUaPU_a 和自己的身份标识符IDAID_A 发送给 Bob。Bob 生成一个会话密钥KsK_s,使用 Alice 发来的公钥PUaPU_a 加密,将密文E(PUa,ks)E(PU_a,k_s) 发送给 Alice。Alice 使用自己的私钥解密就能够得到共享密钥。

很明显这是一个能够防窃听的协议,但也很明显,这个协议会受到中间人攻击。

一个中间人攻击者 Eve 可以通过如图的中间人攻击获取到 Alice 和 Bob 的会话密钥。也就是说这条协议仅在被动的窃听攻击下是安全的。

NEED78 (Needham&Schroeder, 1978) 提出了一种抗主动和被动攻击的密钥分发方案,这个方案是基于 Alice 和 Bob 已经使用了安全的方式交换了密钥的前提下的。也就是,Alice 和 Bob 已经拥有了对方的公钥。

Alice 使用 Bob 的公钥加密自己的身份标识和一个 Nonce 值N1N_1 发送给 B,N1N_1 的存在也就使得这个数据包不能够被重放攻击。接下来,Bob 解密得到其中的N1N_1 并且再生成一个 Nonce 值N2N_2,将N1N2N_1||N_2 发送给 Alice,这个过程保证了 Bob 可以正常解密 Alice 发来的数据且其他人无法正确解密这条数据。同时,Nonce 值N2N_2 也保证了这条消息的不可重放。

Alice 接下来将解密得到的 Nonce 值N2N_2 加密发送给 Bob—— 这个过程保证了 Alice 身份的可靠性,因为只有 Alice 才能解密使用PUaPU_a 加密的消息。

在最后,Alice 生成一个会话密钥KsK_s,并使用自己的私钥进行签名得到E(PRa,Ks)E(PR_a,K_s)(这保证了只有 Alice 才能够发送这条消息),并使用 Bob 的公钥加密发送给 Bob。

# 公钥分发

上面的协议是抗主动和被动攻击的,但是一个很明显的问题在于它需要预先实现公钥交换。

最朴素的思路就是公开发布,也就是说发送方向接收者或者全体用户广播自己的公钥。很显然,如果一个攻击者 Eve 伪造成 Alice 公布了自己的公钥PUePU_e,那么在 Alice 发现并纠正这个问题之前,通信者 Bob 很可能错误地使用 Eve 的公钥去加密消息。

比较可行的方式还是需要去借助于可信的第三方参与者。

一种方式是维护公钥目录。公钥管理员会维护一个公钥目录,其中以 用户:公钥 的形式存储了各个用户的公钥。当用户需要进行通信时,可以直接访问这个公钥目录去获取其中存储的公钥。如果用户需要添加、修改或者删除公钥,则需要通过安全的手段向公钥管理员验证自己的身份(安全的信道,或者干脆线下窗口拿着身份证户口本办理)。

这种方式的安全性依赖于公钥目录自身的安全性 —— 如果公钥目录自己的私钥被破解,那么整个系统的安全性便会被直接破坏。

一个更加严格控制公钥分发的思路是公钥授权。POPE79 (Popek&Kline, 1979) 给出了一种典型的公钥授权分发方案。

严格地说,这里负责公钥授权的机构并不能叫做 KDC。不过为了方便就这样吧(

<!-- @startuml
skinparam sequenceMessageAlign center
participant Alice as A
participant KDC as K
participant Bob as B
A -> K : <latex>request||T_1</latex>
K -> A : <latex>E(PR_{auth},[PU_b||request||T_1])</latex>
A -> B : <latex>E(PU_b,[ID_A||N_1])</latex>
B -> K : <latex>request||T_2</latex>
K -> B : <latex>E(PR_{auth},[PU_a||request||T_2])</latex>
B -> A : <latex>E(PU_a,[N_1||N_2])</latex>
A -> B : <latex>E(PU_b,N_2)</latex>
@enduml -->

KDC 保存有一个公私钥对(PUauth,PRauth)(PU_{auth},PR_{auth})。Alice 如果需要和 Bob 通信的话,需要经过下面的步骤:

Alice 发送一条带有时间戳的请求消息给 KDC。KDC 找到 Bob 的公钥,并将其和 Alice 发来的信息组合起来用自己的私钥签名发送给 Alice。这个步骤的意义在于能够让 Alice 验证这条消息来自 KDC,并且这条消息也是回应自己刚才的请求。
接下来 Alice 将自己的身份标识和一个 Nonce 值N1N_1 发送给 Bob。这条步骤和前面 NEED78 的第一步一致。
Bob 同样采用类似前面的步骤向 KDC 请求 Alice 的公钥并得到回应。接下来的两个步骤和 NEED78 的第 2、3 步骤一致。

而在双方缓存了对方的公钥的情况下,POPE79 的步骤可以不必重复进行。

这个方案的缺陷在于两个用户若需要通信则就要向 KDC 请求对方的公钥。这个过程使得 KDC 成为了整个系统通信效率的瓶颈。

Kohnfelder 提出了使用证书的替代方案。证书包含了公钥和公钥所有者的标识,并有一个可信的第三方机构进行签名。一个用户 Alice 将自己的公钥交给管理员(CA),得到证书之后便可以公开证书。需要使用 Alice 的公钥的用户只需要获得 Alice 的证书并验证有效性就可以和 Alice 进行通信。

Kohnfelder (Kohnfelder, 1978) 指出公钥证书应当有这些特点:

  • 通信方可以读取证书并确定拥有者的身份和公钥
  • 通信方可以验证证书是由证书管理员颁发的
  • 只有证书管理员(CA)才能生成和更新证书
  • 通信方可以验证证书的有效性(Denning, 1983)

用户 Alice 的证书格式满足CA=E(PRauth,[TIDAPUa])C_A=E(PR_{auth},[T||ID_A||PU_a]),其中TT 是一个时间戳,IDAID_A 是 Alice 的身份标识,PUaPU_a 是 Alice 的公钥,CA 的私钥为PRauthPR_{auth}

Bob 需要验证证书时,仅需要使用 CA 的公钥进行验签:D(PUauth,CA)=TIDAPUaD(PU_{auth}, C_A)=T||ID_A||PU_a

那么整个证书交换的流程也就简化为了下面的步骤:

  • 从 CA 获取证书


<!-- @startuml
skinparam sequenceMessageAlign center
participant Alice as A
participant CA as CA
participant Bob as B
A -> CA : <latex>PU_a</latex>
CA -> A : <latex>C_A</latex>
B -> CA : <latex>PU_b</latex>
CA -> B : <latex>C_B</latex>
@enduml -->

  • 证书交换


<!-- @startuml
skinparam sequenceMessageAlign center
participant Alice as A
participant Bob as B
A -> B : <latex>C_A</latex>
B -> A : <latex>C_B</latex>
@enduml -->

以上是基于公钥证书的方案,实际上现在使用的大多也是这个方案。

# X.509

X.509 是证书体系的一个重要的标准。在现在的各种安全体系中都被广泛采用,也可以说是当前互联网的基石之一。X.509 标准中证书的生成和验证基本上是按照下面的逻辑来进行:

一个符合 X.509 格式的证书可以采用下面的形式来定义:

CA<<A>>=CA{V,SN,AI,CA,UCA,A,UA,Ap,TA}CA<<A>>=CA\{V,SN,AI,CA,UCA,A,UA,Ap,T^A\}

这表示了一个由CACA 发布给AA 并由CACA 签名的证书,其中各个字段的定义如下:

  • VV:证书的版本号,默认为 1。存在发行商唯一标识或主体唯一标识则为 2。存在一个或多个扩展则为 3。
  • SNSN:证书的序号,用来在 CA 处唯一标识一个证书。
  • AIAI:标识用于证书签名的算法。
  • CACA:证书颁发机构 CA 的名称。
  • UCAUCA:CA 的可选唯一标识。
  • AA:用户 A 的名称。
  • UAUA:用户 A 的可选唯一标识。
  • ApAp:用户 A 的公钥。
  • TAT^A:证书的有效时间。

以 github pages 的这个证书为例:

$ openssl x509 -noout -text -in _.github.crt  
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            08:b4:bf:75:7f:db:28:dd:f3:b8:c4:c9:35:7c:5a:49
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
        Validity
            Not Before: Apr  7 00:00:00 2022 GMT
            Not After : Apr  7 23:59:59 2023 GMT
        Subject: C = US, ST = California, L = San Francisco, O = "GitHub, Inc.", CN = *.github.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:bc:aa:66:36:ad:93:68:e7:aa:25:a6:a7:a7:ae:
                    ......

可以查看具体的细节。

# 信任链

如果有非常多的用户,那么从一个 CA 处获得证书会变得非常困难且不可靠,而一个解决方案是设置多个 CA,用户的证书由某一个 CA 进行签发。但在这种情况下难免会出现一个问题:当 Alice 和 Bob 通信时,Alice 的证书由CA1CA_1 签发,而 Bob 的证书由CA2CA_2 签发,如果 Alice 不能安全获取CA2_的公钥则不能验证 Bob 的证书是否合法。

那么这就需要几个 CA 之间能够相互交换证书:Alice 从CA1CA_1 处获得由CA1CA_1 签名的CA2CA_2 的证书,并从中获取CA2CA_2 的公钥,再用来验证由CA2CA_2 签名的 Bob 的证书。这也就是证书链,可以表示为:

CA1<<CA2>>CA2<<Bob>>CA_1<<CA_2>>CA_2<<Bob>>

当然很明显,为了 Bob 能够反向验证 Alice 的证书合法,CA2CA_2 也应该有其签名的CA1CA_1 的证书。

证书链也可以延长,构成一个长度为NN 的证书链:

CA1<<CA2>>CA2<<CA3>>CA3<<CA4>>...CAN1<<CAN>>CAN<<Bob>>CA_1<<CA_2>>CA_2<<CA_3>>CA_3<<CA_4>>...CA_{N-1}<<CA_N>>CA_N<<Bob>>

X.509 中存在一个 CA 的层次结构:

每个 CA 有两种证书,前向证书后向证书CAXCA_X 的前向证书是由其他 CA 生成的CAXCA_X 的证书,而CAXCA_X 的后向证书是由CAXCA_X 生成的其他 CA 的证书。

当 Alice 和 Bob 通信时,只需沿途展开各个 CA 的证书就可以获取对方的公钥。

# 撤销证书

每个证书都是存在有效期的,当有效期结束之后证书将不会受到信任。同时还有其他的原因会导致 CA 提前将证书撤销:

  • 用户私钥不安全
  • 用户不再信任 CA
  • CA 证书不安全

每个 CA 会维护一个证书撤销表(CRL),其中记录了证书撤销的信息。用户收到证书后会验证证书是否已经被撤销。

# 公钥基础设施

公钥基础设施(PKI)是用于管理证书和公私钥对的策略、流程、平台、软件、工作站等,能够颁发、维护和撤销公钥证书。PKI 要求 CA 和用户能够满足上面 Kohnfelder 和 Denning 的要求。

整个 PKI 的架构基本如下:

其中包括了:

  • 终端实体:用户、设备、程序或其他能够被证书认证的实体。
  • 证书颁发机构(CA):用于创建和分发公钥证书的机构,同时也负责维护证书撤销表。
  • 注册机构(RA):可选的,用于实体和 CA 之间的注册、认证的功能,可以减轻 CA 的负担。
  • 证书储存库:用于存储检索 PKI 中的证书、CRL 等信息。
  • 信赖方:用于依靠证书数据进行决策的用户。

# 参考资料

[1] 网络安全基础 —— 应用与标准(第六版),William Stallings.

[2] 密码编码学与网络安全 —— 原理与实践(第八版),William Stallings.

此文章已被阅读次数:正在加载...更新于