docs(exec-server): explain Noise channel flow

Co-authored-by: Codex noreply@openai.com
This commit is contained in:
viyatb-oai
2026-06-10 10:11:14 -07:00
co-authored by Codex noreply@openai.com
parent 3088d8f27e
commit b227892a48
2 changed files with 54 additions and 5 deletions
+16
View File
@@ -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<CIPHERTEXT_LEN>;
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<R: Rng>(_rng: &mut R) -> KemResult<KeyPair<Self::PubKey, Self::SecretKey>> {
// 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<R: Rng>(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<Self::Ss> {
// Reject the length before constructing AWS-LC's ciphertext wrapper.
// This keeps malformed wire input classified as an input error rather
+38 -5
View File
@@ -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<<X25519 as Dh>::PubKey, <X25519 as Dh>::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", &"<redacted>")
@@ -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()