# flare.quic.crypto

> A module of flare_mlake's API.

QUIC v1 crypto primitives (RFC 9001).

Sans-I/O. Every entry point is a pure function over byte spans;
no socket, no fd, no TLS context, no allocator that survives the
call. The QUIC server reactor plumbs the AEAD output
into the packet codec; this module owns the math.

## Layering

The full QUIC crypto story has three slabs that compose:

1. **HKDF** (RFC 5869) -- key derivation. Implemented here over the
   ``hmac_sha256`` primitive that ``flare.crypto`` already exports
   from the OpenSSL FFI. Pure math; no AEAD dependency.

2. **HKDF-Expand-Label** (RFC 8446 §7.1) -- the TLS 1.3 label
   wrapper around HKDF-Expand. RFC 9001 §5.1 reuses TLS 1.3's
   schedule with QUIC-specific labels (``quic key``, ``quic iv``,
   ``quic hp``, ``quic ku``). Implemented here.

3. **AEAD packet protection** (RFC 9001 §5.3-5.4) -- AES-128-GCM /
   AES-256-GCM / ChaCha20-Poly1305 encrypt-with-AD over each
   packet's frame contents, plus the AES-128-CTR-based header
   protection mask (§5.4) that hides the packet number and the
   four header low bits. The trait :trait:`QuicCrypto` names the
   surface a backend must implement; :class:`OpenSslQuicCrypto`
   wires that surface to the OpenSSL FFI for h3 server
   connections (the OpenSSL-backed packet protection path), and
   the rustls binding under :mod:`flare.tls.rustls_quic` drives
   the TLS exchange itself.

## Backends

- :class:`StubQuicCrypto` -- typed sentinel for tests that
  want to assert the trait boundary without driving a real
  AEAD. Encrypt / decrypt / mask raise ``NotImplemented``.
- :class:`OpenSslQuicCrypto` -- production AEAD + header-
  protection backend. Binds the ``flare_quic_aead_*`` /
  ``flare_quic_hp_mask`` FFI thunks from ``libflare_tls.so``
  to the trait surface. Constructs from a 32-byte direction
  secret via :py:meth:`OpenSslQuicCrypto.from_secret`, which
  runs the RFC 9001 paragraph 5.1 schedule.

References:
- RFC 5869 "HMAC-based Extract-and-Expand Key Derivation Function".
- RFC 8446 "The Transport Layer Security (TLS) Protocol Version 1.3".
- RFC 9000 "QUIC: A UDP-Based Multiplexed and Secure Transport".
- RFC 9001 "Using TLS to Secure QUIC".

## Structs

### `InitialSecrets`

```mojo
struct InitialSecrets
```

Per-direction initial-secret pair derived from the client's Destination Connection ID per RFC 9001 §5.2.

Each secret is 32 bytes (SHA-256 hash output). The packet-
protection keys (``quic key`` / ``quic iv`` / ``quic hp``) are
derived from each secret independently via HKDF-Expand-Label.

Implements: `Copyable`, `Movable`

Fields:

- `initial_secret: List[UInt8]`: ``HKDF-Extract(QUIC_V1_INITIAL_SALT, dst_cid)`` -- 32 bytes.
- `client_initial_secret: List[UInt8]`: ``HKDF-Expand-Label(initial_secret, "client in", "", 32)``.
- `server_initial_secret: List[UInt8]`: ``HKDF-Expand-Label(initial_secret, "server in", "", 32)``.

### `QuicAead`

```mojo
struct QuicAead
```

RFC 9001 §5.3 AEAD selector codepoint.

The TLS 1.3 cipher suite negotiated during the handshake
selects one of three AEAD algorithms; the AEAD enum carries
the choice forward into the per-direction packet-protection
keying schedule.

All three are mandatory-to-implement for a TLS 1.3 endpoint
per RFC 8446 §9.1, but RFC 9001 §5.1 mandates that *at least*
AES-128-GCM be supported by every QUIC v1 endpoint.

Implements: `Movable`

#### `AES_128_GCM`

```mojo
comptime AES_128_GCM = Int(0)
```

RFC 5288 AES-128 in GCM mode (the QUIC v1 mandatory-to-implement).

#### `AES_256_GCM`

```mojo
comptime AES_256_GCM = Int(1)
```

RFC 5288 AES-256 in GCM mode.

#### `CHACHA20_POLY1305`

```mojo
comptime CHACHA20_POLY1305 = Int(2)
```

RFC 8439 ChaCha20-Poly1305 AEAD construction.

### `StubQuicCrypto`

```mojo
struct StubQuicCrypto
```

Typed sentinel that raises ``NotImplemented`` on every AEAD operation.

Exists so unit tests can exercise the :trait:`QuicCrypto`
trait boundary without driving a real AEAD; production code
constructs :class:`OpenSslQuicCrypto` instead.

Implements: `Copyable`, `Movable`, `QuicCrypto`

Fields:

- `aead_choice: Int`

#### `__init__`

```mojo
def __init__(out self, aead_choice: Int = Int(0))
```

#### `aead`

```mojo
def aead(self) -> Int
```

#### `encrypt`

```mojo
def encrypt(self, plaintext: Span[UInt8], associated_data: Span[UInt8], packet_number: UInt64) -> List[UInt8]
```

#### `decrypt`

```mojo
def decrypt(self, ciphertext: Span[UInt8], associated_data: Span[UInt8], packet_number: UInt64) -> List[UInt8]
```

#### `header_protection_mask`

```mojo
def header_protection_mask(self, sample: Span[UInt8]) -> List[UInt8]
```

### `PacketKeys`

```mojo
struct PacketKeys
```

Per-direction packet-protection keys derived from a 32-byte secret per RFC 9001 §5.1.

The handshake produces two secrets per encryption level (one
per direction); each becomes a ``PacketKeys`` via
:func:`derive_packet_keys`.

Implements: `Copyable`, `Movable`

Fields:

- `aead: Int`: ``QuicAead`` codepoint these keys are scheduled for.
- `key: List[UInt8]`: AEAD key -- 16 bytes (AES-128) or 32 bytes (AES-256, ChaCha20).
- `iv: List[UInt8]`: AEAD IV -- 12 bytes (RFC 9001 §5.3 nonce-construction base).
- `hp: List[UInt8]`: Header-protection key -- same length as ``key`` for AES; 32 bytes for ChaCha20-Poly1305 (RFC 9001 §5.4).

### `OpenSslQuicCrypto`

```mojo
struct OpenSslQuicCrypto
```

Production QUIC v1 AEAD + header-protection backend backed by OpenSSL via the ``flare_quic_aead_*`` / ``flare_quic_hp_mask`` FFI thunks.

The carrier holds the per-direction ``PacketKeys`` (AEAD key,
AEAD IV, header-protection key) for a single encryption level
in a single direction. The QUIC server reactor maintains four
carriers per connection: client/server initial, handshake, and
1-RTT (the initial-secret pair gets dropped after the handshake
completes per RFC 9001 paragraph 4.9.1).

Construction:

- :py:meth:`from_secret` -- derive ``PacketKeys`` from a 32-byte
  direction secret + AEAD codepoint, then wrap into the carrier.
- :py:meth:`__init__` -- bring-your-own ``PacketKeys`` for the
  test path or when the keys come from an outboard schedule
  (e.g. the rustls QUIC binding).

Implements: `Copyable`, `Movable`, `QuicCrypto`

Fields:

- `keys: PacketKeys`

#### `__init__`

```mojo
def __init__(out self, var keys: PacketKeys)
```

#### `from_secret`

```mojo
def from_secret(secret: Span[UInt8], aead_choice: Int) -> Self
```

Derive ``PacketKeys`` from ``secret`` per RFC 9001 §5.1 and wrap into a carrier ready to encrypt / decrypt against the schedule's keys.

#### `aead`

```mojo
def aead(self) -> Int
```

#### `encrypt`

```mojo
def encrypt(self, plaintext: Span[UInt8], associated_data: Span[UInt8], packet_number: UInt64) -> List[UInt8]
```

#### `decrypt`

```mojo
def decrypt(self, ciphertext: Span[UInt8], associated_data: Span[UInt8], packet_number: UInt64) -> List[UInt8]
```

#### `header_protection_mask`

```mojo
def header_protection_mask(self, sample: Span[UInt8]) -> List[UInt8]
```

## Traits

### `QuicCrypto`

```mojo
undefined
```

Pluggable QUIC v1 AEAD + header-protection backend.

The QUIC server reactor drives one carrier per
connection. The carrier owns the per-direction packet-
protection keys derived from the initial-secret pair (and
later the 1-RTT secrets) and exposes the encrypt-with-AD /
decrypt-with-AD operations plus the header-protection mask
used to hide the packet number.

Backends:

* ``OpensslQuicCrypto`` (OpenSSL FFI backend)
  -- production default. Routes through the OpenSSL EVP_AEAD
  surface plus EVP_aes_*_ecb for the AES-128-CTR header-
  protection mask.
* ``RustlsQuicCrypto`` -- alternative backend used
  when ``rustls`` is selected as the QUIC TLS provider.

The :class:`StubQuicCrypto` is a typed sentinel: it raises a
clear ``NotImplemented`` ``Error`` on encrypt / decrypt /
header_protection so test harnesses can confirm the trait
boundary is wired up correctly before the real backend lands.

Implements: `Copyable`, `Movable`

#### `aead`

```mojo
def aead(self) -> Int
```

Return the negotiated :data:`QuicAead` value.

#### `encrypt`

```mojo
def encrypt(self, plaintext: Span[UInt8], associated_data: Span[UInt8], packet_number: UInt64) -> List[UInt8]
```

Encrypt ``plaintext`` against ``associated_data`` with the current packet-protection key, returning ciphertext plus the 16-byte authentication tag appended.

``packet_number`` is XOR'd into the per-key static IV per
RFC 9001 §5.3 to form the AEAD nonce.

#### `decrypt`

```mojo
def decrypt(self, ciphertext: Span[UInt8], associated_data: Span[UInt8], packet_number: UInt64) -> List[UInt8]
```

Decrypt ``ciphertext`` (which must include the 16-byte AEAD tag appended) against ``associated_data``, returning the plaintext. Raises on tag mismatch.

#### `header_protection_mask`

```mojo
def header_protection_mask(self, sample: Span[UInt8]) -> List[UInt8]
```

RFC 9001 §5.4 -- compute the 5-byte header-protection mask from a 16-byte ``sample`` of the packet's encrypted payload (starting at offset 4 into the payload, per §5.4.2). For AES-based AEADs the mask is the first 5 bytes of ``AES-128-ECB(hp_key, sample)``; for ChaCha20-Poly1305 it is the first 5 bytes of ``ChaCha20(hp_key, counter || nonce)`` where the sample's first 4 bytes are the counter and the remaining 12 are the nonce.

## Functions

### `hkdf_extract`

```mojo
def hkdf_extract(salt: Span[UInt8], ikm: Span[UInt8]) -> List[UInt8]
```

RFC 5869 §2.2 -- ``PRK = HMAC-Hash(salt, IKM)``.

Returns the 32-byte pseudorandom key (PRK). The hash is fixed
at SHA-256; QUIC v1 mandates SHA-256 for the initial keys
schedule (RFC 9001 §5.2).

### `hkdf_expand`

```mojo
def hkdf_expand(prk: Span[UInt8], info: Span[UInt8], length: Int) -> List[UInt8]
```

RFC 5869 §2.3 -- ``OKM = HKDF-Expand(PRK, info, L)``.

Iteratively computes ``T(i) = HMAC-Hash(PRK, T(i-1) || info || i)``
where ``T(0)`` is empty and ``i`` is a single byte counter
starting at 1. The output is the first ``length`` bytes of
``T(1) || T(2) || ... || T(N)``. ``length`` must be at most
``255 * 32`` (the SHA-256 hash output size).

### `hkdf_expand_label`

```mojo
def hkdf_expand_label(secret: Span[UInt8], label: String, context: Span[UInt8], length: Int) -> List[UInt8]
```

RFC 8446 §7.1 -- ``HKDF-Expand-Label(Secret, Label, Context, Length)``.

Wraps HKDF-Expand with the canonical TLS 1.3 HkdfLabel struct
serialization:

```
struct {
    uint16 length = Length;
    opaque label<7..255> = "tls13 " + Label;
    opaque context<0..255> = Context;
} HkdfLabel;
```

QUIC v1 (RFC 9001 §5.1) replaces ``"tls13 "`` with ``"tls13 "``
-- same prefix -- and uses labels ``"client in"`` /
``"server in"`` for initial-secret derivation, then ``"quic key"``
/ ``"quic iv"`` / ``"quic hp"`` for the AEAD packet-protection
keys.

### `hkdf_expand_label_empty_context`

```mojo
def hkdf_expand_label_empty_context(secret: Span[UInt8], label: String, length: Int) -> List[UInt8]
```

``hkdf_expand_label`` with an empty Context value.

### `derive_initial_secrets`

```mojo
def derive_initial_secrets(dst_cid: Span[UInt8]) -> InitialSecrets
```

RFC 9001 §5.2 -- derive the initial-direction secrets from the client's Destination Connection ID.

The DCID is the client's first-flight ``Initial`` packet's
Destination Connection ID (RFC 9000 §17.2.2). Bookkeeping
notes:

- Both endpoints derive the same secrets from this DCID.
- Once the handshake produces 1-RTT keys, the initial-secret
  branch is discarded (RFC 9001 §4.9.1).
- DCID length must be between 8 and 20 bytes per RFC 9000
  §7.2; this function doesn't enforce the length so callers
  can pass any byte span (fuzz harnesses include zero-length
  DCIDs to probe the trait surface).

### `aead_key_length`

```mojo
def aead_key_length(aead: Int) -> Int
```

Per-AEAD key length in bytes (RFC 9001 §5.1 + RFC 8439).

### `derive_packet_keys`

```mojo
def derive_packet_keys(secret: Span[UInt8], aead: Int) -> PacketKeys
```

RFC 9001 §5.1 -- derive ``(key, iv, hp)`` from a 32-byte per-direction secret.

Labels are the canonical RFC 9001 §5.1 set:

- ``"quic key"`` -- AEAD key (key length per AEAD).
- ``"quic iv"``  -- AEAD IV (always 12 bytes).
- ``"quic hp"``  -- header-protection key (same length as
  AEAD key for AES; 32 bytes for ChaCha20).

The same schedule applies to initial, handshake, and 1-RTT
secrets; only the source secret changes. The handshake +
1-RTT secrets come from the TLS 1.3 exporter (RFC 9001 §7
handshake state machine, wired once rustls is integrated).

## Aliases

### `QUIC_V1_INITIAL_SALT`

```mojo
comptime QUIC_V1_INITIAL_SALT = List(UInt8(56), UInt8(118), UInt8(44), UInt8(247), UInt8(245), UInt8(89), UInt8(52), UInt8(179), UInt8(77), UInt8(23), UInt8(154), UInt8(230), UInt8(164), UInt8(200), UInt8(12), UInt8(173), UInt8(204), UInt8(187), UInt8(127), UInt8(10), __list_literal__=NoneType(None))
```

RFC 9001 §5.2 QUIC v1 initial salt -- ``0x38762cf7f55934b34d179ae6a4c80cadccbb7f0a``.

Used as the HKDF-Extract salt when deriving the initial-secret
from the client's Destination Connection ID. RFC 9001 §5.2 names
this as the only valid salt for the v1 wire format.

### `SHA256_OUTPUT_BYTES`

```mojo
comptime SHA256_OUTPUT_BYTES = Int(32)
```

SHA-256 hash output size in bytes. Used as the HKDF-Extract output length and the default HKDF-Expand-Label output length for the QUIC initial-secret derivations.
