PASETO 令牌签名无效,但私钥和 public 密钥匹配

PASETO token signature is not valid but the private and public key match

我正在使用 https://www.nuget.org/packages/Paseto.Core/,这就是我生成 PASETO 令牌的方式:

public async Task<TokenResponse> GenerateAsync(Client client, TokenRequest tokenRequest, string issuer, string audience)
    {
        var ed25519pkcs8 = await File.ReadAllTextAsync("private.pem");

        var privatePemReader = new PemReader(new StringReader(ed25519pkcs8));
        var ed25519pkcs8Parameters = (Ed25519PrivateKeyParameters)privatePemReader.ReadObject();
        ISigner signer = new Ed25519Signer();
        signer.Init(true, ed25519pkcs8Parameters);

        var pasetoToken = new PasetoBuilder()
            .Use(ProtocolVersion.V4, Purpose.Public)
            .WithKey(signer.GenerateSignature(), Encryption.AsymmetricSecretKey)
            .Issuer(issuer)
            .Subject(tokenRequest.ClientId)
            .Audience(audience)
            .NotBefore(DateTime.UtcNow)
            .IssuedAt(DateTime.UtcNow)
            .Expiration(DateTime.UtcNow.AddSeconds(client.AccessTokenLifetime))
            .TokenIdentifier(Guid.NewGuid().ToString())
            .AddClaim("client_id", tokenRequest.ClientId)
            .AddClaim("scopes", tokenRequest.Scopes)
            .Encode();

        return new TokenResponse
        {
            AccessToken = pasetoToken,
            Lifetime = client.AccessTokenLifetime,
            Scope = tokenRequest.Scopes
        };
    }

生成的 PASETO 令牌如下所示:v4.public.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMyMyIsInN1YiI6InRlc3RfY3JlZGVudGlhbHMiLCJhdWQiOiJ0ZXN0QXBpUmVzb3VyY2UiLCJuYmYiOiIyMDIyLTA1LTA3VDE4OjM4OjU2LjU0MjM2OTFaIiwiaWF0IjoiMjAyMi0wNS0wN1QxODozODo1Ni41NDI0MzUzWiIsImV4cCI6IjIwMjItMDUtMDdUMTk6Mzg6NTYuNTQyNDcwN1oiLCJqdGkiOiI5ODk3Mzc4Mi1kNWQwLTQzMjktYWY0ZS1kNTU3NGI4Y2Q2YmMiLCJjbGllbnRfaWQiOiJ0ZXN0X2NyZWRlbnRpYWxzIiwic2NvcGVzIjoidGVzdC5yZWFkIn0pQzMpSSXa-inBjgvDBNFgm7tE4w6J-TzzntJfKJErGRfm2ARuswWxJinhQMT-9v5q1ntyk4UtoIMr9ny0t4AH 所以我创建了一个测试 API 来验证令牌,结果总是这样:

{
   "IsValid":false,
   "Paseto":null,
   "Exception":{
      "Expected":null,
      "Received":null,
      "Message":"The token signature is not valid",
      "Data":{
         
      },
      "InnerException":null,
      "HelpLink":null,
      "Source":null,
      "HResult":-2146233088,
      "StackTrace":null
   }
}

这是验证的样子:

        [HttpGet]
    public IActionResult DecodePaseto([FromQuery] string token)
    {
        var ed25519x509 = System.IO.File.ReadAllText("public.pem");

        var publicPemReader = new PemReader(new StringReader(ed25519x509));
        var ed25519x509Parameters = (Ed25519PublicKeyParameters)publicPemReader.ReadObject();

        var paseto = new PasetoBuilder()
            .Use(ProtocolVersion.V4, Purpose.Public)
            .WithKey(ed25519x509Parameters.GetEncoded(), Encryption.AsymmetricPublicKey)
            .Decode(token);

        return Ok(JsonConvert.SerializeObject(paseto));
    }

一切似乎都很好,但存在符号或验证错误。有什么问题吗?

Paseto 使用原始 public 密钥(32 字节)并将原始私钥和原始 public 密钥(32 字节 + 32 字节 = 64 字节)的串联作为密钥,参见 here 用于解释 Ed25519 密钥的不同格式。

虽然在问题的已发布代码中正确导入了 public 密钥,但使用使用空字符串的私钥生成的 Ed25519 签名作为私钥。这是不正确的,但有效(在没有抛出异常的意义上),因为签名的大小为 64 字节,与密钥的长度相同。当然,验证失败。

以下代码显示了 Paseto 密钥的正确构造。为简单起见,应用了 Linq,但也应用了例如Buffer.BlockCopy() 可以使用:

using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using System;
using System.IO;
using System.Linq;

...

string ed25519pkcs8 = @"-----BEGIN PRIVATE KEY-----
                       MC4CAQAwBQYDK2VwBCIEIAYIsKL0xkTkAXDhUN6eDheqODEOGyFZ04jsgFNCFxZf
                       -----END PRIVATE KEY-----";
PemReader privatePemReader = new PemReader(new StringReader(ed25519pkcs8));
Ed25519PrivateKeyParameters ed25519pkcs8Parameters = (Ed25519PrivateKeyParameters)privatePemReader.ReadObject();

string ed25519x509 = @"-----BEGIN PUBLIC KEY-----
                      MCowBQYDK2VwAyEA3mcwgf2DrWLR3mQ6l2d59bGU6qUStwQrln2+rKlKxoA=
                      -----END PUBLIC KEY-----";
PemReader publicPemReader = new PemReader(new StringReader(ed25519x509));
Ed25519PublicKeyParameters ed25519x509Parameters = (Ed25519PublicKeyParameters)publicPemReader.ReadObject();

byte[] publicKey = ed25519x509Parameters.GetEncoded(); // raw 32 bytes public key
byte[] secretKey = ed25519pkcs8Parameters.GetEncoded().Concat(publicKey).ToArray(); // raw 32 bytes private key + raw 32 bytes public key

测试:

使用上面的秘钥,签名是可行的(使用任意测试数据):

using Paseto;
using Paseto.Builder;

...

string pasetoToken = new PasetoBuilder()
    .Use(ProtocolVersion.V4, Purpose.Public)
    .WithSecretKey(secretKey) // short for .WithKey(secretKey, Encryption.AsymmetricSecretKey) 
    .Subject("subject")
    .Issuer("whoever")
    .Audience("https://www.whatever.com/someurl")
    .NotBefore(DateTime.UtcNow)
    .IssuedAt(DateTime.UtcNow)
    .Expiration(DateTime.UtcNow.AddSeconds(3600))
    .TokenIdentifier(Guid.NewGuid().ToString())
    .AddClaim("client_id", "client_id")
    .AddClaim("scopes", "scopes")
    .Encode();

Console.WriteLine(pasetoToken);

并使用上面的 public 密钥进行验证:

using Paseto;
using Paseto.Builder;

...

PasetoTokenValidationParameters validationParameters = new PasetoTokenValidationParameters()
{
    ValidateIssuer = true,
    ValidIssuer = "whoever",
    ValidateAudience = true,
    ValidAudience = "https://www.whatever.com/someurl"
};

PasetoTokenValidationResult paseto = new PasetoBuilder()
    .Use(ProtocolVersion.V4, Purpose.Public)
    .WithPublicKey(publicKey) // short for .WithKey(publicKey, Encryption.AsymmetricPublicKey) 
    .Decode(pasetoToken, validationParameters);

Console.WriteLine(paseto.IsValid ? paseto.Paseto.RawPayload : "Decoding failed");

整个代码的可能输出是:

v4.public.eyJzdWIiOiJzdWJqZWN0IiwiaXNzIjoid2hvZXZlciIsImF1ZCI6Imh0dHBzOi8vd3d3LndoYXRldmVyLmNvbS9zb21ldXJsIiwibmJmIjoiMjAyMi0wNS0wN1QyMjowNzo0NS4yNzA1NjU4WiIsImlhdCI6IjIwMjItMDUtMDdUMjI6MDc6NDUuMjcwNjQ1OVoiLCJleHAiOiIyMDIyLTA1LTA3VDIzOjA3OjQ1LjI3MDY4NzRaIiwianRpIjoiNDU0MWI2NmMtOGRlZi00Mjg3LWFmZGMtYTE3ZDNhMDY3NjYxIiwiY2xpZW50X2lkIjoiY2xpZW50X2lkIiwic2NvcGVzIjoic2NvcGVzIn1RyxW-gjy6va7IA5pL9pZMqcrBjYkYFX16AV7IqTt5Fa5YtQMbIJQkfu24uq7bR2lx0WMLHa0xr2fsJRtdpsAG
{"sub":"subject","iss":"whoever","aud":"https://www.whatever.com/someurl","nbf":"2022-05-07T22:07:45.2705658Z","iat":"2022-05-07T22:07:45.2706459Z","exp":"2022-05-07T23:07:45.2706874Z","jti":"4541b66c-8def-4287-afdc-a17d3a067661","client_id":"client_id","scopes":"scopes"}

这里eyJz...psAG是payload的Bas64url编码和拼接的64字节Ed25519签名