use std::collections::{BTreeMap, HashSet}; use std::fmt::Debug; use std::num::NonZeroU16; use lazaret_core::codec::{CodecError, MAX_SCHEMA_RECORD_BYTES, decode, encode}; use lazaret_core::configuration::parse_trusted_configuration; use lazaret_core::identity::{ BootId, BundleDigest, CapabilityId, CertificateDigest, ClientId, ConfigurationFingerprint, ContainerId, CoordinatorId, DerivedPolicyDigest, DeviceId, DomainId, FileDescriptor, GenerationId, GuestProcessGroupId, GuestProcessId, GuestProcessStartTimeTicks, HostPath, HostPidLocator, IdMapDigest, InodeId, MountId, MountIdentity, NamespaceIdentity, NetworkSidecarId, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyId, PrepareOperationId, PreparedArtifactId, ProcessStartTimeTicks, ProtocolCredentialId, ReadinessFingerprint, RequestDigest, RequestId, RunscBuildId, RunscDigest, SandboxId, SelectedManifestDigest, SessionAnchorId, SessionId, SourceIdentity, StreamDigest, TrustManifestDigest, }; use lazaret_core::network::readiness_fingerprint as derive_readiness_fingerprint; use lazaret_core::protocol::{BootstrapWaitOutcome, BootstrapWaitRequest}; use lazaret_core::records::*; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; #[derive(Default)] struct SchemaCorpus { names: HashSet<&'static str>, digest: Sha256, } #[test] fn sandbox_ownership_decodes_records_without_runsc_identity() { #[derive(Serialize)] struct LegacySandboxOwnership { container_id: Option, runtime_root: Option, runtime_root_identity: Option, bundle_digest: Option, host_coordinator: PersistedHostProcessOwnership, runtime_supervisor: PersistedHostProcessOwnership, attached_runsc: PersistedHostProcessOwnership, } let encoded = postcard::to_stdvec(&LegacySandboxOwnership { container_id: None, runtime_root: None, runtime_root_identity: None, bundle_digest: None, host_coordinator: PersistedHostProcessOwnership::unassigned(), runtime_supervisor: PersistedHostProcessOwnership::unassigned(), attached_runsc: PersistedHostProcessOwnership::unassigned(), }) .unwrap(); let decoded = postcard::from_bytes::(&encoded).unwrap(); assert!(decoded.attached_runsc.is_unassigned()); } impl SchemaCorpus { fn insert(&mut self, name: &'static str, encoded: &[u8]) -> bool { self.digest.update(name.len().to_be_bytes()); self.digest.update(name.as_bytes()); self.digest.update(encoded.len().to_be_bytes()); self.digest.update(encoded); self.names.insert(name) } fn len(&self) -> usize { self.names.len() } fn contains(&self, name: &&str) -> bool { self.names.contains(name) } fn digest(&self) -> [u8; 32] { self.digest.clone().finalize().into() } } #[derive(Deserialize, Serialize)] struct RawParticipant { generation_id: GenerationId, role: ParticipantRole, state: ParticipantState, ownership: Option, } #[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(Serialize)] struct RawSessionTerminal { generation_id: GenerationId, session_id: SessionId, state: SessionTerminalState, container_id: Option, host_coordinator: PersistedHostProcessOwnership, pty_master: Option, exec_client: PersistedHostProcessOwnership, guest_process: Option, } #[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: PidfsProcessIdentityV1, connectivity_helper: PidfsProcessIdentityV1, 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, } #[derive(Serialize)] struct RawMailboxRequest { protocol_version: ProtocolVersion, session_binding: SessionBinding, request_id: RequestId, session_capability: SessionRequestCapability, sandbox_credential_id: ProtocolCredentialId, command_argv: Vec>, justification: String, } #[derive(Serialize)] struct RawTrustedHostConfigurationV1 { schema_id: String, storage: StorageConfigurationV1, network: NetworkConfigurationV1, runtime: RuntimeConfigurationV1, profiles: BTreeMap, } #[derive(Serialize)] struct OversizedRecord(Vec); macro_rules! test_schema_types { ($($record:ty),+ $(,)?) => { $( impl SchemaType for $record { const NAME: &'static str = concat!("test::", stringify!($record)); } )+ }; } test_schema_types!( RawParticipant, RawTlsCredential, RawProtocolCredential, RawSandboxCredentialBundle, RawMetadata, MissingVersion, RawDomainStore, RawStorageCatalog, RawTerminalResize, RawSessionTerminal, InvalidSessionExit, RawDerivedNetworkPolicy, RawNetworkReadinessAttestation, RawNetworkReadinessIdentity, RawMailboxRequest, RawTrustedHostConfigurationV1, OversizedRecord, ); 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 terminal_modes_v2() -> TerminalModesV2 { TerminalModesV2 { input_modes: 1, output_modes: 2, control_modes: 3, local_modes: 4, line_discipline: 0, input_speed: 38_400, output_speed: 38_400, interrupt: 3, quit: 28, erase: 127, kill: 21, eof: 4, time: 0, minimum: 1, switch: 0, start: 17, stop: 19, suspend: 26, end_of_line: 0, reprint: 18, discard: 15, word_erase: 23, literal_next: 22, end_of_line_2: 0, } } fn process(seed: u32) -> PidfsProcessIdentityV1 { PidfsProcessIdentityV1::new( PidfsProcessAuthorityV1 { boot_id: BootId::new([0x42; 16]), pidfs_st_dev: PidfsDeviceId::new(7), pidfs_st_ino: PidfsInodeId::new(u64::from(seed)), }, HostPidLocator::new(seed), ProcessStartTimeTicks::new(u64::from(seed) * 10), ) } fn owned_process(seed: u32) -> PersistedHostProcessOwnership { PersistedHostProcessOwnership::committed(process(seed)) } 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 canary_observation() -> NetworkCanaryObservation { NetworkCanaryObservation { domain_id: domain_id(), generation_id: generation_id(), enforcement_namespace: namespace(), linux_policy_matches: true, endpoints_authenticated_before_policy: true, allowed_endpoint_authenticated: true, denied_endpoint_unreachable: true, imported_interface_matches: true, tap_present: true, ipv6_absent: true, network_capabilities_empty: true, route_mutation_refused: true, cleanup_shape_matched: true, responder_closed: true, peer_namespace_closed: true, host_interface_deleted: true, allowed_endpoint_unreachable_after_cleanup: true, tap_present_after_cleanup: true, ipv6_unchanged_after_cleanup: true, network_admin_dropped: true, } } fn readiness_evidence() -> NetworkReadinessEvidence { let readiness_identity = readiness_identity(); NetworkReadinessEvidence { domain_id: domain_id(), generation_id: generation_id(), network_sidecar_id: sidecar_id(), readiness_fingerprint: derive_readiness_fingerprint(&readiness_identity).unwrap(), attestation: readiness_attestation(), readiness_identity, canary_observation: canary_observation(), } } fn prepared_ownership() -> PreparedLowerOwnership { PreparedLowerOwnership { prepared_artifact_id: Some(PreparedArtifactId::new(opaque(24))), source_identity: Some(SourceIdentity::new(vec![25, 26])), lease_state: Some(ParticipantState::LeaseHeld), path: Some(HostPath::new(vec![b'/', 0xff])), device: Some(DeviceId::new(27)), inode: Some(InodeId::new(28)), released_state_anchor: None, } } fn network_ownership() -> NetworkOwnership { let readiness_evidence = readiness_evidence(); NetworkOwnership { derived_policy_digest: Some(DerivedPolicyDigest::new(opaque(20))), readiness_fingerprint: Some(readiness_evidence.readiness_fingerprint), readiness_evidence: Some(readiness_evidence), namespace: Some(namespace()), runtime_supervisor: owned_process(21), } } fn mutate_network_ownership(mutate: impl FnOnce(&mut NetworkOwnership)) -> NetworkOwnership { let mut ownership = network_ownership(); mutate(&mut ownership); ownership } fn state_anchor_ownership( daemon: PersistedHostProcessOwnership, mount_identity: Option, ) -> StateAnchorOwnership { StateAnchorOwnership { lower_artifact_id: Some(PreparedArtifactId::new(opaque(24))), upper_path: Some(HostPath::new(b"upper".to_vec())), work_path: Some(HostPath::new(b"work".to_vec())), merged_path: Some(HostPath::new(b"merged".to_vec())), daemon, user_namespace: Some(namespace()), mount_namespace: Some(namespace()), mount: mount_identity, } } fn pty() -> PtyIdentity { PtyIdentity { master_fd: FileDescriptor::new(31), device: DeviceId::new(32), inode: InodeId::new(33), } } fn guest_process() -> GuestProcessIdentity { GuestProcessIdentity { leader_pid: GuestProcessId::new(35), leader_start_time: GuestProcessStartTimeTicks::new(36), process_group_id: GuestProcessGroupId::new(37), } } fn active_state(role: ParticipantRole) -> ParticipantState { match role { ParticipantRole::PreparedLower => ParticipantState::LeaseHeld, ParticipantRole::StateAnchor | ParticipantRole::GitRouting => ParticipantState::Prepared, ParticipantRole::Network | ParticipantRole::Helper | ParticipantRole::Sandbox | ParticipantRole::Publication => ParticipantState::Committed, } } fn complete_ownership(role: ParticipantRole, state: ParticipantState) -> ParticipantOwnership { match role { ParticipantRole::PreparedLower => { let mut ownership = prepared_ownership(); ownership.lease_state = Some(state); ParticipantOwnership::PreparedLower(ownership) } ParticipantRole::StateAnchor => ParticipantOwnership::StateAnchor(state_anchor_ownership( owned_process(41), Some(mount()), )), ParticipantRole::GitRouting => ParticipantOwnership::GitRouting(GitRoutingOwnership { mountpoint: Some(HostPath::new(b"workspace".to_vec())), host_root: Some(HostPath::new(b"host".to_vec())), isolated_root: Some(HostPath::new(b"isolated".to_vec())), daemon: owned_process(42), mount: Some(mount()), }), ParticipantRole::Network => ParticipantOwnership::Network(Box::new(network_ownership())), ParticipantRole::Helper => ParticipantOwnership::Helper(HelperOwnership { network_sidecar_id: Some(sidecar_id()), connectivity_helper: owned_process(22), runtime_supervisor: owned_process(21), }), ParticipantRole::Sandbox => ParticipantOwnership::Sandbox(SandboxOwnership { container_id: Some(ContainerId::new(opaque(29))), runtime_root: Some(HostPath::new(b"runtime".to_vec())), runtime_root_identity: Some(mount()), bundle_digest: Some(BundleDigest::new(opaque(43))), host_coordinator: owned_process(30), runtime_supervisor: owned_process(21), attached_runsc: PersistedHostProcessOwnership::Runsc { build_id: RunscBuildId::new(opaque(44)), digest: RunscDigest::new(opaque(45)), process: RunscProcessOwnership::Committed(HostProcessIdentity::pidfs_v1(process( 44, ))), }, }), ParticipantRole::Publication => ParticipantOwnership::Publication(PublicationOwnership { staged_socket_path: Some(HostPath::new(b"staged-socket".to_vec())), staged_identity_path: Some(HostPath::new(b"staged-identity".to_vec())), socket_path: Some(HostPath::new(b"socket".to_vec())), identity_path: Some(HostPath::new(b"identity".to_vec())), socket_identity: Some(mount()), identity_file_identity: Some(mount()), }), } } fn release_ownership(role: ParticipantRole) -> ParticipantOwnership { let state = if role == ParticipantRole::PreparedLower { ParticipantState::ReleaseIntent } else { active_state(role) }; complete_ownership(role, state) } fn terminal_snapshot( state: SessionTerminalState, host_coordinator: PersistedHostProcessOwnership, exec_client: PersistedHostProcessOwnership, has_physical_evidence: bool, ) -> Result { SessionTerminal::snapshot(SessionTerminalFields { generation_id: generation_id(), session_id: session_id(), state, container_id: has_physical_evidence.then(|| ContainerId::new(opaque(29))), host_coordinator, pty_master: has_physical_evidence.then_some(pty()), exec_client, guest_process: has_physical_evidence.then_some(guest_process()), }) } #[derive(Clone, Copy, Debug)] enum ParticipantProcessEdge { StateAnchorDaemon, GitRoutingDaemon, NetworkRuntimeSupervisor, HelperConnectivityHelper, HelperRuntimeSupervisor, SandboxHostCoordinator, SandboxRuntimeSupervisor, SandboxAttachedRunsc, } fn participant_edge_snapshot( edge: ParticipantProcessEdge, process_ownership: PersistedHostProcessOwnership, ) -> Participant { let (role, ownership) = match edge { ParticipantProcessEdge::StateAnchorDaemon => ( ParticipantRole::StateAnchor, ParticipantOwnership::StateAnchor(state_anchor_ownership(process_ownership, None)), ), ParticipantProcessEdge::GitRoutingDaemon => ( ParticipantRole::GitRouting, ParticipantOwnership::GitRouting(GitRoutingOwnership { mountpoint: Some(HostPath::new(b"workspace".to_vec())), host_root: Some(HostPath::new(b"host".to_vec())), isolated_root: Some(HostPath::new(b"isolated".to_vec())), daemon: process_ownership, mount: None, }), ), ParticipantProcessEdge::NetworkRuntimeSupervisor => ( ParticipantRole::Network, ParticipantOwnership::Network(Box::new(NetworkOwnership { derived_policy_digest: Some(DerivedPolicyDigest::new(opaque(20))), readiness_fingerprint: None, readiness_evidence: None, namespace: None, runtime_supervisor: process_ownership, })), ), ParticipantProcessEdge::HelperConnectivityHelper => ( ParticipantRole::Helper, ParticipantOwnership::Helper(HelperOwnership { network_sidecar_id: Some(sidecar_id()), connectivity_helper: process_ownership, runtime_supervisor: owned_process(121), }), ), ParticipantProcessEdge::HelperRuntimeSupervisor => ( ParticipantRole::Helper, ParticipantOwnership::Helper(HelperOwnership { network_sidecar_id: Some(sidecar_id()), connectivity_helper: PersistedHostProcessOwnership::unassigned(), runtime_supervisor: process_ownership, }), ), ParticipantProcessEdge::SandboxHostCoordinator => ( ParticipantRole::Sandbox, ParticipantOwnership::Sandbox(SandboxOwnership { container_id: Some(ContainerId::new(opaque(29))), runtime_root: None, runtime_root_identity: None, bundle_digest: None, host_coordinator: process_ownership, runtime_supervisor: PersistedHostProcessOwnership::unassigned(), attached_runsc: PersistedHostProcessOwnership::unassigned(), }), ), ParticipantProcessEdge::SandboxRuntimeSupervisor => ( ParticipantRole::Sandbox, ParticipantOwnership::Sandbox(SandboxOwnership { container_id: Some(ContainerId::new(opaque(29))), runtime_root: None, runtime_root_identity: None, bundle_digest: None, host_coordinator: owned_process(122), runtime_supervisor: process_ownership, attached_runsc: PersistedHostProcessOwnership::unassigned(), }), ), ParticipantProcessEdge::SandboxAttachedRunsc => ( ParticipantRole::Sandbox, ParticipantOwnership::Sandbox(SandboxOwnership { container_id: Some(ContainerId::new(opaque(29))), runtime_root: None, runtime_root_identity: None, bundle_digest: None, host_coordinator: owned_process(122), runtime_supervisor: owned_process(123), attached_runsc: process_ownership, }), ), }; Participant::snapshot( generation_id(), role, ParticipantState::Intent, Some(ownership), ) .unwrap() } fn verify_shape(record: T, seen: &mut SchemaCorpus) where T: Clone + Debug + DeserializeOwned + Eq + SchemaType + Serialize, { let encoded = encode(&record).expect("representative record encodes"); assert!( seen.insert(T::NAME, &encoded), "duplicate test for {}", T::NAME ); 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); } #[path = "schema_records/credentials.rs"] mod credentials; #[path = "schema_records/documents.rs"] mod documents; #[path = "schema_records/encoding.rs"] mod encoding; #[path = "schema_records/enum_shapes.rs"] mod enum_shapes; #[path = "schema_records/participant_transitions.rs"] mod participant_transitions; #[path = "schema_records/process_evidence.rs"] mod process_evidence; #[path = "schema_records/terminal.rs"] mod terminal;