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?
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:Test:
Using the above secret key, signing is feasible as follows (with arbitrary test data):
and verifying using the above public key:
A possible output of the entire code is:
Here
eyJz...psAGis the Bas64url encoding of payload and concatenated 64 bytes Ed25519 signature.