475 lines
17 KiB
Rust
475 lines
17 KiB
Rust
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::{
|
|
NetworkValidationErrorKind, 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,
|
|
DerivedNetworkPolicyFields, IpAddress, IpNetwork, NetworkCanaryObservation, NetworkObservation,
|
|
NetworkReadinessAttestation, NetworkReadinessAttestationFields, PolicyAction, PolicyDelivery,
|
|
PrefixRule, Resolver,
|
|
};
|
|
|
|
type AttestationMutation = fn(&mut NetworkReadinessAttestation);
|
|
type EvidenceMutation = fn(&mut lazaret_core::records::NetworkReadinessEvidence);
|
|
type CanaryMutation = fn(&mut NetworkCanaryObservation);
|
|
|
|
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,
|
|
canary: NetworkCanaryObservation,
|
|
}
|
|
|
|
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 canary = NetworkCanaryObservation {
|
|
domain_id: DomainId::new(repeated(1)),
|
|
generation_id: GenerationId::new(2),
|
|
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,
|
|
};
|
|
Self {
|
|
policy,
|
|
runtime_supervisor,
|
|
connectivity_helper,
|
|
attestation,
|
|
canary,
|
|
}
|
|
}
|
|
|
|
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.canary.clone(),
|
|
)
|
|
.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.canary.clone(),
|
|
)
|
|
.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.canary_observation.domain_id = DomainId::new(repeated(91));
|
|
}),
|
|
("probe generation", |value| {
|
|
value.canary_observation.generation_id = GenerationId::new(92);
|
|
}),
|
|
("probe namespace", |value| {
|
|
value.canary_observation.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,
|
|
)
|
|
.is_err(),
|
|
"accepted {name}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn incomplete_or_failed_canary_is_refused() {
|
|
let fixture = Fixture::new();
|
|
let mutations: [(&str, CanaryMutation); 6] = [
|
|
("deny reached", |value| {
|
|
value.denied_endpoint_unreachable = false;
|
|
}),
|
|
("permit unreachable", |value| {
|
|
value.allowed_endpoint_authenticated = false;
|
|
}),
|
|
("policy mismatch", |value| {
|
|
value.linux_policy_matches = false;
|
|
}),
|
|
("cleanup mismatch", |value| {
|
|
value.cleanup_shape_matched = false;
|
|
}),
|
|
("capability retained", |value| {
|
|
value.network_admin_dropped = false;
|
|
}),
|
|
("IPv6 present", |value| value.ipv6_absent = false),
|
|
];
|
|
for (name, mutate) in mutations {
|
|
let mut canary = fixture.canary.clone();
|
|
mutate(&mut canary);
|
|
assert!(
|
|
accept_network_readiness(
|
|
&fixture.policy,
|
|
&fixture.expectation(),
|
|
fixture.attestation.clone(),
|
|
canary,
|
|
)
|
|
.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.canary.clone(),
|
|
)
|
|
.is_err(),
|
|
"accepted {name}"
|
|
);
|
|
}
|
|
}
|