Adds X448, the RFC 9180 Auth and AuthPSK modes, and the post-quantum ML-KEM KEMs. Two changes can break an upgrade from 0.2.0: Kem.id and Error.t gain constructors, which exhaustive matches must handle, and hpke is no longer installable on 32-bit architectures.
Kem.X448, DHKEM(X448, HKDF-SHA512), identifier 0x0021, with 56-byte keys and a 64-byte shared secret. Key derivation and clamping follow the X25519 path with the X448 rules of RFC 7748, and a low-order public value is rejected when it is used. The new constructor breaks exhaustive matches on Kem.id, which is why this is not a patch release.curve448 for the X448 primitive. hpke depends on the plain curve448 library, so the pure OCaml implementation links by default and an application selects the C one by adding curve448.c to its own libraries. curve448 needs a 64-bit OCaml, so hpke is no longer installable on 32-bit architectures, whichever KEM an application uses.Rfc9180.setup_auth_sender, setup_auth_receiver, setup_auth_psk_sender, and setup_auth_psk_receiver, and the single-shot seal_auth, open_auth, seal_auth_psk, and open_auth_psk. The sender passes its Private_key.t and the recipient the sender's Public_key.t, both as ~sender; a key of another KEM is a Key_mismatch. Sender authentication is not a signature: whoever holds the recipient's private key, and in AuthPSK mode the PSK as well, can seal as any sender. The additions are new functions only, so existing callers are unaffected.setup_auth_sender and setup_auth_psk_sender to hpke.for_testing.Kem.Mlkem512, Kem.Mlkem768, and Kem.Mlkem1024, the post-quantum ML-KEM KEMs of FIPS 203 with identifiers 0x0040 to 0x0042, as draft-ietf-hpke-pq-05 specifies them. They work in the Base and PSK modes, where a suite with an HKDF runs the RFC 9180 key schedule unchanged. A private key is the 64-byte seed d || z, and generate_key_pair takes it from the generator as ML-KEM.KeyGen does. A public key must pass the modulus check of FIPS 203. Keys are parsed once and kept: parsing a private key runs key generation, and parsing a public key expands its matrix. An encapsulated key is an ML-KEM ciphertext, so Kem.encapsulated_key_size is no longer Kem.public_key_size for every KEM. One of the right length never fails to decapsulate: a forged one yields an unrelated secret (implicit rejection), and the failure surfaces as Open_error at the first open. The draft is not yet an RFC, and derive_key_pair for these KEMs follows it.Kem.supports_auth and Error.Unsupported_mode. ML-KEM has no authenticated encapsulation, so on an ML-KEM suite the Auth and AuthPSK functions return Unsupported_mode before they look at a key or draw randomness, and the single-shot opens leave it distinguishable, as they do Key_mismatch. The new constructor breaks exhaustive matches on Error.t.mlkem for the ML-KEM primitive, and for the SHAKE256 that deriving an ML-KEM key pair takes: mlkem exports the one ML-KEM itself runs on as Mlkem.Fips202, so the library still implements no primitive of its own.hpke.for_testing refuses an ML-KEM suite, which has no ephemeral key to choose: its Base and PSK senders return Invalid_private_key, and its Auth and AuthPSK senders Unsupported_mode, as the ordinary Auth and AuthPSK functions do. A generator that returns the fixed encapsulation randomness, passed as ~rng to the ordinary setup functions, reproduces such vectors.draft-ietf-hpke-pq-05 corpus: its three ML-KEM vectors that use an HKDF, replayed in full, and the fourth as far as the KEM goes.Sender.seal or Receiver.open_ while it marks the context busy. Since 0.1.0, Fun.protect allocated before installing its handler, and on an exception again before releasing the context, and an exception raised at either allocation left the context marked busy for good, so that every later call returned Concurrent_use.Sender.seal or Receiver.open_ rewind a context's sequence number. Since 0.1.0, an exception raised by a signal handler, such as Sys.Break or a timeout, could stop the increment between two byte stores and leave the sequence 255 or more below the number just used. A caller that caught it and kept the context then sealed under nonces it had already used, and a receiver accepted replays. The increment now stores the carried byte first, so an interrupted one can at most skip sequence numbers.Plaintext_too_long. Since 0.1.0 the limit was RFC 5116's, one byte beyond NIST SP 800-38D (RFC 5116 erratum 5219). mirage-crypto refuses that byte itself, so Aead.seal and Rfc9180.Sender.seal returned Internal_error for it, where the interface promises Plaintext_too_long. Every shorter plaintext seals as before.Kdf.extract and Kdf.expand (plain RFC 5869 HKDF) and the single-shot Aead.seal and Aead.open_ under a key prepared by Aead.key and an explicit nonce. Wrong-sized keys, nonces, pseudorandom keys, and output lengths are reported as Invalid_length.Aead.key_size, Aead.nonce_size, Aead.tag_size, Kdf.hash_size, and Kem.secret_size.hpke.for_testing library, which sets up a Base or PSK sender from a caller-chosen ephemeral private key so that vectors which fix skE can be reproduced. It must never be linked outside a test suite.skEm, and every encryption record as known answers for the new entry points.DeriveKeyPair, explicit-RNG key generation, and minimum-length PSK values.