999454a0bdccaaa90b638793c8089b70
与 JOSE 战斗的日子 - 写给 iOS 开发者的密码学入门手册 (理论)

概述

这是关于 JOSE 和密码学的三篇系列文章中的第二篇,你可以在下面的链接中找到其他部分:

  1. 基础 - 什么是 JWT 以及 JOSE
  2. 理论 - JOSE 中的签名和验证流程 (本文)
  3. 实践 - 如何使用 Security.framework 处理 JOSE 中的验证

这一篇中,主要介绍网络传输的密钥的编码和处理方法,以及进行数字签名和验证的基本流程。我们在之后实践一篇里,会使用到这些知识。

密钥的表现形式

显然 JWK 是一种密钥的表现形式,它使用 JSON 的方式,遵守 JWA 的参数,来定义密钥。不过这种表现形式在日常里使用得并不是那么普遍,我们在平时看到得更多的也许是这样的密钥:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAryQICCl6NZ5gDKrnSztO
3Hy8PEUcuyvg/ikC+VcIo2SFFSf18a3IMYldIugqqqZCs4/4uVW3sbdLs/6PfgdX
7O9D22ZiFWHPYA2k2N744MNiCD1UE+tJyllUhSblK48bn+v1oZHCM0nYQ2NqUkvS
j+hwUU3RiWl7x3D2s9wSdNt7XUtW05a/FXehsPSiJfKvHJJnGOX0BgTvkLnkAOTd
OrUZ/wK69Dzu4IvrN4vs9Nes8vbwPa/ddZEzGR0cQMt0JBkhk9kU/qwqUseP1QRJ
5I1jR4g8aYPL/ke9K35PxZWuDp3U0UPAZ3PjFAh+5T+fc7gzCs9dPzSHloruU+gl
FQIDAQAB
-----END PUBLIC KEY-----

这是一个 RSA 公钥的 PEM (Privacy-Enhanced Mail) 表示方式。类似地,对于 ECDSA 密钥,也可以类似表示:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----

在处理 JOSE 相关的验证时,我们其实是不会涉及这种格式的密钥的。但是我们会用到里面的相关的一些编码方式来处理 JWK 密钥和 Security 框架中 SecKey 的转换。所以这里把它作为一节单独介绍。

PEM,ASN.1,X.509 和 DER 编码

上面的 PEM 格式的密钥可以用任意的文本编辑器打开,它就是一个简单的纯 ASCII 字符的文件。由于容易读写复制,所以在交换密钥时这种格式非常流行。每个 PEM 密钥都由 "-----BEGIN #{labe})-----" 标签开头,以 "-----END #{label}-----" 标签结尾。注意,PEM 并非专门为了传递 Key 而生,BEGIN 和 END 之后的 label 并不一定就是例子中的 "PUBLIC KEY",它只是一个让人能读懂的描述,来表示通过这个 PEM 传递的数据到底是什么。

在两个标签之间,就是密钥本身。PEM 中的换行字符需要被忽略掉,可以很清楚地看到,这其实就是一个 Base64 编码的字符串。用上面的 ECDSA 密钥为例,将这个 Base64 还原为字节数据的话,结果是:

30 59 30 13 06 07 2a 86 48 ce 3d 02 01 06 08 2a 86 48 ce 3d 03 01 07 03 42 00 04 11 5b 3f a3 9f ae 41 b4 e3 2f 77 21 ca 72 f8 c1 78 14 83 64 7d ab d5 14 f0 8e 66 12 8b d4 7f ce 90 67 b9 0e 04 88 c9 c2 a9 f3 0f 5a 26 6a 07 84 1d 6c 07 74 13 ba 07 e7 45 69 b9 9d 4f d3 ce c6

很多同学到这里就退缩了,觉得这种二进制没有实际意义。但其实这一串字节是通过 ASN.1 (Abstract Syntax Notation One) 定义的数据。ASN.1 定义了一些表示信息的标准句法,用来对字符串,整数等等进行无歧义和精确地传输。ASN.1 里有很多具体的编码规则,来具体将一些数据按照 ASN.1 的方式进行编码,进行具体表达。在网络传输和密码学中,最简单和最常见的编码方式是 DER (Distinguished Encoding Rules)

ASN.1 格式对应的标准是 X.680,DER 被定义在 X.690 中。

有了编码方式以后,为了能表达密钥,我们还需要定义一些元信息,比如一个密钥应该需要声明自己的身份 (在 ASN.1 中称为 "OBJECT IDENTIFIER"),是一个什么种类的密钥,采用的是什么样的曲线或者 padding,X.509 标准就是做这件事的。最后,对于 ECDSA 来说,还有一个 X9.62 的标准。它是 X.509 中规定的用来编码 ECDSA 密钥的方式。

光这样说会很抽象,具体来讲,可以简单对这几个概念和各自的作用进行总结:

  • ASN.1 - 一种数据或者信息表达时使用的句法,比如 “接下来是一串连续内容 (SEQUENCE),长度是...”;“现在开始一个整数”;“从这里开始是位串 (BITSTRING)” 等这样句法信息。
  • DER - 是 ASN.1 的一种具体编码方式,比如使用 0x30 表示 SEQUENCE 的开始,然后下一个/若干个字节表示这段内容的长度;使用 0x02 表示现在开始是一个整数;使用 0x03 表示 BIT STRING 开始等。
  • X.509 - 在网络证书和公钥传输时,所应该遵守的 ASN.1 形式。它定义了一个特定证书或者公钥应该由哪些部分构成,比如“一开始应该有一个 SEQUENCE,然后紧接着是两个整数来代表密钥值”等。这些构成的部分由 ASN.1 格式表达,一般由 DER 编码。
  • X9.62 - 针对 ECDSA 相关算法的定义。X.509 是一个一般性的密钥编码规定,在 X.509 中指定了 ECDSA 的密钥和签名需要遵守 X9.62。(类似相应地,它也规定了 RSA 的密钥和签名要遵守 PKCS (Public Key Cryptography Standards))。
  • PEM - 将证书或者密钥用 DER 编码后,可以得到一组字节数据。把这些数据转换为 Base64 编码的字符串,然后在前后加上 BEGIN 和 END 标签,就得到 PEM 的表现形式。

标准有点多?没错,这个世界上有很多标准化制定的组织,在这篇文章中,标准来源也不尽相同。从名字基本可以简单分类:

  • RFC (Request For Comments) 是 IETF 这个专门推动互联网标准的组织所发布的
  • "X." 开头的是 ITU-T 相关的标准,比如 ASN.1 (X.680) 是 ISO 和 ITU-T 的联合标准,X.509 是基于 ASN.1 的补充和扩展。
  • "X9." 开头的,比如 X9.62,是 ANSI (美国国家标准学会) 的产品
  • PKCS 是 RSA Security 所制定的标准

我们会看到,在一些 RFC 标准中,会引用和规定需要使用 ANSI 的标准;而本来属于 RSA Security 的一些标准,也出现在了 RFC 中。另外,IETF 也会收录某些其他标准化组织的内容,比如 RFC 3280 其实就是 X.509。和专利市场的相互授权类似,各个标准组织之间也有竞争合作。不过这是另外一个关于爱恨情仇的故事了,其中八卦,我们有机会以后再说。

DER 编码规则

DER 编码的通用规则是,在一个代表类型的字节后面,一般都会接上这个类型的数据所占用的字节长度,然后是实际的数据。

整数

举例说明,在 DER 中,使用 0x02 代表整数,所以如果我们想要编码十进制的 100 这个整数时,会得到:

00000010 00000001 01100100
  0x02     0x01     0x64

0x02 代表之后是一个整数,这个整数占用的字节长度为 1 (0x01),值为 100 (0x64)。

我们在系列的上一篇文章中提到过,如果数值的首个字节超过 0x80 的话,就说明第一个 bit 是 1,在有符号域上这代表一个负数。这时候如果我们想要编码的是一个正数的话,就需要在前面添加一个 0x00 的字节。比如我们如果想要编码 0xCE 29 10 这个整数的话,就需要添加 0x00,因为首位的 0xCE 在二进制下为 0b_1100_1110

0x02 0x04 0x00 0xCE 0x29 0x10

序列

将两个整数前后排列,就可以形成一个序列 (SEQUENCE),序列的类型编码为 0x30,类似地,在类型编码后面也是字节长度值。比如两个整数 0x640xCE2910 编码成一个序列,得到的结果是:

30 09 02 01 64 02 04 00 CE 29 10

为了看上去舒适一些,可以整理一下:

30 09 -> SEQUENCE 9 bytes
   02 01 -> Int 1 byte
      64          -> Value 0x64
   02 04 -> Int 4 byte
      00 CE 29 10 -> Value 0xCE2910
top Created with Sketch.