Skip to content
Open
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
25 changes: 24 additions & 1 deletion attestation-doc-validation/src/cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,30 @@ fn get_epoch() -> CertResult<u64> {
/// Returns a `CertError::UntrustedCert` when the trust chain fails to validate
/// Returns a `CertError::Openssl` if an error occurred while preparing the context
pub fn validate_cert_trust_chain(target: &[u8], intermediates: &[&[u8]]) -> CertResult<()> {
validate_cert_trust_chain_at_time(target, intermediates, get_epoch()?)
}

/// Same as [`validate_cert_trust_chain`], but validates against a caller-supplied
/// unix timestamp instead of the real system clock.
///
/// This exists for downstream consumers who want to write a deterministic test
/// against a real, previously-captured certificate chain. A real Nitro leaf
/// certificate is only valid for a few hours after issuance (this crate's own
/// test suite notes ~3 hours in its `time_sensitive_beta` tests), so a fixture
/// captured today will otherwise only verify successfully for that window. This
/// crate's own `FAKETIME` env-var override (see `get_epoch`) cannot help here:
/// it is `#[cfg(test)]`-gated and so only ever compiles into THIS crate's own
/// test binary, never into the published library a downstream crate depends on.
///
/// # Errors
///
/// Returns a `CertError::UntrustedCert` when the trust chain fails to validate
/// Returns a `CertError::Openssl` if an error occurred while preparing the context
pub fn validate_cert_trust_chain_at_time(
target: &[u8],
intermediates: &[&[u8]],
now: u64,
) -> CertResult<()> {
let end_entity_cert = EndEntityCert::try_from(target).map_err(|_| CertError::DecodeError)?;

let (_, nitro_pem_cert) = x509_parser::pem::parse_x509_pem(NITRO_ROOT_CA_BYTES)
Expand All @@ -116,7 +140,6 @@ pub fn validate_cert_trust_chain(target: &[u8], intermediates: &[&[u8]]) -> Cert
.map_err(|_| CertError::DecodeError)?];
let server_trust_anchors = webpki::TlsServerTrustAnchors(&nitro_trust_anchor);

let now = get_epoch()?;
let time = webpki::Time::from_seconds_since_unix_epoch(now);

end_entity_cert.verify_is_valid_tls_server_cert(
Expand Down
94 changes: 94 additions & 0 deletions attestation-doc-validation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,39 @@ pub fn validate_and_parse_attestation_doc(
Ok(decoded_attestation_doc)
}

/// Same as [`validate_and_parse_attestation_doc`], but validates the certificate
/// trust chain against a caller-supplied unix timestamp instead of the real
/// system clock, via [`cert::validate_cert_trust_chain_at_time`] -- see that
/// function's doc comment for why this exists (deterministically testing
/// against a real, previously-captured document, which a real Nitro leaf
/// certificate's short validity window would otherwise make impossible from
/// outside this crate's own test suite).
///
/// # Errors
///
/// Same as [`validate_and_parse_attestation_doc`].
pub fn validate_and_parse_attestation_doc_at_time(
attestation_doc_cose_sign_1_bytes: &[u8],
now: u64,
) -> error::AttestResult<AttestationDoc> {
let (cose_sign_1_decoded, decoded_attestation_doc) =
attestation_doc::decode_attestation_document(attestation_doc_cose_sign_1_bytes)?;
attestation_doc::validate_attestation_document_structure(&decoded_attestation_doc)?;
let attestation_doc_signing_cert = cert::parse_der_cert(&decoded_attestation_doc.certificate)?;

let intermediate_certs = create_intermediate_cert_stack(&decoded_attestation_doc.cabundle);
cert::validate_cert_trust_chain_at_time(
&decoded_attestation_doc.certificate,
&intermediate_certs,
now,
)?;

let pub_key: nsm::PublicKey = attestation_doc_signing_cert.public_key().try_into()?;
attestation_doc::validate_cose_signature::<CryptoClient>(&pub_key, &cose_sign_1_decoded)?;

Ok(decoded_attestation_doc)
}

#[cfg(test)]
mod test {
use super::*;
Expand Down Expand Up @@ -391,4 +424,65 @@ mod test {
validate_attestation_doc_against_cert(&cert, &attestation_doc_bytes);
assert!(maybe_attestation_doc.is_ok());
}

// Robust, timestamp-value-agnostic proof that `validate_and_parse_attestation_doc_at_time`
// behaves identically to `validate_and_parse_attestation_doc` when given an
// equivalent "now" -- deliberately does NOT depend on knowing any fixture's
// exact valid window. An earlier version of this test asserted success at this
// file's own documented "captured 2023-01-18 ~15:15 (epoch 1674054914)" comment
// and failed: that comment's "approximately" turned out to matter -- the real
// leaf cert's actual NotBefore/NotAfter for THIS specific fixture file didn't
// cover that exact second. Rather than go hunt down a more precise timestamp
// for one fixture, this asserts the thing that actually matters for a safe
// refactor: the new function must reach the SAME conclusion as the existing one
// when given an equivalent clock reading, for any fixture, expired or not.
#[test]
fn validate_and_parse_attestation_doc_at_time_matches_the_real_clock_entry_point_given_the_same_now() {
let attestation_doc_cose_bytes = std::fs::read(std::path::Path::new(
"../test-data/beta/valid-attestation-doc-bytes",
))
.unwrap();

let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();

let via_real_clock = validate_and_parse_attestation_doc(&attestation_doc_cose_bytes);
let via_explicit_time =
validate_and_parse_attestation_doc_at_time(&attestation_doc_cose_bytes, now);

assert_eq!(
via_real_clock.is_ok(),
via_explicit_time.is_ok(),
"the _at_time variant must agree with the real-clock variant given the real \
current time: via_real_clock={via_real_clock:?} via_explicit_time={via_explicit_time:?}"
);
}

// Confirms the explicit time parameter has real effect, independent of any
// fixture's own validity window: 1970 is before every real AWS Nitro
// certificate that has ever existed, so this must always fail.
#[test]
fn validate_and_parse_attestation_doc_at_time_rejects_a_real_doc_at_the_unix_epoch() {
let attestation_doc_cose_bytes = std::fs::read(std::path::Path::new(
"../test-data/beta/valid-attestation-doc-bytes",
))
.unwrap();
let result = validate_and_parse_attestation_doc_at_time(&attestation_doc_cose_bytes, 0);
assert!(result.is_err(), "no real Nitro certificate was valid at the unix epoch");
}

// The same fixture must still fail today via the real-clock entry point
// -- confirms this isn't a case where the new function is simply
// skipping the check. real_fixture's chain genuinely expired years ago.
#[test]
fn validate_and_parse_attestation_doc_rejects_the_same_long_expired_real_doc_at_the_real_current_time() {
let attestation_doc_cose_bytes = std::fs::read(std::path::Path::new(
"../test-data/beta/valid-attestation-doc-bytes",
))
.unwrap();
let result = validate_and_parse_attestation_doc(&attestation_doc_cose_bytes);
assert!(result.is_err(), "a 2023 document's leaf cert must be expired by now");
}
}