use std::io::{Read, Write}; use aes_gcm::aead::{Aead, Payload}; use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; use base64::Engine; use base64::engine::general_purpose::STANDARD; use cultnet_rs::{ CultNetMessage, CultNetWireContract, decode_cultnet_message_from_slice, encode_cultnet_message_to_vec, }; use rand_core::{OsRng, RngCore}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::contract::{ ContractError, ENVELOPE_SCHEMA_ID, TransportInvocation, TransportKey, TransportResult, canonical_invocation_bytes, require_id, wire_decode, wire_encode, }; use crate::readout::SpendReadout; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub enum EnvelopeKind { Invocation, Result, SpendReadoutRequest, SpendReadoutResult, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct TransportEnvelope { pub schema_id: String, pub caller_runtime_id: String, pub request_id: String, pub message_kind: EnvelopeKind, pub nonce: [u8; 12], #[serde(with = "serde_bytes")] #[schemars(schema_with = "binary_schema")] pub ciphertext: Vec, } /// Raw bytes are MessagePack `bin` on the wire, never an array of integers. /// Serves the ciphertext and both reasoning states. pub(crate) fn binary_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { schemars::json_schema!({ "type": "string", "format": "binary", "description": "MessagePack bin: raw bytes, never an array of integers" }) } /// The CultNet service every frame of this connector addresses. pub const CONNECTOR_SERVICE_ID: &str = "gamecult.model.connector"; /// The operation that runs one sealed invocation. pub const INVOKE_OPERATION: &str = "invoke"; /// The operation that reads the spend ledger and the plan's quota. pub const SPEND_READOUT_OPERATION: &str = "spend_readout"; const PAYLOAD_ENCODING: &str = "messagepack-base64"; const OK_STATUS: &str = "ok"; /// The longest caller id, request id or model name a service admits. The /// frame arithmetic below depends on it. A request whose id is longer is /// refused on the wire as malformed. pub const MAX_IDENTITY_BYTES: usize = 256; fn carriage_bytes(message: &CultNetMessage) -> Result, ServiceError> { encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0) .map_err(|_| ServiceError::Carriage) } fn seal_payload(envelope: &TransportEnvelope) -> Result { Ok(STANDARD.encode(wire_encode(envelope)?)) } /// The two operations of the connector service. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Operation { Invoke, SpendReadout, } impl Operation { fn name(self) -> &'static str { match self { Operation::Invoke => INVOKE_OPERATION, Operation::SpendReadout => SPEND_READOUT_OPERATION, } } fn from_name(name: &str) -> Option { [Operation::Invoke, Operation::SpendReadout] .into_iter() .find(|operation| operation.name() == name) } } /// The CultNet frame a request travels in: a `cultnet.operation_request.v0` /// for [`CONNECTOR_SERVICE_ID`] and the named operation, its `message_id` the /// request id and its payload the base64 of the named sealed envelope. fn request_frame( operation: Operation, envelope: &TransportEnvelope, ) -> Result, ServiceError> { carriage_bytes(&CultNetMessage::OperationRequest { message_id: envelope.request_id.clone(), service_id: CONNECTOR_SERVICE_ID.to_string(), operation: operation.name().to_string(), payload_schema: ENVELOPE_SCHEMA_ID.to_string(), payload_encoding: PAYLOAD_ENCODING.to_string(), payload: seal_payload(envelope)?, source_runtime_id: None, target_runtime_id: None, }) } /// The CultNet frame a response travels in: a `cultnet.operation_response.v0` /// with status `ok`, otherwise shaped like the request's. #[cfg(any(feature = "daemon", test))] fn response_frame( operation: Operation, envelope: &TransportEnvelope, ) -> Result, ServiceError> { carriage_bytes(&CultNetMessage::OperationResponse { message_id: envelope.request_id.clone(), service_id: CONNECTOR_SERVICE_ID.to_string(), operation: operation.name().to_string(), status: OK_STATUS.to_string(), payload_schema: ENVELOPE_SCHEMA_ID.to_string(), payload_encoding: PAYLOAD_ENCODING.to_string(), payload: seal_payload(envelope)?, diagnostics: Vec::new(), source_runtime_id: None, }) } pub(crate) fn encode_invocation_frame( envelope: &TransportEnvelope, ) -> Result, ServiceError> { request_frame(Operation::Invoke, envelope) } pub(crate) fn encode_spend_readout_request_frame( envelope: &TransportEnvelope, ) -> Result, ServiceError> { request_frame(Operation::SpendReadout, envelope) } #[cfg(any(feature = "daemon", test))] pub(crate) fn encode_result_frame(envelope: &TransportEnvelope) -> Result, ServiceError> { response_frame(Operation::Invoke, envelope) } #[cfg(feature = "daemon")] pub(crate) fn encode_spend_readout_response_frame( envelope: &TransportEnvelope, ) -> Result, ServiceError> { response_frame(Operation::SpendReadout, envelope) } /// Opens the carriage of one frame. Everything the carriage says is checked /// here, before any decryption: the service, that the operation is one this /// service has, the payload schema, and that the message id is the sealed /// envelope's request id. fn open_carriage( message_id: &str, service_id: &str, operation: &str, payload_schema: &str, payload: &str, ) -> Result<(Operation, TransportEnvelope), ServiceError> { let operation = Operation::from_name(operation).ok_or(ServiceError::Carriage)?; if service_id != CONNECTOR_SERVICE_ID || payload_schema != ENVELOPE_SCHEMA_ID { return Err(ServiceError::Carriage); } let bytes = STANDARD .decode(payload) .map_err(|_| ServiceError::Carriage)?; let envelope: TransportEnvelope = wire_decode(&bytes).map_err(|_| ServiceError::Encoding)?; if envelope.request_id != message_id { return Err(ServiceError::OuterIdentity); } Ok((operation, envelope)) } fn read_carriage(bytes: &[u8]) -> Result { decode_cultnet_message_from_slice(bytes, CultNetWireContract::CultNetSchemaV0) .map_err(|_| ServiceError::Carriage) } /// A request frame, by the operation it names. #[cfg(any(feature = "daemon", test))] pub(crate) enum RequestFrame { Invoke(TransportEnvelope), SpendReadout(TransportEnvelope), } #[cfg(any(feature = "daemon", test))] pub(crate) fn decode_request_frame(bytes: &[u8]) -> Result { match read_carriage(bytes)? { CultNetMessage::OperationRequest { message_id, service_id, operation, payload_schema, payload, .. } => { match open_carriage( &message_id, &service_id, &operation, &payload_schema, &payload, )? { (Operation::Invoke, envelope) => Ok(RequestFrame::Invoke(envelope)), (Operation::SpendReadout, envelope) => Ok(RequestFrame::SpendReadout(envelope)), } } _ => Err(ServiceError::Carriage), } } #[cfg(test)] pub(crate) fn decode_invocation_frame(bytes: &[u8]) -> Result { match decode_request_frame(bytes)? { RequestFrame::Invoke(envelope) => Ok(envelope), RequestFrame::SpendReadout(_) => Err(ServiceError::Carriage), } } fn decode_response_frame( bytes: &[u8], expected: Operation, ) -> Result { match read_carriage(bytes)? { CultNetMessage::OperationResponse { message_id, service_id, operation, status, payload_schema, payload, .. } if status == OK_STATUS => { match open_carriage( &message_id, &service_id, &operation, &payload_schema, &payload, )? { (found, envelope) if found == expected => Ok(envelope), _ => Err(ServiceError::Carriage), } } _ => Err(ServiceError::Carriage), } } pub(crate) fn decode_result_frame(bytes: &[u8]) -> Result { decode_response_frame(bytes, Operation::Invoke) } pub(crate) fn decode_spend_readout_response_frame( bytes: &[u8], ) -> Result { decode_response_frame(bytes, Operation::SpendReadout) } /// The most bytes one frame holding a sealed envelope of `ciphertext_len` /// ciphertext bytes can occupy, with ids at their longest: the frame a caller /// payload bound (`CallerAdmission`'s `max_payload_bytes`, a bound on /// ciphertext length) needs. /// /// A frame is a CultNet operation message whose payload is the base64 of the /// named envelope. The result frame is the larger of the two directions (it /// also carries a status), so this bound holds for an invocation as well. The /// envelope shell and the message shell are measured from their real /// encodings; only the MessagePack length headers and base64 expansion that /// grow with `ciphertext_len` are arithmetic. pub fn frame_bytes_for_payload(ciphertext_len: usize) -> usize { let long = "x".repeat(MAX_IDENTITY_BYTES); let envelope_shell = wire_encode(&TransportEnvelope { schema_id: ENVELOPE_SCHEMA_ID.to_string(), caller_runtime_id: long.clone(), request_id: long.clone(), message_kind: EnvelopeKind::Invocation, nonce: [0xff; 12], ciphertext: Vec::new(), }) .map_or(usize::MAX, |bytes| bytes.len()); let message_shell = encode_cultnet_message_to_vec( &CultNetMessage::OperationResponse { message_id: long, service_id: CONNECTOR_SERVICE_ID.to_string(), operation: INVOKE_OPERATION.to_string(), status: OK_STATUS.to_string(), payload_schema: ENVELOPE_SCHEMA_ID.to_string(), payload_encoding: PAYLOAD_ENCODING.to_string(), payload: "A".to_string(), diagnostics: Vec::new(), source_runtime_id: None, }, CultNetWireContract::CultNetSchemaV0, ) .map_or(usize::MAX, |bytes| bytes.len()); // The shell holds an empty `bin 8`: a marker and a zero length. The shell's // one-character payload takes a one-byte string header and the character. let envelope = envelope_shell .saturating_sub(2) .saturating_add(bin_header_bytes(ciphertext_len)) .saturating_add(ciphertext_len); let payload = envelope.div_ceil(3).saturating_mul(4); message_shell .saturating_sub(2) .saturating_add(str_header_bytes(payload)) .saturating_add(payload) } /// MessagePack `bin` header width for a byte string of `len` bytes. fn bin_header_bytes(len: usize) -> usize { match len { 0..=255 => 2, 256..=65_535 => 3, _ => 5, } } /// MessagePack `str` header width for the base64 payload. The payload always /// exceeds 255 bytes: the envelope it encodes holds two ids of at least /// `MAX_IDENTITY_BYTES` in the worst case this bound measures. fn str_header_bytes(len: usize) -> usize { if len <= 65_535 { 3 } else { 5 } } /// The largest payload bound whose worst-case frame fits `max_frame_bytes`: /// the inverse of [`frame_bytes_for_payload`]. `None` when the frame cannot /// carry even an empty envelope. pub fn payload_bytes_for_frame(max_frame_bytes: usize) -> Option { if frame_bytes_for_payload(0) > max_frame_bytes { return None; } // A frame is longer than its payload, so the answer is below the bound. let (mut fits, mut over) = (0, max_frame_bytes.saturating_add(1)); while over - fits > 1 { let middle = fits + (over - fits) / 2; if frame_bytes_for_payload(middle) <= max_frame_bytes { fits = middle; } else { over = middle; } } Some(fits) } pub fn encrypt_invocation( invocation: &TransportInvocation, security: &TransportKey, ) -> Result { let plaintext = canonical_invocation_bytes(invocation)?; encrypt_message( &invocation.caller_runtime_id, invocation.request_id(), EnvelopeKind::Invocation, &plaintext, security, ) } pub fn decrypt_invocation( envelope: &TransportEnvelope, security: &TransportKey, ) -> Result { validate_envelope(envelope, EnvelopeKind::Invocation)?; let plaintext = decrypt_message(envelope, security)?; let invocation: TransportInvocation = wire_decode(&plaintext).map_err(|_| ServiceError::Encoding)?; if invocation.caller_runtime_id != envelope.caller_runtime_id || invocation.request_id() != envelope.request_id { return Err(ServiceError::OuterIdentity); } Ok(invocation) } pub fn encrypt_result( result: &TransportResult, security: &TransportKey, ) -> Result { let plaintext = wire_encode(result).map_err(|_| ServiceError::Encoding)?; encrypt_message( &result.caller_runtime_id, &result.request_id, EnvelopeKind::Result, &plaintext, security, ) } pub fn decrypt_result( envelope: &TransportEnvelope, security: &TransportKey, invocation: &TransportInvocation, ) -> Result { validate_envelope(envelope, EnvelopeKind::Result)?; let plaintext = decrypt_message(envelope, security)?; let result: TransportResult = wire_decode(&plaintext).map_err(|_| ServiceError::Encoding)?; if result.caller_runtime_id != envelope.caller_runtime_id || result.request_id != envelope.request_id { return Err(ServiceError::OuterIdentity); } result.validate_against(invocation)?; Ok(result) } /// A spend readout request carries no content: the sealed envelope's /// authenticated identity and kind are the whole request. pub(crate) fn encrypt_spend_readout_request( caller_runtime_id: &str, request_id: &str, security: &TransportKey, ) -> Result { encrypt_message( caller_runtime_id, request_id, EnvelopeKind::SpendReadoutRequest, &[], security, ) } #[cfg(any(feature = "daemon", test))] pub(crate) fn decrypt_spend_readout_request( envelope: &TransportEnvelope, security: &TransportKey, ) -> Result<(), ServiceError> { validate_envelope(envelope, EnvelopeKind::SpendReadoutRequest)?; decrypt_message(envelope, security).map(drop) } #[cfg(feature = "daemon")] pub(crate) fn encrypt_spend_readout( caller_runtime_id: &str, request_id: &str, readout: &SpendReadout, security: &TransportKey, ) -> Result { let plaintext = wire_encode(readout).map_err(|_| ServiceError::Encoding)?; encrypt_message( caller_runtime_id, request_id, EnvelopeKind::SpendReadoutResult, &plaintext, security, ) } pub(crate) fn decrypt_spend_readout( envelope: &TransportEnvelope, security: &TransportKey, ) -> Result { validate_envelope(envelope, EnvelopeKind::SpendReadoutResult)?; let plaintext = decrypt_message(envelope, security)?; let readout: SpendReadout = wire_decode(&plaintext).map_err(|_| ServiceError::Encoding)?; readout.validate()?; Ok(readout) } #[derive(Debug)] pub(crate) enum TransportFrameError { Connection(std::io::Error), Size, } pub(crate) fn read_transport_frame( reader: &mut impl Read, max_frame_bytes: usize, ) -> Result, TransportFrameError> { let mut length = [0_u8; 4]; reader .read_exact(&mut length) .map_err(TransportFrameError::Connection)?; let length = u32::from_be_bytes(length) as usize; if length == 0 || length > max_frame_bytes { return Err(TransportFrameError::Size); } let mut payload = vec![0_u8; length]; reader .read_exact(&mut payload) .map_err(TransportFrameError::Connection)?; Ok(payload) } pub(crate) fn write_transport_frame( writer: &mut impl Write, payload: &[u8], max_frame_bytes: usize, ) -> Result<(), TransportFrameError> { if payload.is_empty() || payload.len() > max_frame_bytes || payload.len() > u32::MAX as usize { return Err(TransportFrameError::Size); } writer .write_all(&(payload.len() as u32).to_be_bytes()) .and_then(|()| writer.write_all(payload)) .and_then(|()| writer.flush()) .map_err(TransportFrameError::Connection) } pub(crate) fn encrypt_message( caller_runtime_id: &str, request_id: &str, message_kind: EnvelopeKind, plaintext: &[u8], security: &TransportKey, ) -> Result { require_id(caller_runtime_id, "caller_runtime_id")?; require_id(request_id, "request_id")?; let mut nonce = [0; 12]; OsRng.fill_bytes(&mut nonce); let cipher = Aes256Gcm::new_from_slice(&security.0).map_err(|_| ServiceError::Encryption)?; let aad = envelope_aad( ENVELOPE_SCHEMA_ID, caller_runtime_id, request_id, message_kind, )?; let ciphertext = cipher .encrypt( Nonce::from_slice(&nonce), Payload { msg: plaintext, aad: &aad, }, ) .map_err(|_| ServiceError::Encryption)?; Ok(TransportEnvelope { schema_id: ENVELOPE_SCHEMA_ID.to_string(), caller_runtime_id: caller_runtime_id.to_string(), request_id: request_id.to_string(), message_kind, nonce, ciphertext, }) } pub(crate) fn decrypt_message( envelope: &TransportEnvelope, security: &TransportKey, ) -> Result, ServiceError> { let cipher = Aes256Gcm::new_from_slice(&security.0).map_err(|_| ServiceError::Encryption)?; let aad = envelope_aad( &envelope.schema_id, &envelope.caller_runtime_id, &envelope.request_id, envelope.message_kind, )?; cipher .decrypt( Nonce::from_slice(&envelope.nonce), Payload { msg: &envelope.ciphertext, aad: &aad, }, ) .map_err(|_| ServiceError::Encryption) } /// The authenticated data of a sealed message: a positional tuple, a MAC input /// and not a wire document (a tuple is positional under the named codec too). pub(crate) fn envelope_aad( schema_id: &str, caller_runtime_id: &str, request_id: &str, message_kind: EnvelopeKind, ) -> Result, ServiceError> { wire_encode(&(schema_id, caller_runtime_id, request_id, message_kind)) .map_err(|_| ServiceError::Encoding) } pub(crate) fn validate_envelope( envelope: &TransportEnvelope, expected_kind: EnvelopeKind, ) -> Result<(), ServiceError> { if envelope.schema_id != ENVELOPE_SCHEMA_ID || envelope.message_kind != expected_kind { return Err(ServiceError::Envelope); } require_id(&envelope.caller_runtime_id, "caller_runtime_id")?; require_id(&envelope.request_id, "request_id")?; if envelope.ciphertext.is_empty() { return Err(ServiceError::Envelope); } Ok(()) } #[derive(Debug, Error, PartialEq, Eq)] pub enum ServiceError { #[error(transparent)] Contract(#[from] ContractError), #[error("invalid caller admission")] InvalidAdmission, #[error("caller runtime was admitted twice")] DuplicateCaller, #[error("callers must not share transport keys")] SharedCallerKey, #[error("connector replay store failed: {0}")] ReplayStore(String), #[error("an allowed model has no price: {0}")] UnpricedModel(String), #[error("connector replay record is malformed")] InvalidReplayRecord, #[error("caller key changed while durable replay records still exist")] ReplayCallerKeyMismatch, #[error("caller runtime is not admitted")] CallerNotAdmitted, #[error("unexpected transport envelope")] Envelope, #[error("transport envelope identity substitution")] OuterIdentity, #[error("transport encryption or authentication failed")] Encryption, #[error("transport encoding failed")] Encoding, #[error("frame is not a connector operation message")] Carriage, #[error("completion did not carry a transported provider outcome")] InvalidCompletion, #[error("completion has no active request")] NoActiveRequest, #[error("completion does not match the active request")] ActiveRequestMismatch, #[error("sealed result does not fit the frame bound")] FrameBound, } #[cfg(test)] mod tests { use super::*; use crate::fixtures::*; #[test] fn encrypted_invocation_hides_content_and_binds_outer_identity() { let invocation = invocation(); let epiphany_key = security("epiphany-distinct-test-key"); let envelope = encrypt_invocation(&invocation, &epiphany_key).unwrap(); assert!( !envelope .ciphertext .windows("Projected state".len()) .any(|window| window == b"Projected state") ); assert_eq!( decrypt_invocation(&envelope, &epiphany_key).unwrap(), invocation ); assert_eq!( decrypt_invocation(&envelope, &security("wrong-caller-key")), Err(ServiceError::Encryption) ); let mut substituted = envelope; substituted.request_id = "request-2".to_string(); assert_eq!( decrypt_invocation(&substituted, &epiphany_key), Err(ServiceError::Encryption) ); } fn sealed_invocation(caller: &str, request_id: &str, kind: EnvelopeKind) -> TransportEnvelope { let invocation = invocation(); let plaintext = canonical_invocation_bytes(&invocation).unwrap(); encrypt_message( caller, request_id, kind, &plaintext, &security("epiphany-distinct-test-key"), ) .unwrap() } #[test] fn the_outer_identity_must_match_the_sealed_invocation_on_each_field() { let key = security("epiphany-distinct-test-key"); let invocation = invocation(); let caller = invocation.caller_runtime_id.clone(); let request_id = invocation.request_id().to_string(); let matching = sealed_invocation(&caller, &request_id, EnvelopeKind::Invocation); assert_eq!(decrypt_invocation(&matching, &key).unwrap(), invocation); for envelope in [ sealed_invocation("other-caller", &request_id, EnvelopeKind::Invocation), sealed_invocation(&caller, "other-request", EnvelopeKind::Invocation), ] { assert_eq!( decrypt_invocation(&envelope, &key), Err(ServiceError::OuterIdentity) ); } } #[test] fn the_outer_identity_must_match_the_sealed_result_on_each_field() { let key = security("epiphany-distinct-test-key"); let invocation = invocation(); let result = TransportResult::transported(Vec::new(), receipt(&invocation)); let plaintext = wire_encode(&result).unwrap(); let seal = |caller: &str, request_id: &str| { encrypt_message(caller, request_id, EnvelopeKind::Result, &plaintext, &key).unwrap() }; let caller = result.caller_runtime_id.clone(); let request_id = result.request_id.clone(); assert_eq!( decrypt_result(&seal(&caller, &request_id), &key, &invocation).unwrap(), result ); for envelope in [ seal("other-caller", &request_id), seal(&caller, "other-request"), ] { assert_eq!( decrypt_result(&envelope, &key, &invocation), Err(ServiceError::OuterIdentity) ); } } #[test] fn a_malformed_envelope_is_refused_before_any_decryption() { let key = security("epiphany-distinct-test-key"); let invocation = invocation(); let good = encrypt_invocation(&invocation, &key).unwrap(); let mut schema = good.clone(); schema.schema_id = concat!("gamecult.", "codex.transport_envelope.v2").to_string(); let wrong_kind = sealed_invocation("epiphany-yggdrasil", "request-1", EnvelopeKind::Result); let mut empty = good.clone(); empty.ciphertext.clear(); for envelope in [schema, wrong_kind, empty] { assert_eq!( decrypt_invocation(&envelope, &key), Err(ServiceError::Envelope) ); } for field in ["caller_runtime_id", "request_id"] { let mut blank = good.clone(); if field == "caller_runtime_id" { blank.caller_runtime_id = " ".to_string(); } else { blank.request_id = String::new(); } assert_eq!( decrypt_invocation(&blank, &key), Err(ServiceError::Contract(ContractError::Invalid(field))) ); } } #[test] fn a_frame_is_bounded_inclusively_and_never_empty() { use std::io::Cursor; let read = |length: u32, body: usize, max: usize| { let mut frame = length.to_be_bytes().to_vec(); frame.extend(vec![1; body]); read_transport_frame(&mut Cursor::new(frame), max) }; assert_eq!(read(16, 16, 16).unwrap(), vec![1; 16]); assert!(matches!(read(17, 17, 16), Err(TransportFrameError::Size))); assert!(matches!(read(0, 0, 16), Err(TransportFrameError::Size))); // A bound above the length never turns an empty frame into an empty payload. assert!(matches!(read(0, 0, 1_000), Err(TransportFrameError::Size))); let write = |payload: &[u8], max: usize| { let mut out = Vec::new(); write_transport_frame(&mut out, payload, max).map(|()| out) }; assert_eq!(write(&[1; 16], 16).unwrap().len(), 20); assert!(matches!( write(&[1; 17], 16), Err(TransportFrameError::Size) )); assert!(matches!(write(&[], 16), Err(TransportFrameError::Size))); assert!(matches!(write(&[], 1_000), Err(TransportFrameError::Size))); } /// The real result frame for an envelope with ids at the longest admitted /// and a ciphertext of `length` bytes. fn real_worst_frame(length: usize) -> Vec { let long = "x".repeat(MAX_IDENTITY_BYTES); encode_result_frame(&TransportEnvelope { schema_id: ENVELOPE_SCHEMA_ID.to_string(), caller_runtime_id: long.clone(), request_id: long, message_kind: EnvelopeKind::Invocation, nonce: [0xff; 12], ciphertext: vec![0xff; length], }) .unwrap() } /// Ciphertext sizes either side of every length-header boundary the frame /// crosses: bin 8 to 16 to 32, str 8 to 16 to 32, and each base64 padding. fn boundary_sizes() -> Vec { let mut sizes: Vec = (0..=40).collect(); for center in [ 100, 200, 250, 256, 300, 1000, 3000, 65_535, 65_536, 49_000, 49_100, 49_152, 49_200, 70_000, ] { for delta in 0..=40 { sizes.push(center + delta); sizes.push(center.saturating_sub(delta)); } } sizes.sort_unstable(); sizes.dedup(); sizes } #[test] fn the_frame_bound_is_the_real_carriage_encoding() { for length in boundary_sizes() { assert_eq!( real_worst_frame(length).len(), frame_bytes_for_payload(length), "{length}" ); } } #[test] fn a_sealed_envelope_never_exceeds_the_worst_frame_for_its_ciphertext() { let key = security("epiphany-distinct-test-key"); for id in ["a".to_string(), "i".repeat(MAX_IDENTITY_BYTES)] { for plaintext in [1, 100, 5_000, 70_000] { let sealed = encrypt_message(&id, &id, EnvelopeKind::Result, &vec![0x41; plaintext], &key) .unwrap(); let worst = frame_bytes_for_payload(sealed.ciphertext.len()); for real in [ encode_result_frame(&sealed).unwrap().len(), encode_invocation_frame(&sealed).unwrap().len(), ] { assert!(real <= worst, "{real} > {worst}"); } // The frame is the base64 of the envelope: its worst case is // within the base64 expansion of the real one. if id.len() == MAX_IDENTITY_BYTES { let real = encode_result_frame(&sealed).unwrap().len(); assert!(worst <= real + 32, "{worst} {real}"); } } } } #[test] fn the_payload_bound_a_frame_allows_is_the_largest_payload_that_fits() { // The frame grows in base64 groups of three bytes, so several payloads // share a frame: the inverse is the largest of them. assert_eq!( payload_bytes_for_frame(frame_bytes_for_payload(0) - 1), None ); for bytes in boundary_sizes().into_iter().chain([2_000_000]) { let frame = frame_bytes_for_payload(bytes); let allowed = payload_bytes_for_frame(frame).unwrap(); assert!(allowed >= bytes && allowed - bytes < 3, "{bytes} {allowed}"); assert_eq!(frame_bytes_for_payload(allowed), frame, "{bytes}"); assert!(frame_bytes_for_payload(allowed + 1) > frame, "{bytes}"); assert!( payload_bytes_for_frame(frame - 1).is_none_or(|smaller| smaller < bytes), "{bytes}" ); } } #[test] fn ciphertext_travels_as_binary() { let key = security("epiphany-distinct-test-key"); for plaintext in [10, 300, 70_000] { let sealed = encrypt_message( "caller", "request", EnvelopeKind::Invocation, &vec![0x41; plaintext], &key, ) .unwrap(); let length = sealed.ciphertext.len(); let mut expected = match length { 0..=255 => vec![0xc4, length as u8], 256..=65_535 => [&[0xc5][..], &(length as u16).to_be_bytes()].concat(), _ => [&[0xc6][..], &(length as u32).to_be_bytes()].concat(), }; expected.extend(&sealed.ciphertext); let encoded = wire_encode(&sealed).unwrap(); assert!( encoded .windows(expected.len()) .any(|window| window == expected), "{plaintext}" ); } } fn sealed_for_carriage() -> TransportEnvelope { encrypt_invocation(&invocation(), &security("epiphany-distinct-test-key")).unwrap() } fn request_message(edit: impl FnOnce(&mut CultNetMessage)) -> Vec { let envelope = sealed_for_carriage(); let mut message = decode_cultnet_message_from_slice( &encode_invocation_frame(&envelope).unwrap(), CultNetWireContract::CultNetSchemaV0, ) .unwrap(); edit(&mut message); carriage_bytes(&message).unwrap() } fn response_message(edit: impl FnOnce(&mut CultNetMessage)) -> Vec { let envelope = sealed_for_carriage(); let mut message = decode_cultnet_message_from_slice( &encode_result_frame(&envelope).unwrap(), CultNetWireContract::CultNetSchemaV0, ) .unwrap(); edit(&mut message); carriage_bytes(&message).unwrap() } #[test] fn the_carriage_round_trips_both_directions() { let envelope = sealed_for_carriage(); assert_eq!( decode_invocation_frame(&encode_invocation_frame(&envelope).unwrap()).unwrap(), envelope ); assert_eq!( decode_result_frame(&encode_result_frame(&envelope).unwrap()).unwrap(), envelope ); // A request is not a response, in either direction. assert_eq!( decode_result_frame(&encode_invocation_frame(&envelope).unwrap()), Err(ServiceError::Carriage) ); assert_eq!( decode_invocation_frame(&encode_result_frame(&envelope).unwrap()), Err(ServiceError::Carriage) ); } #[test] fn the_carriage_refuses_another_service_operation_or_schema() { fn edit_request(field: &str, value: &str) -> Vec { request_message(|message| { if let CultNetMessage::OperationRequest { service_id, operation, payload_schema, .. } = message { match field { "service" => *service_id = value.to_string(), "operation" => *operation = value.to_string(), _ => *payload_schema = value.to_string(), } } }) } fn edit_response(field: &str, value: &str) -> Vec { response_message(|message| { if let CultNetMessage::OperationResponse { service_id, operation, payload_schema, status, .. } = message { match field { "service" => *service_id = value.to_string(), "operation" => *operation = value.to_string(), "status" => *status = value.to_string(), _ => *payload_schema = value.to_string(), } } }) } for (field, value) in [ ("service", "gamecult.model.other"), ("operation", "cancel"), ("schema", "gamecult.model.transport_envelope.v2"), ] { assert_eq!( decode_invocation_frame(&edit_request(field, value)), Err(ServiceError::Carriage), "{field}" ); assert_eq!( decode_result_frame(&edit_response(field, value)), Err(ServiceError::Carriage), "{field}" ); } assert_eq!( decode_result_frame(&edit_response("status", "rejected")), Err(ServiceError::Carriage) ); // The unedited message decodes, so each refusal above is the edit's. assert!(decode_invocation_frame(&edit_request("service", CONNECTOR_SERVICE_ID)).is_ok()); assert!(decode_result_frame(&edit_response("status", "ok")).is_ok()); } #[test] fn the_carriage_refuses_a_message_id_that_is_not_the_envelopes_request_id() { let request = request_message(|message| { if let CultNetMessage::OperationRequest { message_id, .. } = message { *message_id = "another-request".to_string(); } }); let response = response_message(|message| { if let CultNetMessage::OperationResponse { message_id, .. } = message { *message_id = "another-request".to_string(); } }); assert_eq!( decode_invocation_frame(&request), Err(ServiceError::OuterIdentity) ); assert_eq!( decode_result_frame(&response), Err(ServiceError::OuterIdentity) ); } #[test] fn the_carriage_refuses_what_is_not_a_connector_message() { let good = encode_invocation_frame(&sealed_for_carriage()).unwrap(); // Not MessagePack of any CultNet message, nor a truncated one. assert_eq!( decode_invocation_frame(&[0xc1, 0x00, 0x01]), Err(ServiceError::Carriage) ); assert_eq!( decode_invocation_frame(&good[..good.len() / 2]), Err(ServiceError::Carriage) ); // A payload that is not base64, and base64 of something else. let not_base64 = request_message(|message| { if let CultNetMessage::OperationRequest { payload, .. } = message { *payload = "!!not base64!!".to_string(); } }); assert_eq!( decode_invocation_frame(¬_base64), Err(ServiceError::Carriage) ); let not_an_envelope = request_message(|message| { if let CultNetMessage::OperationRequest { payload, .. } = message { *payload = STANDARD.encode(b"\x01\x02"); } }); assert_eq!( decode_invocation_frame(¬_an_envelope), Err(ServiceError::Encoding) ); } fn golden_frame() -> Vec { std::fs::read( std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/wire_v3/carriage_invocation.frame"), ) .expect("carriage_invocation.frame is committed") } #[test] fn the_committed_golden_frame_is_what_the_kit_reads_and_writes() { let frame = golden_frame(); let envelope = decode_invocation_frame(&frame).unwrap(); assert_eq!(encode_invocation_frame(&envelope).unwrap(), frame); } fn top_level(frame: &[u8]) -> std::collections::BTreeMap { let rmpv::Value::Map(entries) = rmpv::decode::read_value(&mut &frame[..]).unwrap() else { panic!("a CultNet frame is a map"); }; entries .into_iter() .map(|(key, value)| (key.as_str().unwrap().to_string(), value)) .collect() } #[test] fn the_frames_name_the_connector_service_and_its_one_operation_on_the_wire() { let envelope = sealed_for_carriage(); let text = |map: &std::collections::BTreeMap, key: &str| { map[key].as_str().unwrap().to_string() }; let request = top_level(&encode_invocation_frame(&envelope).unwrap()); assert_eq!( text(&request, "schemaVersion"), "cultnet.operation_request.v0" ); assert_eq!(text(&request, "serviceId"), "gamecult.model.connector"); assert_eq!(text(&request, "operation"), "invoke"); assert_eq!( text(&request, "payloadSchema"), "gamecult.model.transport_envelope.v3" ); assert_eq!(text(&request, "payloadEncoding"), "messagepack-base64"); assert_eq!(text(&request, "messageId"), envelope.request_id); let response = top_level(&encode_result_frame(&envelope).unwrap()); assert_eq!( text(&response, "schemaVersion"), "cultnet.operation_response.v0" ); assert_eq!(text(&response, "serviceId"), "gamecult.model.connector"); assert_eq!(text(&response, "operation"), "invoke"); assert_eq!(text(&response, "status"), "ok"); assert_eq!( text(&response, "payloadSchema"), "gamecult.model.transport_envelope.v3" ); assert_eq!(text(&response, "payloadEncoding"), "messagepack-base64"); assert_eq!(text(&response, "messageId"), envelope.request_id); } fn value_of(bytes: &[u8]) -> rmpv::Value { rmpv::decode::read_value(&mut &bytes[..]).unwrap() } fn bytes_of(value: &rmpv::Value) -> Vec { let mut out = Vec::new(); rmpv::encode::write_value(&mut out, value).unwrap(); out } fn with_entry(value: rmpv::Value, name: &str, entry: rmpv::Value) -> rmpv::Value { let rmpv::Value::Map(mut entries) = value else { panic!("a named document is a map"); }; let slot = entries .iter_mut() .find(|(key, _)| key.as_str() == Some(name)) .unwrap(); slot.1 = entry; rmpv::Value::Map(entries) } fn reverse_maps(value: rmpv::Value) -> rmpv::Value { match value { rmpv::Value::Map(entries) => rmpv::Value::Map( entries .into_iter() .rev() .map(|(key, entry)| (key, reverse_maps(entry))) .collect(), ), rmpv::Value::Array(items) => { rmpv::Value::Array(items.into_iter().map(reverse_maps).collect()) } other => other, } } fn without_nils(value: rmpv::Value) -> rmpv::Value { match value { rmpv::Value::Map(entries) => rmpv::Value::Map( entries .into_iter() .filter(|(_, entry)| !entry.is_nil()) .map(|(key, entry)| (key, without_nils(entry))) .collect(), ), rmpv::Value::Array(items) => { rmpv::Value::Array(items.into_iter().map(without_nils).collect()) } other => other, } } fn request_frame_carrying(payload: &[u8]) -> Vec { request_message(|message| { if let CultNetMessage::OperationRequest { payload: sent, .. } = message { *sent = STANDARD.encode(payload); } }) } fn response_frame_carrying(payload: &[u8]) -> Vec { response_message(|message| { if let CultNetMessage::OperationResponse { payload: sent, .. } = message { *sent = STANDARD.encode(payload); } }) } #[test] fn the_carriage_refuses_every_dialect_the_wire_does_not_name() { let envelope = sealed_for_carriage(); let named = value_of(&wire_encode(&envelope).unwrap()); let integer_ciphertext = rmpv::Value::Array( envelope .ciphertext .iter() .map(|byte| rmpv::Value::from(*byte)) .collect(), ); let dialects = [ ("positional envelope", rmp_serde::to_vec(&envelope).unwrap()), ( "integer-array ciphertext", bytes_of(&with_entry(named.clone(), "ciphertext", integer_ciphertext)), ), ( "enum as its index", bytes_of(&with_entry(named, "message_kind", 0.into())), ), ( "bytes after the document", [wire_encode(&envelope).unwrap(), vec![0xc0]].concat(), ), ]; for (dialect, payload) in &dialects { assert_eq!( decode_invocation_frame(&request_frame_carrying(payload)), Err(ServiceError::Encoding), "{dialect} on a request" ); assert_eq!( decode_result_frame(&response_frame_carrying(payload)), Err(ServiceError::Encoding), "{dialect} on a response" ); } // The named form itself is read, so the refusals above are the dialect's. let named_bytes = wire_encode(&envelope).unwrap(); assert_eq!( decode_invocation_frame(&request_frame_carrying(&named_bytes)), Ok(envelope) ); } #[test] fn a_sealed_positional_invocation_is_refused_after_decryption() { let key = security("epiphany-distinct-test-key"); let invocation = invocation(); let sealed = encrypt_message( &invocation.caller_runtime_id, invocation.request_id(), EnvelopeKind::Invocation, &rmp_serde::to_vec(&invocation).unwrap(), &key, ) .unwrap(); let arrived = decode_invocation_frame(&encode_invocation_frame(&sealed).unwrap()).unwrap(); assert_eq!( decrypt_invocation(&arrived, &key), Err(ServiceError::Encoding) ); let sealed = encrypt_invocation(&invocation, &key).unwrap(); assert_eq!(decrypt_invocation(&sealed, &key), Ok(invocation)); } #[test] fn named_maps_in_any_key_order_and_without_absent_options_are_the_named_form() { let envelope = sealed_for_carriage(); let reversed = bytes_of(&reverse_maps(value_of(&wire_encode(&envelope).unwrap()))); assert_ne!(reversed, wire_encode(&envelope).unwrap()); assert_eq!(wire_decode::(&reversed), Ok(envelope)); let invocation = invocation(); let named = value_of(&wire_encode(&invocation).unwrap()); for edited in [reverse_maps(named.clone()), without_nils(named)] { let bytes = bytes_of(&edited); assert_ne!(bytes, wire_encode(&invocation).unwrap()); assert_eq!( wire_decode::(&bytes), Ok(invocation.clone()) ); } } #[test] fn a_readout_request_is_not_an_invocation_and_names_its_own_operation() { let key = TransportKey::from_connection_secret("readout-test-key").unwrap(); let sealed = encrypt_spend_readout_request("caller-1", "readout-1", &key).unwrap(); let frame = encode_spend_readout_request_frame(&sealed).unwrap(); assert!(matches!( decode_request_frame(&frame), Ok(RequestFrame::SpendReadout(found)) if found == sealed )); assert_eq!(decode_invocation_frame(&frame), Err(ServiceError::Carriage)); // Sealed as one kind, the authenticated data refuses it as another. assert!(decrypt_spend_readout_request(&sealed, &key).is_ok()); assert_eq!( decrypt_spend_readout(&sealed, &key).unwrap_err(), ServiceError::Envelope ); let mut relabelled = sealed.clone(); relabelled.message_kind = EnvelopeKind::SpendReadoutResult; assert_eq!( decrypt_spend_readout(&relabelled, &key).unwrap_err(), ServiceError::Encryption ); } #[cfg(feature = "daemon")] #[test] fn a_response_is_read_as_its_own_operation_only() { use crate::readout::PlanQuotaSnapshot; let key = TransportKey::from_connection_secret("readout-test-key").unwrap(); let readout = SpendReadout::new(1, Vec::new(), PlanQuotaSnapshot::default()); let sealed = encrypt_spend_readout("caller-1", "readout-1", &readout, &key).unwrap(); let readout_frame = encode_spend_readout_response_frame(&sealed).unwrap(); let invoke_frame = encode_result_frame(&sealed).unwrap(); assert_eq!( decode_spend_readout_response_frame(&readout_frame).unwrap(), sealed ); assert_eq!(decode_result_frame(&invoke_frame).unwrap(), sealed); assert_eq!( decode_result_frame(&readout_frame), Err(ServiceError::Carriage) ); assert_eq!( decode_spend_readout_response_frame(&invoke_frame), Err(ServiceError::Carriage) ); assert_eq!(decrypt_spend_readout(&sealed, &key).unwrap(), readout); } }