diff --git a/codex-rs/exec-server/src/aws_lc_ml_kem.rs b/codex-rs/exec-server/src/aws_lc_ml_kem.rs index c177dd1233..9c41005627 100644 --- a/codex-rs/exec-server/src/aws_lc_ml_kem.rs +++ b/codex-rs/exec-server/src/aws_lc_ml_kem.rs @@ -1,3 +1,10 @@ +//! Bridges AWS-LC's ML-KEM-768 implementation into Clatter's `Kem` trait. +//! +//! Clatter owns the hybrid Noise state machine, while AWS-LC owns the concrete +//! post-quantum operations and their randomness. This module is intentionally +//! limited to converting between the two libraries' fixed-size key, ciphertext, +//! and shared-secret representations; it does not add protocol behavior. + use aws_lc_rs::kem::Ciphertext; use aws_lc_rs::kem::DecapsulationKey; use aws_lc_rs::kem::EncapsulationKey; @@ -33,6 +40,10 @@ impl Kem for AwsLcMlKem768 { type Ct = HeapArray; type Ss = SensitiveByteArray<[u8; SHARED_SECRET_LEN]>; + // Generate the long-lived static KEM keypair used by hybrid IK. AWS-LC + // returns serialized key bytes; copying them into Clatter's fixed-size + // containers gives the handshake implementation the exact lengths encoded + // by this suite and keeps the secret half in a zeroizing container. fn genkey_rng(_rng: &mut R) -> KemResult> { // AWS-LC owns ML-KEM key-generation randomness internally, so // Clatter's injectable RNG cannot be plumbed through this provider. @@ -54,6 +65,9 @@ impl Kem for AwsLcMlKem768 { }) } + // Parse the peer's serialized public key at the provider boundary, then + // return the ciphertext and shared secret in Clatter's fixed-size types. + // AWS-LC performs both public-key validation and encapsulation randomness. fn encapsulate(pk: &[u8], _rng: &mut R) -> KemResult<(Self::Ct, Self::Ss)> { let encapsulation_key = EncapsulationKey::new(&ML_KEM_768, pk).map_err(|_| KemError::Input)?; @@ -67,6 +81,8 @@ impl Kem for AwsLcMlKem768 { )) } + // Reconstruct the provider key from process-local secret bytes and recover + // the same shared secret the peer mixed into the hybrid handshake. fn decapsulate(ct: &[u8], sk: &[u8]) -> KemResult { // Reject the length before constructing AWS-LC's ciphertext wrapper. // This keeps malformed wire input classified as an input error rather diff --git a/codex-rs/exec-server/src/noise_channel.rs b/codex-rs/exec-server/src/noise_channel.rs index 226319231f..d60b69cc66 100644 --- a/codex-rs/exec-server/src/noise_channel.rs +++ b/codex-rs/exec-server/src/noise_channel.rs @@ -1,11 +1,29 @@ //! Narrow, misuse-resistant wrapper around the Clatter primitives used by the //! remote exec-server relay. //! -//! The harness is the Noise hybrid-IK initiator and pins the registry-provided -//! exec-server static public key. The exec-server is the responder; after it -//! reads the first IK message, Noise exposes the authenticated harness static -//! public key for registry authorization. Application data is allowed only -//! after both the cryptographic handshake and that external authorization pass. +//! # Protocol overview +//! +//! Noise is a framework for turning Diffie-Hellman operations into an +//! authenticated handshake and then an encrypted byte channel. This module uses +//! the `IK` handshake pattern: the harness is the initiator and already knows +//! the exec-server's static public key, while the exec-server learns and +//! authenticates the harness's static public key from the first handshake +//! message. That lets the harness reject the wrong executor immediately and +//! gives the executor a cryptographic identity it can authorize with the +//! environment registry. +//! +//! The suite is "hybrid" because the handshake combines classical X25519 with +//! post-quantum ML-KEM-768. Clatter runs the Noise state machine and mixes both +//! key-agreement results into the session keys; AWS-LC supplies the ML-KEM +//! operations. AES-GCM then protects ordered transport records after the two +//! handshake messages complete. +//! +//! The handshake authenticates keys, not product permissions. The first message +//! therefore carries a registry-issued harness authorization inside its +//! encrypted payload. The exec-server pauses after authenticating that message, +//! asks the registry whether the authenticated harness key is allowed, and only +//! then sends the second handshake message and exposes JSON-RPC. Application +//! data is never accepted before both checks pass. use base64::Engine; use base64::engine::general_purpose::STANDARD; @@ -18,6 +36,11 @@ use serde::Deserialize; use serde::Serialize; use crate::aws_lc_ml_kem::AwsLcMlKem768; + +/// Stable identifier for the complete handshake and transport algorithm suite. +/// +/// This value travels with public keys so configuration cannot silently combine +/// key material generated for a different Noise pattern or algorithm set. pub const NOISE_CHANNEL_SUITE: &str = "Noise_hybridIK_X25519+MLKEM768_AESGCM_SHA256"; type DhKeyPair = KeyPair<::PubKey, ::PrivateKey>; @@ -38,6 +61,9 @@ pub struct NoiseChannelPublicKey { impl std::fmt::Debug for NoiseChannelPublicKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Public keys are not secrets, but logging complete identities makes + // correlation across environments unnecessarily easy. Keep only the + // suite visible in routine diagnostics. f.debug_struct("NoiseChannelPublicKey") .field("suite", &self.suite) .field("x25519_public_key", &"") @@ -47,6 +73,10 @@ impl std::fmt::Debug for NoiseChannelPublicKey { } impl NoiseChannelPublicKey { + /// Serialize both public components as one suite-tagged registry value. + /// + /// Keeping the components together prevents callers from accidentally + /// pairing an X25519 key from one identity with an ML-KEM key from another. fn from_keypairs(dh: &DhKeyPair, kem: &KemKeyPair) -> Self { Self { suite: NOISE_CHANNEL_SUITE.to_string(), @@ -68,6 +98,9 @@ pub struct NoiseChannelIdentity { impl std::fmt::Debug for NoiseChannelIdentity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Never delegate to the keypair debug implementations: this type owns + // both private keys. Its public projection is sufficient to identify + // which endpoint identity a log entry refers to. f.debug_struct("NoiseChannelIdentity") .field("public_key", &self.public_key()) .finish_non_exhaustive()