diff --git a/crates/openshell-isolation-interface/src/contract.rs b/crates/openshell-isolation-interface/src/contract.rs index b16dfdd724..be375cd466 100644 --- a/crates/openshell-isolation-interface/src/contract.rs +++ b/crates/openshell-isolation-interface/src/contract.rs @@ -141,7 +141,13 @@ pub struct BackendDescriptor { } /// Exact non-root identity selected before the immutable workload is created. +/// +/// Validation and normalization run in [`ResolvedWorkloadIdentity::new`] and +/// again when a value is decoded from a serialized descriptor: deserialization +/// routes through the same constructor, so an invalid identity fails to decode +/// instead of bypassing the contract. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "RawResolvedWorkloadIdentity")] pub struct ResolvedWorkloadIdentity { /// Effective and real user ID used by sandbox and all workload children. pub uid: u32, @@ -187,6 +193,32 @@ impl ResolvedWorkloadIdentity { } } +/// Unvalidated wire shape of [`ResolvedWorkloadIdentity`], existing only so +/// serde decoding can route through [`ResolvedWorkloadIdentity::new`]. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RawResolvedWorkloadIdentity { + uid: u32, + gid: u32, + supplementary_gids: Vec, + source: String, + resource_digest: String, +} + +impl TryFrom for ResolvedWorkloadIdentity { + type Error = BackendError; + + fn try_from(raw: RawResolvedWorkloadIdentity) -> Result { + Self::new( + raw.uid, + raw.gid, + raw.supplementary_gids, + raw.source, + raw.resource_digest, + ) + } +} + /// The trusted sandbox context, constructed by trusted common code after the /// control plane assigns the resource to the admitted sandbox. /// diff --git a/crates/openshell-isolation-interface/tests/backend_conformance.rs b/crates/openshell-isolation-interface/tests/backend_conformance.rs index e34e6215bd..9928b02073 100644 --- a/crates/openshell-isolation-interface/tests/backend_conformance.rs +++ b/crates/openshell-isolation-interface/tests/backend_conformance.rs @@ -77,6 +77,115 @@ fn workload_identity_rejects_root_and_normalizes_groups() { assert_eq!(identity.supplementary_gids, vec![1002, 1003]); } +#[test] +fn workload_identity_deserialization_enforces_new_invariants() { + fn payload(uid: u32, gid: u32, gids: &[u32], source: &str, digest: &str) -> serde_json::Value { + serde_json::json!({ + "uid": uid, + "gid": gid, + "supplementary_gids": gids, + "source": source, + "resource_digest": digest, + }) + } + + assert!( + serde_json::from_value::(payload( + 0, + 1000, + &[], + "policy", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 0, + &[], + "policy", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 1000, + &[0], + "policy", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 1000, + &[], + "", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload(1000, 1000, &[], "policy", "")) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 1000, + &[], + " ", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload(1000, 1000, &[], "policy", " ")) + .is_err() + ); + + let identity = workload_identity(); + let encoded = serde_json::to_value(&identity).unwrap(); + assert_eq!( + serde_json::from_value::(encoded).unwrap(), + identity + ); + + let decoded = serde_json::from_value::(payload( + 1000, + 1000, + &[1003, 1001, 1002, 1003], + "policy", + "sha256:test", + )) + .unwrap(); + assert_eq!(decoded.supplementary_gids, vec![1001, 1002, 1003]); + + let decoded = serde_json::from_value::(payload( + 1000, + 1000, + &[1000, 1001], + "policy", + "sha256:test", + )) + .unwrap(); + assert_eq!(decoded.supplementary_gids, vec![1001]); +} + +#[test] +fn boundary_confirmation_rejects_invalid_embedded_identity() { + let encoded = serde_json::to_value(confirmation()).unwrap(); + serde_json::from_value::(encoded.clone()).unwrap(); + + let mut invalid = encoded; + invalid["identity"]["uid"] = serde_json::json!(0); + assert!(serde_json::from_value::(invalid).is_err()); +} + #[test] fn confirmation_rejects_missing_generation_or_identity_drift() { let expected = workload_identity();