PostQuantum.Configuration 0.2.0-preview.2

This is a prerelease version of PostQuantum.Configuration.
dotnet add package PostQuantum.Configuration --version 0.2.0-preview.2
                    
NuGet\Install-Package PostQuantum.Configuration -Version 0.2.0-preview.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PostQuantum.Configuration" Version="0.2.0-preview.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PostQuantum.Configuration" Version="0.2.0-preview.2" />
                    
Directory.Packages.props
<PackageReference Include="PostQuantum.Configuration" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PostQuantum.Configuration --version 0.2.0-preview.2
                    
#r "nuget: PostQuantum.Configuration, 0.2.0-preview.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PostQuantum.Configuration@0.2.0-preview.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PostQuantum.Configuration&version=0.2.0-preview.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=PostQuantum.Configuration&version=0.2.0-preview.2&prerelease
                    
Install as a Cake Tool

PostQuantum.Configuration

Secure-by-default encryption for sensitive configuration — connection strings, API keys, and whole appsettings sections — for .NET 8, 9, and 10.

License: MIT Target

Status: 0.2.0-preview.1. The API and token format may change before 1.0. Not independently audited. See Security posture, SECURITY.md, and KNOWN-GAPS.md — we would rather under-claim than overstate.

New in 0.2: an optional hybrid post-quantum key-wrapping provider (ML-KEM-768 + ECDH P-256), a zeroable Secret return type, re-seal helpers for rotation, and the pqc-config CLI.


Table of contents


Why this library exists

Secrets end up in configuration. Connection strings, third-party API keys, signing secrets, SMTP passwords — they live in appsettings.json, environment variables, and config servers, and they leak the same way every time: a repo goes public, a backup is over-shared, a log line prints a section, a laptop is lost.

The standard .NET answer is IDataProtection (great, but DPAPI / ASP.NET-keyring shaped and classical-only) or "put it in a vault" (correct, but not every value justifies a Key Vault round-trip, and you still want defence in depth for what does land on disk). What's missing is a small, honest, secure-by-default primitive to encrypt individual configuration values so the bytes at rest are ciphertext — and to decrypt them transparently when the app reads them.

PostQuantum.Configuration is that primitive. It builds directly on PostQuantum.KeyManagement's envelope-encryption engine, so key custody, rotation, and (later) cloud-KMS providers are a solved problem you plug into, not something this library reinvents. Each value is sealed with its own 256-bit content key under AES-256-GCM; the content key is wrapped by an Argon2id-derived (or KMS) key. The result is a compact token you can commit to source control, and a configuration layer that hands your code plaintext while the repo only ever holds ciphertext.

It is part of the PostQuantum.* family alongside PostQuantum.Jwt and PostQuantum.KeyManagement, and holds to the same discipline: honesty over polish, fail-closed always, no rolled-your-own crypto.

When to use this (and when not to)

Reach for it when:

  • You want secrets at rest in appsettings.json / config servers / env vars to be ciphertext, not plaintext, with decryption that's transparent to application code.
  • You want a single, auditable primitive for "encrypt this config value" across services, with key rotation handled by PostQuantum.KeyManagement.
  • You want defence in depth in front of, or instead of, a vault for values that don't justify a per-read network call — and a forward-looking posture as post-quantum migration begins.
  • You want to keep an air-gapped / local story (Argon2id-derived keys, no external service) and later swap in a cloud KMS without touching application code.

Prefer something else when:

Need Better fit
Centralised secret storage, access policies, audit logs, dynamic secrets A managed vault (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault) — and use this for the values that still land on disk.
Protecting ASP.NET cookies, antiforgery tokens, OAuth state Microsoft.AspNetCore.DataProtection — that's exactly its job.
Quantum-safe key exchange / transport for tokens on the wire PostQuantum.Jwt (PQ JOSE/JWE). This library's hybrid provider does PQ asymmetric key wrapping for config values, not transport.
Encrypting files or large blobs A streaming AEAD / file-encryption library.

Honest framing: the win here is good defaults and ergonomics, not novel cryptography. The primitives are the ones the .NET BCL already ships.

Install

dotnet add package PostQuantum.Configuration --prerelease

This pulls in PostQuantum.KeyManagement (the key engine) and the Microsoft.Extensions.Configuration / DI abstractions.

60-second tour

using Microsoft.Extensions.Configuration;
using PostQuantum.Configuration;
using PostQuantum.KeyManagement.Local;

// 1. A key provider. (In a host: AddPostQuantumKeyManagement. Passphrase from a secret store.)
using var keys = LocalContentKeyProvider.Create("a strong passphrase", LocalKekOptions.Interactive);
IConfigurationProtector protector = new PostQuantumConfigProtector(keys);

// 2. Seal a secret → a token safe to commit.
string token = protector.Protect("Host=db;Username=app;Password=s3cr3t");

// 3. Read it back transparently through configuration.
IConfiguration config = new ConfigurationBuilder()
    .AddEncrypted(
        new Microsoft.Extensions.Configuration.Memory.MemoryConfigurationSource
        {
            InitialData = new Dictionary<string, string?> { ["ConnectionStrings:Db"] = token },
        },
        protector)
    .Build();

string? plaintext = config.GetConnectionString("Db"); // "Host=db;Username=app;Password=s3cr3t"

Usage

Protect and read a value

string token = protector.Protect("sk-live-0123456789");   // -> "pqc.v1.…"
string secret = protector.Unprotect(token);               // -> "sk-live-0123456789"

// Untrusted input? Don't let a bad token throw:
if (protector.TryUnprotect(token, out string? value))
{
    // use value
}

// Async variants exist for cloud-KMS providers where unwrap is a network call:
string t = await protector.ProtectAsync("secret");
string s = await protector.UnprotectAsync(t);

Transparent decryption in IConfiguration

Wrap any configuration source. Protected (pqc.v1.) values are decrypted on read; everything else passes through untouched:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.Sources.Clear();
builder.Configuration
    .AddEncrypted(
        new JsonConfigurationSource { Path = "appsettings.json", Optional = false, ReloadOnChange = true },
        () => protector)              // factory: resolved lazily, on first protected read
    .AddEnvironmentVariables();

Now appsettings.json can hold "ConnectionStrings:Default": "pqc.v1.…" and your handlers read it as an ordinary string. Decryption is lazy and cached; a config reload clears the cache.

Dependency injection

// PostQuantum.KeyManagement provides the key engine…
builder.Services.AddPostQuantumKeyManagement(o =>
{
    o.Passphrase = builder.Configuration["KeyManagement:Passphrase"]!; // from a secret store / env
    o.KeyringPath = "keyring.bin";                                     // durable, non-secret
});

// …and this library layers the protector on top.
builder.Services.AddPostQuantumConfiguration();   // registers IConfigurationProtector

Resolve IConfigurationProtector anywhere, or use it as the factory for AddEncrypted.

Context binding (swap resistance)

Optionally bind a value to a logical slot so a token can't be lifted from one place and dropped into another (e.g. swapping the primary DB password onto the replica key):

string token = protector.Protect(secret, context: "ConnectionStrings:Primary");
protector.Unprotect(token, context: "ConnectionStrings:Primary"); // ok
protector.Unprotect(token, context: "ConnectionStrings:Replica"); // throws — context mismatch

The transparent layer can bind each value's configuration key as its context automatically:

builder.Configuration.AddEncrypted(jsonSource, () => protector, bindKeyAsContext: true);

Key rotation

Rotation lives in PostQuantum.KeyManagement. Rotate the key-encryption key; old tokens still open because previous KEKs stay available for unwrapping, and new values seal under the new active KEK:

keys.Rotate("a new passphrase", LocalKekOptions.Interactive);
protector.Unprotect(oldToken); // still works

Re-sealing after rotation

After a rotation, old tokens still open but remain wrapped under the old key. Migrate them to the new active key with the re-seal helpers (plaintext is handled through a zeroable Secret internally):

// One value:
string fresh = protector.Reprotect(oldToken);

// A whole map of configuration values, in place — plaintext entries are left untouched:
int resealed = await protector.ReprotectAllAsync(values);   // values: IDictionary<string,string?>

Avoiding lingering plaintext with Secret

Unprotect returns a string, which the CLR cannot reliably zero. For paths that can work with bytes, recover into a Secret that zeroes its buffer on dispose:

using Secret secret = protector.UnprotectToSecret(token);
Use(secret.Bytes);              // ReadOnlySpan<byte>, valid until disposed
// string s = secret.Reveal(); // only if an API forces a string on you

This is a mitigation, not a guarantee — see KNOWN-GAPS.md — but it removes the unavoidable lingering-string for byte-friendly code.

Optional: hybrid post-quantum key wrapping (ML-KEM-768 + ECDH P-256)

By default, content keys are wrapped by PostQuantum.KeyManagement (symmetric, Argon2id-derived KEK — post-quantum by key size). For true post-quantum asymmetric key wrapping, use the hybrid provider: each content key is wrapped to a recipient key pair using ML-KEM-768 (FIPS 203, the post-quantum half) and ECDH P-256 (the classical half), combined via HKDF-SHA256 + AES-256-GCM. The wrap stays secure unless both are broken.

using PostQuantum.Configuration.Hybrid;

// Recipient (the service that decrypts): generate once, persist the private key in a secret store/KMS.
using var recipient = HybridKemContentKeyProvider.Generate();
byte[] publicKey  = recipient.ExportPublicKey();   // distribute to senders; safe to share
byte[] privateKey = recipient.ExportPrivateKey();  // SENSITIVE — store in a secret manager only

// Anyone with the public key can seal (wrap-only):
using var sealer = HybridKemContentKeyProvider.ImportPublicKey(publicKey);
string token = new PostQuantumConfigProtector(sealer).Protect("Host=db;Password=quantum-safe;");

// The recipient (private key) opens it:
using var opener = HybridKemContentKeyProvider.ImportPrivateKey(privateKey);
string secret = new PostQuantumConfigProtector(opener).Unprotect(token);

HybridKemContentKeyProvider is an IContentKeyProvider, so it drops into everything above — AddEncrypted, DI, Reprotect, Secret.

Requirements & honesty. Needs .NET 10+ and a platform where ML-KEM is available (on Linux, OpenSSL 3.5+); the factory methods throw PlatformNotSupportedException otherwise. The combiner follows the standard concatenate-into-HKDF, transcript-bound pattern, but this specific construction is not a named standard and has not been independently audited. See KNOWN-GAPS.md and docs/threat-model.md.

CLI: pqc-config

A companion dotnet tool (PostQuantum.Configuration.Tool) protects, unprotects, and rotates from a shell or CI pipeline:

dotnet tool install --global PostQuantum.Configuration.Tool --prerelease

export PQC_PASSPHRASE='a strong passphrase'           # keep secrets out of shell history
echo 'Host=db;Password=s3cr3t' | pqc-config protect --keyring keyring.txt   # -> pqc.v1.…
pqc-config unprotect --keyring keyring.txt --token pqc.v1.…
pqc-config rotate    --keyring keyring.txt            # fresh key, old tokens still open

Token format

A protected value is a self-contained envelope rendered as text:

pqc.v1.<base64url(body)>
  • pqc.v1. — a stable, greppable prefix. IConfigurationProtector.IsProtected(value) is just a prefix check; the transparent layer uses it to decide what to decrypt.
  • body — a compact, big-endian, length-prefixed binary blob: [version][wrapped-content-key token][12-byte nonce][AES-256-GCM ciphertext][16-byte tag].

The wrapped content key is PostQuantum.KeyManagement's own portable WrappedContentKey token, so a value carries everything needed to recover its data-encryption key from the provider — no shared per-process state. (With the hybrid provider, that wrapped-key blob carries the ML-KEM ciphertext and the ephemeral ECDH public key instead — the token envelope is unchanged.) The decoder uses overflow-safe length arithmetic and caps every field at 1 MiB, so a hostile token cannot trigger a giant allocation or an out-of-bounds read.

Public API at a glance

Member Purpose
IConfigurationProtector Protect / Unprotect / TryUnprotect / UnprotectToSecret (+ async), and static IsProtected.
PostQuantumConfigProtector Default implementation over IContentKeyProvider.
Secret Zeroable, byte-backed recovered secret (disposes → zeroed).
ConfigurationProtectionException Single, opaque failure type for malformed / tampered / wrong-context tokens.
builder.AddEncrypted(source, …) Transparent decrypt-on-read wrapper for any IConfigurationSource.
config.GetDecrypted(key, protector) Explicit decrypt-on-read for one value.
protector.DecryptIfProtected(value) Decrypt if it's a token, pass through otherwise.
protector.Reprotect / ReprotectAllAsync Re-seal values under the active key after rotation.
services.AddPostQuantumConfiguration() DI registration over a registered IContentKeyProvider.
HybridKemContentKeyProvider (net10+) Hybrid ML-KEM-768 + ECDH P-256 key-wrapping IContentKeyProvider.
pqc-config (separate tool package) CLI to protect / unprotect / rotate.

Security posture

We aim to be honest about exactly what this library does and does not give you.

Scope of the "post-quantum" claim. With the default (symmetric) key provider, the post-quantum property is symmetric-by-key-size — AES-256-GCM and Argon2id keep useful margin against a quantum adversary because Grover's algorithm only halves their effective strength (AES-256 → ~128-bit), and no asymmetric KEM is involved. With the optional hybrid provider (ML-KEM-768 + ECDH P-256), you also get post-quantum asymmetric key wrapping — but that construction is non-standard and unaudited (see its usage note). Choose your wording to match the provider you deploy, and see KNOWN-GAPS.md for the precise scope.

What you get

  • Authenticated encryption. AES-256-GCM — tampering with any byte is detected and fails closed, never silently decrypted to garbage. Every single-byte corruption of a token is rejected (there's a test that proves it).
  • Fresh key + nonce per value. Each Protect mints a new 256-bit content key and a new random nonce, so identical plaintexts produce different tokens and there is no deterministic-equality leak.
  • Native primitives, no hand-rolled crypto. AES-GCM is the .NET BCL; key custody, Argon2id, and wrapping are PostQuantum.KeyManagement. This library writes the envelope and the framing, not the cryptography.
  • Fail-closed, opaque failures. Malformed token, tampered ciphertext, wrong key, or wrong context all surface as one ConfigurationProtectionException (or TryUnprotect == false) — callers can't distinguish failure modes, and the message never leaks which one it was.
  • Hostile-input resistance. Token decoding uses overflow-safe length arithmetic and a 1 MiB field cap; TryUnprotect never throws on bad input.
  • Optional context binding for swap resistance (above).

What you must know

  • Recovered plaintext is a string. .NET strings are immutable and can't be reliably zeroed, so a decrypted secret may linger on the managed heap until GC. Intermediate byte buffers are zeroed. This is an inherent limit of any string-returning API — see KNOWN-GAPS.md.
  • Confidentiality rests entirely on the key provider. A weak passphrase, a leaked keyring + passphrase, or an unprotected KMS makes the ciphertext openable. Use a strong Argon2id work factor and treat the passphrase as a real secret.
  • Not a vault. No access policies, no audit log, no per-secret authorisation. Pair with one for those properties.
  • Preview. Treat the API and pqc.v1 token format as unstable until 1.0.

Full detail: SECURITY.md, docs/threat-model.md, KNOWN-GAPS.md.

Threat model (short form)

The library defends against… …but not
Plaintext secrets in a repo, backup, or config file An attacker who holds both the keyring / KMS access and the passphrase
Tampering with stored ciphertext (AES-GCM authentication) A weak passphrase / low Argon2id work factor (offline guessing)
A value being moved to the wrong slot (with context binding) Secrets read from process memory after decryption (mitigated, not solved, by Secret)
Hostile / oversized tokens (overflow-safe, capped decoding) A quantum break of the classical half alone — the ML-KEM half still holds (hybrid provider)

The full attacker model and security invariants are in docs/threat-model.md.

Supply chain

This package is built for verifiable provenance:

  • Deterministic, reproducible builds (Deterministic, ContinuousIntegrationBuild in CI).

  • Build-provenance attestation — the release workflow attests every .nupkg with actions/attest-build-provenance; verify with gh attestation verify.

  • SourceLink + embedded untracked sources + a symbol package (.snupkg) so stack traces resolve to exact source.

  • SBOM (CycloneDX) generated for every release — regenerate and inspect locally:

    ./build/generate-sbom.sh           # writes sbom/PostQuantum.Configuration.cdx.json
    
  • Verify a downloaded package before trusting it:

    # NuGet signature (author + repository countersignature)
    dotnet nuget verify PostQuantum.Configuration.0.1.0-preview.1.nupkg
    
    # Pin and restore with integrity checking
    dotnet restore --locked-mode      # honours packages.lock.json hashes
    

What is not yet in place is stated plainly in docs/supply-chain.md: an author code-signing certificate and an external security audit are still roadmap.

Samples

See samples/:

  • QuickStart — a self-contained console tour (protect, transparent read, tamper rejection, rotation). dotnet run --project samples/QuickStart.
  • WebApi — a production-shaped ASP.NET Core minimal API with encrypted tokens committed in appsettings.json, DI wiring, and a token-minting endpoint. dotnet run --project samples/WebApi.

Compatibility

Surface Supported
Target frameworks net8.0, net9.0, net10.0
OS Windows, Linux, macOS — anywhere .NET 8+ runs. AES-GCM is hardware-accelerated on modern CPUs.
AOT / trimming IsAotCompatible=true. The public surface is string in, string out.
Hybrid provider .NET 10+ and a host with ML-KEM (on Linux, OpenSSL 3.5+). Everything else has no such requirement.
Dependencies PostQuantum.KeyManagement, Microsoft.Extensions.Configuration.Abstractions / .Primitives / .DependencyInjection.Abstractions.

The core library needs no native post-quantum primitives, so it runs identically everywhere. Only the optional hybrid provider requires .NET 10 + ML-KEM; it degrades to a clear PlatformNotSupportedException where unavailable.

Building from source

dotnet build       # builds net8.0, net9.0, net10.0 — zero warnings (warnings are errors)
dotnet test        # 69 tests; hybrid ML-KEM tests run where ML-KEM is available, skip cleanly otherwise
dotnet format --verify-no-changes
dotnet pack -c Release

The hybrid ML-KEM tests skip themselves (with a clear reason) on hosts without ML-KEM. To run them, use .NET 10 with OpenSSL 3.5+ — for example, point the runtime at a newer OpenSSL:

LD_LIBRARY_PATH=/path/to/openssl-3.5/lib dotnet test   # 69 tests, zero skips

Project status & roadmap

0.2.0-preview.1 — the 0.1 roadmap is done: the pqc-config CLI, the zeroable Secret return, the Reprotect / ReprotectAllAsync re-seal helpers, and the hybrid ML-KEM-768 + ECDH P-256 provider all ship and are tested. Core protect / unprotect, the transparent IConfiguration layer, DI, and context binding are complete. The API and token format are not yet frozen.

Toward 1.0 (see KNOWN-GAPS.md for the honest gap list):

  1. External security review of the envelope and the hybrid combiner — the prerequisite for dropping the "unaudited" caveat and for a stable 1.0.
  2. Author code-signing certificate to complement the build-provenance attestations already produced.
  3. Standardised hybrid — track the IETF/NIST hybrid-KEM work and align the construction with a named scheme once one stabilises.
  4. Freeze the API and pqc.v1 token format and commit to SemVer wire-compatibility.

License

MIT © 2026 Paul Clark.


To God be the glory — 1 Corinthians 10:31.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0-preview.2 52 6/3/2026
0.2.0-preview.1 62 6/3/2026

0.2.0-preview.1: Closes the 0.1 roadmap. Adds (1) an optional HYBRID post-quantum key-wrapping provider — HybridKemContentKeyProvider — that wraps each content key with ML-KEM-768 (FIPS 203) AND ECDH P-256 combined through HKDF-SHA256 + AES-256-GCM (native BCL primitives; .NET 10+ where ML-KEM is available); (2) a zeroable Secret return type and UnprotectToSecret to avoid lingering plaintext strings; (3) Reprotect / ReprotectAllAsync re-seal helpers for key rotation; (4) the pqc-config dotnet CLI (separate package). Honest scope: the hybrid construction uses standard primitives but is NOT a named standard and is NOT independently audited; the default (non-hybrid) provider remains symmetric-only. Treat the API and token format as unstable until 1.0. See CHANGELOG.md and KNOWN-GAPS.md.