ZYB ARTICLES REPOS

WebRTC中DTLS协议验证数字签名的方法

密钥交换及数字签名验证算法

主流的密钥交换算有: RSA和DH,但如果密钥泄露的话,RSA就不能保证前向安全,所以已经在TLS 1.3中禁用了。因此本文仅针对DH的一个交换算法ECDH和数字签名算法ECDSA为例介绍。

WebRTC中对证书(Certificate)的认证方法

一般来说数字证书是由公认可信的证书颁发机构(CA)来签发的,证书的接收者可以用来验证对方的身份。然而获得一个数字证书还是有很高的技术门槛的,在WebRTC中,通信的参与方通常是无法获得CA颁发的数字证书的。因此在WebRTC中基本只能使用自签名证书。使用自签名证书的话就没有CA来作信任背书,因此就需要解决自签名证书的信任问题。在RFC4572中定义了与SDP结合来认证证书的一个机制:首先WebRTC的参与方对自己生成的自签名数字证书求出hash值,通常称为证书指纹,然后把这个指纹加在SDP中。 一般形式为:

a=fingerprint:sha-256 B2:67:81:98:9E:4D:97:16:BD:09:9B:0A:12:82:50:69:59:CB:7C:8B:44:11:49:90:5E:AE:B3:08:E3:70:5F:5D

然后只需要保证SDP是通过安全的渠道交换即可。一般的,通过httpswss来完成交换。因为当参与方用httpswss协议时,就已经保证了通道的安全性。

证书的指纹的验证方法可以参看《WebRTC开发总结

解决了数字证书(Certificate)的信任问题,那就接下来就可以基于该数字证书进行数字签名和验证了。

典型的DTLS握手流程

RFC6347 4.2.4节第21页

   Client                                          Server
   ------                                          ------

   ClientHello             -------->                           Flight 1

                           <-------    HelloVerifyRequest      Flight 2

   ClientHello             -------->                           Flight 3

                                              ServerHello    \
                                             Certificate*     \
                                       ServerKeyExchange*      Flight 4
                                      CertificateRequest*     /
                           <--------      ServerHelloDone    /

   Certificate*                                              \
   ClientKeyExchange                                          \
   CertificateVerify*                                          Flight 5
   [ChangeCipherSpec]                                         /
   Finished                -------->                         /

                                       [ChangeCipherSpec]    \ Flight 6
                           <--------             Finished    /

DTLS 中 HelloVerifyRequest 是为防止 DoS 攻击增加的消息,在WebRTC应用中不太会出现这种情况,因此本文描述的协议从第二个ClientHello开始。

举例

例如,如下是一个DTLS密钥协商过程的抓包示例。点此下载示例PCAP文件。

主流程

客户端发起:ClientHello

服务器回复:ServerHelloCertificateServerKeyExchangeCertificateRequestServerHelloDone。同时可以看到服务器最终选择的密码套件为TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256

客户端回复:CertificateClientKeyExchangeCertificateVerifyChangeCipherSpec以及加密数据。

验证ServerKeyExchange的签名的方法

一般ServerKeyExchange是随在ServerHelloCertificate之后发送给客户端的。

如果协商的是采用ECDHE作为密钥交换算法,ECDSA作为身份认证算法的话,那么Certificate里的公钥就会参与对ServerKeyExchange。Signature即数字签名字段的值的计算。

具体方法是:

  1. ClientHello.RandomServerHello.RandomServerKeyExchange.ECDHParam组合在一起作为明文输入。

  2. 根据协商的签名Hash函数,对该明文输入求出其Hash值

  3. CertificatePublicKey用来和第2步中的Hash值一起按ECDSA算法求出数字签名

  4. 比较数字签名是否与ServerKeyExchange.Signature里的值一致,一致则验证成功,否则失败。

特别的,如果是服务器需要生成ServerKeyExchange.Signature里的值,只需要把第3步中的服务器的Certificate里的公钥换成对应的私钥即可。

以抓包举例来说:

ClientHello.Random = 00b0399166d69fbe26a62c37f5948948c6c6f8cfbb77ad7a2d90ce10ecddc510
ServerHello.Random = 60c205e468d2fd148fc9e3b0acdec4f5f23c6a5368fe8dc4ccf7bf2d7d0401f4
ServerKeyExchange.ECDHParam =  03-001d-20-55e5b7f1054e936ff64c49026c666974ccfb0fbea03af44021f2517a7a061c14

ServerKeyExchange.ECDHParam的详细结构可以参见下图

以上三部分数据组合起来构造一个求Hash值的明文。


var clientRandomDataStr string = `
0000   00 b0 39 91 66 d6 9f be 26 a6 2c 37 f5 94 89 48
0010   c6 c6 f8 cf bb 77 ad 7a 2d 90 ce 10 ec dd c5 10
`
var serverRandomDataStr string = `
0000   60 c2 05 e4 68 d2 fd 14 8f c9 e3 b0 ac de c4 f5
0010   f2 3c 6a 53 68 fe 8d c4 cc f7 bf 2d 7d 04 01 f4
`
var ecdhPubKeyDataStr string = `
0000   55 e5 b7 f1 05 4e 93 6f f6 4c 49 02 6c 66 69 74
0010   cc fb 0f be a0 3a f4 40 21 f2 51 7a 7a 06 1c 14
`

func makePlainText(clientRandom, serverRandom, ecdhParams []byte) []byte {
	p := make([]byte, 0)
	p = append(p, clientRandom...)
	p = append(p, serverRandom...)
	p = append(p, ecdhParams...)
	return p
}

// 获取随机数
clientRandomData := getData(clientRandomDataStr)
serverRandomData := getData(serverRandomDataStr)

// 获取ECDH的PubKey
ecdhPubKeyData := getData(ecdhPubKeyDataStr)

// 构造明文
plainTextData := makePlainText(clientRandomData, serverRandomData, append([]byte{0x03, 0x00, 0x1d, 0x20}, ecdhPubKeyData...))

然后就可以对上面的plainTextData求Hash值,由于本次抓包协商的函数是SHA-256,因此求Hash的代码如下:

	// 求plainTextData的SHA256.Hash
	var algo crypto.Hash
	algo = crypto.SHA256
	h := algo.New()
	h.Write(plainTextData)
	hashValue := h.Sum(nil)
	log.Printf("Message.SHA256.Hash: %x", hashValue)

其中,从wireshark复制出的字符串解析成十六进制的getData代码如下:

func getData(dataStr string) []byte {
	lines := strings.Split(dataStr, "\n")
	var data []byte
	for _, line := range lines {
		if len(line) <= 7 {
			continue
		}

		line = strings.TrimLeft(line, " ")
		line = line[7:]
		digits := strings.Split(line, " ")
		for _, digit := range digits {
			n := byte(0)
			fmt.Sscanf(digit, "%02x", &n)
			data = append(data, n)
		}
	}

	return data
}

对于Certificate里的数据可以用来解析成x509证书。

var certDataStr string = `
0000   30 82 01 16 30 81 bd a0 03 02 01 02 02 09 00 96
0010   23 4a 29 08 8f 8e 6e 30 0a 06 08 2a 86 48 ce 3d
0020   04 03 02 30 11 31 0f 30 0d 06 03 55 04 03 0c 06
0030   57 65 62 52 54 43 30 1e 17 0d 32 31 30 36 30 39
0040   31 32 33 30 32 31 5a 17 0d 32 31 30 37 31 30 31
0050   32 33 30 32 31 5a 30 11 31 0f 30 0d 06 03 55 04
0060   03 0c 06 57 65 62 52 54 43 30 59 30 13 06 07 2a
0070   86 48 ce 3d 02 01 06 08 2a 86 48 ce 3d 03 01 07
0080   03 42 00 04 96 8a e8 18 9a 0a 09 05 ea 12 be 4d
0090   50 88 d5 d5 04 f4 79 bb 61 1d 37 3c 5a 99 11 41
00a0   a7 db d9 32 35 0c 46 50 e3 a0 7d 20 02 23 d0 da
00b0   b7 de 56 f8 c6 a7 f1 0e 1b 1b d4 21 fc 86 8a d5
00c0   14 bf b0 64 30 0a 06 08 2a 86 48 ce 3d 04 03 02
00d0   03 48 00 30 45 02 20 30 6d d1 02 8f 20 05 00 f9
00e0   99 15 5c 24 b4 f6 d4 54 c3 13 ec 98 86 82 c6 22
00f0   12 35 81 d0 6c 4e d0 02 21 00 c1 6e ce 91 af 9f
0100   02 ba 7a b0 0e a0 d4 61 e5 e0 91 b5 33 83 f4 05
0110   77 61 c2 4d ef 40 b3 83 c3 36
`

certData := getData(certDataStr)
cert, err := x509.ParseCertificate(certData)
if err != nil {
  log.Printf("parse certificate failed: %v", err)
  return
}

log.Printf("PublicKeyAlgorithm: %v", cert.PublicKeyAlgorithm)

cert我们就可以得到数字证书的公钥,且根据PublicKeyAlgorithm的打印值可以得出公钥类型是ECDSA,所以可以得到ECDSA类型的公钥。

ecdhPubKey := cert.PublicKey.(*ecdsa.PublicKey)

最后我们拿出ServerKeyExchange.Signature的值,为验证数字签名做准备。

var ecdhSignatureDataStr string = `
0000   30 46 02 21 00 95 13 28 bb e2 ba ec 12 eb 45 5a
0010   db 42 f1 e2 ad 64 8c 40 6f db 29 ee ab 70 a0 97
0020   f9 49 b4 ce c4 02 21 00 cb 74 ab 0d 5a 8a a6 c4
0030   65 f4 4e c2 ed 36 bc 98 dc bc c3 6b fc 51 bf 20
0040   57 bf 42 27 52 ee 4a 28
`

ecdhSignatureData := getData(ecdhSignatureDataStr)

验证签名

至此,我们得到的参数有

  1. hashValue,即将要被公钥用来加密的明文

  2. ecdhPubKey,即用来加密hashValue的公钥

  3. ecdhSignatureData,即数字签名要验证的值

最终验证逻辑如下:

var signatureValid bool

signatureValid = ecdsa.VerifyASN1(ecdhPubKey, hashValue, ecdhSignatureData)
log.Printf("signature valid: %v", signatureValid)

其它验证方法一

与上述方法略微不同之处,就是会进一步解析ecdhSignatureData

type ecdsaSignature struct {
	R *big.Int
	S *big.Int
}

ecdhSignature := &ecdsaSignature{}
_, err = asn1.Unmarshal(ecdhSignatureData, ecdhSignature)
if err != nil {
  log.Fatalf("unmarshal signature failed %v", err)
}

if ecdhSignature.R.Sign() <= 0 {
  log.Fatalf("signature:R not valid")
}
if ecdhSignature.S.Sign() <= 0 {
  log.Fatalf("signature:R not valid")
}

signatureValid = ecdsa.Verify(ecdhPubKey, hashValue, ecdhSignature.R, ecdhSignature.S)
log.Printf("signature valid: %v", signatureValid)

其它验证方法二

与上述方法不同的是,本方法不用主动求Hash值,只需要传入求Hash的算法。

signatureValid = ecdsa.HashVerify(ecdhPubKey, plainTextData, ecdhSignature.R, ecdhSignature.S, crypto.SHA256)
log.Printf("signature valid: %v", signatureValid)

验证CertificateVerify里的数字签名的方法

要验证CertificateVerify.Signature里的签名值

第一步,服务器收集所有的handshake交互的数据,包括:

  1. 客户端发送的第一个ClientHello

  2. 服务器发送的 ServerHello、Certificate、ServerKeyExchange、CertificateRequest、ServerHelloDone

  3. 客户端发送的 Certificate、ClientKeyExchange

服务器把这些数据按收发的顺序收集在一起,并去除它们的recordlayer头部后,再最终组合成一块数据。

第二步,服务器对第一步得到的数据用协商的签名Hash函数求出其Hash值

第三步,使用客户端的Certificate里的公钥对上述Hash值进行数字签名运算

第四步,验证上述数字签名是否与客户端发送的CertificateVerify.Signature里的值是否一致,一致则验证通过,否则验证失败。

特别的,如果是客户端要生成CertificateVerify.Signature里的数字签名值,只需要把第三步中的客户端的Certificate里的公钥换成私钥即可。

用程序验证CertificateVerify的方法

第一步,先根据客户端和服务器之间收发的数据,组织出需要求Hash值的明文

var allDataStr []string = []string{

	// ClientHello
`
0000   16 fe ff 00 00 00 00 00 00 00 00 00 8e 01 00 00
0010   82 00 00 00 00 00 00 00 82 fe fd 00 b0 39 91 66
0020   d6 9f be 26 a6 2c 37 f5 94 89 48 c6 c6 f8 cf bb
0030   77 ad 7a 2d 90 ce 10 ec dd c5 10 00 00 00 18 cc
0040   a9 cc a8 c0 2b c0 2f c0 09 c0 13 c0 0a c0 14 00
0050   9c 00 2f 00 35 00 0a 01 00 00 40 00 17 00 00 ff
0060   01 00 01 00 00 0a 00 08 00 06 00 1d 00 17 00 18
0070   00 0b 00 02 01 00 00 23 00 00 00 0d 00 14 00 12
0080   04 03 08 04 04 01 05 03 08 05 05 01 08 06 06 01
0090   02 01 00 0e 00 05 00 02 00 01 00
`,

  // ServerHello、Certificate、ServerKeyExchange、CertificateRequest、ServerHelloDone
`
0000   16 fe fd 00 00 00 00 00 00 00 00 00 50 02 00 00
0010   44 00 00 00 00 00 00 00 44 fe fd 60 c2 05 e4 68
0020   d2 fd 14 8f c9 e3 b0 ac de c4 f5 f2 3c 6a 53 68
0030   fe 8d c4 cc f7 bf 2d 7d 04 01 f4 00 cc a9 00 00
0040   1c 00 17 00 00 ff 01 00 01 00 00 0b 00 02 01 00
0050   00 23 00 00 00 0e 00 05 00 02 00 01 00 16 fe fd
0060   00 00 00 00 00 00 00 01 01 2c 0b 00 01 20 00 01
0070   00 00 00 00 01 20 00 01 1d 00 01 1a 30 82 01 16
0080   30 81 bd a0 03 02 01 02 02 09 00 96 23 4a 29 08
0090   8f 8e 6e 30 0a 06 08 2a 86 48 ce 3d 04 03 02 30
00a0   11 31 0f 30 0d 06 03 55 04 03 0c 06 57 65 62 52
00b0   54 43 30 1e 17 0d 32 31 30 36 30 39 31 32 33 30
00c0   32 31 5a 17 0d 32 31 30 37 31 30 31 32 33 30 32
00d0   31 5a 30 11 31 0f 30 0d 06 03 55 04 03 0c 06 57
00e0   65 62 52 54 43 30 59 30 13 06 07 2a 86 48 ce 3d
00f0   02 01 06 08 2a 86 48 ce 3d 03 01 07 03 42 00 04
0100   96 8a e8 18 9a 0a 09 05 ea 12 be 4d 50 88 d5 d5
0110   04 f4 79 bb 61 1d 37 3c 5a 99 11 41 a7 db d9 32
0120   35 0c 46 50 e3 a0 7d 20 02 23 d0 da b7 de 56 f8
0130   c6 a7 f1 0e 1b 1b d4 21 fc 86 8a d5 14 bf b0 64
0140   30 0a 06 08 2a 86 48 ce 3d 04 03 02 03 48 00 30
0150   45 02 20 30 6d d1 02 8f 20 05 00 f9 99 15 5c 24
0160   b4 f6 d4 54 c3 13 ec 98 86 82 c6 22 12 35 81 d0
0170   6c 4e d0 02 21 00 c1 6e ce 91 af 9f 02 ba 7a b0
0180   0e a0 d4 61 e5 e0 91 b5 33 83 f4 05 77 61 c2 4d
0190   ef 40 b3 83 c3 36 16 fe fd 00 00 00 00 00 00 00
01a0   02 00 7c 0c 00 00 70 00 02 00 00 00 00 00 70 03
01b0   00 1d 20 55 e5 b7 f1 05 4e 93 6f f6 4c 49 02 6c
01c0   66 69 74 cc fb 0f be a0 3a f4 40 21 f2 51 7a 7a
01d0   06 1c 14 04 03 00 48 30 46 02 21 00 95 13 28 bb
01e0   e2 ba ec 12 eb 45 5a db 42 f1 e2 ad 64 8c 40 6f
01f0   db 29 ee ab 70 a0 97 f9 49 b4 ce c4 02 21 00 cb
0200   74 ab 0d 5a 8a a6 c4 65 f4 4e c2 ed 36 bc 98 dc
0210   bc c3 6b fc 51 bf 20 57 bf 42 27 52 ee 4a 28 16
0220   fe fd 00 00 00 00 00 00 00 03 00 25 0d 00 00 19
0230   00 03 00 00 00 00 00 19 02 01 40 00 12 04 03 08
0240   04 04 01 05 03 08 05 05 01 08 06 06 01 02 01 00
0250   00 16 fe fd 00 00 00 00 00 00 00 04 00 0c 0e 00
0260   00 00 00 04 00 00 00 00 00 00
`,

	// Client.Certificate
`
0000   16 fe fd 00 00 00 00 00 00 00 01 01 2c 0b 00 01
0010   20 00 01 00 00 00 00 01 20 00 01 1d 00 01 1a 30
0020   82 01 16 30 81 bd a0 03 02 01 02 02 09 00 b3 ae
0030   5a 54 b2 71 df 02 30 0a 06 08 2a 86 48 ce 3d 04
0040   03 02 30 11 31 0f 30 0d 06 03 55 04 03 0c 06 57
0050   65 62 52 54 43 30 1e 17 0d 32 31 30 36 30 39 31
0060   32 33 30 31 30 5a 17 0d 32 31 30 37 31 30 31 32
0070   33 30 31 30 5a 30 11 31 0f 30 0d 06 03 55 04 03
0080   0c 06 57 65 62 52 54 43 30 59 30 13 06 07 2a 86
0090   48 ce 3d 02 01 06 08 2a 86 48 ce 3d 03 01 07 03
00a0   42 00 04 c2 b8 5e 5f 4f c7 96 91 f4 9c 6a b8 11
00b0   29 25 9a 61 d7 2c 23 64 c4 0a 32 45 7e 12 d5 5d
00c0   7d e8 c9 34 ae c9 61 b3 77 01 d3 0b 8f 45 47 46
00d0   0b 19 21 81 ba 75 17 6b a6 57 72 00 c7 3f 6a 5c
00e0   13 60 c8 30 0a 06 08 2a 86 48 ce 3d 04 03 02 03
00f0   48 00 30 45 02 20 5f 9f 57 05 3c 12 d1 c5 30 33
0100   65 fa c6 62 d1 f5 3d a0 e4 c6 ac 8e 71 8f c8 c4
0110   29 8c 6f 7e 01 d2 02 21 00 c1 6a 90 30 e1 46 41
0120   cf f4 b1 f5 f4 f8 3e ea ac 7f 15 d8 8c 06 2c c3
0130   32 9a af c9 f7 50 42 ba 9d
`,

	// Client.ClientKeyExchange
`
0000   16 fe fd 00 00 00 00 00 00 00 02 00 2d 10 00 00
0010   21 00 02 00 00 00 00 00 21 20 79 69 6f c1 f6 9b
0020   ed c5 89 82 0b 02 cd 1c c9 4e 29 32 e9 09 dc b0
0030   f2 83 44 d4 44 67 6b 9a a0 05
`,
}

将这些wireshark数据字符串转换成十六进制数据

allDatas := make([][]byte, 0)
for _, ds := range allDataStr {
  allDatas = append(allDatas, getData(ds))
}

再把所有十六进制数据组合到一起到allData里。需要注意的是,组合的时候,会把recordlayer层的头部去掉。

allData := make([]byte, 0, 8192)
for _, data := range allDatas {
  for pos := 0; pos < len(data); {
    // 读取每个record里的数据长度
    dataLen := dtls.Bin.Uint16(data[pos+11:])

    // 只取record里的数据,不要recordlayer层
    pos += 13
    allData = append(allData, data[pos:pos+int(dataLen)]...)

    pos += int(dataLen)
  }
}

第二步,求allData的Hash值到hashValue里。

	// 求出Data的Hash
	var algo crypto.Hash
	algo = crypto.SHA256
	h := algo.New()
	h.Write(allData)
	hashValue := h.Sum(nil)

第三步,根据客户端的Certificate解析出其x509格式的证书:

var clientCertificateDataStr string = `
0000   30 82 01 16 30 81 bd a0 03 02 01 02 02 09 00 b3
0010   ae 5a 54 b2 71 df 02 30 0a 06 08 2a 86 48 ce 3d
0020   04 03 02 30 11 31 0f 30 0d 06 03 55 04 03 0c 06
0030   57 65 62 52 54 43 30 1e 17 0d 32 31 30 36 30 39
0040   31 32 33 30 31 30 5a 17 0d 32 31 30 37 31 30 31
0050   32 33 30 31 30 5a 30 11 31 0f 30 0d 06 03 55 04
0060   03 0c 06 57 65 62 52 54 43 30 59 30 13 06 07 2a
0070   86 48 ce 3d 02 01 06 08 2a 86 48 ce 3d 03 01 07
0080   03 42 00 04 c2 b8 5e 5f 4f c7 96 91 f4 9c 6a b8
0090   11 29 25 9a 61 d7 2c 23 64 c4 0a 32 45 7e 12 d5
00a0   5d 7d e8 c9 34 ae c9 61 b3 77 01 d3 0b 8f 45 47
00b0   46 0b 19 21 81 ba 75 17 6b a6 57 72 00 c7 3f 6a
00c0   5c 13 60 c8 30 0a 06 08 2a 86 48 ce 3d 04 03 02
00d0   03 48 00 30 45 02 20 5f 9f 57 05 3c 12 d1 c5 30
00e0   33 65 fa c6 62 d1 f5 3d a0 e4 c6 ac 8e 71 8f c8
00f0   c4 29 8c 6f 7e 01 d2 02 21 00 c1 6a 90 30 e1 46
0100   41 cf f4 b1 f5 f4 f8 3e ea ac 7f 15 d8 8c 06 2c
0110   c3 32 9a af c9 f7 50 42 ba 9d
`

clientCertificateData := getData(clientCertificateDataStr)

cert, err := x509.ParseCertificate(clientCertificateData)
if err != nil {
  log.Printf("parse certificate failed: %v", err)
}

拿到客户端数字证书里的公钥

ecdhPubKey := cert.PublicKey.(*ecdsa.PublicKey)

第四步,获取客户端发送的CertificateVerify里的签名。


var signatureDataStr string = `
0000   30 45 02 20 3e 02 f6 0f 0a ae 86 85 b3 3c f8 e2
0010   5d e5 ad c1 08 6f 98 45 a6 c0 1b b2 1b c9 8d 77
0020   f4 54 d5 ab 02 21 00 af 8c d1 41 58 20 8a 01 72
0030   ba 58 46 87 7d 6d 81 72 2c 7d d0 01 ba 4f d5 9d
0040   e9 52 00 d5 50 8c ad
`

signatureData := getData(signatureDataStr)

第五步,最终验证签名。

此时,我们拥有的数据为,hashValue,客户端数字证书的公钥ecdhPubKey和要验证是否相等的数字签名值signatureData

var signatureValid bool
signatureValid = ecdsa.VerifyASN1(ecdhPubKey, hashValue, signatureData)
log.Printf("signature valid: %v", signatureValid)

其它验证方法与本文上述的“验证ServerKeyExchange的签名的方法”中描述的类似。