{"id":50486813,"url":"https://github.com/systemslibrarian/postquantum-securechannel","last_synced_at":"2026-06-01T23:02:31.329Z","repository":{"id":361709105,"uuid":"1255458525","full_name":"systemslibrarian/postquantum-securechannel","owner":"systemslibrarian","description":"Easy-to-use hybrid post-quantum secure channel / session encryption library for .NET 10.","archived":false,"fork":false,"pushed_at":"2026-05-31T23:49:53.000Z","size":164,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-01T00:11:51.092Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/systemslibrarian.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T21:14:38.000Z","updated_at":"2026-05-31T23:49:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/systemslibrarian/postquantum-securechannel","commit_stats":null,"previous_names":["systemslibrarian/postquantum-securechannel"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/systemslibrarian/postquantum-securechannel","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fpostquantum-securechannel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fpostquantum-securechannel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fpostquantum-securechannel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fpostquantum-securechannel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/systemslibrarian","download_url":"https://codeload.github.com/systemslibrarian/postquantum-securechannel/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/systemslibrarian%2Fpostquantum-securechannel/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33797128,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-01T23:02:30.675Z","updated_at":"2026-06-01T23:02:31.324Z","avatar_url":"https://github.com/systemslibrarian.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PostQuantum.SecureChannel\n\n\u003e **Post-quantum, mutually-authenticated, transport-agnostic encrypted channels for .NET — three\n\u003e messages to a live session, secure by default, no insecure knobs.**\n\n[![CI](https://github.com/systemslibrarian/postquantum-securechannel/actions/workflows/ci.yml/badge.svg)](https://github.com/systemslibrarian/postquantum-securechannel/actions/workflows/ci.yml)\n[![NuGet](https://img.shields.io/nuget/vpre/PostQuantum.SecureChannel.svg)](https://www.nuget.org/packages/PostQuantum.SecureChannel)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![.NET](https://img.shields.io/badge/.NET-8.0%20%7C%209.0%20%7C%2010.0-512BD4)](https://dotnet.microsoft.com/)\n\nPostQuantum.SecureChannel establishes a mutually-verifiable, authenticated session between two .NET\nendpoints that stays secure against both today's adversaries and tomorrow's quantum computers — with\nan API small enough to fit in your head.\n\n```\n   ┌────────────┐   ClientHello  (X-Wing public key)    ┌────────────┐\n   │            │ ───────────────────────────────────▶ │            │\n   │   Client   │   ServerHello  (ciphertext + ML-DSA)  │   Server   │\n   │            │ ◀─────────────────────────────────── │            │\n   │            │   ClientFinished (key confirmation)   │            │\n   │            │ ───────────────────────────────────▶ │            │\n   └────────────┘                                       └────────────┘\n        │                                                     │\n        └──────────  AES-256-GCM session records  ────────────┘\n```\n\nIt combines **[X-Wing](https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/) hybrid key\nagreement** (ML-KEM-768 + X25519 — safe as long as *either* primitive holds),\n**[ML-DSA-65](https://csrc.nist.gov/pubs/fips/204/final) signatures** for handshake authentication,\nand **AES-256-GCM** for record encryption. Algorithms and parameters are fixed by design; there is\nno way to configure your way into a weak session.\n\n---\n\n## Install\n\n```bash\ndotnet add package PostQuantum.SecureChannel --version 0.3.0-preview.1\n\n# Optional companions:\ndotnet add package PostQuantum.SecureChannel.AspNetCore --version 0.3.0-preview.1   # DI, WebSocket\ndotnet add package PostQuantum.SecureChannel.Testing    --version 0.3.0-preview.1   # tests only\n```\n\nTargets `net8.0`, `net9.0`, and `net10.0`. Wire-format-stable across the 0.2.x → 0.3.x line.\n\n## Try it in 30 seconds\n\n```bash\ngit clone https://github.com/systemslibrarian/postquantum-securechannel\ncd postquantum-securechannel\ndotnet run --project samples/EchoDemo\n```\n\nYou'll see a TCP client and server complete a post-quantum handshake on loopback, verify each\nother's identity by fingerprint, exchange three encrypted messages, and ratchet keys mid-session.\nThe whole thing runs in well under a second.\n\nThen explore the more realistic samples:\n\n| Sample | Shape | What it shows |\n| --- | --- | --- |\n| [`samples/MicroserviceWebSocket.*`](samples/MicroserviceWebSocket.Server) | ASP.NET Core ↔ client | DI registration, WebSocket adapter, config-driven identity loading |\n| [`samples/WorkerControlPlane`](samples/WorkerControlPlane) | `BackgroundService` ↔ TCP coordinator | Long-lived connection, auto-rekey, hosted-service pattern |\n| [`samples/QueueEnvelope`](samples/QueueEnvelope) | Producer → broker → consumer | Envelope encryption — the broker never sees plaintext |\n| [`samples/EchoDemo`](samples/EchoDemo) | TCP loopback | Minimal end-to-end demonstration |\n\n---\n\n## Security posture at a glance\n\n| Property | How it's achieved |\n| --- | --- |\n| **Post-quantum confidentiality** | X-Wing hybrid (ML-KEM-768 + X25519); session secret holds as long as either primitive does |\n| **Forward secrecy** | Fresh ephemeral X-Wing key pair per handshake; private seeds zeroed after use |\n| **Server authentication** | ML-DSA-65 signature over the full handshake transcript, verified against a pinned key |\n| **Mutual authentication (opt-in)** | Client ML-DSA-65 signature + optional fingerprint allowlist |\n| **Key confirmation** | HMAC-SHA256 Finished MAC proves both sides derived the same keys |\n| **Transcript integrity** | Every signature/MAC covers SHA-256 of all prior handshake bytes |\n| **Record confidentiality + integrity** | AES-256-GCM with per-direction keys and per-epoch nonce-prefix HKDF derivation |\n| **Nonce-reuse safety** | Per-direction 64-bit counters + HKDF-derived IV prefixes; a nonce is never reused under a key |\n| **AES-GCM safety bounds** | NIST SP 800-38D caps enforced: 2³² records / 2³⁶ bytes per epoch (auto-rekey trips earlier) |\n| **Replay / reorder protection** | Strict in-order check (default) or fixed-size sliding-window bitmap for unordered transports |\n| **In-band rekeying** | `UpdateSendKey()` ratchets each direction to fresh keys without a re-handshake |\n| **Strong key separation** | HKDF-SHA256 with distinct, versioned domain-separation labels for every derived secret |\n| **Version negotiation** | Every handshake selects the highest mutually-supported protocol version |\n| **DoS-resistant replay window** | Sliding window is a fixed-size bitmap; a peer cannot influence receiver memory |\n| **Observable** | `EventSource` + `Meter` + `ActivitySource` named `PostQuantum.SecureChannel` |\n\n\u003e ⚠ **Preview release.** The cryptographic core is validated against published IETF/NIST test\n\u003e vectors, but this library has **not** had an independent security audit. Read\n\u003e [`KNOWN-GAPS.md`](KNOWN-GAPS.md) and [`docs/threat-model.md`](docs/threat-model.md) before relying\n\u003e on it for high-value secrets.\n\n---\n\n## Why post-quantum, and why hybrid?\n\nA sufficiently large quantum computer running Shor's algorithm would break the classical key\nexchange (ECDH/RSA) that protects most traffic today. The threat is **\"harvest now, decrypt later\"**:\nan adversary can record encrypted traffic today and decrypt it years later once such a machine\nexists. Anything that needs to stay confidential into the 2030s+ needs post-quantum protection now.\n\n**Hybrid** key agreement is the conservative path the IETF and NIST recommend during the\ntransition: combine a new lattice KEM with a battle-tested classical one. X-Wing does exactly this.\nIf ML-KEM is later found flawed, X25519 still protects you; if a quantum computer breaks X25519,\nML-KEM still protects you. You only lose if **both** fall.\n\n---\n\n## Documentation by scenario\n\n- **[Architecture](docs/architecture.md)** — how it fits together (layers, handshake, key schedule).\n- **[Decision guide](docs/decision-guide.md)** — when to use this vs TLS / Noise / libsodium.\n- **[Threat model](docs/threat-model.md)** — goals, non-goals, and adversary capabilities.\n- **[Operations guide](docs/operations.md)** — pinning, rotation, alerts, incident response.\n- **[Troubleshooting](docs/troubleshooting.md)** — every common exception with a recovery path.\n- **[Protocol spec](docs/protocol.md)** — wire format, key schedule, KAT references.\n- **[Changelog](CHANGELOG.md)** — full version history.\n\n### What's new in 0.3.0\n\n- **`PostQuantum.SecureChannel.AspNetCore`** — DI registration, `IConfiguration` binding, WebSocket\n  adapter, `MapPqWebSocket()` endpoint helper.\n- **`PostQuantum.SecureChannel.Testing`** — in-memory duplex stream and one-call handshake harness.\n- **OpenTelemetry-friendly tracing** — `ActivitySource` alongside the existing `Meter` / `EventSource`.\n- **Production-shaped samples** — microservice WebSocket, worker → control-plane, queue envelope.\n- **Scenario-first docs** — architecture, threat model, decision guide, operations, troubleshooting.\n\nEarlier release notes (0.2.x DoS-resistant replay, NIST caps, multi-pin, presets, observability) are\nin the [changelog](CHANGELOG.md).\n\n---\n\n## How this library is different\n\nPostQuantum.SecureChannel is a **high-level secure-channel library**, not a primitives bundle.\n\n- **Not this:** \"here is ML-KEM. Here is ML-DSA. Here is HKDF. Wire them together correctly,\n  remember the key schedule, defeat replay yourself, get the AEAD nonce construction right, and\n  please don't reuse a nonce.\" That is the BouncyCastle or libsodium experience.\n- **This:** `var (session, _) = ...handshake; session.Encrypt(payload)`. The protocol, key\n  schedule, replay protection, ratcheting, and NIST safety bounds are decided for you — there are\n  no insecure knobs.\n\nIf you need primitives, use BouncyCastle directly. If you need a **session** between two .NET\nendpoints that is mutually authenticated, forward-secret, post-quantum-safe, and observable —\nthat's what this library is for.\n\n---\n\n## Quick start — a secure channel over TCP (server-authenticated)\n\nThis is the realistic adoption path: wrap any bidirectional `Stream`, get back something that\nbehaves like `Stream` but transparently encrypts everything. The library drives the three-message\nhandshake and the AES-256-GCM record layer for you.\n\n```csharp\nusing System.Net;\nusing System.Net.Sockets;\nusing System.Text;\nusing PostQuantum.SecureChannel;\nusing PostQuantum.SecureChannel.Transport;\n\n// ── One-time setup: the server has a long-term identity. Distribute the public ─\n// half out of band (config / Key Vault / Secrets Manager) and pin it on clients.\nusing var serverIdentity = PqIdentity.Create();\nstring pinnedBase64 = serverIdentity.PublicKey.ToBase64();\nConsole.WriteLine($\"Pin this on the client: {serverIdentity.PublicKey.ShortFingerprint()}\");\n\n// ── Server side ────────────────────────────────────────────────────────────────\nvar listener = new TcpListener(IPAddress.Loopback, 5001);\nlistener.Start();\n_ = Task.Run(async () =\u003e\n{\n    using var conn = await listener.AcceptTcpClientAsync();\n    await using var channel = await PqSecureChannel.AcceptAsync(\n        conn.GetStream(),\n        new PqServerOptions { Identity = serverIdentity });\n\n    var buf = new byte[1024];\n    int read = await channel.ReadAsync(buf);\n    await channel.WriteAsync(Encoding.UTF8.GetBytes($\"echo: {Encoding.UTF8.GetString(buf, 0, read)}\"));\n});\n\n// ── Client side ────────────────────────────────────────────────────────────────\nusing var tcp = new TcpClient();\nawait tcp.ConnectAsync(IPAddress.Loopback, 5001);\n\nawait using var channel = await PqSecureChannel.ConnectAsync(\n    tcp.GetStream(),\n    new PqClientOptions { ServerIdentity = PqIdentityPublicKey.FromBase64(pinnedBase64) });\n\nConsole.WriteLine($\"Verified server {channel.Session.RemoteIdentity!.ShortFingerprint()}\");\n\nawait channel.WriteAsync(Encoding.UTF8.GetBytes(\"hello, post-quantum world\"));\nvar reply = new byte[64];\nint n = await channel.ReadAsync(reply);\nConsole.WriteLine(Encoding.UTF8.GetString(reply, 0, n)); // \"echo: hello, post-quantum world\"\n```\n\nThat's it. The handshake, the AES-256-GCM record framing, the per-direction sequence counters,\nthe replay check, and the key-update policy are all handled by `PqSecureChannelStream`.\n\n### Pinning and verifying the server's fingerprint\n\n```csharp\n// Distribute the key (e.g. in config) and print a fingerprint to compare out of band:\nstring pinned = serverIdentity.PublicKey.ToBase64();             // store this on the client\nConsole.WriteLine(serverIdentity.PublicKey.Fingerprint());       // full SHA-256, lowercase hex\nConsole.WriteLine(serverIdentity.PublicKey.ShortFingerprint());  // e.g. \"9f:86:d0:81:88:4c:7d:65\"\nConsole.WriteLine(serverIdentity.PublicKey);                     // \"pq:9f:86:d0:81:88:4c:7d:65\"\n```\n\nIf a client ever sees a different fingerprint, the server's key has changed (or someone is in the\nmiddle) — and the handshake fails with `PqAuthenticationException` *before* any traffic is\nexchanged.\n\n---\n\n## Advanced — driving the handshake yourself (no I/O in the library)\n\nFor transports that aren't `Stream`-shaped (a message queue, a gRPC metadata channel, an HTTP\nrequest/response round trip), drive the three handshake messages by hand. The library does no I/O\nof its own at this layer — you exchange `byte[]`s however you like.\n\n```csharp\nusing var serverIdentity = PqIdentity.Create();\nbyte[] pinned = serverIdentity.PublicKey.Export();\n\nusing var client = PqSecureChannel.CreateClient(new PqClientOptions\n{\n    ServerIdentity = PqIdentityPublicKey.Import(pinned),\n});\nusing var server = PqSecureChannel.CreateServer(new PqServerOptions\n{\n    Identity = serverIdentity,\n});\n\nbyte[] clientHello    = client.CreateClientHello();                  // 1 → send to server\nbyte[] serverHello    = server.ProcessClientHello(clientHello);      // 2 → send to client\nvar    handshake      = client.ProcessServerHello(serverHello);\nbyte[] clientFinished = handshake.ClientFinished;                    // 3 → send to server\nPqSession clientSession = handshake.Session;\nPqSession serverSession = server.ProcessClientFinished(clientFinished);\n\n// Both sides now have a live PqSession. Encrypt / Decrypt are symmetric.\nbyte[] record = clientSession.Encrypt(Encoding.UTF8.GetBytes(\"hello\"));\nstring text   = Encoding.UTF8.GetString(serverSession.Decrypt(record));\n```\n\n---\n\n## Mutual authentication\n\nHave the client present its own identity, and require it on the server:\n\n```csharp\nusing var clientIdentity = PqIdentity.Create();\n\nvar client = PqSecureChannel.CreateClient(new PqClientOptions\n{\n    ServerIdentity = PqIdentityPublicKey.Import(pinnedServerKey),\n    ClientIdentity = clientIdentity,            // ← client authenticates too\n});\n\nvar server = PqSecureChannel.CreateServer(new PqServerOptions\n{\n    Identity = serverIdentity,\n    RequireClientAuthentication = true,         // ← reject anonymous clients\n    AuthorizedClients = [clientIdentity.PublicKey], // optional allowlist\n});\n\n// After the handshake, the server knows exactly who connected:\nConsole.WriteLine(serverSession.RemoteIdentity!.Fingerprint());\n```\n\n---\n\n## Encrypting with associated data (AAD)\n\nBind a record to context that is authenticated but not encrypted — a message type, a route, a\nconnection id. The same value must be supplied on decrypt or the record is rejected.\n\n```csharp\nbyte[] aad = Encoding.UTF8.GetBytes(\"channel:orders\");\nbyte[] record = clientSession.Encrypt(payload, aad);\nbyte[] plain  = serverSession.Decrypt(record, aad);   // must match\n```\n\n---\n\n## Rekeying a long-lived session (key update)\n\nFor long-lived connections, ratchet to fresh keys periodically without re-handshaking. The peer\nhandles it transparently.\n\n```csharp\n// On a raw session:\nbyte[] keyUpdate = clientSession.UpdateSendKey();   // send these bytes to the peer\nPqIncomingRecord r = serverSession.Open(keyUpdate); // r.ContentType == PqContentType.KeyUpdate\n\n// On a stream:\nawait channel.UpdateSendKeyAsync();                 // peer's reads continue seamlessly\n```\n\nEach direction ratchets independently, and the old epoch's keys can no longer decrypt new traffic.\n\n**Automatic rekeying.** Set a policy and the stream ratchets for you once a threshold is crossed:\n\n```csharp\nvar options = new PqClientOptions\n{\n    ServerIdentity = PqIdentityPublicKey.FromBase64(pinnedServerKey),\n    SessionOptions = new PqSessionOptions { KeyUpdatePolicy = PqKeyUpdatePolicy.Recommended },\n    // or: new PqKeyUpdatePolicy { MaxRecordsBeforeUpdate = 1_000_000, MaxBytesBeforeUpdate = 1L \u003c\u003c 30 }\n};\n// On a raw session, check it yourself:\nif (session.NeedsKeyUpdate) { var ku = session.UpdateSendKey(); /* send ku */ }\n```\n\n---\n\n## Replay \u0026 reorder protection\n\nEvery record carries a per-direction, per-epoch 64-bit sequence number that feeds both the AES-GCM\nnonce *and* the receive-side replay check. Two modes are available:\n\n| Mode | Behavior | Use when |\n| --- | --- | --- |\n| **`PqReplayProtection.StrictOrdered`** *(default)* | Records must arrive in exact order. Any gap or repeat is rejected with `PqDecryptionException`. | Reliable, ordered transports (TCP, named pipes, ordered WebSockets). |\n| **`PqReplayProtection.SlidingWindow`** | A fixed-size bitmap (DTLS-style) accepts in-window, not-yet-seen sequences in any order. Replays and records older than the window are rejected. | Unordered / lossy transports (UDP-style, message queues with no ordering guarantee). |\n\n```csharp\nvar options = new PqServerOptions\n{\n    Identity = serverIdentity,\n    SessionOptions = new PqSessionOptions\n    {\n        ReplayProtection = PqReplayProtection.SlidingWindow,\n        ReplayWindowSize = 64,     // valid range: 8…1024\n    },\n};\n```\n\nThe sliding-window bitmap is allocated **once at session construction** — a peer cannot influence\nthe receiver's memory footprint by sending crafted sequence numbers. Every check and commit is O(1)\nand allocation-free.\n\nReplay rejections are emitted on the `pqsc.records.rejected` metric counter (tagged with\n`reason=sequence-replay-or-reorder` vs `aead-auth-failure`), so they are easy to alert on.\n\n---\n\n## Rotating the server identity\n\nReal deployments rotate keys. Pin both during the overlap window, then drop the old one once every\nserver has rolled:\n\n```csharp\nvar client = PqSecureChannel.CreateClient(new PqClientOptions\n{\n    ServerIdentity = newKey,                          // the key you're rolling toward\n    AllowedServerIdentities = [oldKey, newKey],       // accept either while rolling\n});\n```\n\nA client that sees a server still presenting the old key succeeds; a server presenting an unrelated\nkey still fails with `PqAuthenticationException`.\n\n---\n\n## Observability\n\nEvery handshake outcome, replay rejection, key update, and exhausted epoch is emitted on both an\n`EventSource` (`PostQuantum.SecureChannel`) and a `Meter` of the same name — no extra logging\ndependencies, no configuration to enable.\n\n```bash\n# Live counters in a terminal:\ndotnet-counters monitor --counters PostQuantum.SecureChannel\n\n# Capture a trace for PerfView/Speedscope:\ndotnet-trace collect --providers PostQuantum.SecureChannel\n```\n\nCounters emitted (tagged with role/reason/direction where relevant):\n`pqsc.handshakes.started`, `pqsc.handshakes.completed`, `pqsc.handshakes.failed`,\n`pqsc.records.rejected`, `pqsc.key_updates.sent`, `pqsc.key_updates.received`,\n`pqsc.epochs.exhausted`. OpenTelemetry consumers can subscribe to the `Meter` directly.\n\n---\n\n## Pick a session preset\n\nFor most callers the named presets on `PqSessionOptions` remove the need to hand-tune anything:\n\n```csharp\nSessionOptions = PqSessionOptions.Recommended       // long-lived TCP with auto-rekey\nSessionOptions = PqSessionOptions.UnorderedTransport // UDP-style / message queues\nSessionOptions = PqSessionOptions.HighThroughput    // larger rekey thresholds\n```\n\n---\n\n## Resumption (experimental)\n\nBind a new session to a previous one by caching its resumption secret on both ends. A full,\nforward-secret X-Wing handshake still runs; the secret simply provides continuity. See\n[`KNOWN-GAPS.md`](KNOWN-GAPS.md).\n\n```csharp\nbyte[] psk = previousSession.ExportResumptionSecret();   // cache on both peers\n// next time:\nvar client = new PqClientOptions { ServerIdentity = pinned, ResumptionSecret = psk };\nvar server = new PqServerOptions { Identity = serverIdentity, ResumptionSecret = psk };\n```\n\n---\n\n## Security posture\n\n**What this library guarantees when used as documented:**\n\n| Property | How it is achieved |\n| --- | --- |\n| Post-quantum confidentiality | X-Wing (ML-KEM-768 + X25519) hybrid key agreement |\n| Forward secrecy | A fresh ephemeral X-Wing key pair per handshake; private seeds are zeroed after use |\n| Server authentication | ML-DSA-65 signature over the full handshake transcript, verified against a pinned key |\n| Mutual authentication (optional) | Client ML-DSA-65 signature + optional allowlist |\n| Key confirmation | HMAC-SHA256 `Finished` MAC proves both sides derived the same keys |\n| Transcript integrity | Every signature/MAC covers a SHA-256 hash of all prior handshake bytes |\n| Record confidentiality + integrity | AES-256-GCM with per-direction keys |\n| Nonce-reuse safety | Per-direction 64-bit counters with HKDF-derived nonce prefixes — a nonce is never reused under a key |\n| Replay / reordering protection | Strict in-order sequence checking (default), or a DTLS-style sliding window for unordered transports |\n| Rekeying | In-band key update ratchets each direction to fresh keys without a new handshake |\n| Strong key separation | HKDF-SHA256 with distinct, versioned domain-separation labels for every derived secret |\n| Version negotiation | Each handshake selects the highest mutually-supported protocol version |\n\n**Defaults are not configurable on purpose.** Algorithms and parameters (ML-KEM-768, ML-DSA-65,\nAES-256-GCM, SHA-256/SHA3-256) are fixed so there are no insecure knobs to misconfigure. A protocol\nversion byte on every message leaves room to evolve safely.\n\n**Please also read [`KNOWN-GAPS.md`](KNOWN-GAPS.md)** for an honest account of what this library does\n*not* yet do, and [`SECURITY.md`](SECURITY.md) for how to report a vulnerability.\n\n---\n\n## How it works (one level deeper)\n\n1. **Key agreement.** The client sends an ephemeral X-Wing public key. The server encapsulates to it,\n   yielding a shared secret and a ciphertext. X-Wing's shared secret is\n   `SHA3-256(ss_MLKEM ‖ ss_X25519 ‖ ct_X25519 ‖ pk_X25519 ‖ label)` — binding both primitives together.\n2. **Authentication.** The server signs `SHA-256(ClientHello ‖ ServerHello-body)` with ML-DSA-65. The\n   client verifies it against the pinned identity before trusting anything.\n3. **Key schedule.** `HKDF-Extract(salt = clientRandom ‖ serverRandom, IKM = X-Wing secret)` →\n   `HKDF-Expand` into a master secret bound to the transcript hash, then into directional AES-256-GCM\n   keys, nonce prefixes, and `Finished` keys.\n4. **Confirmation.** The client returns an HMAC `Finished` over the full transcript (and, for mutual\n   auth, its own signature). The server verifies it before the session is considered live.\n5. **Records.** Each `Encrypt` emits `version ‖ sequence ‖ ciphertext ‖ tag`; the sequence number\n   feeds both the GCM nonce and a strict replay check.\n\nThe X-Wing combiner is validated **byte-for-byte against the IETF draft test vectors** (see\n`tests/PostQuantum.SecureChannel.Tests/XWingTests.cs`).\n\n---\n\n## Project layout\n\n```\nsrc/PostQuantum.SecureChannel/     The library\n  Cryptography/                    X-Wing KEM, ML-DSA signatures (validated primitives)\n  Internal/                        Key schedule, wire format, transcript, anti-replay, protocol constants\n  Transport/                       Async stream adapter + length-prefixed framing\n  PqSecureChannel.cs               Handshake orchestration (client + server)\n  PqSession.cs                     Established session: Encrypt / Decrypt / key update\n  PqIdentity*.cs                   Long-term ML-DSA identities\ntests/PostQuantum.SecureChannel.Tests/   KAT vectors, handshake, session, key update, anti-replay,\n                                          resumption, version negotiation, stream, auto-rekey, and fuzz tests\nsamples/EchoDemo/                  Runnable TCP echo client/server demo\ndocs/protocol.md                   Wire-format and key-schedule specification\n```\n\n---\n\n## Building and testing\n\n```bash\ndotnet build -c Release\ndotnet test\n```\n\n---\n\n## The PostQuantum.* family\n\nPostQuantum.SecureChannel follows the same standards, transparency, and engineering discipline as the\nother `PostQuantum.*` libraries: vetted primitives, secure-by-default APIs, validated test vectors,\nand honest documentation of limitations.\n\n---\n\n## License\n\n[MIT](LICENSE) © 2026 Paul Clark.\n\n---\n\n*Built with care, and an honest accounting of its limits.*\n\n**To God be the glory.** — *1 Corinthians 10:31*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystemslibrarian%2Fpostquantum-securechannel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsystemslibrarian%2Fpostquantum-securechannel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsystemslibrarian%2Fpostquantum-securechannel/lists"}