如何使用 Web Crypto API 的 SubtleCrypto 验证已签名的 JWT?

How to verify a signed JWT with SubtleCrypto of the Web Crypto API?

我正在尝试使用 Web Crypto 的 SubtleCrypto 接口验证 JWT 的签名 API。

我的代码不会验证令牌签名,而 JWT.io 的调试工具会,我不知道为什么。这是我的验证函数:

function verify (jwToken, jwKey) {
  const partialToken = jwToken.split('.').slice(0, 2).join('.')
  const signaturePart = jwToken.split('.')[2]
  const encoder = new TextEncoder()
  return window.crypto.subtle
    .importKey('jwk', jwKey, { 
         name: 'RSASSA-PKCS1-v1_5', 
         hash: { name: 'SHA-256' } 
       }, false, ['verify'])
    .then(publicKey =>
      window.crypto.subtle.verify(
        { name: 'RSASSA-PKCS1-v1_5' },
        publicKey,
        encoder.encode(atob(signaturePart)),
        encoder.encode(partialToken)
      ).then(isValid => alert(isValid ? 'Valid token' : 'Invalid token'))
    )
}

我希望该代码能够工作并提供对正确签名的 JWT 的积极验证。相反,示例代码无法验证签名令牌。该示例对我来说在 Chrome 71 中失败。

我还使用 RFC 7520 中的示例数据设置了一些 tests

要使用 SubtleCrypto 验证 JWS,您需要小心地在二进制和 base64url 表示之间正确编码和解码数据。不幸的是,btoa()atob() 的浏览器中的 standard implementation 很难使用,因为它们使用“仅包含 U+0000 到 U+00FF 范围内的字符的 Unicode 字符串,每个代表值分别为 0x00 到 0xFF 的二进制字节”作为二进制数据的表示。

在 Javascript 中表示二进制数据的更好解决方案是使用 ES6 对象 TypedArray and use a Javascript library (or write the encoder yourself) to convert them to base64url that honors RFC 4648

Side note: The difference between base64 and base64url is the characters selected for value 62 and 63 in the standard, base64 encode them to + and / while base64url encode - and _.

Javascript 中此类库的一个示例是 rfc4648.js

import { base64url } from 'rfc4648'

async function verify (jwsObject, jwKey) {
  const jwsSigningInput = jwsObject.split('.').slice(0, 2).join('.')
  const jwsSignature = jwsObject.split('.')[2]
  return window.crypto.subtle
    .importKey('jwk', jwKey, { 
         name: 'RSASSA-PKCS1-v1_5', 
         hash: { name: 'SHA-256' } 
       }, false, ['verify'])
    .then(key=>
      window.crypto.subtle.verify(
        { name: 'RSASSA-PKCS1-v1_5' },
        key,
        base64url.parse(jwsSignature, { loose: true }),
        new TextEncoder().encode(jwsSigningInput))
      ).then(isValid => alert(isValid ? 'Valid token' : 'Invalid token'))
    )
}