From 4542a60785901bfd875d3a8f4b581696c2464a5e Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Wed, 15 Jul 2026 22:24:47 -0400 Subject: [PATCH 1/2] qos_core: add fake manifest builder for tests Add a mock-gated boot::fake module with FakeManifestBuilder and helpers for generating fake v2 manifests, envelopes, and quorum members in tests, including real threshold signatures over the manifest hash via build_envelope_approved_by. Ported from rust-sdk's tvc-utils crate. qos_core: add attestation and manifest verification helpers Add boot::verify with verify_attestation_and_manifest, which checks a manifest envelope against caller-supplied VerificationExpectations, binds it to an NSM attestation document (user data and PCR0-3), verifies the document up to a root CA, checks manifest-set threshold approvals, and returns the enclave's ephemeral public key. Ported from rust-sdk's tvc-utils crate. qos_core: add manifest/share set expectations to verify helpers Allow callers of verify_attestation_and_manifest to verify the manifest set and share set, either by exact match (threshold and members) or by expecting individual members to be in a set. qos_core: verify boot-phase manifest commitment PCR in verify helpers verify_attestation_and_manifest now takes a ManifestCommitmentKind and checks that PCR16 (setup) or PCR17 (live) commits to the manifest hash and the enclave's ephemeral public key, matching the commitments QOS extends and locks at boot. qos_core: drop the fake manifest builder Remove the boot::fake module and its re-exports; the verify tests now construct manifests with minimal test-local helpers. qos_core: separate verify helper tests --- Cargo.lock | 1 + src/qos_core/Cargo.toml | 1 + src/qos_core/src/lib.rs | 1 + src/qos_core/src/verify.rs | 667 +++++++++++++++++++++++++++++++ src/qos_core/src/verify/tests.rs | 640 +++++++++++++++++++++++++++++ src/qos_nsm/src/nitro/error.rs | 15 + src/qos_nsm/src/nitro/mod.rs | 126 +++++- 7 files changed, 1447 insertions(+), 4 deletions(-) create mode 100644 src/qos_core/src/verify.rs create mode 100644 src/qos_core/src/verify/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 9db049ed1..e7bbc5eb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2176,6 +2176,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", + "thiserror 2.0.17", "tokio", "tokio-vsock", "webpki-roots", diff --git a/src/qos_core/Cargo.toml b/src/qos_core/Cargo.toml index 02967e7b3..50b8d43af 100644 --- a/src/qos_core/Cargo.toml +++ b/src/qos_core/Cargo.toml @@ -31,6 +31,7 @@ aws-nitro-enclaves-nsm-api = { workspace = true } serde_bytes = { workspace = true } serde_json = { workspace = true } serde = { workspace = true } +thiserror = { workspace = true } zeroize = { workspace = true, features = ["alloc"] } futures = { workspace = true } diff --git a/src/qos_core/src/lib.rs b/src/qos_core/src/lib.rs index 45cd880bc..07d952332 100644 --- a/src/qos_core/src/lib.rs +++ b/src/qos_core/src/lib.rs @@ -9,6 +9,7 @@ pub mod parser; pub mod protocol; pub mod reaper; pub mod server; +pub mod verify; #[cfg(feature = "egress")] pub mod egress; diff --git a/src/qos_core/src/verify.rs b/src/qos_core/src/verify.rs new file mode 100644 index 000000000..946f29c61 --- /dev/null +++ b/src/qos_core/src/verify.rs @@ -0,0 +1,667 @@ +//! Verification of the QuorumOS trust chain: an NSM attestation document and +//! an in-memory manifest or manifest envelope, checked against caller-supplied +//! trust and policy. +//! +//! [`verify_attestation_and_manifest`] verifies an attestation document +//! against a manifest. [`verify_attestation_and_manifest_envelope`] first +//! anchors and verifies the manifest envelope, then delegates to the manifest +//! verifier. Deserialization is deliberately left to callers. On success both +//! return authenticated attestation evidence, including the enclave's +//! ephemeral public key. + +pub use qos_nsm::nitro::ManifestCommitmentKind; +use qos_nsm::nitro::{self, AttestError}; +use qos_p256::{P256Error, P256Public}; +use std::{ + fmt, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; +use thiserror::Error; + +use crate::protocol::{ + ProtocolError, + services::boot::{ + ManifestSet, ShareSet, VersionedManifest, VersionedManifestEnvelope, + }, +}; + +/// Errors from manifest and manifest-envelope verification, one per failure +/// class. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum VerifyError { + /// The manifest namespace name does not match the expected value. + #[error( + "manifest namespace name mismatch: expected {expected}, got {actual}" + )] + NamespaceNameMismatch { + /// Expected namespace name. + expected: String, + /// Namespace name in the manifest. + actual: String, + }, + /// The manifest namespace nonce does not match the expected value. + #[error( + "manifest namespace nonce mismatch: expected {expected}, got {actual}" + )] + NonceMismatch { + /// Expected namespace nonce. + expected: u32, + /// Namespace nonce in the manifest. + actual: u32, + }, + /// The manifest quorum key does not match the expected value. + #[error("manifest quorum key mismatch: expected {expected}, got {actual}")] + QuorumKeyMismatch { + /// Expected quorum public key, hex encoded. + expected: String, + /// Quorum public key in the manifest, hex encoded. + actual: String, + }, + /// A manifest PCR does not match the expected value. + #[error("manifest PCR{index} mismatch: expected {expected}, got {actual}")] + PcrMismatch { + /// PCR register index (0 through 3). + index: u8, + /// Expected PCR value, hex encoded. + expected: String, + /// PCR value in the manifest, hex encoded. + actual: String, + }, + /// The manifest pivot (app binary) hash does not match the expected + /// value. + #[error("manifest pivot hash mismatch: expected {expected}, got {actual}")] + PivotHashMismatch { + /// Expected pivot hash, hex encoded. + expected: String, + /// Pivot hash in the manifest, hex encoded. + actual: String, + }, + /// The manifest hash does not match the expected value. + #[error("manifest hash mismatch: expected {expected}, got {actual}")] + ManifestHashMismatch { + /// Expected manifest hash, hex encoded. + expected: String, + /// Hash of the manifest in the envelope, hex encoded. + actual: String, + }, + /// The manifest set does not match the expected value. + #[error("manifest set mismatch: expected {expected:?}, got {actual:?}")] + ManifestSetMismatch { + /// Expected manifest set. + expected: ManifestSet, + /// Manifest set in the manifest. + actual: ManifestSet, + }, + /// The share set does not match the expected value. + #[error("share set mismatch: expected {expected:?}, got {actual:?}")] + ShareSetMismatch { + /// Expected share set. + expected: ShareSet, + /// Share set in the manifest. + actual: ShareSet, + }, + /// The attestation document could not be decoded or did not verify up to + /// the root certificate authority. + #[error("attestation document verification failed: {0}")] + AttestationDoc(#[from] AttestError), + /// The authenticated attestation document does not match the manifest, + /// nonce policy, or manifest commitment PCR. + #[error("attestation document does not match the manifest: {0}")] + AttestationManifest(#[source] AttestError), + /// The attestation document is older than the permitted freshness window. + #[error( + "attestation document is stale: timestamp {timestamp_ms}ms, earliest permitted {earliest_ms}ms" + )] + AttestationTooOld { + /// Timestamp carried by the attestation document. + timestamp_ms: u64, + /// Earliest timestamp permitted by the freshness policy. + earliest_ms: u64, + }, + /// The attestation document is too far in the future. + #[error( + "attestation document is from the future: timestamp {timestamp_ms}ms, latest permitted {latest_ms}ms" + )] + AttestationFromFuture { + /// Timestamp carried by the attestation document. + timestamp_ms: u64, + /// Latest timestamp permitted by the clock-skew policy. + latest_ms: u64, + }, + /// The system clock is before the Unix epoch. + #[error("system clock is before the Unix epoch")] + SystemTimeBeforeUnixEpoch, + /// The current time cannot be represented in milliseconds. + #[error("current time is outside the supported millisecond range")] + SystemTimeOutOfRange, + /// A configured freshness duration cannot be represented in milliseconds. + #[error("attestation freshness duration is outside the supported range")] + FreshnessDurationOutOfRange, + /// The manifest-set approvals over the manifest hash did not verify. + #[error("manifest set approval verification failed: {0}")] + ManifestSetApprovals(#[from] ProtocolError), + /// The attestation document carries no public key. + #[error("attestation document is missing a public key")] + MissingPublicKey, + /// The attestation document public key is not a valid P-256 public key. + #[error("failed to decode ephemeral public key: {0:?}")] + EphemeralKeyDecode(P256Error), + /// A caller-supplied manifest policy rejected the manifest. + #[error("manifest policy rejected the manifest: {0}")] + ManifestPolicy(String), +} + +/// Policy for the nonce carried by an attestation document. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NoncePolicy { + /// The attestation document must not contain a nonce. + Absent, + /// The attestation document must contain exactly these nonce bytes. + Exact(Vec), +} + +/// Certificate, freshness, nonce, and boot-phase policy for an attestation. +#[derive(Debug, Clone)] +pub struct AttestationPolicy<'a> { + root_ca: &'a [u8], + max_age: Duration, + max_future_skew: Duration, + nonce: NoncePolicy, + commitment_kind: ManifestCommitmentKind, +} + +impl<'a> AttestationPolicy<'a> { + /// Create a policy that requires an absent nonce and permits no future + /// clock skew. + #[must_use] + pub fn new( + root_ca: &'a [u8], + max_age: Duration, + commitment_kind: ManifestCommitmentKind, + ) -> Self { + Self { + root_ca, + max_age, + max_future_skew: Duration::ZERO, + nonce: NoncePolicy::Absent, + commitment_kind, + } + } + + /// Permit attestation timestamps up to this far in the future. + #[must_use] + pub fn max_future_skew(mut self, max_future_skew: Duration) -> Self { + self.max_future_skew = max_future_skew; + self + } + + /// Set the expected attestation nonce. + #[must_use] + pub fn nonce(mut self, nonce: NoncePolicy) -> Self { + self.nonce = nonce; + self + } +} + +/// Caller-owned trust anchor for a manifest envelope. +/// +/// Envelope approvals are checked only after the embedded manifest is anchored +/// to one of these values. This prevents an envelope's self-described manifest +/// set from being its own root of trust. +#[derive(Debug, Clone, Copy)] +pub enum ManifestEnvelopeTrust<'a> { + /// Trust exactly this manifest hash. + ManifestHash([u8; 32]), + /// Trust this manifest set, comparing members without regard to ordering. + ManifestSet(&'a ManifestSet), +} + +/// Authenticated evidence returned after manifest attestation verification. +pub struct VerifiedManifestAttestation { + /// Ephemeral public key bound into the manifest commitment PCR. + pub ephemeral_key: P256Public, + /// Hash of the verified manifest. + pub manifest_hash: [u8; 32], + /// Boot phase whose commitment PCR was verified. + pub commitment_kind: ManifestCommitmentKind, + /// Timestamp carried by the authenticated attestation document. + pub attestation_timestamp_ms: u64, +} + +impl fmt::Debug for VerifiedManifestAttestation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("VerifiedManifestAttestation") + .field( + "ephemeral_key", + &qos_hex::encode(&self.ephemeral_key.to_bytes()), + ) + .field("manifest_hash", &qos_hex::encode(&self.manifest_hash)) + .field("commitment_kind", &self.commitment_kind) + .field("attestation_timestamp_ms", &self.attestation_timestamp_ms) + .finish() + } +} + +/// Caller-defined policy for accepting a manifest. +pub trait ManifestPolicy { + /// Check the manifest after it has been supplied or externally anchored. + /// + /// # Errors + /// + /// Returns a [`VerifyError`] describing why the policy rejected the + /// manifest. + fn verify(&self, manifest: &VersionedManifest) -> Result<(), VerifyError>; +} + +/// Expected values to check the manifest against. Only supplied values are +/// compared. For direct manifest verification, the manifest itself is assumed +/// to come from a trusted caller. Envelope verification separately requires a +/// [`ManifestEnvelopeTrust`] value. +#[derive(Debug, Clone, Default)] +pub struct VerificationExpectations { + namespace_name: Option, + nonce: Option, + quorum_key: Option>, + pcr0: Option>, + pcr1: Option>, + pcr2: Option>, + pcr3: Option>, + pivot_hash: Option<[u8; 32]>, + manifest_hash: Option<[u8; 32]>, + manifest_set: Option, + share_set: Option, +} + +impl VerificationExpectations { + /// Create an empty set of expectations. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Expect the manifest namespace name. + #[must_use] + pub fn namespace_name(mut self, name: &str) -> Self { + self.namespace_name = Some(name.to_string()); + self + } + + /// Expect the manifest namespace nonce. + #[must_use] + pub fn nonce(mut self, nonce: u32) -> Self { + self.nonce = Some(nonce); + self + } + + /// Expect the manifest quorum public key bytes. + #[must_use] + pub fn quorum_key(mut self, quorum_key: Vec) -> Self { + self.quorum_key = Some(quorum_key); + self + } + + /// Expect the manifest PCR0 value. + #[must_use] + pub fn pcr0(mut self, pcr0: Vec) -> Self { + self.pcr0 = Some(pcr0); + self + } + + /// Expect the manifest PCR1 value. + #[must_use] + pub fn pcr1(mut self, pcr1: Vec) -> Self { + self.pcr1 = Some(pcr1); + self + } + + /// Expect the manifest PCR2 value. + #[must_use] + pub fn pcr2(mut self, pcr2: Vec) -> Self { + self.pcr2 = Some(pcr2); + self + } + + /// Expect the manifest PCR3 value. + #[must_use] + pub fn pcr3(mut self, pcr3: Vec) -> Self { + self.pcr3 = Some(pcr3); + self + } + + /// Expect the manifest pivot (app binary) hash. + #[must_use] + pub fn pivot_hash(mut self, pivot_hash: [u8; 32]) -> Self { + self.pivot_hash = Some(pivot_hash); + self + } + + /// Expect the manifest hash. + #[must_use] + pub fn manifest_hash(mut self, manifest_hash: [u8; 32]) -> Self { + self.manifest_hash = Some(manifest_hash); + self + } + + /// Expect the manifest set: threshold and members must match, regardless + /// of member ordering. + #[must_use] + pub fn manifest_set(mut self, manifest_set: ManifestSet) -> Self { + self.manifest_set = Some(manifest_set); + self + } + + /// Expect the share set: threshold and members must match, regardless of + /// member ordering. + #[must_use] + pub fn share_set(mut self, share_set: ShareSet) -> Self { + self.share_set = Some(share_set); + self + } +} + +impl ManifestPolicy for VerificationExpectations { + fn verify(&self, manifest: &VersionedManifest) -> Result<(), VerifyError> { + check_expectations(self, manifest, &manifest.manifest_hash()) + } +} + +/// Verify an attestation document against a trusted in-memory manifest. +/// +/// # Arguments +/// +/// * `attestation_doc` - DER encoded COSE Sign1 attestation document from the +/// NSM. +/// * `manifest` - manifest to verify against the attestation document. +/// * `attestation_policy` - certificate trust, freshness, nonce, and boot-phase +/// requirements. Certificate validity and freshness are evaluated at the +/// current system time. +/// * `manifest_policy` - caller-defined requirements for the trusted manifest. +/// +/// # Errors +/// +/// Returns a [`VerifyError`] variant identifying the failed check. +pub fn verify_attestation_and_manifest( + attestation_doc: &[u8], + manifest: &VersionedManifest, + attestation_policy: &AttestationPolicy<'_>, + manifest_policy: &dyn ManifestPolicy, +) -> Result { + verify_attestation_and_manifest_at_time( + attestation_doc, + manifest, + attestation_policy, + manifest_policy, + current_time()?, + ) +} + +fn verify_attestation_and_manifest_at_time( + attestation_doc: &[u8], + manifest: &VersionedManifest, + attestation_policy: &AttestationPolicy<'_>, + manifest_policy: &dyn ManifestPolicy, + current_time: Duration, +) -> Result { + let manifest_hash = manifest.manifest_hash(); + + manifest_policy.verify(manifest)?; + + let doc = nitro::attestation_doc_from_der( + attestation_doc, + attestation_policy.root_ca, + current_time.as_secs(), + )?; + check_attestation_freshness( + doc.timestamp, + current_time_ms(current_time)?, + attestation_policy, + )?; + + let enclave = manifest.enclave(); + let public_key = + doc.public_key.as_deref().ok_or(VerifyError::MissingPublicKey)?; + let expected_nonce = match &attestation_policy.nonce { + NoncePolicy::Absent => None, + NoncePolicy::Exact(nonce) => Some(nonce.as_slice()), + }; + nitro::verify_attestation_doc_against_manifest_with_nonce( + attestation_policy.commitment_kind, + &doc, + nitro::ManifestAttestationInput { + manifest_hash: &manifest_hash, + pcr0: &enclave.pcr0, + pcr1: &enclave.pcr1, + pcr2: &enclave.pcr2, + pcr3: &enclave.pcr3, + }, + expected_nonce, + ) + .map_err(VerifyError::AttestationManifest)?; + + let ephemeral_key = P256Public::from_bytes(public_key) + .map_err(VerifyError::EphemeralKeyDecode)?; + + Ok(VerifiedManifestAttestation { + ephemeral_key, + manifest_hash, + commitment_kind: attestation_policy.commitment_kind, + attestation_timestamp_ms: doc.timestamp, + }) +} + +/// Verify an in-memory manifest envelope and attestation document. +/// +/// The embedded manifest must first match the caller-owned `trust` anchor. +/// Manifest-set approvals are then checked before the manifest is passed to +/// [`verify_attestation_and_manifest`]. Deserialization is intentionally not +/// part of this function. +/// +/// # Errors +/// +/// Returns a [`VerifyError`] variant identifying the failed check. +pub fn verify_attestation_and_manifest_envelope( + attestation_doc: &[u8], + manifest_envelope: &VersionedManifestEnvelope, + trust: ManifestEnvelopeTrust<'_>, + attestation_policy: &AttestationPolicy<'_>, + manifest_policy: &dyn ManifestPolicy, +) -> Result { + verify_attestation_and_manifest_envelope_at_time( + attestation_doc, + manifest_envelope, + trust, + attestation_policy, + manifest_policy, + current_time()?, + ) +} + +fn verify_attestation_and_manifest_envelope_at_time( + attestation_doc: &[u8], + manifest_envelope: &VersionedManifestEnvelope, + trust: ManifestEnvelopeTrust<'_>, + attestation_policy: &AttestationPolicy<'_>, + manifest_policy: &dyn ManifestPolicy, + current_time: Duration, +) -> Result { + check_envelope_trust(manifest_envelope, trust)?; + manifest_envelope.check_approvals()?; + + verify_attestation_and_manifest_at_time( + attestation_doc, + &manifest_envelope.clone().manifest(), + attestation_policy, + manifest_policy, + current_time, + ) +} + +fn current_time() -> Result { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| VerifyError::SystemTimeBeforeUnixEpoch) +} + +fn current_time_ms(current_time: Duration) -> Result { + u64::try_from(current_time.as_millis()) + .map_err(|_| VerifyError::SystemTimeOutOfRange) +} + +fn check_attestation_freshness( + timestamp_ms: u64, + current_time_ms: u64, + policy: &AttestationPolicy<'_>, +) -> Result<(), VerifyError> { + let max_age_ms = u64::try_from(policy.max_age.as_millis()) + .map_err(|_| VerifyError::FreshnessDurationOutOfRange)?; + let max_future_skew_ms = u64::try_from(policy.max_future_skew.as_millis()) + .map_err(|_| VerifyError::FreshnessDurationOutOfRange)?; + let earliest_ms = current_time_ms.saturating_sub(max_age_ms); + if timestamp_ms < earliest_ms { + return Err(VerifyError::AttestationTooOld { + timestamp_ms, + earliest_ms, + }); + } + let latest_ms = current_time_ms.saturating_add(max_future_skew_ms); + if timestamp_ms > latest_ms { + return Err(VerifyError::AttestationFromFuture { + timestamp_ms, + latest_ms, + }); + } + Ok(()) +} + +fn check_envelope_trust( + envelope: &VersionedManifestEnvelope, + trust: ManifestEnvelopeTrust<'_>, +) -> Result<(), VerifyError> { + match trust { + ManifestEnvelopeTrust::ManifestHash(expected) => { + let actual = envelope.manifest_hash(); + if expected != actual { + return Err(VerifyError::ManifestHashMismatch { + expected: qos_hex::encode(&expected), + actual: qos_hex::encode(&actual), + }); + } + } + ManifestEnvelopeTrust::ManifestSet(expected) => { + let actual = envelope.manifest_set(); + if !manifest_sets_equal(expected, actual) { + return Err(VerifyError::ManifestSetMismatch { + expected: expected.clone(), + actual: actual.clone(), + }); + } + } + } + Ok(()) +} + +fn manifest_sets_equal(expected: &ManifestSet, actual: &ManifestSet) -> bool { + let mut expected_members = expected.members.clone(); + let mut actual_members = actual.members.clone(); + expected_members.sort(); + actual_members.sort(); + expected.threshold == actual.threshold && expected_members == actual_members +} + +fn share_sets_equal(expected: &ShareSet, actual: &ShareSet) -> bool { + let mut expected_members = expected.members.clone(); + let mut actual_members = actual.members.clone(); + expected_members.sort(); + actual_members.sort(); + expected.threshold == actual.threshold && expected_members == actual_members +} + +fn check_expectations( + expectations: &VerificationExpectations, + manifest: &VersionedManifest, + manifest_hash: &[u8; 32], +) -> Result<(), VerifyError> { + let namespace = manifest.namespace(); + if let Some(expected) = &expectations.namespace_name + && expected != &namespace.name + { + return Err(VerifyError::NamespaceNameMismatch { + expected: expected.clone(), + actual: namespace.name.clone(), + }); + } + if let Some(expected) = expectations.nonce + && expected != namespace.nonce + { + return Err(VerifyError::NonceMismatch { + expected, + actual: namespace.nonce, + }); + } + if let Some(expected) = &expectations.quorum_key + && expected != &namespace.quorum_key + { + return Err(VerifyError::QuorumKeyMismatch { + expected: qos_hex::encode(expected), + actual: qos_hex::encode(&namespace.quorum_key), + }); + } + + let enclave = manifest.enclave(); + for (index, expected, actual) in [ + (0u8, &expectations.pcr0, &enclave.pcr0), + (1, &expectations.pcr1, &enclave.pcr1), + (2, &expectations.pcr2, &enclave.pcr2), + (3, &expectations.pcr3, &enclave.pcr3), + ] { + if let Some(expected) = expected + && expected != actual + { + return Err(VerifyError::PcrMismatch { + index, + expected: qos_hex::encode(expected), + actual: qos_hex::encode(actual), + }); + } + } + + if let Some(expected) = expectations.pivot_hash + && &expected != manifest.pivot_hash() + { + return Err(VerifyError::PivotHashMismatch { + expected: qos_hex::encode(&expected), + actual: qos_hex::encode(manifest.pivot_hash()), + }); + } + if let Some(expected) = expectations.manifest_hash + && &expected != manifest_hash + { + return Err(VerifyError::ManifestHashMismatch { + expected: qos_hex::encode(&expected), + actual: qos_hex::encode(manifest_hash), + }); + } + + if let Some(expected) = &expectations.manifest_set + && !manifest_sets_equal(expected, manifest.manifest_set()) + { + return Err(VerifyError::ManifestSetMismatch { + expected: expected.clone(), + actual: manifest.manifest_set().clone(), + }); + } + if let Some(expected) = &expectations.share_set + && !share_sets_equal(expected, manifest.share_set()) + { + return Err(VerifyError::ShareSetMismatch { + expected: expected.clone(), + actual: manifest.share_set().clone(), + }); + } + Ok(()) +} + +#[cfg(test)] +#[path = "verify/tests.rs"] +mod tests; diff --git a/src/qos_core/src/verify/tests.rs b/src/qos_core/src/verify/tests.rs new file mode 100644 index 000000000..50351b32a --- /dev/null +++ b/src/qos_core/src/verify/tests.rs @@ -0,0 +1,640 @@ +use std::time::Duration; + +use qos_nsm::{ + NsmProvider, + mock::{ + MOCK_ATTESTATION_DOC_TIMESTAMP, MOCK_ROOT_CERT_DER, + MOCK_SECONDS_SINCE_EPOCH, MockNsm, + }, + nitro, + types::{NsmRequest, NsmResponse}, +}; +use qos_p256::P256Pair; + +use super::{ + AttestationPolicy, ManifestCommitmentKind, ManifestEnvelopeTrust, + ManifestPolicy, ManifestSet, NoncePolicy, ShareSet, + VerificationExpectations, VerifyError, VersionedManifestEnvelope, + check_attestation_freshness, verify_attestation_and_manifest_at_time, + verify_attestation_and_manifest_envelope_at_time, +}; +use crate::protocol::{ + ProtocolError, + services::boot::{ + Approval, ManifestEnvelopeV2, ManifestV2, ManifestVersion, Namespace, + NitroConfig, PivotConfigV2, PivotEnv, QuorumMember, RestartPolicy, + }, +}; + +fn test_member(alias: &str) -> (QuorumMember, P256Pair) { + let pair = P256Pair::generate().expect("key should generate"); + let member = QuorumMember { + alias: alias.to_string(), + pub_key: pair.public_key().to_bytes(), + }; + (member, pair) +} + +fn test_envelope( + manifest_set: ManifestSet, + approvers: &[(QuorumMember, P256Pair)], +) -> VersionedManifestEnvelope { + let manifest = ManifestV2 { + version: ManifestVersion::V2, + namespace: Namespace { + name: "test-namespace".to_string(), + nonce: 1, + quorum_key: P256Pair::generate() + .expect("key should generate") + .public_key() + .to_bytes(), + }, + pivot: PivotConfigV2 { + hash: [7; 32], + restart: RestartPolicy::Never, + bridge_config: vec![], + debug_mode: false, + args: vec![], + env: PivotEnv::new(), + }, + manifest_set, + share_set: ShareSet { + threshold: 1, + members: vec![test_member("share-member").0], + }, + enclave: NitroConfig { + pcr0: vec![0; 48], + pcr1: vec![1; 48], + pcr2: vec![2; 48], + pcr3: vec![3; 48], + aws_root_certificate: vec![], + qos_commit: "test-qos-commit".to_string(), + }, + dns: None, + }; + let mut envelope = ManifestEnvelopeV2 { + manifest, + manifest_set_approvals: vec![], + share_set_approvals: vec![], + }; + let manifest_hash = + VersionedManifestEnvelope::V2(envelope.clone()).manifest_hash(); + envelope.manifest_set_approvals = approvers + .iter() + .map(|(member, pair)| Approval { + signature: pair + .sign(&manifest_hash) + .expect("approval signing should not fail"), + member: member.clone(), + }) + .collect(); + VersionedManifestEnvelope::V2(envelope) +} + +fn approved_envelope( + approvers: &[(QuorumMember, P256Pair)], +) -> VersionedManifestEnvelope { + let manifest_set = ManifestSet { + threshold: u32::try_from(approvers.len()) + .expect("approver count should fit in u32"), + members: approvers.iter().map(|(m, _)| m.clone()).collect(), + }; + test_envelope(manifest_set, approvers) +} + +struct TrustChain { + envelope: VersionedManifestEnvelope, + attestation_doc: Vec, + ephemeral_key: P256Pair, +} + +impl TrustChain { + fn new() -> Self { + Self::with_envelope(approved_envelope(&[test_member("member")])) + } + + fn with_envelope(envelope: VersionedManifestEnvelope) -> Self { + let ephemeral_key = P256Pair::generate().expect("key should generate"); + let user_data = envelope.manifest_hash().to_vec(); + let attestation_doc = attest( + &envelope, + Some(user_data), + Some(ephemeral_key.public_key().to_bytes()), + None, + ); + Self { envelope, attestation_doc, ephemeral_key } + } + + fn verify( + &self, + expectations: &VerificationExpectations, + ) -> Result, VerifyError> { + verify_envelope( + &self.attestation_doc, + &self.envelope, + ManifestCommitmentKind::Setup, + expectations, + ) + } +} + +fn attestation_policy( + commitment_kind: ManifestCommitmentKind, +) -> AttestationPolicy<'static> { + AttestationPolicy::new( + MOCK_ROOT_CERT_DER, + Duration::from_secs(5 * 60), + commitment_kind, + ) +} + +fn mock_current_time() -> Duration { + Duration::from_secs(MOCK_SECONDS_SINCE_EPOCH) +} + +fn verify_envelope( + attestation_doc: &[u8], + manifest_envelope: &VersionedManifestEnvelope, + commitment_kind: ManifestCommitmentKind, + manifest_policy: &dyn ManifestPolicy, +) -> Result, VerifyError> { + verify_attestation_and_manifest_envelope_at_time( + attestation_doc, + manifest_envelope, + ManifestEnvelopeTrust::ManifestHash(manifest_envelope.manifest_hash()), + &attestation_policy(commitment_kind), + manifest_policy, + mock_current_time(), + ) + .map(|verified| verified.ephemeral_key.to_bytes()) +} + +fn attest( + envelope: &VersionedManifestEnvelope, + user_data: Option>, + public_key: Option>, + nonce: Option>, +) -> Vec { + let manifest = envelope.clone().manifest(); + let enclave = manifest.enclave(); + let mut nsm = MockNsm::new() + .with_pcr(0, enclave.pcr0.clone()) + .with_pcr(1, enclave.pcr1.clone()) + .with_pcr(2, enclave.pcr2.clone()) + .with_pcr(3, enclave.pcr3.clone()); + if let (Some(user_data), Some(public_key)) = (&user_data, &public_key) { + let pcr16 = nitro::expected_manifest_commitment_pcr( + ManifestCommitmentKind::Setup, + user_data, + public_key, + ) + .expect("commitment PCR should compute"); + nsm = nsm.with_pcr(16, pcr16.to_vec()); + } + match nsm.nsm_process_request(NsmRequest::Attestation { + user_data, + nonce, + public_key, + }) { + NsmResponse::Attestation { document } => document, + other => panic!("unexpected NSM response: {other:?}"), + } +} + +#[test] +fn manifest_verification_does_not_require_an_envelope() { + let chain = TrustChain::new(); + let manifest = chain.envelope.clone().manifest(); + + let verified = verify_attestation_and_manifest_at_time( + &chain.attestation_doc, + &manifest, + &attestation_policy(ManifestCommitmentKind::Setup), + &VerificationExpectations::new(), + mock_current_time(), + ) + .expect("manifest and attestation document should verify"); + + assert_eq!( + verified.ephemeral_key.to_bytes(), + chain.ephemeral_key.public_key().to_bytes() + ); + assert_eq!(verified.manifest_hash, manifest.manifest_hash()); + assert_eq!(verified.commitment_kind, ManifestCommitmentKind::Setup); + assert_eq!( + verified.attestation_timestamp_ms, + MOCK_ATTESTATION_DOC_TIMESTAMP + ); +} + +#[test] +fn full_expectations_pass_and_return_the_ephemeral_key() { + let chain = TrustChain::new(); + let manifest = chain.envelope.clone().manifest(); + + let key_bytes = chain + .verify( + &VerificationExpectations::new() + .namespace_name(&manifest.namespace().name) + .nonce(manifest.namespace().nonce) + .quorum_key(manifest.namespace().quorum_key.clone()) + .pcr0(manifest.enclave().pcr0.clone()) + .pcr1(manifest.enclave().pcr1.clone()) + .pcr2(manifest.enclave().pcr2.clone()) + .pcr3(manifest.enclave().pcr3.clone()) + .pivot_hash(*manifest.pivot_hash()) + .manifest_hash(chain.envelope.manifest_hash()) + .manifest_set(manifest.manifest_set().clone()) + .share_set(manifest.share_set().clone()), + ) + .expect("full expectations should verify"); + + assert_eq!(key_bytes, chain.ephemeral_key.public_key().to_bytes()); +} + +#[test] +fn envelope_hash_anchor_allows_empty_additional_policy() { + let chain = TrustChain::new(); + + let key_bytes = chain + .verify(&VerificationExpectations::new()) + .expect("trust chain should verify without expectations"); + + assert_eq!(key_bytes, chain.ephemeral_key.public_key().to_bytes()); +} + +#[test] +fn each_supplied_expectation_is_checked() { + let chain = TrustChain::new(); + + for (expectations, name) in [ + ( + VerificationExpectations::new().namespace_name("other-namespace"), + "namespace name", + ), + (VerificationExpectations::new().nonce(1337), "nonce"), + (VerificationExpectations::new().quorum_key(vec![9; 65]), "quorum key"), + (VerificationExpectations::new().pcr0(vec![9; 48]), "pcr0"), + (VerificationExpectations::new().pcr1(vec![9; 48]), "pcr1"), + (VerificationExpectations::new().pcr2(vec![9; 48]), "pcr2"), + (VerificationExpectations::new().pcr3(vec![9; 48]), "pcr3"), + (VerificationExpectations::new().pivot_hash([9; 32]), "pivot hash"), + ( + VerificationExpectations::new().manifest_hash([9; 32]), + "manifest hash", + ), + ( + VerificationExpectations::new() + .manifest_set(ManifestSet::default()), + "manifest set", + ), + ( + VerificationExpectations::new().share_set(ShareSet::default()), + "share set", + ), + ] { + let err = chain + .verify(&expectations) + .expect_err(&format!("wrong {name} should fail")); + let matched = matches!( + (name, &err), + ("namespace name", VerifyError::NamespaceNameMismatch { .. }) + | ("nonce", VerifyError::NonceMismatch { .. }) + | ("quorum key", VerifyError::QuorumKeyMismatch { .. }) + | ("pcr0", VerifyError::PcrMismatch { index: 0, .. }) + | ("pcr1", VerifyError::PcrMismatch { index: 1, .. }) + | ("pcr2", VerifyError::PcrMismatch { index: 2, .. }) + | ("pcr3", VerifyError::PcrMismatch { index: 3, .. }) + | ("pivot hash", VerifyError::PivotHashMismatch { .. }) + | ("manifest hash", VerifyError::ManifestHashMismatch { .. }) + | ("manifest set", VerifyError::ManifestSetMismatch { .. }) + | ("share set", VerifyError::ShareSetMismatch { .. }) + ); + assert!(matched, "wrong {name} should map to its own error: {err:?}"); + } +} + +#[test] +fn attestation_doc_user_data_must_match_the_manifest_hash() { + let envelope = approved_envelope(&[test_member("member")]); + let ephemeral_key = P256Pair::generate().expect("key should generate"); + let attestation_doc = attest( + &envelope, + Some(vec![9; 32]), + Some(ephemeral_key.public_key().to_bytes()), + None, + ); + + let err = verify_envelope( + &attestation_doc, + &envelope, + ManifestCommitmentKind::Setup, + &VerificationExpectations::new(), + ) + .expect_err("mismatched user data should fail"); + + assert!(matches!(err, VerifyError::AttestationManifest(_))); +} + +#[test] +fn attestation_doc_pcrs_must_match_the_manifest() { + let envelope = approved_envelope(&[test_member("member")]); + let ephemeral_key = P256Pair::generate().expect("key should generate"); + let nsm = MockNsm::new(); + let attestation_doc = + match nsm.nsm_process_request(NsmRequest::Attestation { + user_data: Some(envelope.manifest_hash().to_vec()), + nonce: None, + public_key: Some(ephemeral_key.public_key().to_bytes()), + }) { + NsmResponse::Attestation { document } => document, + other => panic!("unexpected NSM response: {other:?}"), + }; + + let err = verify_envelope( + &attestation_doc, + &envelope, + ManifestCommitmentKind::Setup, + &VerificationExpectations::new(), + ) + .expect_err("attestation PCRs differing from the manifest should fail"); + + assert!(matches!(err, VerifyError::AttestationManifest(_))); +} + +#[test] +fn attestation_doc_must_verify_against_the_root_ca() { + let chain = TrustChain::new(); + + let policy = AttestationPolicy::new( + MOCK_ROOT_CERT_DER, + Duration::from_secs(5 * 60), + ManifestCommitmentKind::Setup, + ); + let err = verify_attestation_and_manifest_envelope_at_time( + &chain.attestation_doc, + &chain.envelope, + ManifestEnvelopeTrust::ManifestHash(chain.envelope.manifest_hash()), + &policy, + &VerificationExpectations::new(), + Duration::from_secs( + MOCK_SECONDS_SINCE_EPOCH + 60 * 60 * 24 * 365 * 100, + ), + ) + .expect_err("expired certificate chain should fail"); + + assert!(matches!(err, VerifyError::AttestationDoc(_))); +} + +#[test] +fn manifest_commitment_pcr_is_checked_for_the_boot_phase() { + let chain = TrustChain::new(); + + let err = verify_envelope( + &chain.attestation_doc, + &chain.envelope, + ManifestCommitmentKind::Live, + &VerificationExpectations::new(), + ) + .expect_err("setup attestation should not verify as live"); + + assert!(matches!(err, VerifyError::AttestationManifest(_))); +} + +#[test] +fn manifest_set_approvals_must_meet_the_threshold() { + let (member_a, pair_a) = test_member("member-a"); + let (member_b, _) = test_member("member-b"); + let chain = TrustChain::with_envelope(test_envelope( + ManifestSet { threshold: 2, members: vec![member_a.clone(), member_b] }, + &[(member_a, pair_a)], + )); + let err = chain + .verify(&VerificationExpectations::new()) + .expect_err("sub-threshold approvals should fail"); + + assert!(matches!( + err, + VerifyError::ManifestSetApprovals(ProtocolError::NotEnoughApprovals) + )); +} + +#[test] +fn approvals_from_outside_the_manifest_set_fail() { + let outsider = test_member("outsider"); + let chain = TrustChain::with_envelope(test_envelope( + ManifestSet { threshold: 1, members: vec![test_member("insider").0] }, + &[outsider], + )); + + let err = chain + .verify(&VerificationExpectations::new()) + .expect_err("approval from a non-member should fail"); + + assert!(matches!( + err, + VerifyError::ManifestSetApprovals(ProtocolError::NotManifestSetMember) + )); +} + +#[test] +fn envelope_must_match_the_external_trust_anchor() { + let chain = TrustChain::new(); + + let err = verify_attestation_and_manifest_envelope_at_time( + &chain.attestation_doc, + &chain.envelope, + ManifestEnvelopeTrust::ManifestHash([9; 32]), + &attestation_policy(ManifestCommitmentKind::Setup), + &VerificationExpectations::new(), + mock_current_time(), + ) + .expect_err("an untrusted manifest should fail"); + + assert!(matches!(err, VerifyError::ManifestHashMismatch { .. })); +} + +#[test] +fn envelope_manifest_set_anchor_is_order_independent() { + let approvers = [test_member("member-a"), test_member("member-b")]; + let chain = TrustChain::with_envelope(approved_envelope(&approvers)); + let mut trusted_set = chain.envelope.manifest_set().clone(); + trusted_set.members.reverse(); + + let verified = verify_attestation_and_manifest_envelope_at_time( + &chain.attestation_doc, + &chain.envelope, + ManifestEnvelopeTrust::ManifestSet(&trusted_set), + &attestation_policy(ManifestCommitmentKind::Setup), + &VerificationExpectations::new(), + mock_current_time(), + ) + .expect("member ordering should not affect manifest-set trust"); + + assert_eq!( + verified.ephemeral_key.to_bytes(), + chain.ephemeral_key.public_key().to_bytes() + ); +} + +#[test] +fn custom_manifest_policy_can_reject_a_manifest() { + struct RejectManifest; + + impl ManifestPolicy for RejectManifest { + fn verify( + &self, + _manifest: &super::VersionedManifest, + ) -> Result<(), VerifyError> { + Err(VerifyError::ManifestPolicy("rejected for test".to_string())) + } + } + + let chain = TrustChain::new(); + let err = verify_envelope( + &chain.attestation_doc, + &chain.envelope, + ManifestCommitmentKind::Setup, + &RejectManifest, + ) + .expect_err("the custom manifest policy should run"); + + assert!(matches!(err, VerifyError::ManifestPolicy(_))); +} + +#[test] +fn absent_nonce_policy_rejects_a_nonce() { + let envelope = approved_envelope(&[test_member("member")]); + let ephemeral_key = P256Pair::generate().expect("key should generate"); + let attestation_doc = attest( + &envelope, + Some(envelope.manifest_hash().to_vec()), + Some(ephemeral_key.public_key().to_bytes()), + Some(vec![1, 2, 3]), + ); + + let err = verify_envelope( + &attestation_doc, + &envelope, + ManifestCommitmentKind::Setup, + &VerificationExpectations::new(), + ) + .expect_err("a nonce should fail the default absent-nonce policy"); + + assert!(matches!( + err, + VerifyError::AttestationManifest( + nitro::AttestError::UnexpectedAttestationDocNonce + ) + )); +} + +#[test] +fn exact_nonce_policy_requires_the_expected_nonce() { + let envelope = approved_envelope(&[test_member("member")]); + let ephemeral_key = P256Pair::generate().expect("key should generate"); + let nonce = vec![1, 2, 3]; + let attestation_doc = attest( + &envelope, + Some(envelope.manifest_hash().to_vec()), + Some(ephemeral_key.public_key().to_bytes()), + Some(nonce.clone()), + ); + let policy = attestation_policy(ManifestCommitmentKind::Setup) + .nonce(NoncePolicy::Exact(nonce)); + + let verified = verify_attestation_and_manifest_envelope_at_time( + &attestation_doc, + &envelope, + ManifestEnvelopeTrust::ManifestHash(envelope.manifest_hash()), + &policy, + &VerificationExpectations::new(), + mock_current_time(), + ) + .expect("the expected nonce should verify"); + + assert_eq!( + verified.ephemeral_key.to_bytes(), + ephemeral_key.public_key().to_bytes() + ); +} + +#[test] +fn stale_attestation_is_rejected() { + let chain = TrustChain::new(); + let policy = AttestationPolicy::new( + MOCK_ROOT_CERT_DER, + Duration::from_secs(1), + ManifestCommitmentKind::Setup, + ); + + let err = verify_attestation_and_manifest_envelope_at_time( + &chain.attestation_doc, + &chain.envelope, + ManifestEnvelopeTrust::ManifestHash(chain.envelope.manifest_hash()), + &policy, + &VerificationExpectations::new(), + Duration::from_millis(MOCK_ATTESTATION_DOC_TIMESTAMP + 10_000), + ) + .expect_err("a stale attestation should fail"); + + assert!(matches!(err, VerifyError::AttestationTooOld { .. })); +} + +#[test] +fn attestation_beyond_future_clock_skew_is_rejected() { + let chain = TrustChain::new(); + let policy = AttestationPolicy::new( + MOCK_ROOT_CERT_DER, + Duration::from_secs(5 * 60), + ManifestCommitmentKind::Setup, + ) + .max_future_skew(Duration::from_secs(1)); + + let err = verify_attestation_and_manifest_envelope_at_time( + &chain.attestation_doc, + &chain.envelope, + ManifestEnvelopeTrust::ManifestHash(chain.envelope.manifest_hash()), + &policy, + &VerificationExpectations::new(), + Duration::from_millis(MOCK_ATTESTATION_DOC_TIMESTAMP - 10_000), + ) + .expect_err("an attestation too far in the future should fail"); + + assert!(matches!(err, VerifyError::AttestationFromFuture { .. })); +} + +#[test] +fn freshness_duration_must_fit_in_milliseconds() { + let policy = AttestationPolicy::new( + MOCK_ROOT_CERT_DER, + Duration::MAX, + ManifestCommitmentKind::Setup, + ); + + let err = check_attestation_freshness( + MOCK_ATTESTATION_DOC_TIMESTAMP, + MOCK_ATTESTATION_DOC_TIMESTAMP, + &policy, + ) + .expect_err("an unrepresentable freshness duration should fail"); + + assert!(matches!(err, VerifyError::FreshnessDurationOutOfRange)); +} + +#[test] +fn attestation_doc_without_a_public_key_fails() { + let envelope = approved_envelope(&[test_member("member")]); + let attestation_doc = + attest(&envelope, Some(envelope.manifest_hash().to_vec()), None, None); + + let err = verify_envelope( + &attestation_doc, + &envelope, + ManifestCommitmentKind::Setup, + &VerificationExpectations::new(), + ) + .expect_err("attestation document without a public key should fail"); + + assert!(matches!(err, VerifyError::MissingPublicKey)); +} diff --git a/src/qos_nsm/src/nitro/error.rs b/src/qos_nsm/src/nitro/error.rs index 3758cda2c..11a2ee6b2 100644 --- a/src/qos_nsm/src/nitro/error.rs +++ b/src/qos_nsm/src/nitro/error.rs @@ -53,6 +53,15 @@ pub enum AttestError { }, /// The attestation doc has a nonce when none was expected. UnexpectedAttestationDocNonce, + /// The attestation doc does not have the expected nonce. + MissingAttestationDocNonce, + /// The attestation doc nonce does not match the expected value. + DifferentAttestationDocNonce { + /// Expected value as a hex string. + expected: String, + /// Actual value as a hex string. + actual: String, + }, /// The attestation doc does not contain a pcr0. MissingPcr0, /// The pcr0 in the attestation doc does not match. @@ -165,6 +174,12 @@ impl std::fmt::Display for AttestError { Self::UnexpectedAttestationDocNonce => { write!(f, "unexpected nonce in attestation document") } + Self::MissingAttestationDocNonce => { + write!(f, "nonce missing in attestation document") + } + Self::DifferentAttestationDocNonce { expected, actual } => { + write!(f, "different nonce: expected {expected}, got {actual}") + } Self::MissingPcr0 => { write!(f, "PCR0 missing in attestation document") } diff --git a/src/qos_nsm/src/nitro/mod.rs b/src/qos_nsm/src/nitro/mod.rs index dedc58ab7..a56d46f7d 100644 --- a/src/qos_nsm/src/nitro/mod.rs +++ b/src/qos_nsm/src/nitro/mod.rs @@ -238,13 +238,36 @@ pub fn verify_attestation_doc_against_manifest( attestation_doc: &AttestationDoc, expected: ManifestAttestationInput<'_>, ) -> Result<(), AttestError> { - verify_attestation_doc_against_user_input( + verify_attestation_doc_against_manifest_with_nonce( + kind, + attestation_doc, + expected, + None, + ) +} + +/// Verify a standard QOS attestation document and its nonce. +/// +/// `expected_nonce` is `None` when the document must not carry a nonce and +/// `Some` when it must carry exactly the supplied bytes. +/// +/// # Errors +/// +/// Returns [`AttestError`] if validation fails. +pub fn verify_attestation_doc_against_manifest_with_nonce( + kind: ManifestCommitmentKind, + attestation_doc: &AttestationDoc, + expected: ManifestAttestationInput<'_>, + expected_nonce: Option<&[u8]>, +) -> Result<(), AttestError> { + verify_attestation_doc_against_user_input_with_nonce( attestation_doc, expected.manifest_hash, expected.pcr0, expected.pcr1, expected.pcr2, expected.pcr3, + expected_nonce, )?; verify_attestation_doc_manifest_commitment( attestation_doc, @@ -327,6 +350,26 @@ pub fn verify_attestation_doc_against_user_input( pcr1: &[u8], pcr2: &[u8], pcr3: &[u8], +) -> Result<(), AttestError> { + verify_attestation_doc_against_user_input_with_nonce( + attestation_doc, + user_data, + pcr0, + pcr1, + pcr2, + pcr3, + None, + ) +} + +fn verify_attestation_doc_against_user_input_with_nonce( + attestation_doc: &AttestationDoc, + user_data: &[u8], + pcr0: &[u8], + pcr1: &[u8], + pcr2: &[u8], + pcr3: &[u8], + expected_nonce: Option<&[u8]>, ) -> Result<(), AttestError> { let doc_user_data = attestation_doc .user_data @@ -340,9 +383,20 @@ pub fn verify_attestation_doc_against_user_input( }); } - // nonce is none - if attestation_doc.nonce.is_some() { - return Err(AttestError::UnexpectedAttestationDocNonce); + match (expected_nonce, attestation_doc.nonce.as_deref()) { + (None, Some(_)) => { + return Err(AttestError::UnexpectedAttestationDocNonce); + } + (Some(_), None) => { + return Err(AttestError::MissingAttestationDocNonce); + } + (Some(expected), Some(actual)) if expected != actual => { + return Err(AttestError::DifferentAttestationDocNonce { + expected: qos_hex::encode(expected), + actual: qos_hex::encode(actual), + }); + } + (None, None) | (Some(_), Some(_)) => {} } let doc_pcr0 = attestation_doc @@ -1187,6 +1241,70 @@ mod test { assert!(matches!(err, AttestError::UnexpectedAttestationDocNonce)); } + #[test] + fn verify_attestation_doc_against_user_input_accepts_expected_nonce() { + let mut attestation_doc = + unsafe_attestation_doc_from_der(MOCK_NSM_ATTESTATION_DOCUMENT) + .unwrap(); + let nonce = [1, 2, 3]; + attestation_doc.nonce = Some(ByteBuf::from(nonce.to_vec())); + + verify_attestation_doc_against_user_input_with_nonce( + &attestation_doc, + &qos_hex::decode(MOCK_USER_DATA_NSM_ATTESTATION_DOCUMENT).unwrap(), + &qos_hex::decode(MOCK_PCR0).unwrap(), + &qos_hex::decode(MOCK_PCR1).unwrap(), + &qos_hex::decode(MOCK_PCR2).unwrap(), + &qos_hex::decode(MOCK_PCR3).unwrap(), + Some(&nonce), + ) + .expect("the expected nonce should verify"); + } + + #[test] + fn verify_attestation_doc_against_user_input_rejects_wrong_nonce() { + let mut attestation_doc = + unsafe_attestation_doc_from_der(MOCK_NSM_ATTESTATION_DOCUMENT) + .unwrap(); + attestation_doc.nonce = Some(ByteBuf::from(vec![1, 2, 3])); + + let err = verify_attestation_doc_against_user_input_with_nonce( + &attestation_doc, + &qos_hex::decode(MOCK_USER_DATA_NSM_ATTESTATION_DOCUMENT).unwrap(), + &qos_hex::decode(MOCK_PCR0).unwrap(), + &qos_hex::decode(MOCK_PCR1).unwrap(), + &qos_hex::decode(MOCK_PCR2).unwrap(), + &qos_hex::decode(MOCK_PCR3).unwrap(), + Some(&[4, 5, 6]), + ) + .expect_err("a different nonce should fail"); + + assert!(matches!( + err, + AttestError::DifferentAttestationDocNonce { .. } + )); + } + + #[test] + fn verify_attestation_doc_against_user_input_rejects_missing_nonce() { + let attestation_doc = + unsafe_attestation_doc_from_der(MOCK_NSM_ATTESTATION_DOCUMENT) + .unwrap(); + + let err = verify_attestation_doc_against_user_input_with_nonce( + &attestation_doc, + &qos_hex::decode(MOCK_USER_DATA_NSM_ATTESTATION_DOCUMENT).unwrap(), + &qos_hex::decode(MOCK_PCR0).unwrap(), + &qos_hex::decode(MOCK_PCR1).unwrap(), + &qos_hex::decode(MOCK_PCR2).unwrap(), + &qos_hex::decode(MOCK_PCR3).unwrap(), + Some(&[1, 2, 3]), + ) + .expect_err("a missing expected nonce should fail"); + + assert!(matches!(err, AttestError::MissingAttestationDocNonce)); + } + #[test] fn verify_attestation_doc_against_user_input_panics_invalid_pcr0() { let attestation_doc = From 89604c0cb3fee0c91461ec49eea894c7c0c640ec Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Thu, 23 Jul 2026 15:46:01 -0400 Subject: [PATCH 2/2] migrate to using the verification helpers everywhere --- src/qos_client/src/cli/services.rs | 299 ++++--------- src/qos_core/src/protocol/services/key.rs | 523 +++++----------------- src/qos_core/src/verify.rs | 17 + 3 files changed, 212 insertions(+), 627 deletions(-) diff --git a/src/qos_client/src/cli/services.rs b/src/qos_client/src/cli/services.rs index 6229e644f..3e044d23d 100644 --- a/src/qos_client/src/cli/services.rs +++ b/src/qos_client/src/cli/services.rs @@ -4,33 +4,40 @@ use std::{ mem, net::IpAddr, path::{Path, PathBuf}, + time::Duration, }; use aws_nitro_enclaves_nsm_api::api::AttestationDoc; use borsh::BorshDeserialize; -use qos_core::protocol::{ - QosHash, - msg::{JsonBytes, ProtocolMsg, ProtocolMsgEncoding}, - services::{ - boot::{ - Approval, BridgeConfig, DnsConfig, Manifest as ManifestV1, - ManifestEnvelope as ManifestEnvelopeV1, ManifestEnvelopeV0, - ManifestEnvelopeV2, ManifestSet, ManifestV2, ManifestVersion, - MemberPubKey, Namespace, NitroConfig, PatchSet, - PivotConfig as PivotConfigV1, PivotConfigV2, PivotEnv, - QuorumMember, RestartPolicy, ShareSet, VersionedManifest, - VersionedManifestEnvelope, +use qos_core::{ + protocol::{ + QosHash, + msg::{JsonBytes, ProtocolMsg, ProtocolMsgEncoding}, + services::{ + boot::{ + Approval, BridgeConfig, DnsConfig, Manifest as ManifestV1, + ManifestEnvelope as ManifestEnvelopeV1, ManifestEnvelopeV0, + ManifestEnvelopeV2, ManifestSet, ManifestV2, ManifestVersion, + MemberPubKey, Namespace, NitroConfig, PatchSet, + PivotConfig as PivotConfigV1, PivotConfigV2, PivotEnv, + QuorumMember, RestartPolicy, ShareSet, VersionedManifest, + VersionedManifestEnvelope, + }, + genesis::{GenesisOutput, GenesisSet}, + key::EncryptedQuorumKey, }, - genesis::{GenesisOutput, GenesisSet}, - key::EncryptedQuorumKey, + }, + verify::{ + AttestationPolicy, ManifestCommitmentKind, ManifestEnvelopeTrust, + VerificationExpectations, VerifyError, verify_attestation_and_manifest, + verify_attestation_and_manifest_envelope, }, }; use qos_crypto::{sha_256, sha_384, sha_512}; use qos_nsm::{ nitro::{ - AWS_ROOT_CERT_PEM, ManifestAttestationInput, attestation_doc_from_der, - cert_from_pem, unsafe_attestation_doc_from_der, - verify_attestation_doc_against_manifest_setup, + AWS_ROOT_CERT_PEM, attestation_doc_from_der, cert_from_pem, + unsafe_attestation_doc_from_der, verify_attestation_doc_against_user_input, }, types::NsmResponse, @@ -50,6 +57,7 @@ const QUORUM_THRESHOLD_FILE: &str = "quorum_threshold"; const DR_WRAPPED_QUORUM_KEY: &str = "dr_wrapped_quorum_key"; const PCRS_PATH: &str = "aws-x86_64.pcrs"; const GENESIS_DR_ARTIFACTS: &str = "genesis_dr_artifacts"; +const ATTESTATION_MAX_AGE: Duration = Duration::from_secs(5 * 60); const DANGEROUS_DEV_BOOT_MEMBER: &str = "DANGEROUS_DEV_BOOT_MEMBER"; const DANGEROUS_DEV_BOOT_NAMESPACE: &str = @@ -200,6 +208,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: VerifyError) -> Self { + Self::QosAttest(err.to_string()) + } +} + /// Use a P256 key pair or Yubikey for signing operations. pub enum PairOrYubi { /// Yubikey @@ -1418,30 +1432,22 @@ pub(crate) fn boot_standard>( pivot, )?; - let attestation_doc = - extract_attestation_doc(&cose_sign1, unsafe_skip_attestation, None); - // Verify attestation document if unsafe_skip_attestation { println!("**WARNING:** Skipping attestation document verification."); } else { - verify_attestation_doc_against_manifest_setup( - &attestation_doc, - ManifestAttestationInput { - manifest_hash: &manifest.manifest_hash(), - pcr0: &manifest.enclave().pcr0, - pcr1: &manifest.enclave().pcr1, - pcr2: &manifest.enclave().pcr2, - pcr3: &extract_pcr3(pcr3_preimage_path), - }, + let root_ca = cert_from_pem(AWS_ROOT_CERT_PEM)?; + verify_attestation_and_manifest( + &cose_sign1, + &manifest, + &AttestationPolicy::new( + &root_ca, + ATTESTATION_MAX_AGE, + ManifestCommitmentKind::Setup, + ), + &VerificationExpectations::new() + .pcr3(extract_pcr3(pcr3_preimage_path)), )?; - - // Sanity check the ephemeral key is valid - let eph_pub_bytes = attestation_doc - .public_key - .expect("No ephemeral key in the attestation doc"); - P256Public::from_bytes(&eph_pub_bytes) - .expect("Ephemeral key not valid public key"); } Ok(()) @@ -1520,51 +1526,59 @@ pub(crate) fn proxy_re_encrypt_share>( ) -> Result<(), Error> { let manifest_envelope = read_manifest_envelope_compat(&manifest_envelope_path)?; - let attestation_doc = - read_attestation_doc(&attestation_doc_path, unsafe_skip_attestation)?; + let attestation_doc = fs::read(&attestation_doc_path) + .map_err(Error::FailedToReadAttestationDoc)?; let encrypted_share = std::fs::read(share_path) .map_err(|e| Error::ReadShare(e.to_string()))?; let pcr3_preimage = find_pcr3(&pcr3_preimage_path); let manifest = manifest_envelope.clone().manifest(); + let manifest_set = get_manifest_set(manifest_set_dir); - // Verify the attestation doc matches up with the pcrs in the manifest - if unsafe_skip_attestation { + let verified_eph = if unsafe_skip_attestation { println!("**WARNING:** Skipping attestation document verification."); + None } else { - verify_attestation_doc_against_manifest_setup( - &attestation_doc, - ManifestAttestationInput { - manifest_hash: &manifest_envelope.manifest_hash(), - pcr0: &manifest.enclave().pcr0, - pcr1: &manifest.enclave().pcr1, - pcr2: &manifest.enclave().pcr2, - pcr3: &extract_pcr3(pcr3_preimage_path), - }, - )?; - } + let root_ca = cert_from_pem(AWS_ROOT_CERT_PEM)?; + Some( + verify_attestation_and_manifest_envelope( + &attestation_doc, + &manifest_envelope, + ManifestEnvelopeTrust::ManifestSet(&manifest_set), + &AttestationPolicy::new( + &root_ca, + ATTESTATION_MAX_AGE, + ManifestCommitmentKind::Setup, + ), + &VerificationExpectations::new() + .pcr3(extract_pcr3(pcr3_preimage_path)), + )? + .ephemeral_key, + ) + }; - // Pull out the ephemeral key or use the override - let eph_pub: P256Public = if let Some(ref eph_path) = - unsafe_eph_path_override - { + // Pull out the verified ephemeral key or use an explicit unsafe source. + let eph_pub = if let Some(ref eph_path) = unsafe_eph_path_override { P256Pair::from_hex_file(eph_path) .unwrap_or_else(|e| panic!("proxy_re_encrypt_share: Could not read ephemeral key from {eph_path:?}: {e:?}")) .public_key() + } else if let Some(eph_pub) = verified_eph { + eph_pub } else { - P256Public::from_bytes(&attestation_doc.public_key.expect( - "proxy_re_encrypt_share: No ephemeral key in the attestation doc", - )) - .expect("proxy_re_encrypt_share: Ephemeral key not valid public key") + let unsafe_doc = unsafe_attestation_doc_from_der(&attestation_doc)?; + P256Public::from_bytes(&unsafe_doc.public_key.ok_or_else(|| { + Error::QosAttest( + "proxy_re_encrypt_share: no ephemeral key in attestation document" + .to_string(), + ) + })?)? }; let member = QuorumMember { pub_key: pair.public_key_bytes()?, alias }; - - if !proxy_re_encrypt_share_programmatic_verifications( - &manifest_envelope, - &get_manifest_set(manifest_set_dir), - &member, - ) { + if !manifest.share_set().members.contains(&member) { + eprintln!( + "The provided share set key and alias are not part of the Share Set" + ); eprintln!("Exiting early without re-encrypting / approving"); std::process::exit(1); } @@ -1613,35 +1627,6 @@ pub(crate) fn proxy_re_encrypt_share>( Ok(()) } -fn proxy_re_encrypt_share_programmatic_verifications( - manifest_envelope: &VersionedManifestEnvelope, - manifest_set: &ManifestSet, - member: &QuorumMember, -) -> bool { - let manifest = manifest_envelope.clone().manifest(); - - if let Err(e) = manifest_envelope.check_approvals() { - eprintln!("Manifest envelope did not have valid approvals: {e:?}"); - return false; - } - - if *manifest.manifest_set() != *manifest_set { - eprintln!( - "Manifest's manifest set does not match locally found Manifest Set" - ); - return false; - } - - if !manifest.share_set().members.contains(member) { - eprintln!( - "The provided share set key and alias are not part of the Share Set" - ); - return false; - } - - true -} - fn proxy_re_encrypt_share_human_verifications( manifest_envelope: &VersionedManifestEnvelope, pcr3_preimage: &str, @@ -2364,20 +2349,6 @@ fn read_manifest_v1_compat>( } } -fn read_attestation_doc>( - path: P, - unsafe_skip_attestation: bool, -) -> Result { - let cose_sign1_der = - fs::read(path).map_err(Error::FailedToReadAttestationDoc)?; - - Ok(extract_attestation_doc( - cose_sign1_der.as_ref(), - unsafe_skip_attestation, - None, - )) -} - fn read_manifest_envelope_compat>( file: P, ) -> Result { @@ -2608,7 +2579,6 @@ mod tests { Prompter, approve_manifest_human_verifications, approve_manifest_programmatic_verifications, proxy_re_encrypt_share_human_verifications, - proxy_re_encrypt_share_programmatic_verifications, }; struct Setup { @@ -2729,21 +2699,6 @@ mod tests { } } - /// Return a mutable v1 manifest envelope for tests that intentionally build - /// v1 fixtures. - /// - /// # Panics - /// - /// Panics if the fixture is not a v1 manifest envelope. - fn v1_manifest_envelope_mut( - envelope: &mut VersionedManifestEnvelope, - ) -> &mut ManifestEnvelope { - match envelope { - VersionedManifestEnvelope::V1(envelope) => envelope, - _ => panic!("expected v1 manifest envelope in test setup"), - } - } - #[test] fn manifest_envelope_protocol_encodings_try_v1_borsh_first() { let Setup { manifest_envelope, .. } = setup(); @@ -3184,102 +3139,6 @@ mod tests { } } - mod proxy_re_encrypt_share_programmatic_verifications { - use super::*; - - #[test] - fn accepts_valid() { - let Setup { manifest_set, share_set, manifest_envelope, .. } = - setup(); - - let member = share_set.members[0].clone(); - assert!(proxy_re_encrypt_share_programmatic_verifications( - &manifest_envelope, - &manifest_set, - &member - )); - } - - #[test] - fn rejects_invalid_approval() { - let Setup { - manifest_set, share_set, mut manifest_envelope, .. - } = setup(); - - v1_manifest_envelope_mut(&mut manifest_envelope) - .manifest_set_approvals - .get_mut(0) - .unwrap() - .signature = vec![0; 32]; - - let member = share_set.members[0].clone(); - assert!(!proxy_re_encrypt_share_programmatic_verifications( - &manifest_envelope, - &manifest_set, - &member - )); - } - - #[test] - fn rejects_approval_from_member_not_part_of_manifest_set() { - let Setup { - manifest_set, share_set, mut manifest_envelope, .. - } = setup(); - - v1_manifest_envelope_mut(&mut manifest_envelope) - .manifest_set_approvals - .get_mut(0) - .unwrap() - .member - .alias = "not-a-member".to_string(); - - let member = share_set.members[0].clone(); - assert!(!proxy_re_encrypt_share_programmatic_verifications( - &manifest_envelope, - &manifest_set, - &member - )); - } - - #[test] - fn rejects_if_not_enough_approvals() { - let Setup { - manifest_set, share_set, mut manifest_envelope, .. - } = setup(); - - v1_manifest_envelope_mut(&mut manifest_envelope) - .manifest_set_approvals - .pop() - .unwrap(); - - let member = share_set.members[0].clone(); - assert!(!proxy_re_encrypt_share_programmatic_verifications( - &manifest_envelope, - &manifest_set, - &member - )); - } - - #[test] - fn rejects_mismatched_manifest_sets() { - let Setup { - mut manifest_set, share_set, manifest_envelope, .. - } = setup(); - - manifest_set.members.push(QuorumMember { - alias: "got what plants need".to_string(), - pub_key: P256Pair::generate().unwrap().public_key().to_bytes(), - }); - - let member = share_set.members[0].clone(); - assert!(!proxy_re_encrypt_share_programmatic_verifications( - &manifest_envelope, - &manifest_set, - &member - )); - } - } - mod proxy_re_encrypt_share_human_verifications { use super::*; #[test] diff --git a/src/qos_core/src/protocol/services/key.rs b/src/qos_core/src/protocol/services/key.rs index 7e3cf6551..3d96e4403 100644 --- a/src/qos_core/src/protocol/services/key.rs +++ b/src/qos_core/src/protocol/services/key.rs @@ -1,16 +1,24 @@ //! The services involved in the key forwarding flow. -use aws_nitro_enclaves_nsm_api::api::AttestationDoc; use borsh::{BorshDeserialize, BorshSerialize}; -use qos_nsm::{nitro::attestation_doc_from_der, types::NsmResponse}; +use qos_nsm::types::NsmResponse; use qos_p256::{P256Pair, P256Public}; use serde::{Deserialize, Serialize}; - -use crate::protocol::{ - ProtocolError, ProtocolState, QosHash, - services::boot::{VersionedManifestEnvelope, put_manifest_and_pivot}, +use std::time::Duration; + +use crate::{ + protocol::{ + ProtocolError, ProtocolState, QosHash, + services::boot::{VersionedManifestEnvelope, put_manifest_and_pivot}, + }, + verify::{ + AttestationPolicy, ManifestCommitmentKind, VerificationExpectations, + verify_attestation_and_manifest, + }, }; +const ATTESTATION_MAX_AGE: Duration = Duration::from_secs(5 * 60); + /// An encrypted quorum key along with a signature over the encrypted payload /// from the sender. #[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize)] @@ -91,39 +99,60 @@ pub(in crate::protocol) fn export_key( cose_sign1_attestation_document: &[u8], ) -> Result { let new_manifest_envelope = new_manifest_envelope.into(); - // 1. Check the basic validity of the attestation doc (cert chain etc). - // Ensures that the attestation document is actually from an AWS controlled - // NSM module and the document's timestamp was recent. - let attestation_doc = verify_and_extract_attestation_doc_from_der( - cose_sign1_attestation_document, - &*state.attestor, - )?; + let old_manifest_envelope = state.handles.get_manifest_envelope()?; + validate_manifest(&new_manifest_envelope, &old_manifest_envelope)?; + let new_manifest = new_manifest_envelope.manifest(); - export_key_internal(state, &new_manifest_envelope, &attestation_doc) + let root_ca = state.attestor.attestation_root_ca_der(); + let verified = verify_attestation_and_manifest( + cose_sign1_attestation_document, + &new_manifest, + &AttestationPolicy::new( + &root_ca, + ATTESTATION_MAX_AGE, + ManifestCommitmentKind::Setup, + ), + &VerificationExpectations::new(), + ) + .map_err(|err| ProtocolError::QosAttestError(err.to_string()))?; + + export_key_internal(state, &verified.ephemeral_key) } -// Primary logic of `export_key` pulled out so it can be unit tested. -fn export_key_internal( +#[cfg(test)] +fn export_key_at_time( state: &mut ProtocolState, new_manifest_envelope: impl Into, - attestation_doc: &AttestationDoc, + cose_sign1_attestation_document: &[u8], + current_time: Duration, ) -> Result { let new_manifest_envelope = new_manifest_envelope.into(); let old_manifest_envelope = state.handles.get_manifest_envelope()?; - // steps 2 through 9 - validate_manifest( - &new_manifest_envelope, - &old_manifest_envelope, - attestation_doc, - )?; - - let eph_key_bytes = attestation_doc - .public_key - .as_ref() - .ok_or(ProtocolError::MissingEphemeralKey)?; - let eph_key = P256Public::from_bytes(eph_key_bytes) - .map_err(|_| ProtocolError::InvalidEphemeralKey)?; + validate_manifest(&new_manifest_envelope, &old_manifest_envelope)?; + let new_manifest = new_manifest_envelope.manifest(); + let root_ca = state.attestor.attestation_root_ca_der(); + let verified = + crate::verify::verify_attestation_and_manifest_at_time_for_test( + cose_sign1_attestation_document, + &new_manifest, + &AttestationPolicy::new( + &root_ca, + ATTESTATION_MAX_AGE, + ManifestCommitmentKind::Setup, + ), + &VerificationExpectations::new(), + current_time, + ) + .map_err(|err| ProtocolError::QosAttestError(err.to_string()))?; + + export_key_internal(state, &verified.ephemeral_key) +} + +fn export_key_internal( + state: &mut ProtocolState, + eph_key: &P256Public, +) -> Result { let quorum_key = state.handles.get_quorum_key()?; // 10. Return the Quorum Key encrypted to the New Node's Ephemeral Key // extracted from the attestation document and a signature over the @@ -140,7 +169,6 @@ fn export_key_internal( fn validate_manifest( new_manifest_envelope: impl Into, old_manifest_envelope: impl Into, - attestation_doc: &AttestationDoc, ) -> Result<(), ProtocolError> { let new_manifest_envelope = new_manifest_envelope.into(); let old_manifest_envelope = old_manifest_envelope.into(); @@ -232,26 +260,7 @@ fn validate_manifest( }); } - // 7. Verify the setup attestation against the New Manifest. - // This checks that the new manifest hash is in `user_data`, PCR0 through - // PCR3 match the New Manifest, every release-pinned PCR is present, and - // PCR16 matches the setup manifest/key commitment for the attested public - // key. This ensures the New Manifest was used against a Nitro enclave - // booted with the intended version of QOS. Note that we assume the values - // for PCR{0, 1, 2} correspond to a desired version of QOS because the - // Manifest Set Members had K approvals. - qos_nsm::nitro::verify_attestation_doc_against_manifest_setup( - attestation_doc, - qos_nsm::nitro::ManifestAttestationInput { - manifest_hash: &new_manifest.manifest_hash(), - pcr0: &new_manifest.enclave().pcr0, - pcr1: &new_manifest.enclave().pcr1, - pcr2: &new_manifest.enclave().pcr2, - pcr3: &new_manifest.enclave().pcr3, - }, - )?; - - // 9. Check that PCR3 in the New Manifest is in the Local Manifests. PCR3 is + // 7. Check that PCR3 in the New Manifest is in the Local Manifests. PCR3 is // the IAM role assigned to the EC2 host of the enclave. An IAM role // contains an AWS organization's unique ID. By only using the approved PCR3 // value we ensure that we only ever send the Quorum Key to an enclave that @@ -268,37 +277,23 @@ fn validate_manifest( Ok(()) } -fn verify_and_extract_attestation_doc_from_der( - cose_sign1_der: &[u8], - nsm: &dyn qos_nsm::NsmProvider, -) -> Result { - let current_time_milliseconds = nsm.timestamp_ms()?; - let current_time_seconds = current_time_milliseconds / 1_000; - // The trust anchor follows the NSM provider: the AWS Nitro root CA in - // production, or the mock root CA when running with a mock provider. - let der_cert = nsm.attestation_root_ca_der(); - attestation_doc_from_der(cose_sign1_der, &der_cert, current_time_seconds) - .map_err(Into::into) -} - #[cfg(test)] mod test { - use std::collections::BTreeMap; + use std::time::Duration; - use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest}; use qos_crypto::sha_256; use qos_nsm::{ NsmProvider, - mock::MockNsm, + mock::{MOCK_SECONDS_SINCE_EPOCH, MockNsm}, nitro, types::{NsmRequest, NsmResponse}, }; use qos_p256::P256Pair; use qos_test_primitives::PathWrapper; - use serde_bytes::ByteBuf; use super::{ - boot_key_forward, export_key, export_key_internal, validate_manifest, + boot_key_forward, export_key_at_time, export_key_internal, + validate_manifest, }; use crate::{ handles::Handles, @@ -317,9 +312,6 @@ mod test { struct TestArgs { manifest_envelope: ManifestEnvelope, - #[allow(dead_code)] - members_with_keys: Vec<(P256Pair, QuorumMember)>, - att_doc: AttestationDoc, eph_pair: P256Pair, quorum_pair: P256Pair, pivot: Vec, @@ -349,7 +341,7 @@ mod test { }, ]; - let members_with_keys = vec![ + let members_with_keys = [ (member1_pair, quorum_members.first().unwrap().clone()), (member2_pair, quorum_members.get(1).unwrap().clone()), (member3_pair, quorum_members.get(2).unwrap().clone()), @@ -395,43 +387,6 @@ mod test { .collect(); let eph_pair = P256Pair::generate().unwrap(); - let eph_pub_key = eph_pair.public_key().to_bytes(); - - let mut pcr_map = BTreeMap::new(); - for idx in 0..nitro::ATTESTABLE_PCR_COUNT { - pcr_map.insert( - usize::from(idx), - ByteBuf::from(vec![0u8; nitro::PCR_SHA384_LEN]), - ); - } - pcr_map.insert(0, ByteBuf::from(pcr0)); - pcr_map.insert(1, ByteBuf::from(pcr1)); - pcr_map.insert(2, ByteBuf::from(pcr2)); - pcr_map.insert(3, ByteBuf::from(pcr3)); - pcr_map.insert( - usize::from(nitro::SETUP_MANIFEST_COMMITMENT_PCR_INDEX), - ByteBuf::from( - nitro::expected_manifest_commitment_pcr( - nitro::ManifestCommitmentKind::Setup, - &manifest.qos_hash(), - &eph_pub_key, - ) - .unwrap() - .to_vec(), - ), - ); - - let att_doc = AttestationDoc { - module_id: String::default(), - cabundle: Vec::default(), - pcrs: pcr_map, - timestamp: u64::default(), - nonce: None, - public_key: Some(ByteBuf::from(eph_pub_key)), - user_data: Some(ByteBuf::from(manifest.qos_hash())), - digest: Digest::SHA384, - certificate: ByteBuf::default(), - }; let manifest_envelope = ManifestEnvelope { manifest, @@ -439,14 +394,7 @@ mod test { share_set_approvals: Vec::default(), }; - TestArgs { - manifest_envelope, - members_with_keys, - att_doc, - eph_pair, - quorum_pair, - pivot, - } + TestArgs { manifest_envelope, eph_pair, quorum_pair, pivot } } mod boot_key_forward { @@ -690,68 +638,52 @@ mod test { use super::*; #[test] fn accepts_matching_manifests() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); assert!( - validate_manifest( - &manifest_envelope, - &manifest_envelope, - &att_doc - ) - .is_ok() + validate_manifest(&manifest_envelope, &manifest_envelope,) + .is_ok() ); } #[test] fn accepts_manifest_with_greater_nonce() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut old_manifest_envelope = manifest_envelope.clone(); old_manifest_envelope.manifest.namespace.nonce -= 1; assert!( - validate_manifest( - &manifest_envelope, - &old_manifest_envelope, - &att_doc - ) - .is_ok() + validate_manifest(&manifest_envelope, &old_manifest_envelope,) + .is_ok() ); } #[test] fn rejects_manifest_with_lower_nonce() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut old_manifest_envelope = manifest_envelope.clone(); old_manifest_envelope.manifest.namespace.nonce += 1; assert!(matches!( - validate_manifest( - &manifest_envelope, - &old_manifest_envelope, - &att_doc - ), + validate_manifest(&manifest_envelope, &old_manifest_envelope,), Err(ProtocolError::LowNonce { .. }) )); } #[test] fn rejects_manifest_with_matching_nonce_different_hash() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut old_manifest_envelope = manifest_envelope.clone(); old_manifest_envelope.manifest.enclave.pcr0 = vec![128; 32]; assert!(matches!( - validate_manifest( - &manifest_envelope, - &old_manifest_envelope, - &att_doc - ), + validate_manifest(&manifest_envelope, &old_manifest_envelope,), Err(ProtocolError::DifferentManifest { .. }) )); } #[test] fn rejects_manifest_with_different_quorum_key() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut old_manifest_envelope = manifest_envelope.clone(); let different_quorum_key = P256Pair::generate().unwrap().public_key().to_bytes(); @@ -759,46 +691,34 @@ mod test { different_quorum_key; assert!(matches!( - validate_manifest( - &manifest_envelope, - &old_manifest_envelope, - &att_doc - ), + validate_manifest(&manifest_envelope, &old_manifest_envelope,), Err(ProtocolError::DifferentQuorumKey { .. }) )); } #[test] fn does_not_accept_manifest_with_different_manifest_set() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut old_manifest_envelope = manifest_envelope.clone(); old_manifest_envelope.manifest.manifest_set.members.pop(); old_manifest_envelope.manifest.namespace.nonce -= 1; assert!(matches!( - validate_manifest( - &manifest_envelope, - &old_manifest_envelope, - &att_doc - ), + validate_manifest(&manifest_envelope, &old_manifest_envelope,), Err(ProtocolError::DifferentManifestSet { .. }) )); let mut old_manifest_envelope = manifest_envelope.clone(); old_manifest_envelope.manifest.manifest_set.threshold = 1; assert!(matches!( - validate_manifest( - &manifest_envelope, - &old_manifest_envelope, - &att_doc - ), + validate_manifest(&manifest_envelope, &old_manifest_envelope,), Err(ProtocolError::DifferentManifestSet { .. }) )); } #[test] fn accepts_manifest_with_different_ordered_manifest_set_members() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut old_manifest_envelope = manifest_envelope.clone(); let last_member = old_manifest_envelope.manifest.manifest_set.members.remove(2); @@ -811,68 +731,52 @@ mod test { old_manifest_envelope.manifest.namespace.nonce -= 1; assert!( - validate_manifest( - &manifest_envelope, - &old_manifest_envelope, - &att_doc - ) - .is_ok(), + validate_manifest(&manifest_envelope, &old_manifest_envelope,) + .is_ok(), ); } #[test] fn rejects_manifest_with_different_namespace_name() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut old_manifest_envelope = manifest_envelope.clone(); old_manifest_envelope.manifest.namespace.name = "other namespace".to_string(); assert!(matches!( - validate_manifest( - &manifest_envelope, - &old_manifest_envelope, - &att_doc - ), + validate_manifest(&manifest_envelope, &old_manifest_envelope,), Err(ProtocolError::DifferentNamespaceName { .. }), )); } #[test] fn reject_manifest_with_different_pcr3() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut old_manifest_envelope = manifest_envelope.clone(); old_manifest_envelope.manifest.enclave.pcr3 = vec![128; 32]; old_manifest_envelope.manifest.namespace.nonce -= 1; assert!(matches!( - validate_manifest( - &manifest_envelope, - &old_manifest_envelope, - &att_doc - ), + validate_manifest(&manifest_envelope, &old_manifest_envelope,), Err(ProtocolError::DifferentPcr3 { .. }), )); } #[test] fn errors_with_two_few_manifest_approvals() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut new_manifest_envelope = manifest_envelope.clone(); new_manifest_envelope.manifest_set_approvals.pop().unwrap(); assert_eq!( - validate_manifest( - &new_manifest_envelope, - &manifest_envelope, - &att_doc - ), + validate_manifest(&new_manifest_envelope, &manifest_envelope,), Err(ProtocolError::NotEnoughApprovals) ); } #[test] fn rejects_manifest_with_bad_approval_signature() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut new_manifest_envelope = manifest_envelope.clone(); new_manifest_envelope.manifest_set_approvals[0].signature = @@ -881,18 +785,14 @@ mod test { new_manifest_envelope.manifest_set_approvals[0].clone(); assert_eq!( - validate_manifest( - &new_manifest_envelope, - &manifest_envelope, - &att_doc - ), + validate_manifest(&new_manifest_envelope, &manifest_envelope,), Err(ProtocolError::InvalidManifestApproval(bad_approval)) ); } #[test] fn rejects_manifest_with_approval_from_non_member() { - let TestArgs { manifest_envelope, att_doc, .. } = get_test_args(); + let TestArgs { manifest_envelope, .. } = get_test_args(); let mut new_manifest_envelope = manifest_envelope.clone(); let non_member_pair = P256Pair::generate().unwrap(); @@ -912,209 +812,12 @@ mod test { .push(non_member_approval); assert_eq!( - validate_manifest( - &new_manifest_envelope, - &manifest_envelope, - &att_doc - ), + validate_manifest(&new_manifest_envelope, &manifest_envelope,), Err(ProtocolError::NotManifestSetMember) ); } } - mod validate_manifest_attestation_tests { - use super::*; - #[test] - fn errors_if_pcr0_does_match_attestation_doc() { - let TestArgs { - manifest_envelope, - mut att_doc, - members_with_keys, - .. - } = get_test_args(); - let mut new_manifest_envelope = manifest_envelope.clone(); - new_manifest_envelope.manifest.namespace.nonce += 1; - new_manifest_envelope.manifest.enclave.pcr0 = vec![128; 32]; - - let new_manifest_hash = new_manifest_envelope.manifest.qos_hash(); - att_doc.user_data = Some(ByteBuf::from(new_manifest_hash)); - - let manifest_set_approvals = (0..2) - .map(|i| { - let (pair, member) = &members_with_keys[i]; - Approval { - signature: pair.sign(&new_manifest_hash).unwrap(), - member: member.clone(), - } - }) - .collect(); - new_manifest_envelope.manifest_set_approvals = - manifest_set_approvals; - - assert_eq!( - validate_manifest( - &new_manifest_envelope, - &manifest_envelope, - &att_doc - ), - Err(ProtocolError::QosAttestError( - "DifferentPcr0 { expected: \"8080808080808080808080808080808080808080808080808080808080808080\", actual: \"0404040404040404040404040404040404040404040404040404040404040404\" }".to_string() - )) - ); - } - - #[test] - fn errors_if_pcr1_does_match_attestation_doc() { - let TestArgs { - manifest_envelope, - mut att_doc, - members_with_keys, - .. - } = get_test_args(); - let mut new_manifest_envelope = manifest_envelope.clone(); - new_manifest_envelope.manifest.namespace.nonce += 1; - new_manifest_envelope.manifest.enclave.pcr1 = vec![128; 32]; - - let new_manifest_hash = new_manifest_envelope.manifest.qos_hash(); - att_doc.user_data = Some(ByteBuf::from(new_manifest_hash)); - - let manifest_set_approvals = (0..2) - .map(|i| { - let (pair, member) = &members_with_keys[i]; - Approval { - signature: pair.sign(&new_manifest_hash).unwrap(), - member: member.clone(), - } - }) - .collect(); - new_manifest_envelope.manifest_set_approvals = - manifest_set_approvals; - - assert_eq!( - validate_manifest( - &new_manifest_envelope, - &manifest_envelope, - &att_doc - ), - Err(ProtocolError::QosAttestError( - "DifferentPcr1 { expected: \"8080808080808080808080808080808080808080808080808080808080808080\", actual: \"0303030303030303030303030303030303030303030303030303030303030303\" }".to_string() - )) - ); - } - - #[test] - fn errors_if_pcr2_does_match_attesation_doc() { - let TestArgs { - manifest_envelope, - mut att_doc, - members_with_keys, - .. - } = get_test_args(); - let mut new_manifest_envelope = manifest_envelope.clone(); - new_manifest_envelope.manifest.namespace.nonce += 1; - new_manifest_envelope.manifest.enclave.pcr2 = vec![128; 32]; - - let new_manifest_hash = new_manifest_envelope.manifest.qos_hash(); - att_doc.user_data = Some(ByteBuf::from(new_manifest_hash)); - - let manifest_set_approvals = (0..2) - .map(|i| { - let (pair, member) = &members_with_keys[i]; - Approval { - signature: pair.sign(&new_manifest_hash).unwrap(), - member: member.clone(), - } - }) - .collect(); - new_manifest_envelope.manifest_set_approvals = - manifest_set_approvals; - - assert_eq!( - validate_manifest( - &new_manifest_envelope, - &manifest_envelope, - &att_doc - ), - Err(ProtocolError::QosAttestError( - "DifferentPcr2 { expected: \"8080808080808080808080808080808080808080808080808080808080808080\", actual: \"0202020202020202020202020202020202020202020202020202020202020202\" }".to_string() - )) - ); - } - - #[test] - fn errors_if_pcr3_does_match_attestation_doc() { - let TestArgs { - manifest_envelope, - mut att_doc, - members_with_keys, - .. - } = get_test_args(); - let mut new_manifest_envelope = manifest_envelope.clone(); - new_manifest_envelope.manifest.namespace.nonce += 1; - new_manifest_envelope.manifest.enclave.pcr3 = vec![128; 32]; - - let new_manifest_hash = new_manifest_envelope.manifest.qos_hash(); - att_doc.user_data = Some(ByteBuf::from(new_manifest_hash)); - - let manifest_set_approvals = (0..2) - .map(|i| { - let (pair, member) = &members_with_keys[i]; - Approval { - signature: pair.sign(&new_manifest_hash).unwrap(), - member: member.clone(), - } - }) - .collect(); - new_manifest_envelope.manifest_set_approvals = - manifest_set_approvals; - - assert_eq!( - validate_manifest( - &new_manifest_envelope, - &manifest_envelope, - &att_doc - ), - Err(ProtocolError::QosAttestError( - "DifferentPcr3 { expected: \"8080808080808080808080808080808080808080808080808080808080808080\", actual: \"0101010101010101010101010101010101010101010101010101010101010101\" }".to_string() - )) - ); - } - - #[test] - fn errors_if_manifest_hash_does_not_match_attestation_doc() { - let TestArgs { - manifest_envelope, att_doc, members_with_keys, .. - } = get_test_args(); - let mut new_manifest_envelope = manifest_envelope.clone(); - new_manifest_envelope.manifest.namespace.nonce += 1; - - let manifest_set_approvals = (0..2) - .map(|i| { - let (pair, member) = &members_with_keys[i]; - Approval { - signature: pair - .sign(&new_manifest_envelope.manifest.qos_hash()) - .unwrap(), - member: member.clone(), - } - }) - .collect(); - new_manifest_envelope.manifest_set_approvals = - manifest_set_approvals; - - // Don't update the manifest hash in the attestation doc - - let err = validate_manifest( - &new_manifest_envelope, - &manifest_envelope, - &att_doc, - ); - // The hash values are dynamically generated, so we check the error format - assert!( - matches!(&err, Err(ProtocolError::QosAttestError(msg)) if msg.starts_with("DifferentUserData {")) - ); - } - } mod export_key_inner { use super::*; use crate::protocol::services::key::EncryptedQuorumKey; @@ -1222,7 +925,13 @@ mod test { let mut state = ProtocolState::new(Box::new(nsm), handles, None); let EncryptedQuorumKey { encrypted_quorum_key, signature } = - export_key(&mut state, &manifest_envelope, &document).unwrap(); + export_key_at_time( + &mut state, + &manifest_envelope, + &document, + Duration::from_secs(MOCK_SECONDS_SINCE_EPOCH), + ) + .unwrap(); // quorum key signature over payload is valid assert!( @@ -1281,9 +990,12 @@ mod test { handles, None, ); - let Err(err) = - export_key(&mut state, &manifest_envelope, &document) - else { + let Err(err) = export_key_at_time( + &mut state, + &manifest_envelope, + &document, + Duration::from_secs(MOCK_SECONDS_SINCE_EPOCH), + ) else { panic!("expected export_key to reject the document"); }; @@ -1310,9 +1022,12 @@ mod test { ); let mut state = ProtocolState::new(Box::new(nsm), handles, None); - let Err(err) = - export_key(&mut state, &manifest_envelope, &document) - else { + let Err(err) = export_key_at_time( + &mut state, + &manifest_envelope, + &document, + Duration::from_secs(MOCK_SECONDS_SINCE_EPOCH), + ) else { panic!("expected export_key to reject the document"); }; @@ -1321,13 +1036,8 @@ mod test { #[test] fn works() { - let TestArgs { - manifest_envelope, - att_doc, - eph_pair, - quorum_pair, - .. - } = get_test_args(); + let TestArgs { manifest_envelope, eph_pair, quorum_pair, .. } = + get_test_args(); let ephemeral_file = PathWrapper::from("export_key_inner_works.eph.secret"); @@ -1357,8 +1067,7 @@ mod test { let EncryptedQuorumKey { encrypted_quorum_key, signature } = export_key_internal( &mut protocol_state, - &manifest_envelope, - &att_doc, + &eph_pair.public_key(), ) .unwrap(); diff --git a/src/qos_core/src/verify.rs b/src/qos_core/src/verify.rs index 946f29c61..11fa3d1b0 100644 --- a/src/qos_core/src/verify.rs +++ b/src/qos_core/src/verify.rs @@ -450,6 +450,23 @@ fn verify_attestation_and_manifest_at_time( }) } +#[cfg(test)] +pub(crate) fn verify_attestation_and_manifest_at_time_for_test( + attestation_doc: &[u8], + manifest: &VersionedManifest, + attestation_policy: &AttestationPolicy<'_>, + manifest_policy: &dyn ManifestPolicy, + current_time: Duration, +) -> Result { + verify_attestation_and_manifest_at_time( + attestation_doc, + manifest, + attestation_policy, + manifest_policy, + current_time, + ) +} + /// Verify an in-memory manifest envelope and attestation document. /// /// The embedded manifest must first match the caller-owned `trust` anchor.