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签名
我正在使用 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签名