【问题标题】:Unable to reproduce an example from JWT spec (RFC 7519) and JWS spec (RFC 7571)无法从 JWT 规范 (RFC 7519) 和 JWS 规范 (RFC 7571) 重现示例
【发布时间】:2021-06-08 18:07:26
【问题描述】:

我试图深入了解 JWT 到底是什么,所以我决定去看看规范。

第一个例子似乎成功地阻止了我继续前进。

JWT spec 中的这个示例(JWS spec 提供了更多上下文)展示了如何创建 JWT。

对于标题,它有

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

这是base64编码成eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9;

对于有效载荷,它有

{
  "iss": "joe",
  "exp": 1300819380,
  "http://example.com/is_root": true
}

基于64位编码为eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ

顺便说一下,我从这个post了解到,JWT中使用的based64字符串与btoa返回的并不完全相同

有问题的部分是签名。

在 JWS 中,它说该示例使用对称密钥对标头和有效负载进行签名,以 jwk 格式表示为

{
  "kty": "oct",
  "k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}

base64编码后的签名数据为dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

我试图在浏览器中重现这个例子。这是我的代码

(async () => {

    const header = {
        typ: "jwt",
        alg: "HS256"
    };
    const payload = {
        "iss": "joe",
        "exp": 1300819380,
        "http://example.com/is_root": true
    };

    const headerEncoded = strToSafeUrlBase64(JSON.stringify(header));
    console.log('header', headerEncoded);
    // prints out eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9
    // Same as Example

    const payloadEncoded = strToSafeUrlBase64(JSON.stringify(payload));
    console.log('payload', payloadEncoded);
    // prints out eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
    // Same as Example


    const _key = {
        kty: "oct",
        k: "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
    }
    const HmacKeyParams = {
        name: "HMAC",
        hash: "SHA-256"
    };
    const key = await crypto.subtle.importKey(
        'jwk',
        _key,
        HmacKeyParams,
        true,
        ['sign', 'verify']);

    const signedValue = await crypto.subtle.sign(
        'HMAC', key, new TextEncoder().encode(`${headerEncoded}.${payloadEncoded}`));

    console.log(uint8ToBase64(new Uint8Array(signedValue)));
    // this prints out _Y6kHA7DqgEFgqbaKMMCGUwPuZMMczSXV0w34CfblCA
    // not the same as the example !!

    function strToSafeUrlBase64(bin) {
        return btoa(bin)
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=+/g, '');
    }

    function uint8ToBase64(uint8) {
        let s = '';
        uint8.forEach(b => s += String.fromCharCode(b));
        return strToSafeUrlBase64(s);
    }

})();

我错过了什么?

我在密码学方面完全是个业余爱好者,所以不要信守诺言,用每一个残酷的事实来抨击我

【问题讨论】:

  • 您的问题写得非常好,直到您遇到问题为止。我看不出你的最终代码应该如何表现以及它的实际表现如何。

标签: web encryption jwt


【解决方案1】:

不同的签名是由以下原因造成的:

  • RFC 在标头中使用typ JWT,而发布的代码适用jwt
  • RFC 在 JSON 字符串中的逗号后使用换行符和空格 (0x0d0a20),而发布的代码没有换行符和空格。

因此,Base64 编码的标头和有效负载在发布的 JavaScript 代码和 RFC 中有所不同:

JavaScript: eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9
RFC:        eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9

JavaScript: eyJpc3MiOiJqb2UiLCJleHAiOjEzMDA4MTkzODAsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
RFC:        eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ

因此是签名。

如果这些差异得到修复,来自 RFC 的签名结果:

(async () => {

    const header = {
        typ: "JWT",                                                             // Replace jwt by JWT
        alg: "HS256"
    };
    const payload = {
        "iss": "joe",
        "exp": 1300819380,
        "http://example.com/is_root": true
    };

    const headerStringified = JSON.stringify(header).replace(/,/g,',\r\n ');   // Replace comma by comma, linebreak and blank
    const headerEncoded = strToSafeUrlBase64(headerStringified); 
    console.log('header', headerEncoded.replace(/(.{65})/g,'$1\n'));

    const payloadStringified = JSON.stringify(payload).replace(/,/g,',\r\n '); // Replace comma by comma, linebreak and blank
    const payloadEncoded = strToSafeUrlBase64(payloadStringified); 
    console.log('payload', payloadEncoded.replace(/(.{65})/g,'$1\n'));

    const _key = {
        kty: "oct",
        k: "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
    }
    const HmacKeyParams = {
        name: "HMAC",
        hash: "SHA-256"
    };
    const key = await crypto.subtle.importKey(
        'jwk',
        _key,
        HmacKeyParams,
        true,
        ['sign', 'verify']);

    const signedValue = await crypto.subtle.sign(
        'HMAC', key, new TextEncoder().encode(`${headerEncoded}.${payloadEncoded}`));

    console.log('signature', uint8ToBase64(new Uint8Array(signedValue)).replace(/(.{65})/g,'$1\n'));

    function strToSafeUrlBase64(bin) {
        return btoa(bin)
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=+/g, '');
    }

    function uint8ToBase64(uint8) {
        let s = '';
        uint8.forEach(b => s += String.fromCharCode(b));
        return strToSafeUrlBase64(s);
    }

})();

【讨论】:

  • RFC 示例中奇怪的部分是,他们对标头和有效负载进行了编码,包括换行符和空格,而 JSON.stringify 通常会去除所有空格,这对于获得最短的结果是有意义的。我什至无法从 jwt.io 上的示例中重新创建签名,因为所有空格都被删除了。
  • 哇!不逐字节检查编码的有效负载和标头是我的坏事。但即使我这样做了,我仍然无法弄清楚那里缺少什么。从字面上看,你的回答救了我的命。谢谢!我现在可以重现这个例子了。
  • @jps - RFC7515 描述了(相当非常规的)编码(A.1.1):请注意,由于换行符的不同平台表示(CRLF 与 LF)、不同的间距在行的开头和结尾,最后一行是否有终止换行符,以及其他原因。在此示例中使用的表示中,第一行没有前导或尾随空格,CRLF 换行符 (13, 10) 发生在第一行和第二行之间,第二行有一个前导空格 (32) 并且没有尾随空格,并且最后一行没有终止换行符。
猜你喜欢
  • 2011-04-02
  • 1970-01-01
  • 2011-03-04
  • 1970-01-01
  • 1970-01-01
  • 2019-08-31
  • 2012-10-11
  • 1970-01-01
  • 2018-07-16
相关资源
最近更新 更多