lazaret/crates/lazaret-core/tests/schema_records.rs

1247 lines
40 KiB
Rust

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<u8>,
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<u8>,
issuer_signature: Vec<u8>,
}
#[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<u8>,
}
#[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<Generation>,
participants: Vec<Participant>,
sessions: Vec<Session>,
session_terminals: Vec<SessionTerminal>,
mailboxes: Vec<Mailbox>,
}
#[derive(Serialize)]
struct RawStorageCatalog {
metadata: StoreMetadata,
prepared_artifacts: Vec<PreparedArtifact>,
artifact_leases: Vec<ArtifactLease>,
prepare_operations: Vec<PrepareOperation>,
}
#[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<i32>,
}
#[derive(Serialize)]
struct RawDerivedNetworkPolicy {
schema_id: String,
address_families: Vec<AddressFamilyDisposition>,
default_actions: Vec<DefaultAction>,
prefix_rules: Vec<PrefixRule>,
resolvers: Vec<Resolver>,
}
#[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<T>(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::<T>(&encoded).unwrap(), record);
let mut truncated = encoded.clone();
truncated.pop();
assert!(decode::<T>(&truncated).is_err());
let mut trailing = encoded;
trailing.push(0);
assert!(matches!(
decode::<T>(&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::<HashSet<_>>();
let registered = ALL_SCHEMA_TYPE_NAMES
.iter()
.copied()
.collect::<HashSet<_>>();
assert_eq!(documented, registered);
}
fn assert_unknown_protocol_version<T>(record: &T)
where
T: DeserializeOwned + Serialize,
{
let mut encoded = encode(record).unwrap();
encoded[0] = u8::MAX;
assert!(matches!(
decode::<T>(&encoded),
Err(CodecError::MalformedPayload(_))
));
}
#[test]
fn trailing_and_nonminimal_postcard_encodings_are_rejected() {
assert!(matches!(
decode::<ParticipantState>(&[0, 0]),
Err(CodecError::TrailingPayload)
));
assert!(matches!(
decode::<ParticipantState>(&[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::<TerminalInput>(&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::<TrustManifest>(&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::<StoreMetadata>(&overlong_store_id),
Err(CodecError::NonMinimalPayload)
));
}
#[test]
fn unknown_enums_and_protocol_versions_are_rejected() {
assert!(matches!(
decode::<ParticipantState>(&[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::<Participant>(&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::<TlsCredential>(&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::<ProtocolCredential>(&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::<SandboxCredentialBundle>(&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::<StoreMetadata>(&encode(&metadata).unwrap()),
Err(CodecError::MalformedPayload(_))
));
}
assert!(matches!(
decode::<StoreMetadata>(
&postcard::to_stdvec(&MissingVersion {
schema_id: DOMAIN_STORE_SCHEMA_ID.into(),
})
.unwrap()
),
Err(CodecError::MalformedPayload(_))
));
assert!(matches!(
decode::<DomainStoreMetadata>(&encode(&StoreMetadata::storage_catalog_v1()).unwrap()),
Err(CodecError::MalformedPayload(_))
));
assert!(matches!(
decode::<StorageCatalogMetadata>(&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::<DomainStore>(&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::<StorageCatalog>(&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::<DerivedNetworkPolicy>(&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::<NetworkReadinessAttestation>(&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::<NetworkReadinessIdentity>(&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::<TerminalResize>(&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::<Vec<u8>>(&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::<SessionExit>(&encoded),
Err(CodecError::MalformedPayload(_))
));
}
#[test]
fn record_encoding_has_stable_byte_order() {
assert_eq!(encode(&ParticipantState::Intent).unwrap(), [0]);
}