C0d00c74baaaab74cf98d708ad8a8fba
与 JOSE 战斗的日子 - 写给 iOS 开发者的密码学入门手册 (基础)

概述

事情的缘由很简单,工作上在做 LINE SDK 的开发,在拿 token 的时候有一步额外的验证:从 Server 会发回一个 JWT (JSON Web Token),客户端需要对这个 JWT 进行签名和内容的验证,以确保信息没有被人篡改。Server 在签名中使用的算法类型会在 JWT 中写明,验证签名所需要的公钥 ID 也可以在 JWT 中找到。这个公钥是以 JWK (JSON Web Key) 的形式公开,客户端拿到 JWK 后即可在本地对收到的 JWT 进行验证。用一张图的话,大概是这样:

步骤

如果你现在对下面说步骤不理解的话 (这挺正常的,毕竟这篇文章都还没正式开始 😂),可以先跳过这部分,等我们有一些基础知识以后再回头看看就好。如果你很清楚这些步骤的话,那真是好棒棒,你应该能无压力阅读该系列剩余部分内容了。

LINE SDK 里使用 JWT 验证用户的逻辑如下:

  1. 向登录服务器请求 access token,登录服务器返回 access token,同时返回一个 JWT。
  2. JWT 中包含应该使用的算法和密钥的 ID。通过密钥 ID,去找预先定义好的 Host 拿到 JWK 形式的该 ID 的密钥。
  3. 将 1 的 JWT 和 2 的密钥转换为 Security.framework 接受的形式,进行签名验证。

这个过程想法很简单,但会涉及到一系列比较基础的密码学知识和标准的阅读,难度不大,但是枯燥乏味。另外,由于 iOS 并没有直接将 JWK 转换为 native 的 SecKey 的方式,自己也没有任何密码学的基础,所以在处理密钥转换上也花了一些工夫。为了后来者能比较顺利地处理相关内容 (包括 JWT 解析验证,JWK 特别是 RSA 和 EC 算法的密钥转换等),也为了过一段时间自己还能有地方回忆这些内容,所以将一些关键的理论知识和步骤记录下来。

系列文章的内容

整个系列会比较长,为了阅读压力小一些,我会分成三个部分:

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

全部读完的话应该能对网络相关的密码学有一个肤浅的了解,特别是常见的签名算法和密钥种类,编码规则,怎么处理拿到的密钥,怎么做签名验证等方面的内容。如果你在工作中有相关需求,但不知道如何下手的话,可以仔细阅读整个系列,并参看开源的 LINE SDK Swift 的相关实现,甚至直接 copy 部分代码去使用 (可以的话,也请顺便点一下 star,KPI 啊 KPI~)。如果你只是感兴趣想要简单了解的话,可以只看 JOSE 和 JWT 的概念和流程部分的内容,作为知识面的扩展,等以后有实际需要了再回头看实践部分的内容。

在文章结尾,我还列举了一些常见的问题,包括笔者自己在学习时的思考和最后的选择。如果您有什么见解,也欢迎发表在评论里,我会继续总结和补充。

声明:笔者自身对密码学也是初学,而本文介绍的密码学知识也都是自己的一些理解,同时尽量不涉及过于原理性的内容,一切以普通工程师实用为目标原则。其中可以想象在很多地方会有理解的错误,还请多包涵。如您发现问题,也往不吝赐教指正,感激不尽。

JWT 以及 JOSE

什么是 JWT

估计大部分 Swift 的开发者对 JWT 会比较陌生,所以先简单介绍一下它是什么,以及可以用来做什么。JWT (JSON Web Token) 是一个编码后的字符串,比如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

一个典型的 JWT 由三部分组成,通过点号 . 进行分割。每个部分都是经过 Base64Url 编码的字符串。第一部分 (Header) 和第二部分 (Payload) 在解码后应该是有效的 JSON,最后一部分 (签名) 是通过一定算法作用在前两部分上所得到的签名数据。接收方可以通过这个签名数据来验证 token 的 Header 及 Payload 部分的数据是否可信。

为了视觉上看起来轻松一些,在上面的 JWT 例子中每个点号后加入了换行。实际的 JWT 中不应该存在任何换行的情况。

严格来说,JWT 有两种实现,分别是 JWS (JSON Web Signature) 和 JWE (JSON Web Encryption)。由于 JWS 的应用更为广泛,所以一般说起 JWT 大家默认会认为是 JWS。JWS 的 Payload 是 Base64Url 的明文,而 JWE 的数据则是经过加密的。相对地,相比于 JWS 的三个部分,JWE 有五个部分组成。本文中提到 JWT 的时候,所指的都是用于签名认证的 JWS 实现。

关于 Base64Url 编码和处理,在本文后面部分会再提到。

Header

Header 包含了 JWT 的一些元信息。我们可以尝试将上面的 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 这个 Header 解码,得到:

{"alg":"HS256","typ":"JWT"}

关于在数据的不同格式之间互相转换 (明文,Base64,Hex Bytes 等),我推荐这个非常不错的 web app。

在 JWT Header 中,"alg" 是必须指定的值,它表示这个 JWT 的签名方式。上例中 JWT 使用的是 HS256 进行签名,也就是使用 SHA-256 作为摘要算法的 HMAC。常见的选择还有 RS256ES256 等等。总结一下:

  • HSXXX 或者说 HMAC:一种对称算法 (symmetric algorithm),也就是加密密钥和解密密钥是同一个。类似于我们创建 zip 文件时设定的密码,验证方需要知道和签名方同样的密钥,才能得到正确的验证结果。
  • RSXXX:使用 RSA 进行签名。RSA 是一种基于极大整数做因数分解的非对称算法 (asymmetric algorithm)。相比于对称算法的 HMAC 只有一对密钥,RSA 使用成对的公钥 (public key) 和私钥 (private key) 来进行签名和验证。大多数 HTTPS 中验证证书和加密传输数据使用的是 RSA 算法。
  • ESXXX:使用 椭圆曲线数字签名算法 (ECDSA) 进行签名。和 RSA 类似,它也是一种非对称算法。不过它是基于椭圆曲线的。ECDSA 最著名的使用场景是比特币的数字签名。
  • PSXXX: 和 RSXXX 类似使用 RSA 算法,但是使用 PSS 作为 padding 进行签名。作为对比,RSXXX 中使用的是 PKCS1-v1_5 的 padding。

如果你对这些介绍一头雾水,也不必担心。关于各个算法的一些更细节的内容,会在后面实践部分再详细说明。现在,你只需要知道 Header 中 "alg" key 为我们指明了签名所使用的签名算法和散列算法。我们之后需要依据这里的指示来验证签名。

除了 "alg" 外,在 Header 中发行方还可以放入其他有帮助的内容。JWS 的标准定义了一些预留的 Header key。在本文中,除了 "alg" 以外,我们还会用到 "kid",它用来表示在验证时所需要的,从 JWK Host 中获取的公钥的 key ID。现在我们先集中于 JWT 的构造,之后在 JWK 的部分我们再对它的使用进行介绍。

Payload

Payload 是想要进行交换的实际有意义的数据部分。上面例子解码后的 Payload 部分是:

{"sub":"1234567890","name":"John Doe","iat":1516239022}

和 Header 类似,payload 中也有一些预先定义和保留的 key,我们称它们为 claim。常见的预定义的 key 包括有:

  • "iss" (Issuer):JWT 的签发者名字,一般是公司名或者项目名
  • "sub" (Subject):JWT 的主题
  • "exp" (Expiration Time):过期时间,在这个时间之后应当视为无效
  • "iat" (Issued At):发行时间,在这个时间之前应当视为无效

当然,你还可以在 Payload 里添加任何你想要传递的信息。

我们在验证签名后,就可以检查 Payload 里的各个条目是否有效:比如发行者名字是否正确,这个 JWT 是否在有效期内等等。因为一旦签名检查通过,我们就可以保证 Payload 的东西是可靠的,所以这很适合用来进行消息验证。

注意,在 JWS 里,Header 和 Payload 是 Base64Url 编码的明文,所以你不应该用 JWS 来传输任何敏感信息。如果你需要加密,应该选择 JWE。

Signature

top Created with Sketch.