I'm using https://www.nuget.org/packages/Paseto.Core/ and this is how I generate the PASETO token:
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
};
}
Generated PASETO token looks like that: v4.public.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMyMyIsInN1YiI6InRlc3RfY3JlZGVudGlhbHMiLCJhdWQiOiJ0ZXN0QXBpUmVzb3VyY2UiLCJuYmYiOiIyMDIyLTA1LTA3VDE4OjM4OjU2LjU0MjM2OTFaIiwiaWF0IjoiMjAyMi0wNS0wN1QxODozODo1Ni41NDI0MzUzWiIsImV4cCI6IjIwMjItMDUtMDdUMTk6Mzg6NTYuNTQyNDcwN1oiLCJqdGkiOiI5ODk3Mzc4Mi1kNWQwLTQzMjktYWY0ZS1kNTU3NGI4Y2Q2YmMiLCJjbGllbnRfaWQiOiJ0ZXN0X2NyZWRlbnRpYWxzIiwic2NvcGVzIjoidGVzdC5yZWFkIn0pQzMpSSXa-inBjgvDBNFgm7tE4w6J-TzzntJfKJErGRfm2ARuswWxJinhQMT-9v5q1ntyk4UtoIMr9ny0t4AH
So I created a test API for validating tokens, and the result always looks like this:
{
"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
}
}
This is what validation looks like:
[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));
}
Everything seems fine and yet there is sign or validation error. What could be wrong?
CodePudding user response:
Paseto uses the raw public key (32 bytes) and as secret key the concatenation of raw private and raw public key (32 bytes 32 bytes = 64 bytes), see here for an explanation of the different formats of a secret Ed25519 key.
While the public key is imported correctly in the posted code of the question, as private key the Ed25519 signature generated with the private key for an empty string is used. This is incorrect, but works (in the sense that no exception is thrown) because the signature is 64 bytes in size, which is the same length as the secret key. Of course, verification fails.
The following code shows the correct construction of the secret key for Paseto. For simplicity Linq is applied, but also e.g. Buffer.BlockCopy()
can be used:
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
Test:
Using the above secret key, signing is feasible as follows (with arbitrary test data):
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);
and verifying using the above public key:
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");
A possible output of the entire code is:
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"}
Here eyJz...psAG
is the Bas64url encoding of payload and concatenated 64 bytes Ed25519 signature.