From 1ff951dbb4c3a03cda745280fa78a1030b7240d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 12:25:58 +0000 Subject: [PATCH] =?UTF-8?q?docs(quic-plan):=20drop=20BouncyCastle=20?= =?UTF-8?q?=E2=80=94=20Quartz=20already=20has=20every=20primitive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited Quartz's crypto surface and found every primitive the QUIC plan called out for BouncyCastle is already present in commonMain: - `utils/ciphers/AESGCM.kt` — AES-GCM with AAD (QUIC-TLS AEAD) - `nip44Encryption/crypto/ChaCha20Poly1305.kt` — pure-Kotlin AEAD - `nip44Encryption/crypto/Hkdf.kt` + `utils/mac/MacInstance.kt` — HKDF - `utils/sha256/Sha256.kt` — SHA-256 - `marmot/mls/crypto/X25519.kt` — X25519 ECDH (TLS 1.3 key exchange) - `marmot/mls/crypto/Ed25519.kt` — Ed25519 signatures - `utils/SecureRandom.kt` Plan update highlights: - "What we delegate" section rewritten to point at Quartz primitives. - Phase B renamed from "TLS via BouncyCastle" to "TLS 1.3 client state machine on Quartz primitives"; bumped from 2 to 3 weeks because we write the state machine ourselves, but eliminates the entire BC-adapter integration risk. - "Dependencies to add" section zeroed out — nothing goes in gradle/libs.versions.toml. The only new primitive is a thin HKDF-Expand-Label helper on top of existing `MacInstance`, which we can upstream to Quartz's `Hkdf` as a general `expand(prk, info, length)`. - Risk table rewritten: removed BC-specific risks, added Quartz-specific ones (verify `X25519.dh` against RFC 7748 vector on Phase B day-1). - Cert chain signature verification uses JDK `Signature` for RSA + ECDSA plus Quartz `Ed25519` for Ed25519 leaves. Total timeline moves from 16-18 weeks to 17-19 weeks — same band, but with zero external dependencies and zero Maven-resolution risk. https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D --- ...4-22-pure-kotlin-quic-webtransport-plan.md | 134 ++++++++++++------ 1 file changed, 90 insertions(+), 44 deletions(-) diff --git a/docs/plans/2026-04-22-pure-kotlin-quic-webtransport-plan.md b/docs/plans/2026-04-22-pure-kotlin-quic-webtransport-plan.md index f64943a33..7005b6f65 100644 --- a/docs/plans/2026-04-22-pure-kotlin-quic-webtransport-plan.md +++ b/docs/plans/2026-04-22-pure-kotlin-quic-webtransport-plan.md @@ -38,10 +38,30 @@ on top to interop with nests. This plan scopes that work honestly. ### What we delegate -- **TLS 1.3 handshake state machine + AEAD primitives** → BouncyCastle - (`org.bouncycastle:bctls-jdk18on:1.79` plus `bcprov` + `bcpkix` already cached). -- **X.509 cert validation** → JDK `TrustManagerFactory` (Android system trust store). -- **AES-GCM, ChaCha20-Poly1305, HKDF** → JCA via BC provider. +- **TLS 1.3 AEAD ciphers** → Quartz's existing `AESGCM` (common, with AAD + support) + `ChaCha20Poly1305` (common, pure-Kotlin). +- **HKDF primitives** → Quartz's existing `Hkdf` class + its `MacInstance` + (HMAC-SHA256). We add one thin helper — HKDF-Expand-Label from RFC 8446 §7.1 + — on top, plus a general `hkdfExpand(prk, info, length)` since Quartz only + ships NIP-44's specialised `fastExpand`. +- **Key agreement** → Quartz's existing `X25519` (common expect/actual). +- **SHA-256 hashing** → Quartz's existing `Sha256` (common expect/actual). +- **Cert chain signature verification** → Quartz's `Ed25519` where the leaf + uses Ed25519; JDK `Signature.getInstance("SHA256withRSA")` / + `"SHA256withECDSA"` for the common cases (nests typically uses Let's Encrypt + ECDSA certs). +- **X.509 parsing + chain validation** → JDK `CertificateFactory` + + `TrustManagerFactory.getInstance(...).init(null as KeyStore?)` (Android + system trust store). +- **AES-ECB for QUIC header protection** (one block, 5-byte sample) → JDK + `Cipher.getInstance("AES/ECB/NoPadding")` — trivial, no dep. +- **SecureRandom** → Quartz's `RandomInstance` / `SecureRandom`. + +**No BouncyCastle dependency.** The QUIC-TLS cryptographic surface is already +fully covered by Quartz's primitives — nip44Encryption/crypto (AEAD + HKDF + +ChaCha20 + Poly1305), marmot/mls/crypto (X25519 + Ed25519 + Curve25519Field), +utils/ciphers/AESGCM, utils/mac, utils/sha256, utils/SecureRandom. We write +the TLS 1.3 client state machine ourselves using those primitives. ### Explicitly out of scope @@ -97,18 +117,35 @@ plus an integration test against the next layer up. - Largest-acked / next-pn tracking per space. - **Tests:** varint already done; add CID + packet-number-space unit tests. -### Phase B — TLS via BouncyCastle (2 weeks) -- `BcQuicTlsClient` adapts `org.bouncycastle.tls.TlsClientProtocol`. BC's TLS - client wants two streams; we wire a `PipedInputStream` / `PipedOutputStream` - pair for the input side and capture the output side via a custom - `OutputStream` that forwards bytes to the `CRYPTO`-frame queue. -- Hook `TlsCryptoProvider` callbacks to capture the per-encryption-level secrets - (Initial, Handshake, 0-RTT, 1-RTT) BC derives during the handshake. -- Implement `QuicTransportParameters` extension (codec for the TLS extension - blob carrying `initial_max_data`, `max_idle_timeout`, etc.). -- **Tests:** stand up an in-process server using BC's `TlsServerProtocol` with - the same shim; assert client + server drive each other to the Application - Data state and emit matching 1-RTT secrets. +### Phase B — TLS 1.3 client state machine (3 weeks) + +Built entirely on Quartz primitives; no BouncyCastle. + +- `HkdfExpandLabel` helper per RFC 8446 §7.1 on top of Quartz's `Hkdf.extract` + + a new `hkdfExpand(prk, info, length)` written against `MacInstance`. +- `TlsKeySchedule` — derive Early / Handshake / Master secrets + per-direction + traffic secrets per RFC 8446 §7.1. +- `TlsTranscriptHash` — running SHA-256 over the handshake messages using + Quartz's `Sha256`. +- `TlsCipherSuite` — the three QUIC-mandatory suites: TLS_AES_128_GCM_SHA256 + (uses Quartz `AESGCM`), TLS_CHACHA20_POLY1305_SHA256 (uses Quartz + `ChaCha20Poly1305`), and optionally TLS_AES_256_GCM_SHA384 (SHA-384 is the + only primitive not yet in Quartz; skip this suite for v1). +- `TlsKeyExchange` — X25519 via Quartz `X25519.generateKeyPair()` + + `X25519.dh()`. +- `TlsClientHello` / `TlsServerHello` / `TlsEncryptedExtensions` / + `TlsCertificate` / `TlsCertificateVerify` / `TlsFinished` encode/decode. +- QUIC transport parameters TLS extension (codec for the opaque blob carrying + `initial_max_data`, `max_idle_timeout`, `max_datagram_frame_size`, etc.). +- Certificate chain validation delegated to JDK `TrustManagerFactory`. +- `TlsClient` state machine: drive handshake by consuming CRYPTO-frame payload + bytes and producing outbound CRYPTO-frame payload bytes; expose per- + encryption-level secrets as they're derived so the QUIC layer can install + keys. +- **Tests:** RFC 8448 §3 (Simple 1-RTT Handshake) test vectors — bit-for-bit + reproduction of the Client's generated bytes given fixed randomness. In- + process handshake against a minimal test TLS server built with the same + primitives. ### Phase C — Initial + Handshake packets (2 weeks) - Long-header packet codec (Type, Version, DCID, SCID, Token, Length, PN). @@ -210,10 +247,12 @@ plus an integration test against the next layer up. | Risk | Mitigation | Stop condition | |---|---|---| -| BouncyCastle TLS 1.3 doesn't expose the secret-derivation hooks QUIC needs | Investigate BC's `TlsCryptoProvider` early in Phase B before committing the rest | If by end of Phase B we can't get per-encryption-level secrets out of BC, switch to `bctls`'s lower-level `TlsCrypto` API and implement the TLS 1.3 record layer ourselves on top (~+4 weeks) | +| Quartz's `Hkdf` only ships the NIP-44-specialised `fastExpand`, not a general-purpose expand | Write generic `hkdfExpand(prk, info, length)` in Phase B on top of `MacInstance`; upstream the helper back to Quartz `utils/` | Trivial — add one file | +| Quartz's `X25519` doesn't accept arbitrary-length secrets or has subtle incompatibilities with TLS 1.3's derivation | Verify in Phase B day-1 with a micro-test: `X25519.dh(fixed_priv, fixed_pub)` against RFC 7748 test vector | If mismatched, pin a specific Quartz version or fork the primitive | | Header protection edge cases mis-encode 4-byte packet numbers | RFC 9001 has explicit test vectors | None — must work | | QPACK dynamic table from nests is more aggressive than literal-only | Decoder supports full dynamic table; only encoder is literal-only | If nests rejects literal-only encoded headers, add minimal encoder dynamic table (~+1 week) | | Loss recovery oscillates badly under real RTT variance | NewReno is conservative enough | If empirically bad, swap to BBRv1 implementation reference (~+2 weeks) | +| Server certs use RSA + SHA-256 / ECDSA P-256 / Ed25519 — we need all three sig verifiers | JDK `Signature.getInstance("SHA256withRSA")` + `Signature.getInstance("SHA256withECDSA")` cover RSA + ECDSA; Quartz `Ed25519` covers Ed25519 | None — all three are cheap | | WebTransport draft mismatch with nests | Pin to whatever draft nests serves; advertise both legacy + current setting IDs | Hard fail back to documenting the version skew | **Hard abandonment trigger:** if at end of Phase D (~6 weeks in) we cannot pass @@ -227,40 +266,47 @@ elsewhere. | Phase | Weeks | Cumulative | |---|---|---| | A. Foundations | 1 | 1 | -| B. TLS via BC | 2 | 3 | -| C. Initial + Handshake packets | 2 | 5 | -| D. 1-RTT + STREAM | 1 | 6 | -| E. ACK + flow control | 1 | 7 | -| F. Loss + congestion | 1 | 8 | -| G. Datagrams | ½ | 8.5 | -| H. Connection lifecycle | 1 | 9.5 | -| I. HTTP/3 | 2 | 11.5 | -| J. QPACK | 2 | 13.5 | -| K. Extended CONNECT + WT | 1 | 14.5 | -| L. Interop + hardening | 2 | 16.5 | +| B. TLS 1.3 client on Quartz primitives | 3 | 4 | +| C. Initial + Handshake packets | 2 | 6 | +| D. 1-RTT + STREAM | 1 | 7 | +| E. ACK + flow control | 1 | 8 | +| F. Loss + congestion | 1 | 9 | +| G. Datagrams | ½ | 9.5 | +| H. Connection lifecycle | 1 | 10.5 | +| I. HTTP/3 | 2 | 12.5 | +| J. QPACK | 2 | 14.5 | +| K. Extended CONNECT + WT | 1 | 15.5 | +| L. Interop + hardening | 2 | 17.5 | -**16-18 weeks of full-time work for one developer**, or 5-6 months at a normal -review-and-iteration cadence. Add a security review of the QUIC + TLS code paths -before shipping to users — minimum 2 weeks calendar time, possibly external. +**17-19 weeks of full-time work for one developer**, or 5-6 months at a +normal review-and-iteration cadence. TLS 1.3 is +1 week vs. the original +BC-adapter estimate because we write the state machine ourselves, but the +total is within the same band and we've eliminated the external-library +integration risk entirely. Add a security review of the QUIC + TLS + new +HKDF-Expand helper before shipping to users — minimum 2 weeks calendar +time, possibly external. ## Dependencies to add -```toml -# gradle/libs.versions.toml -[versions] -bouncycastle = "1.79" # bcprov already transitively included; add bctls +**None.** Every cryptographic primitive lives in Quartz already: -[libraries] -bouncycastle-tls = { group = "org.bouncycastle", name = "bctls-jdk18on", version.ref = "bouncycastle" } -``` +| Primitive | Quartz location | +|---|---| +| AES-128-GCM with AAD | `utils/ciphers/AESGCM.kt` (commonMain expect, jvmAndroid actual via JCA) | +| ChaCha20-Poly1305 | `nip44Encryption/crypto/ChaCha20Poly1305.kt` (commonMain, pure Kotlin) | +| HKDF-Extract + MAC | `nip44Encryption/crypto/Hkdf.kt` + `utils/mac/MacInstance.kt` | +| SHA-256 | `utils/sha256/Sha256.kt` | +| X25519 ECDH | `marmot/mls/crypto/X25519.kt` (commonMain expect) | +| Ed25519 signatures | `marmot/mls/crypto/Ed25519.kt` | +| SecureRandom | `utils/SecureRandom.kt` + `utils/RandomInstance.kt` | -```kotlin -// nestsClient/build.gradle.kts under jvmAndroid.dependencies -implementation(libs.bouncycastle.tls) -``` +Nothing goes into `gradle/libs.versions.toml`. AES-ECB for header protection +is one-block and uses JDK `javax.crypto.Cipher` directly (no provider swap). -`bcprov-jdk18on` is already cached; verify on Phase B start that `bctls` resolves -from Maven Central before committing further. +One small helper lives in the new `quic/crypto/` package: +`HkdfExpandLabel` (RFC 8446 §7.1) built on `MacInstance`. If it's clean, we +upstream it to Quartz's `Hkdf` as a general-purpose `expand(prk, info, +length)`. ## What ships first