use std::fmt::Write; use lazaret_core::identity::{ BootId, ConfigurationFingerprint, DerivedPolicyDigest, DeviceId, DomainId, GenerationId, HostPidLocator, InodeId, NamespaceIdentity, NetworkSidecarId, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, ProcessStartTimeTicks, ReadinessFingerprint, }; use lazaret_core::network::{ DnsQueryExpectation, NetworkValidationErrorKind, ReachabilityExpectation, ReadinessExpectation, accept_network_readiness, canonical_readiness_identity_bytes, derive_policy, readiness_fingerprint, readiness_identity, validate_network_readiness_evidence, }; use lazaret_core::records::{ AddressFamily, AddressFamilyDisposition, AddressFamilyDispositionValue, DefaultAction, DeniedDestinationObservation, DerivedNetworkPolicyFields, DnsObservation, IpAddress, IpNetwork, NetworkObservation, NetworkReachabilityProbe, NetworkReadinessAttestation, NetworkReadinessAttestationFields, PermittedDestinationObservation, PolicyAction, PolicyDelivery, PrefixRule, Resolver, }; type AttestationMutation = fn(&mut NetworkReadinessAttestation); type EvidenceMutation = fn(&mut lazaret_core::records::NetworkReadinessEvidence); type ProbeMutation = fn(&mut NetworkReachabilityProbe); fn repeated(value: u8) -> [u8; 32] { [value; 32] } fn namespace() -> NamespaceIdentity { NamespaceIdentity { device: DeviceId::new(41), inode: InodeId::new(42), } } fn process(value: u8) -> PidfsProcessIdentityV1 { PidfsProcessIdentityV1::new( PidfsProcessAuthorityV1 { boot_id: BootId::new([value; 16]), pidfs_st_dev: PidfsDeviceId::new(u64::from(value)), pidfs_st_ino: PidfsInodeId::new(u64::from(value) + 1), }, HostPidLocator::new(u32::from(value)), ProcessStartTimeTicks::new(u64::from(value) + 2), ) } fn policy_fields() -> DerivedNetworkPolicyFields { DerivedNetworkPolicyFields { address_families: vec![ AddressFamilyDisposition { family: AddressFamily::Ipv4, disposition: AddressFamilyDispositionValue::Enabled, }, AddressFamilyDisposition { family: AddressFamily::Ipv6, disposition: AddressFamilyDispositionValue::Disabled, }, ], default_actions: vec![DefaultAction { family: AddressFamily::Ipv4, action: PolicyAction::Allow, }], prefix_rules: vec![ PrefixRule { family: AddressFamily::Ipv4, network: IpNetwork { address: IpAddress::V4([10, 0, 0, 0]), prefix_length: 8, }, action: PolicyAction::Deny, }, PrefixRule { family: AddressFamily::Ipv4, network: IpNetwork { address: IpAddress::V4([10, 20, 0, 0]), prefix_length: 16, }, action: PolicyAction::Allow, }, PrefixRule { family: AddressFamily::Ipv4, network: IpNetwork { address: IpAddress::V4([172, 16, 0, 0]), prefix_length: 12, }, action: PolicyAction::Deny, }, PrefixRule { family: AddressFamily::Ipv4, network: IpNetwork { address: IpAddress::V4([192, 168, 0, 0]), prefix_length: 16, }, action: PolicyAction::Deny, }, ], resolvers: vec![Resolver { family: AddressFamily::Ipv4, address: IpAddress::V4([192, 0, 2, 53]), }], } } struct Fixture { policy: lazaret_core::network::CanonicalDerivedNetworkPolicy, runtime_supervisor: PidfsProcessIdentityV1, connectivity_helper: PidfsProcessIdentityV1, attestation: NetworkReadinessAttestation, probe: NetworkReachabilityProbe, reachability: ReachabilityExpectation, } impl Fixture { fn new() -> Self { let policy = derive_policy(policy_fields()).unwrap(); let runtime_supervisor = process(51); let connectivity_helper = process(61); let attestation = NetworkReadinessAttestation::v1(NetworkReadinessAttestationFields { domain_id: DomainId::new(repeated(1)), generation_id: GenerationId::new(2), configuration_fingerprint: ConfigurationFingerprint::new(repeated(3)), network_sidecar_id: NetworkSidecarId::new(repeated(4)), derived_policy_digest: policy.digest(), enforcement_namespace: namespace(), runtime_supervisor, connectivity_helper, observation: 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, }, }); let probe = NetworkReachabilityProbe { domain_id: DomainId::new(repeated(1)), generation_id: GenerationId::new(2), enforcement_namespace: namespace(), denied_destinations: vec![DeniedDestinationObservation { destination: IpAddress::V4([10, 1, 2, 3]), unreachable: true, }], permitted_destinations: vec![PermittedDestinationObservation { destination: IpAddress::V4([10, 20, 1, 2]), reachable: true, }], dns_observations: vec![DnsObservation { resolver: IpAddress::V4([192, 0, 2, 53]), query_name: "sandbox.invalid".into(), resolved_addresses: vec![IpAddress::V4([192, 0, 2, 10])], }], }; let reachability = ReachabilityExpectation { denied_destinations: vec![IpAddress::V4([10, 1, 2, 3])], permitted_destinations: vec![IpAddress::V4([10, 20, 1, 2])], dns_queries: vec![DnsQueryExpectation { resolver: IpAddress::V4([192, 0, 2, 53]), query_name: "sandbox.invalid".into(), }], }; Self { policy, runtime_supervisor, connectivity_helper, attestation, probe, reachability, } } fn expectation(&self) -> ReadinessExpectation<'_> { ReadinessExpectation { domain_id: DomainId::new(repeated(1)), generation_id: GenerationId::new(2), configuration_fingerprint: ConfigurationFingerprint::new(repeated(3)), network_sidecar_id: NetworkSidecarId::new(repeated(4)), enforcement_namespace: namespace(), runtime_supervisor: &self.runtime_supervisor, connectivity_helper: &self.connectivity_helper, } } fn evidence(&self) -> lazaret_core::records::NetworkReadinessEvidence { accept_network_readiness( &self.policy, &self.expectation(), self.attestation.clone(), self.probe.clone(), &self.reachability, ) .unwrap() } } fn hex(bytes: &[u8]) -> String { bytes.iter().fold(String::new(), |mut encoded, byte| { write!(encoded, "{byte:02x}").unwrap(); encoded }) } #[test] fn canonical_policy_and_readiness_vectors_are_stable() { let fixture = Fixture::new(); let identity = readiness_identity(&fixture.attestation); assert_eq!( hex(fixture.policy.canonical_bytes()), "216c617a617265742d646572697665642d6e6574776f726b2d706f6c6963792d763102000001010100000400000a000000080100000a14000010000000ac1000000c010000c0a800001001010000c0000235" ); assert_eq!( hex(fixture.policy.digest().as_bytes()), "01b72b130912b96c591271308b6b54f99431b90b0f6eee252eb4ada6aa0a42e3" ); assert_eq!( hex(&canonical_readiness_identity_bytes(&identity).unwrap()), "256c617a617265742d6e6574776f726b2d72656164696e6573732d6964656e746974792d76310101010101010101010101010101010101010101010101010101010101010101020303030303030303030303030303030303030303030303030303030303030303040404040404040404040404040404040404040404040404040404040404040401b72b130912b96c591271308b6b54f99431b90b0f6eee252eb4ada6aa0a42e3292a" ); assert_eq!( hex(readiness_fingerprint(&identity).unwrap().as_bytes()), "1faf0b0bb7807002e223a113d3da93520bfe91b350380d5d890e20caed3d95af" ); } #[test] fn semantically_reordered_policy_has_one_identity() { let expected = derive_policy(policy_fields()).unwrap(); let mut reordered = policy_fields(); reordered.address_families.reverse(); reordered.prefix_rules.reverse(); let actual = derive_policy(reordered).unwrap(); assert_eq!(actual, expected); } #[test] fn malformed_policy_inputs_are_refused() { let cases = [ ("missing family", { let mut fields = policy_fields(); fields.address_families.pop(); fields }), ("enabled IPv6", { let mut fields = policy_fields(); fields.address_families[1].disposition = AddressFamilyDispositionValue::Enabled; fields }), ("wrong default", { let mut fields = policy_fields(); fields.default_actions[0].action = PolicyAction::Deny; fields }), ("missing default private deny", { let mut fields = policy_fields(); fields.prefix_rules.pop(); fields }), ("noncanonical CIDR", { let mut fields = policy_fields(); fields.prefix_rules[0].network.address = IpAddress::V4([10, 0, 0, 1]); fields }), ("oversized prefix", { let mut fields = policy_fields(); fields.prefix_rules[0].network.prefix_length = 33; fields }), ("prefix family mismatch", { let mut fields = policy_fields(); fields.prefix_rules[0].network.address = IpAddress::V6([0; 16]); fields }), ("duplicate prefix", { let mut fields = policy_fields(); fields.prefix_rules.push(fields.prefix_rules[0]); fields }), ("conflicting prefix", { let mut fields = policy_fields(); let mut conflicting = fields.prefix_rules[0]; conflicting.action = PolicyAction::Allow; fields.prefix_rules.push(conflicting); fields }), ("no resolver", { let mut fields = policy_fields(); fields.resolvers.clear(); fields }), ("resolver family mismatch", { let mut fields = policy_fields(); fields.resolvers[0].address = IpAddress::V6([0; 16]); fields }), ("duplicate resolver", { let mut fields = policy_fields(); fields.resolvers.push(fields.resolvers[0]); fields }), ]; for (name, fields) in cases { assert!(derive_policy(fields).is_err(), "accepted {name}"); } } #[test] fn every_attestation_identity_mismatch_is_refused() { let fixture = Fixture::new(); let mutations: [(&str, AttestationMutation); 9] = [ ("domain", |value| { value.domain_id = DomainId::new(repeated(71)); }), ("generation", |value| { value.generation_id = GenerationId::new(72); }), ("configuration", |value| { value.configuration_fingerprint = ConfigurationFingerprint::new(repeated(73)); }), ("sidecar", |value| { value.network_sidecar_id = NetworkSidecarId::new(repeated(74)); }), ("policy", |value| { value.derived_policy_digest = DerivedPolicyDigest::new(repeated(75)); }), ("namespace", |value| { value.enforcement_namespace.inode = InodeId::new(76); }), ("namespace device", |value| { value.enforcement_namespace.device = DeviceId::new(77); }), ("runtime supervisor", |value| { value.runtime_supervisor = process(78); }), ("connectivity helper", |value| { value.connectivity_helper = process(79); }), ]; for (name, mutate) in mutations { let mut attestation = fixture.attestation.clone(); mutate(&mut attestation); let error = accept_network_readiness( &fixture.policy, &fixture.expectation(), attestation, fixture.probe.clone(), &fixture.reachability, ) .unwrap_err(); assert_eq!( error.kind(), NetworkValidationErrorKind::Attestation, "{name}" ); } } #[test] fn every_evidence_identity_mismatch_is_refused() { let fixture = Fixture::new(); let mutations: [(&str, EvidenceMutation); 13] = [ ("outer domain", |value| { value.domain_id = DomainId::new(repeated(81)); }), ("outer generation", |value| { value.generation_id = GenerationId::new(82); }), ("outer sidecar", |value| { value.network_sidecar_id = NetworkSidecarId::new(repeated(83)); }), ("identity domain", |value| { value.readiness_identity.domain_id = DomainId::new(repeated(84)); }), ("identity generation", |value| { value.readiness_identity.generation_id = GenerationId::new(85); }), ("identity configuration", |value| { value.readiness_identity.configuration_fingerprint = ConfigurationFingerprint::new(repeated(86)); }), ("identity sidecar", |value| { value.readiness_identity.network_sidecar_id = NetworkSidecarId::new(repeated(87)); }), ("identity policy", |value| { value.readiness_identity.derived_policy_digest = DerivedPolicyDigest::new(repeated(88)); }), ("identity namespace", |value| { value.readiness_identity.enforcement_namespace.inode = InodeId::new(89); }), ("fingerprint", |value| { value.readiness_fingerprint = ReadinessFingerprint::new(repeated(90)); }), ("probe domain", |value| { value.reachability_probe.domain_id = DomainId::new(repeated(91)); }), ("probe generation", |value| { value.reachability_probe.generation_id = GenerationId::new(92); }), ("probe namespace", |value| { value.reachability_probe.enforcement_namespace.inode = InodeId::new(93); }), ]; for (name, mutate) in mutations { let mut evidence = fixture.evidence(); mutate(&mut evidence); assert!( validate_network_readiness_evidence( &fixture.policy, &fixture.expectation(), &evidence, &fixture.reachability, ) .is_err(), "accepted {name}" ); } } #[test] fn incomplete_or_failed_reachability_is_refused() { let fixture = Fixture::new(); let mutations: [(&str, ProbeMutation); 6] = [ ("deny reached", |value| { value.denied_destinations[0].unreachable = false; }), ("permit unreachable", |value| { value.permitted_destinations[0].reachable = false; }), ("missing deny", |value| value.denied_destinations.clear()), ("missing permit", |value| { value.permitted_destinations.clear(); }), ("missing DNS", |value| value.dns_observations.clear()), ("empty DNS result", |value| { value.dns_observations[0].resolved_addresses.clear(); }), ]; for (name, mutate) in mutations { let mut probe = fixture.probe.clone(); mutate(&mut probe); assert!( accept_network_readiness( &fixture.policy, &fixture.expectation(), fixture.attestation.clone(), probe, &fixture.reachability, ) .is_err(), "accepted {name}" ); } } #[test] fn incomplete_or_malformed_attestation_is_refused() { let fixture = Fixture::new(); let mutations: [(&str, AttestationMutation); 3] = [ ("helper not ready", |value| { value.observation.helper_ready = false; }), ("routes not attested", |value| { value.observation.routes_attested = false; }), ("noncanonical route", |value| { value.observation.observed_routes[0].address = IpAddress::V4([10, 0, 0, 1]); }), ]; for (name, mutate) in mutations { let mut attestation = fixture.attestation.clone(); mutate(&mut attestation); assert!( accept_network_readiness( &fixture.policy, &fixture.expectation(), attestation, fixture.probe.clone(), &fixture.reachability, ) .is_err(), "accepted {name}" ); } }