Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions crates/openshell-isolation-interface/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<u32>,
source: String,
resource_digest: String,
}

impl TryFrom<RawResolvedWorkloadIdentity> for ResolvedWorkloadIdentity {
type Error = BackendError;

fn try_from(raw: RawResolvedWorkloadIdentity) -> Result<Self, Self::Error> {
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.
///
Expand Down
109 changes: 109 additions & 0 deletions crates/openshell-isolation-interface/tests/backend_conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<ResolvedWorkloadIdentity>(payload(
0,
1000,
&[],
"policy",
"sha256:test"
))
.is_err()
);
assert!(
serde_json::from_value::<ResolvedWorkloadIdentity>(payload(
1000,
0,
&[],
"policy",
"sha256:test"
))
.is_err()
);
assert!(
serde_json::from_value::<ResolvedWorkloadIdentity>(payload(
1000,
1000,
&[0],
"policy",
"sha256:test"
))
.is_err()
);
assert!(
serde_json::from_value::<ResolvedWorkloadIdentity>(payload(
1000,
1000,
&[],
"",
"sha256:test"
))
.is_err()
);
assert!(
serde_json::from_value::<ResolvedWorkloadIdentity>(payload(1000, 1000, &[], "policy", ""))
.is_err()
);
assert!(
serde_json::from_value::<ResolvedWorkloadIdentity>(payload(
1000,
1000,
&[],
" ",
"sha256:test"
))
.is_err()
);
assert!(
serde_json::from_value::<ResolvedWorkloadIdentity>(payload(1000, 1000, &[], "policy", " "))
.is_err()
);

let identity = workload_identity();
let encoded = serde_json::to_value(&identity).unwrap();
assert_eq!(
serde_json::from_value::<ResolvedWorkloadIdentity>(encoded).unwrap(),
identity
);

let decoded = serde_json::from_value::<ResolvedWorkloadIdentity>(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::<ResolvedWorkloadIdentity>(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::<BoundaryConfirmation>(encoded.clone()).unwrap();

let mut invalid = encoded;
invalid["identity"]["uid"] = serde_json::json!(0);
assert!(serde_json::from_value::<BoundaryConfirmation>(invalid).is_err());
}

#[test]
fn confirmation_rejects_missing_generation_or_identity_drift() {
let expected = workload_identity();
Expand Down
Loading