Implement canonical network readiness derivation
This commit is contained in:
parent
d274e1835d
commit
304fb225d6
1 changed files with 1149 additions and 11 deletions
485
crates/lazaret-core/tests/network.rs
Normal file
485
crates/lazaret-core/tests/network.rs
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue