use std::collections::HashSet; use std::fmt::Debug; use std::num::NonZeroU16; use lazaret_core::codec::{CodecError, MAX_SCHEMA_RECORD_BYTES, decode, encode}; use lazaret_core::identity::{ BundleDigest, CapabilityId, CertificateDigest, ClientId, ConfigurationFingerprint, ContainerId, CoordinatorId, DerivedPolicyDigest, DeviceId, DomainId, FileDescriptor, GenerationId, GuestProcessGroupId, HostPath, IdMapDigest, InodeId, MountId, MountIdentity, NamespaceIdentity, NetworkSidecarId, PolicyId, PrepareOperationId, PreparedArtifactId, ProcessId, ProcessIdentity, ProcessStartTime, ProtocolCredentialId, ReadinessFingerprint, RequestDigest, RequestId, SandboxId, SelectedManifestDigest, SessionAnchorId, SessionId, SourceIdentity, StreamDigest, TrustManifestDigest, }; use lazaret_core::records::*; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] struct RawParticipant { generation_id: GenerationId, role: ParticipantRole, state: ParticipantState, ownership: ParticipantOwnership, } #[derive(Deserialize, Serialize)] struct RawTlsCredential { generation_identity: GenerationIdentity, role: TlsRole, subject_id: TlsSubjectId, certificate_der: Vec, certificate_sha256: CertificateDigest, } #[derive(Deserialize, Serialize)] struct RawProtocolCredential { protocol_version: ProtocolVersion, credential_id: ProtocolCredentialId, generation_identity: GenerationIdentity, role: ProtocolCredentialRole, subject_id: ProtocolSubjectId, public_key: Vec, issuer_signature: Vec, } #[derive(Deserialize, Serialize)] struct RawSandboxCredentialBundle { protocol_version: ProtocolVersion, generation_identity: GenerationIdentity, trust_manifest: TrustManifest, coordinator_protocol_credential: ProtocolCredential, sandbox_protocol_credential: ProtocolCredential, sandbox_private_key: Vec, } #[derive(Deserialize, Serialize)] struct RawMetadata { schema_id: String, schema_version: u32, } #[derive(Serialize)] struct MissingVersion { schema_id: String, } #[derive(Serialize)] struct RawDomainStore { metadata: StoreMetadata, domain: Domain, generations: Vec, participants: Vec, sessions: Vec, session_terminals: Vec, mailboxes: Vec, } #[derive(Serialize)] struct RawStorageCatalog { metadata: StoreMetadata, prepared_artifacts: Vec, artifact_leases: Vec, prepare_operations: Vec, } #[derive(Serialize)] struct RawTerminalResize { session_id: SessionId, rows: u16, columns: u16, pixel_width: u16, pixel_height: u16, } #[derive(Deserialize, Serialize)] struct InvalidSessionExit { session_id: SessionId, outcome: SessionExitOutcome, exit_code: Option, } #[derive(Serialize)] struct RawDerivedNetworkPolicy { schema_id: String, address_families: Vec, default_actions: Vec, prefix_rules: Vec, resolvers: Vec, } #[derive(Serialize)] struct RawNetworkReadinessAttestation { schema_id: String, domain_id: DomainId, generation_id: GenerationId, configuration_fingerprint: ConfigurationFingerprint, network_sidecar_id: NetworkSidecarId, derived_policy_digest: DerivedPolicyDigest, enforcement_namespace: NamespaceIdentity, runtime_supervisor: ProcessIdentity, connectivity_helper: ProcessIdentity, observation: NetworkObservation, } #[derive(Serialize)] struct RawNetworkReadinessIdentity { schema_id: String, domain_id: DomainId, generation_id: GenerationId, configuration_fingerprint: ConfigurationFingerprint, network_sidecar_id: NetworkSidecarId, derived_policy_digest: DerivedPolicyDigest, enforcement_namespace: NamespaceIdentity, } fn opaque(byte: u8) -> [u8; 32] { [byte; 32] } fn domain_id() -> DomainId { DomainId::new(opaque(1)) } fn generation_id() -> GenerationId { GenerationId::new(7) } fn coordinator_id() -> CoordinatorId { CoordinatorId::new(opaque(2)) } fn sidecar_id() -> NetworkSidecarId { NetworkSidecarId::new(opaque(3)) } fn sandbox_id() -> SandboxId { SandboxId::new(opaque(4)) } fn session_id() -> SessionId { SessionId::new(opaque(5)) } fn process(seed: u32) -> ProcessIdentity { ProcessIdentity { pid: ProcessId::new(seed), start_time: ProcessStartTime::new(u64::from(seed) * 10), } } fn namespace() -> NamespaceIdentity { NamespaceIdentity { device: DeviceId::new(8), inode: InodeId::new(9), } } fn mount() -> MountIdentity { MountIdentity { mount_id: MountId::new(10), device: DeviceId::new(11), inode: InodeId::new(12), } } fn generation_identity() -> GenerationIdentity { GenerationIdentity { agent_profile: "trusted".into(), domain_id: domain_id(), generation_id: generation_id(), configuration_fingerprint: ConfigurationFingerprint::new(opaque(6)), coordinator_id: coordinator_id(), sandbox_id: sandbox_id(), } } fn trust_manifest() -> TrustManifest { TrustManifest { protocol_version: ProtocolVersion::V1, generation_identity: generation_identity(), tls_ca_sha256: CertificateDigest::new(opaque(7)), protocol_issuer_public_key: vec![8; 32], issuer_signature: vec![9; 64], } } fn protocol_credential(role: ProtocolCredentialRole) -> ProtocolCredential { match role { ProtocolCredentialRole::SessionCoordinator => ProtocolCredential::session_coordinator( ProtocolCredentialId::new(opaque(10)), generation_identity(), coordinator_id(), vec![11; 32], vec![12; 64], ), ProtocolCredentialRole::AgentSandbox => ProtocolCredential::agent_sandbox( ProtocolCredentialId::new(opaque(10)), generation_identity(), sandbox_id(), vec![11; 32], vec![12; 64], ), } } fn session_binding() -> SessionBinding { SessionBinding { generation_identity: generation_identity(), cli_id: ClientId::new(opaque(13)), session_id: session_id(), session_anchor_public_key: vec![14; 32], } } fn session_capability() -> SessionRequestCapability { SessionRequestCapability { protocol_version: ProtocolVersion::V1, capability_id: CapabilityId::new(opaque(15)), session_binding: session_binding(), coordinator_credential_id: ProtocolCredentialId::new(opaque(16)), sandbox_credential_id: ProtocolCredentialId::new(opaque(17)), coordinator_signature: vec![18; 64], } } fn network_observation() -> NetworkObservation { NetworkObservation { policy_delivery: PolicyDelivery::SealedMemfd, helper_ready: true, observed_routes: vec![IpNetwork { address: IpAddress::V4([10, 0, 0, 0]), prefix_length: 8, }], routes_attested: true, } } fn readiness_attestation() -> NetworkReadinessAttestation { NetworkReadinessAttestation::v1(NetworkReadinessAttestationFields { domain_id: domain_id(), generation_id: generation_id(), configuration_fingerprint: ConfigurationFingerprint::new(opaque(19)), network_sidecar_id: sidecar_id(), derived_policy_digest: DerivedPolicyDigest::new(opaque(20)), enforcement_namespace: namespace(), runtime_supervisor: process(21), connectivity_helper: process(22), observation: network_observation(), }) } fn readiness_identity() -> NetworkReadinessIdentity { NetworkReadinessIdentity::v1(NetworkReadinessIdentityFields { domain_id: domain_id(), generation_id: generation_id(), configuration_fingerprint: ConfigurationFingerprint::new(opaque(19)), network_sidecar_id: sidecar_id(), derived_policy_digest: DerivedPolicyDigest::new(opaque(20)), enforcement_namespace: namespace(), }) } fn reachability_probe() -> NetworkReachabilityProbe { NetworkReachabilityProbe { domain_id: domain_id(), generation_id: generation_id(), enforcement_namespace: namespace(), denied_destinations: vec![DeniedDestinationObservation { destination: IpAddress::V4([10, 1, 2, 3]), unreachable: true, }], permitted_destinations: vec![PermittedDestinationObservation { destination: IpAddress::V4([1, 1, 1, 1]), reachable: true, }], dns_observations: vec![DnsObservation { resolver: IpAddress::V4([9, 9, 9, 9]), query_name: "example.invalid".into(), resolved_addresses: vec![IpAddress::V6([1; 16])], }], } } fn readiness_evidence() -> NetworkReadinessEvidence { NetworkReadinessEvidence { domain_id: domain_id(), generation_id: generation_id(), network_sidecar_id: sidecar_id(), readiness_fingerprint: ReadinessFingerprint::new(opaque(23)), attestation: readiness_attestation(), readiness_identity: readiness_identity(), reachability_probe: reachability_probe(), } } fn prepared_ownership() -> PreparedLowerOwnership { PreparedLowerOwnership { prepared_artifact_id: PreparedArtifactId::new(opaque(24)), source_identity: SourceIdentity::new(vec![25, 26]), lease_state: ParticipantState::LeaseHeld, path: HostPath::new(vec![b'/', 0xff]), device: DeviceId::new(27), inode: InodeId::new(28), } } fn network_ownership() -> NetworkOwnership { NetworkOwnership { derived_policy_digest: DerivedPolicyDigest::new(opaque(20)), readiness_fingerprint: ReadinessFingerprint::new(opaque(23)), readiness_evidence: readiness_evidence(), namespace: namespace(), runtime_supervisor: process(21), } } fn verify_shape(record: T, seen: &mut HashSet<&'static str>) where T: Clone + Debug + DeserializeOwned + Eq + SchemaType + Serialize, { assert!(seen.insert(T::NAME), "duplicate test for {}", T::NAME); let encoded = encode(&record).expect("representative record encodes"); assert_eq!(encode(&record).unwrap(), encoded); assert_eq!(decode::(&encoded).unwrap(), record); let mut truncated = encoded.clone(); truncated.pop(); assert!(decode::(&truncated).is_err()); let mut trailing = encoded; trailing.push(0); assert!(matches!( decode::(&trailing), Err(CodecError::TrailingPayload) )); drop(record); } #[test] #[allow(clippy::too_many_lines)] fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() { let terminal = SessionTerminal { generation_id: generation_id(), session_id: session_id(), state: SessionTerminalState::Running, container_id: ContainerId::new(opaque(29)), host_coordinator: process(30), pty_master: PtyIdentity { master_fd: FileDescriptor::new(31), device: DeviceId::new(32), inode: InodeId::new(33), }, exec_client: process(34), guest_process: GuestProcessIdentity { leader_pid: ProcessId::new(35), leader_start_time: ProcessStartTime::new(36), process_group_id: GuestProcessGroupId::new(37), }, }; let domain = Domain { domain_id: domain_id(), profile_name: "trusted".into(), canonical_workspace: HostPath::new(vec![b'/', b'w', 0xfe]), }; let generation = Generation { domain_id: domain_id(), generation_id: generation_id(), configuration_fingerprint: ConfigurationFingerprint::new(opaque(6)), coordinator_id: coordinator_id(), network_sidecar_id: sidecar_id(), sandbox_id: sandbox_id(), durable_decision: DurableDecision::Preparing, }; let participant = Participant::new( generation_id(), ParticipantState::LeaseHeld, ParticipantOwnership::PreparedLower(prepared_ownership()), ); let session = Session { generation_id: generation_id(), session_id: session_id(), cli_id: ClientId::new(opaque(13)), session_anchor_id: SessionAnchorId::new(opaque(38)), session_phase: SessionPhase::Attached, }; let mailbox = Mailbox { generation_id: generation_id(), identity_path: HostPath::new(b"identity".to_vec()), inbox_path: HostPath::new(b"inbox".to_vec()), outbox_path: HostPath::new(b"outbox".to_vec()), state: ParticipantState::Committed, }; let metadata = StoreMetadata::domain_store_v1(); let tls_credential = TlsCredential::session_coordinator( generation_identity(), coordinator_id(), vec![39, 40], CertificateDigest::new(opaque(41)), ); let request = MailboxRequest { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: RequestId::new(opaque(42)), session_capability: session_capability(), sandbox_credential_id: ProtocolCredentialId::new(opaque(17)), command_argv: vec![b"printf".to_vec(), vec![0xff]], justification: "build dependency".into(), }; let prepared_artifact = PreparedArtifact { prepared_artifact_id: PreparedArtifactId::new(opaque(24)), source_identity: SourceIdentity::new(vec![25, 26]), selected_manifest_digest: SelectedManifestDigest::new(opaque(43)), policy_id: PolicyId::new(opaque(44)), id_map_digest: IdMapDigest::new(opaque(45)), path: HostPath::new(b"artifact".to_vec()), device: DeviceId::new(46), inode: InodeId::new(47), state: ArtifactState::Available, }; let mut seen = HashSet::new(); verify_shape(metadata.clone(), &mut seen); verify_shape(domain.clone(), &mut seen); verify_shape(generation.clone(), &mut seen); verify_shape(participant.clone(), &mut seen); verify_shape(ParticipantState::ReleaseIntent, &mut seen); verify_shape( ParticipantOwnership::Network(Box::new(network_ownership())), &mut seen, ); verify_shape(session.clone(), &mut seen); verify_shape(terminal.clone(), &mut seen); verify_shape(SessionTerminalState::ReleaseIntent, &mut seen); verify_shape(mailbox.clone(), &mut seen); verify_shape(terminal.pty_master, &mut seen); verify_shape(process(48), &mut seen); verify_shape(terminal.guest_process, &mut seen); verify_shape( SessionTerminalEvent::Input(TerminalInput { session_id: session_id(), bytes: vec![49], }), &mut seen, ); verify_shape( TerminalInput { session_id: session_id(), bytes: vec![50], }, &mut seen, ); verify_shape( TerminalResize { session_id: session_id(), rows: NonZeroU16::new(24).unwrap(), columns: NonZeroU16::new(80).unwrap(), pixel_width: 640, pixel_height: 480, }, &mut seen, ); verify_shape( TerminalSignal { session_id: session_id(), signal: TerminalSignalKind::Suspend, }, &mut seen, ); verify_shape(TerminalSignalKind::Interrupt, &mut seen); verify_shape( SessionClose { session_id: session_id(), }, &mut seen, ); verify_shape( SessionTerminalMessage::Output(TerminalOutput { session_id: session_id(), bytes: vec![51], }), &mut seen, ); verify_shape( TerminalOutput { session_id: session_id(), bytes: vec![52], }, &mut seen, ); verify_shape(SessionExit::exited(session_id(), 3), &mut seen); verify_shape(SessionExitOutcome::RuntimeLost, &mut seen); verify_shape(generation_identity(), &mut seen); verify_shape(trust_manifest(), &mut seen); verify_shape(tls_credential.clone(), &mut seen); verify_shape( ProtocolPublicationIdentity { protocol_version: ProtocolVersion::V1, generation_identity: generation_identity(), tls_ca_certificate_der: vec![53], trust_manifest_sha256: TrustManifestDigest::new(opaque(54)), coordinator_tls_certificate_sha256: CertificateDigest::new(opaque(55)), }, &mut seen, ); verify_shape( SandboxCredentialBundle::new( generation_identity(), trust_manifest(), protocol_credential(ProtocolCredentialRole::SessionCoordinator), protocol_credential(ProtocolCredentialRole::AgentSandbox), vec![56; 32], ) .unwrap(), &mut seen, ); verify_shape(TlsRole::ClientCli, &mut seen); verify_shape( protocol_credential(ProtocolCredentialRole::AgentSandbox), &mut seen, ); verify_shape(ProtocolCredentialRole::SessionCoordinator, &mut seen); verify_shape(session_binding(), &mut seen); verify_shape(session_capability(), &mut seen); verify_shape( SignedProtocolRecord { payload_bytes: vec![57], signature: vec![58; 64], }, &mut seen, ); verify_shape(request.clone(), &mut seen); verify_shape( MailboxCancellation { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: RequestId::new(opaque(42)), session_capability: session_capability(), sandbox_credential_id: ProtocolCredentialId::new(opaque(17)), }, &mut seen, ); verify_shape( MailboxResultAck { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: RequestId::new(opaque(42)), request_sha256: RequestDigest::new(opaque(59)), session_capability: session_capability(), sandbox_credential_id: ProtocolCredentialId::new(opaque(17)), highest_contiguous_sequence: 4, }, &mut seen, ); verify_shape( DeveloperDecision { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: RequestId::new(opaque(42)), request_sha256: RequestDigest::new(opaque(59)), decision: DeveloperDecisionKind::Approve, approved_command_argv: request.command_argv.clone(), }, &mut seen, ); verify_shape(DeveloperDecisionKind::Deny, &mut seen); verify_shape( MailboxResult { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: RequestId::new(opaque(42)), request_sha256: RequestDigest::new(opaque(59)), coordinator_credential_id: ProtocolCredentialId::new(opaque(16)), outcome: EscalationOutcome::Completed, exit_code: Some(0), chunk_count: 2, stdout_sha256: StreamDigest::new(opaque(60)), stderr_sha256: StreamDigest::new(opaque(61)), }, &mut seen, ); verify_shape( MailboxResultChunk { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: RequestId::new(opaque(42)), request_sha256: RequestDigest::new(opaque(59)), coordinator_credential_id: ProtocolCredentialId::new(opaque(16)), sequence: 1, stream: ResultStream::Stderr, bytes: vec![62], }, &mut seen, ); verify_shape(ResultStream::Stdout, &mut seen); verify_shape(EscalationOutcome::TimedOut, &mut seen); verify_shape(namespace(), &mut seen); verify_shape(mount(), &mut seen); verify_shape(prepared_ownership(), &mut seen); verify_shape( StateAnchorOwnership { lower_artifact_id: PreparedArtifactId::new(opaque(24)), upper_path: HostPath::new(b"upper".to_vec()), work_path: HostPath::new(b"work".to_vec()), merged_path: HostPath::new(b"merged".to_vec()), daemon: process(63), mount: mount(), }, &mut seen, ); verify_shape( GitRoutingOwnership { mountpoint: HostPath::new(b"workspace".to_vec()), host_root: HostPath::new(b"host".to_vec()), isolated_root: HostPath::new(b"isolated".to_vec()), daemon: process(64), mount: mount(), }, &mut seen, ); verify_shape(network_ownership(), &mut seen); verify_shape( HelperOwnership { network_sidecar_id: sidecar_id(), connectivity_helper: process(65), runtime_supervisor: process(21), }, &mut seen, ); verify_shape( SandboxOwnership { container_id: ContainerId::new(opaque(29)), runtime_root: HostPath::new(b"runtime".to_vec()), bundle_digest: BundleDigest::new(opaque(66)), host_coordinator: process(30), runtime_supervisor: process(21), attached_runsc: process(67), }, &mut seen, ); verify_shape( PublicationOwnership { socket_path: HostPath::new(b"socket".to_vec()), identity_path: HostPath::new(b"identity".to_vec()), }, &mut seen, ); verify_shape(prepared_artifact.clone(), &mut seen); verify_shape(ArtifactState::Reclaiming, &mut seen); verify_shape( ArtifactLease { prepared_artifact_id: PreparedArtifactId::new(opaque(24)), domain_id: domain_id(), generation_id: generation_id(), }, &mut seen, ); verify_shape( PrepareOperation { operation_id: PrepareOperationId::new(opaque(68)), expected_artifact_id: PreparedArtifactId::new(opaque(24)), source_identity: SourceIdentity::new(vec![25, 26]), stage_path: HostPath::new(b"stage".to_vec()), device: DeviceId::new(69), inode: InodeId::new(70), }, &mut seen, ); verify_shape( AddressFamilyDisposition { family: AddressFamily::Ipv6, disposition: AddressFamilyDispositionValue::Disabled, }, &mut seen, ); verify_shape( DefaultAction { family: AddressFamily::Ipv4, action: PolicyAction::Allow, }, &mut seen, ); verify_shape( PrefixRule { family: AddressFamily::Ipv4, network: IpNetwork { address: IpAddress::V4([192, 168, 0, 0]), prefix_length: 16, }, action: PolicyAction::Deny, }, &mut seen, ); verify_shape( Resolver { family: AddressFamily::Ipv4, address: IpAddress::V4([9, 9, 9, 9]), }, &mut seen, ); verify_shape(AddressFamily::Ipv4, &mut seen); verify_shape(PolicyAction::Deny, &mut seen); verify_shape( DerivedNetworkPolicy::v1(DerivedNetworkPolicyFields { address_families: vec![AddressFamilyDisposition { family: AddressFamily::Ipv4, disposition: AddressFamilyDispositionValue::Enabled, }], default_actions: vec![DefaultAction { family: AddressFamily::Ipv4, action: PolicyAction::Allow, }], prefix_rules: vec![], resolvers: vec![Resolver { family: AddressFamily::Ipv4, address: IpAddress::V4([9, 9, 9, 9]), }], }), &mut seen, ); verify_shape(readiness_evidence(), &mut seen); verify_shape(readiness_attestation(), &mut seen); verify_shape(network_observation(), &mut seen); verify_shape(PolicyDelivery::SealedMemfd, &mut seen); verify_shape(readiness_identity(), &mut seen); verify_shape(reachability_probe(), &mut seen); verify_shape( DeniedDestinationObservation { destination: IpAddress::V4([10, 0, 0, 1]), unreachable: true, }, &mut seen, ); verify_shape( PermittedDestinationObservation { destination: IpAddress::V4([1, 0, 0, 1]), reachable: true, }, &mut seen, ); verify_shape( DnsObservation { resolver: IpAddress::V4([9, 9, 9, 9]), query_name: "example.invalid".into(), resolved_addresses: vec![IpAddress::V4([203, 0, 113, 1])], }, &mut seen, ); verify_shape( DomainStore { metadata: DomainStoreMetadata::v1(), domain, generations: vec![generation], participants: vec![participant], sessions: vec![session], session_terminals: vec![terminal], mailboxes: vec![mailbox], }, &mut seen, ); verify_shape( StorageCatalog { metadata: StorageCatalogMetadata::v1(), prepared_artifacts: vec![prepared_artifact], artifact_leases: vec![], prepare_operations: vec![], }, &mut seen, ); assert_eq!(seen.len(), ALL_SCHEMA_TYPE_NAMES.len()); assert!(ALL_SCHEMA_TYPE_NAMES.iter().all(|name| seen.contains(name))); } #[test] fn shape_registry_matches_the_normative_schema_classes() { let documented = include_str!("../../../docs/schema.md") .lines() .filter_map(|line| line.strip_prefix(" class ")) .map(|name| name.split_whitespace().next().unwrap_or(name)) .collect::>(); let registered = ALL_SCHEMA_TYPE_NAMES .iter() .copied() .collect::>(); assert_eq!(documented, registered); } fn assert_unknown_protocol_version(record: &T) where T: DeserializeOwned + Serialize, { let mut encoded = encode(record).unwrap(); encoded[0] = u8::MAX; assert!(matches!( decode::(&encoded), Err(CodecError::MalformedPayload(_)) )); } #[test] fn trailing_and_nonminimal_postcard_encodings_are_rejected() { assert!(matches!( decode::(&[0, 0]), Err(CodecError::TrailingPayload) )); assert!(matches!( decode::(&[0x80, 0]), Err(CodecError::NonMinimalPayload) )); let input = TerminalInput { session_id: session_id(), bytes: vec![1], }; let mut overlong_length = encode(&input).unwrap(); overlong_length[32] = 0x81; overlong_length.insert(33, 0); assert!(matches!( decode::(&overlong_length), Err(CodecError::NonMinimalPayload) )); let mut overlong_protocol_version = encode(&trust_manifest()).unwrap(); overlong_protocol_version[0] = 0x80; overlong_protocol_version.insert(1, 0); assert!(matches!( decode::(&overlong_protocol_version), Err(CodecError::NonMinimalPayload) )); let mut overlong_store_id = encode(&StoreMetadata::domain_store_v1()).unwrap(); overlong_store_id[0] |= 0x80; overlong_store_id.insert(1, 0); assert!(matches!( decode::(&overlong_store_id), Err(CodecError::NonMinimalPayload) )); } #[test] fn unknown_enums_and_protocol_versions_are_rejected() { assert!(matches!( decode::(&[u8::MAX]), Err(CodecError::MalformedPayload(_)) )); assert_unknown_protocol_version(&trust_manifest()); assert_unknown_protocol_version(&ProtocolPublicationIdentity { protocol_version: ProtocolVersion::V1, generation_identity: generation_identity(), tls_ca_certificate_der: vec![1], trust_manifest_sha256: TrustManifestDigest::new(opaque(2)), coordinator_tls_certificate_sha256: CertificateDigest::new(opaque(3)), }); assert_unknown_protocol_version( &SandboxCredentialBundle::new( generation_identity(), trust_manifest(), protocol_credential(ProtocolCredentialRole::SessionCoordinator), protocol_credential(ProtocolCredentialRole::AgentSandbox), vec![4; 32], ) .unwrap(), ); assert_unknown_protocol_version(&protocol_credential( ProtocolCredentialRole::SessionCoordinator, )); assert_unknown_protocol_version(&session_capability()); let request = MailboxRequest { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: RequestId::new(opaque(5)), session_capability: session_capability(), sandbox_credential_id: ProtocolCredentialId::new(opaque(6)), command_argv: vec![b"true".to_vec()], justification: "test".into(), }; assert_unknown_protocol_version(&request); assert_unknown_protocol_version(&MailboxCancellation { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: request.request_id, session_capability: session_capability(), sandbox_credential_id: ProtocolCredentialId::new(opaque(6)), }); assert_unknown_protocol_version(&MailboxResultAck { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: request.request_id, request_sha256: RequestDigest::new(opaque(7)), session_capability: session_capability(), sandbox_credential_id: ProtocolCredentialId::new(opaque(6)), highest_contiguous_sequence: 0, }); assert_unknown_protocol_version(&DeveloperDecision { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: request.request_id, request_sha256: RequestDigest::new(opaque(7)), decision: DeveloperDecisionKind::Deny, approved_command_argv: vec![], }); assert_unknown_protocol_version(&MailboxResult { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: request.request_id, request_sha256: RequestDigest::new(opaque(7)), coordinator_credential_id: ProtocolCredentialId::new(opaque(8)), outcome: EscalationOutcome::Denied, exit_code: None, chunk_count: 0, stdout_sha256: StreamDigest::new(opaque(9)), stderr_sha256: StreamDigest::new(opaque(10)), }); assert_unknown_protocol_version(&MailboxResultChunk { protocol_version: ProtocolVersion::V1, session_binding: session_binding(), request_id: request.request_id, request_sha256: RequestDigest::new(opaque(7)), coordinator_credential_id: ProtocolCredentialId::new(opaque(8)), sequence: 0, stream: ResultStream::Stdout, bytes: vec![], }); } #[test] fn participant_and_credential_role_mismatches_are_rejected() { let participant = Participant::new( generation_id(), ParticipantState::Prepared, ParticipantOwnership::PreparedLower(prepared_ownership()), ); assert_eq!(participant.role(), ParticipantRole::PreparedLower); let mismatched_participant = encode(&RawParticipant { generation_id: generation_id(), role: ParticipantRole::Network, state: ParticipantState::Prepared, ownership: ParticipantOwnership::PreparedLower(prepared_ownership()), }) .unwrap(); assert!(matches!( decode::(&mismatched_participant), Err(CodecError::MalformedPayload(_)) )); let tls = TlsCredential::session_coordinator( generation_identity(), coordinator_id(), vec![1], CertificateDigest::new(opaque(2)), ); assert_eq!(tls.role(), TlsRole::SessionCoordinator); let mismatched_tls = encode(&RawTlsCredential { generation_identity: generation_identity(), role: TlsRole::SessionCoordinator, subject_id: TlsSubjectId::ClientCli(ClientId::new(opaque(3))), certificate_der: vec![1], certificate_sha256: CertificateDigest::new(opaque(2)), }) .unwrap(); assert!(matches!( decode::(&mismatched_tls), Err(CodecError::MalformedPayload(_)) )); let mismatched_protocol = encode(&RawProtocolCredential { protocol_version: ProtocolVersion::V1, credential_id: ProtocolCredentialId::new(opaque(4)), generation_identity: generation_identity(), role: ProtocolCredentialRole::AgentSandbox, subject_id: ProtocolSubjectId::SessionCoordinator(coordinator_id()), public_key: vec![5; 32], issuer_signature: vec![6; 64], }) .unwrap(); assert!(matches!( decode::(&mismatched_protocol), Err(CodecError::MalformedPayload(_)) )); let coordinator = protocol_credential(ProtocolCredentialRole::SessionCoordinator); let sandbox = protocol_credential(ProtocolCredentialRole::AgentSandbox); assert!( SandboxCredentialBundle::new( generation_identity(), trust_manifest(), sandbox.clone(), coordinator.clone(), vec![7; 32], ) .is_err() ); let swapped_bundle = encode(&RawSandboxCredentialBundle { protocol_version: ProtocolVersion::V1, generation_identity: generation_identity(), trust_manifest: trust_manifest(), coordinator_protocol_credential: sandbox, sandbox_protocol_credential: coordinator, sandbox_private_key: vec![7; 32], }) .unwrap(); assert!(matches!( decode::(&swapped_bundle), Err(CodecError::MalformedPayload(_)) )); } #[test] fn store_identity_and_versions_dispatch_before_rows() { for metadata in [ RawMetadata { schema_id: String::new(), schema_version: STORE_SCHEMA_VERSION, }, RawMetadata { schema_id: DOMAIN_STORE_SCHEMA_ID.into(), schema_version: 0, }, RawMetadata { schema_id: DOMAIN_STORE_SCHEMA_ID.into(), schema_version: STORE_SCHEMA_VERSION + 1, }, RawMetadata { schema_id: "unknown-store".into(), schema_version: STORE_SCHEMA_VERSION, }, ] { assert!(matches!( decode::(&encode(&metadata).unwrap()), Err(CodecError::MalformedPayload(_)) )); } assert!(matches!( decode::( &postcard::to_stdvec(&MissingVersion { schema_id: DOMAIN_STORE_SCHEMA_ID.into(), }) .unwrap() ), Err(CodecError::MalformedPayload(_)) )); assert!(matches!( decode::(&encode(&StoreMetadata::storage_catalog_v1()).unwrap()), Err(CodecError::MalformedPayload(_)) )); assert!(matches!( decode::(&encode(&StoreMetadata::domain_store_v1()).unwrap()), Err(CodecError::MalformedPayload(_)) )); let wrong_domain_store = postcard::to_stdvec(&RawDomainStore { metadata: StoreMetadata::storage_catalog_v1(), domain: Domain { domain_id: domain_id(), profile_name: "trusted".into(), canonical_workspace: HostPath::new(b"workspace".to_vec()), }, generations: vec![], participants: vec![], sessions: vec![], session_terminals: vec![], mailboxes: vec![], }) .unwrap(); assert!(matches!( decode::(&wrong_domain_store), Err(CodecError::MalformedPayload(_)) )); let wrong_catalog = postcard::to_stdvec(&RawStorageCatalog { metadata: StoreMetadata::domain_store_v1(), prepared_artifacts: vec![], artifact_leases: vec![], prepare_operations: vec![], }) .unwrap(); assert!(matches!( decode::(&wrong_catalog), Err(CodecError::MalformedPayload(_)) )); } #[test] fn network_document_schema_identities_are_fixed_to_v1() { let invalid_ids = |schema_id: &str, wrong_document_id: &str| { [ String::new(), schema_id.replace("-v1", "-v0"), schema_id.replace("-v1", "-v2"), wrong_document_id.into(), ] }; let policy = DerivedNetworkPolicy::v1(DerivedNetworkPolicyFields { address_families: vec![], default_actions: vec![], prefix_rules: vec![], resolvers: vec![], }); assert_eq!(policy.schema_id(), DERIVED_NETWORK_POLICY_SCHEMA_ID); for schema_id in invalid_ids( DERIVED_NETWORK_POLICY_SCHEMA_ID, NETWORK_READINESS_ATTESTATION_SCHEMA_ID, ) { let encoded = postcard::to_stdvec(&RawDerivedNetworkPolicy { schema_id, address_families: vec![], default_actions: vec![], prefix_rules: vec![], resolvers: vec![], }) .unwrap(); assert!(matches!( decode::(&encoded), Err(CodecError::MalformedPayload(_)) )); } let attestation = readiness_attestation(); assert_eq!( attestation.schema_id(), NETWORK_READINESS_ATTESTATION_SCHEMA_ID ); for schema_id in invalid_ids( NETWORK_READINESS_ATTESTATION_SCHEMA_ID, NETWORK_READINESS_IDENTITY_SCHEMA_ID, ) { let encoded = postcard::to_stdvec(&RawNetworkReadinessAttestation { schema_id, domain_id: domain_id(), generation_id: generation_id(), configuration_fingerprint: ConfigurationFingerprint::new(opaque(1)), network_sidecar_id: sidecar_id(), derived_policy_digest: DerivedPolicyDigest::new(opaque(2)), enforcement_namespace: namespace(), runtime_supervisor: process(3), connectivity_helper: process(4), observation: network_observation(), }) .unwrap(); assert!(matches!( decode::(&encoded), Err(CodecError::MalformedPayload(_)) )); } let identity = readiness_identity(); assert_eq!(identity.schema_id(), NETWORK_READINESS_IDENTITY_SCHEMA_ID); for schema_id in invalid_ids( NETWORK_READINESS_IDENTITY_SCHEMA_ID, NETWORK_READINESS_ATTESTATION_SCHEMA_ID, ) { let encoded = postcard::to_stdvec(&RawNetworkReadinessIdentity { schema_id, domain_id: domain_id(), generation_id: generation_id(), configuration_fingerprint: ConfigurationFingerprint::new(opaque(1)), network_sidecar_id: sidecar_id(), derived_policy_digest: DerivedPolicyDigest::new(opaque(2)), enforcement_namespace: namespace(), }) .unwrap(); assert!(matches!( decode::(&encoded), Err(CodecError::MalformedPayload(_)) )); } } #[test] fn zero_terminal_dimensions_are_rejected() { assert!(NonZeroU16::new(0).is_none()); for (rows, columns) in [(0, 80), (24, 0)] { let encoded = postcard::to_stdvec(&RawTerminalResize { session_id: session_id(), rows, columns, pixel_width: 0, pixel_height: 0, }) .unwrap(); assert!(matches!( decode::(&encoded), Err(CodecError::MalformedPayload(_)) )); } } #[test] fn generic_schema_record_limit_is_enforced_before_decode() { let oversized = vec![0; MAX_SCHEMA_RECORD_BYTES + 1]; assert!(matches!( decode::>(&oversized), Err(CodecError::RecordTooLarge { .. }) )); assert!(matches!( encode(&oversized), Err(CodecError::RecordTooLarge { .. }) )); } #[test] fn session_exit_code_is_accepted_only_for_exited() { let encoded = encode(&InvalidSessionExit { session_id: session_id(), outcome: SessionExitOutcome::Terminated, exit_code: Some(1), }) .unwrap(); assert!(matches!( decode::(&encoded), Err(CodecError::MalformedPayload(_)) )); } #[test] fn record_encoding_has_stable_byte_order() { assert_eq!(encode(&ParticipantState::Intent).unwrap(), [0]); }