JWS 也就是 Json Web Signature,是构造 JWT 的基础结构(JWT 其实涵盖了 JWS 和 JWE 两类,其中 JWT 的载荷还可以是嵌套的 JWT),包括三部分 JOSE Header、JWS Payload、JWS Signature。
这里的 Signature 可以有两种生成方式,一种是标准的签名,使用非对称加密,因为私钥的保密性,能够确认签名的主体,同时能保护完整性;另一种是消息认证码 MAC(Message Authentication Code),使用对称秘钥,该秘钥需要在签发、验证的多个主体间共享,因此无法确认签发的主体,只能起到保护完整性的作用。
JWS 最终有两种序列化的表现形式,一种是 JWS Compact Serialization,为一串字符;另一种是 JWS JSON Serialization,是一个标准的 Json 对象,允许为同样的内容生成多个签名/消息认证码。
JWS Compact Serialization,各部分以 '.' 分隔。
BASE64URL(UTF8(JWS Protected Header)) || ’.’ ||
BASE64URL(JWS Payload) || ’.’ ||
BASE64URL(JWS Signature)
JWS Json Serialization 还可以分为两种子格式:通用、扁平。
通用格式,最外层为 payload、signatures。signatures 中可以包含多个 json 对象,内层的 json 对象由 protected、header、signature 组成。不同的 protected header 生成不同的 Signature。
{
"payload": "",
"signatures":
[
{
"protected": "",
"header": "",
"signature": ""
},
...
{
"protected": "",
"header": "",
"signature": ""
}
]
}
扁平格式,就是为只有一个 signature/mac 准备的。
{
"payload": "",
"protected": "",
"header": "",
"signature": ""
}
JOSE Header:Json Object Signing and Encryption Header。描述加密行为及其他用到的参数,是 JWS Protected/Unprotected Header 的合集。
JWS Protected Header,有完整性保护的头参数。
JWS Unprotected Header,无完整性保护头参数,仅出现在 JWS Json Serialization 格式中。
以下列出了 JOSE Header 中的规定参数,各参数详细信息请参考 rfc7515
头参数 | 全称 | 解释 | 必选 |
---|---|---|---|
alg | algorithm | 指定签名算法,为 none 时,表示不使用签名来保护完整性 | 是 |
jku | JWK set URL | 签名所用 key 对应公匙所在的 URI | 否 |
jwk | json web key | 签名所用 key 对应的公匙 | 否 |
kid | key id | 签名所用 key 的 id | 否 |
typ | Type | 指明整个 jws 的媒体类型,JOSE 意味着为compact,JOSE+JSON意味着为json | 否 |
cty | Content Type | 载荷的媒体类型 | 否 |
crit | Critical | 此字段列出的扩展头参数必须被接收者理解并处理,否则该 jws 无效,该字段为数组格式 | 否 |
x5u | X.509 URL | -- | 否 |
x5c | X.509 Certificate Chain | -- | 否 |
x5t | X.509 Certificate SHA-1 Thumbprint | -- | 否 |
x5t#S256 | X.509 Certificate SHA-256 Thumbprint | -- | 否 |
# crit 参数例子
{
"alg":"ES256",
"crit":["exp","iss"],
"exp":1363284000,
"iss":"test"
}
以下为 rfc7515 中提供的一个案例,使用 HMAC_SHA256 生成 JWS Signature。
1. 头部就是一个紧凑的字符串,不换行,也无空格。
Header = {"typ":"JWT","alg":"HS256"}base64url(Header) = eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
import base64
header_encoded = base64.urlsafe_b64encode(b'{"typ":"JWT","alg":"HS256"}')
print(header_encoded)
2. 载荷是一个包括了换行和空格的 json 对象,换行取 win 系统的 CRLF,且除第一行外,每一行开头有一个空格,行尾无空格。
Payload = {"iss":"joe",rn "exp":1300819380,rn "http://example.com/is_root":true}base64url(Payload) = eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
import base64
Payload = {"iss":"joe",\r\n "exp":1300819380,\r\n "http://example.com/is_root":true}
payload_encoded = (b'{"iss":"joe",\r\n "exp":1300819380,\r\n "http://example.com/is_root":true}')
print(payload_encoded)
3. 生成 Signature 时将头部和载荷视为一体
Message = ASCII(BASE64URL(UTF8(JWS Protected Header)) || ’.’ || BASE64URL(JWS Payload))
HMAC_SHA256 签名时需要用到对称密匙 Key,这里的 Key 是预先商量好的
Key = AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow
因为 Key 也是 base64url 编码后的内容,所以要获取 Key 的字节数组需要 base64url 解码一把,解码时由于 Key 的长度为 86,86%4=2,需要添加 '==' 后再进行解码
# 也即解码时用的是这个
Key = AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow==Signature = base64url( HMAC_SHA256(Message, Key) )
最后得到签名为 dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk=
import hashlib
import hmac
import base64
message = bytes('eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ','ascii')
secret = base64.urlsafe_b64decode('AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow==')
signature = base64.urlsafe_b64encode(hmac.new(secret, message, digestmod=hashlib.sha256).digest())
print(signature)
去掉多余的 '=' ,就是最终的 Signature: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
这样,最终的 JWS 就为(换行是为了方便看,实际就是一串)
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
当然,实际中可能会选用 RSA 之类的算法来进行签名,可参考 RFC 文档中的案例。
参考文档: rfc7515