docs(quic-plan): drop BouncyCastle — Quartz already has every primitive
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
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user