The Principal Dev – Masterclass for Tech Leads

The Principal Dev – Masterclass for Tech Leads28-29 May

Join

Paseto.NET, a Paseto (Platform-Agnostic Security Tokens) implementation for .NET

CI Maintenance contributions welcome

Table of Contents

Features

PASETO protocols

purpose v1 v2 v3 v4
local
public

PASERK extension

purpose type support
local local
local lid
local local-wrap
local local-pw
local seal ✅ (v3/v4)
public public
public pid
public secret
public sid
public secret-wrap
public secret-pw

Installation

NuGet Version

Targets net8.0 and net10.0.

Install the Paseto.Core NuGet package from the .NET CLI using:

dotnet add package Paseto.Core

or from the NuGet package manager:

Install-Package Paseto.Core

Usage

PASETO

The library exposes a Fluent API with several method overloads found in Use(), WithKey(), AddClaim(), AddFooter() and so on to provide the flexibility needed for encoding and decoding PASETO tokens and also for generating the required symmetric or asymmetric key pairs. However, you can use the Protocols and Handlers directly if you like.

[!NOTE] Implicit assertions (AddImplicitAssertion()) are only supported by protocol versions v3 and v4. For v1 and v2 tokens the assertion is ignored, as the PASETO spec provides no way to bind it — do not rely on it for those versions.

Below are a couple of examples for the most common use cases:

Generating a crypto random Symmetric Key

var pasetoKey = new PasetoBuilder().Use(version, Purpose.Local)
                                   .GenerateSymmetricKey();

Generating an Asymmetric Key Pair

var pasetoKey = new PasetoBuilder().Use(version, Purpose.Public)
                                   .GenerateAsymmetricKeyPair(seed);

NOTE: A seed is not required for protocol v1.

Generating a Token

var token = new PasetoBuilder().Use(version, purpose)
                               .WithKey(key)
                               .AddClaim("data", "this is a secret message")
                               .Issuer("https://github.com/daviddesmet/paseto-dotnet")
                               .Subject(Guid.NewGuid().ToString())
                               .Audience("https://paseto.io")
                               .NotBefore(DateTime.UtcNow.AddMinutes(5))
                               .IssuedAt(DateTime.UtcNow)
                               .Expiration(DateTime.UtcNow.AddHours(1))
                               .TokenIdentifier("123456ABCD")
                               .AddFooter("arbitrary-string-that-isn't-json")
                               .Encode();

Decoding a Token

var result = new PasetoBuilder().Use(version, purpose)
                                .WithKey(key)
                                .Decode(token);

Or validate the token's payload while decoding (the header and signature is always validated):

var valParams = new PasetoTokenValidationParameters
{
    ValidateLifetime = true,
    ValidateAudience = true,
    ValidateIssuer = true,
    ValidAudience = "https://paseto.io",
    ValidIssuer = "https://github.com/daviddesmet/paseto-dotnet"
};

var result = new PasetoBuilder().Use(version, purpose)
                                .WithKey(key)
                                .Decode(token, valParams);

PASERK

The library also provides the PASERK extension for encoding and decoding a key.

A serialized key in PASERK has the format:

k[version].[type].[data]

Encoding a Key

var paserk = Paserk.Encode(pasetoKey, PaserkType.Local);

Decoding a Key

var key = Paserk.Decode(paserk);

Key identifiers (lid / sid / pid)

Identifier types produce a stable, one-way fingerprint of a key. They can only be encoded — Paserk.Decode intentionally throws for them, since an identifier is used to look up a key you already hold, not to recover one.

var kid = Paserk.Encode(pasetoKey, PaserkType.Lid); // sid for secret keys, pid for public keys

Key wrapping (local-wrap / secret-wrap)

Wraps a key with another symmetric wrapping key using the "pie" key-wrapping protocol (AES-256-CTR + HMAC-SHA384 for v1/v3, XChaCha20 + BLAKE2b for v2/v4):

// wk is a PasetoSymmetricKey used to wrap/unwrap
var paserk = Paserk.Encode(localKey, PaserkType.LocalWrap, wrappingKey);
var key = Paserk.Decode(paserk, wrappingKey);

// or via the builder
var paserk = new PasetoBuilder().Use(ProtocolVersion.V4, Purpose.Local)
                                .WithKey(localKey)
                                .GenerateSerializedKey(PaserkType.LocalWrap, wrappingKey);

Password-based key wrapping (local-pw / secret-pw)

Wraps a key with a password using PBKW (PBKDF2-SHA384 + AES-256-CTR for v1/v3, Argon2id + XChaCha20 for v2/v4). Tune the work factors via PbkwOptions:

var password = Encoding.UTF8.GetBytes("correct horse battery staple");
var options = new PbkwOptions
{
    MemoryLimitBytes = 67_108_864, // Argon2id (v2/v4)
    OpsLimit = 2,
    Iterations = 100_000,          // PBKDF2 (v1/v3)
};

var paserk = Paserk.Encode(localKey, PaserkType.LocalPassword, password, options);
var key = Paserk.Decode(paserk, password);

[!NOTE] The PbkwOptions defaults are interactive-grade. For keys stored at rest, raise the work factors (higher MemoryLimitBytes/OpsLimit for Argon2id, more Iterations for PBKDF2) to match your threat model and hardware.

Public-key sealing (seal)

Encrypts a symmetric (local) key to a recipient's asymmetric public key using PKE (P-384 ECDH + AES-256-CTR + HMAC-SHA384 for v3, X25519 + XChaCha20 + BLAKE2b for v4). Only the holder of the matching secret key can unseal it:

// sealingKeyPair is a PasetoAsymmetricKeyPair (the recipient's key pair)
var paserk = Paserk.Encode(localKey, PaserkType.Seal, sealingKeyPair.PublicKey);
var key = Paserk.Decode(paserk, sealingKeyPair.SecretKey);

// or via the builder
var paserk = new PasetoBuilder().Use(ProtocolVersion.V4, Purpose.Local)
                                .WithKey(localKey)
                                .GenerateSerializedKey(PaserkType.Seal, sealingKeyPair.PublicKey);

[!NOTE] Sealing is supported for v3 (P-384) and v4 (X25519). v1/v2 are not implemented.

Roadmap

Test Coverage

codecov

Cryptography

Dependency Lock Files

The repository uses NuGet lock files (packages.lock.json, committed per project) to pin the full dependency graph, including transitive packages. CI restores with locked mode enabled, so a build fails if the resolved packages differ from the lock files — protecting against floating transitive versions and dependency-confusion attacks.

Upgrading packages therefore changes slightly:

Learn More

License

Join libs.tech

...and unlock some superpowers

GitHub

We won't share your data with anyone else.