From 3b01538524363eda4b7b96964ae6a4bc46fef50f Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Sat, 5 Sep 2026 12:33:34 +0800 Subject: [PATCH] fix(connect): preserve enrollment validation order --- rustfs/src/connect/offline/enrollment.rs | 12 +++++++----- rustfs/tests/connect_offline_enrollment.rs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/rustfs/src/connect/offline/enrollment.rs b/rustfs/src/connect/offline/enrollment.rs index 9c8e0d6c9..b767d272c 100644 --- a/rustfs/src/connect/offline/enrollment.rs +++ b/rustfs/src/connect/offline/enrollment.rs @@ -380,6 +380,12 @@ impl OfflineEnrollment { return Err(EnrollmentError::MalformedDocument); } let issued_at = parse_timestamp(&routing.issued_at)?; + + // The frozen decision order classifies the top-level signature before + // parsing any trust-link routing fields. Otherwise a malformed first + // link could mask a malformed artifact signature with DOCUMENT_MALFORMED. + let signature = decode_signature(&envelope.signature)?; + let first = routing.trust_chain.first().ok_or(EnrollmentError::MalformedDocument)?; let first_bytes = decode_document_bytes(&first.bytes)?; let first_routing: TrustLinkRouting = @@ -388,10 +394,6 @@ impl OfflineEnrollment { return Err(EnrollmentError::MalformedDocument); } - // Only after the document can route verification do we classify the - // top-level signature spelling and algorithm. - let signature = decode_signature(&envelope.signature)?; - // Steps 3 to 5. let connect_key = verify_trust_chain( &routing.trust_chain, @@ -585,7 +587,7 @@ fn decode_trust_link(entry: &SignedDocument) -> Result<(TrustLink, Vec), Enr } fn decode_document_bytes(value: &str) -> Result, EnrollmentError> { - if value.len() % 4 != 0 { + if !value.len().is_multiple_of(4) { return Err(EnrollmentError::MalformedDocument); } diff --git a/rustfs/tests/connect_offline_enrollment.rs b/rustfs/tests/connect_offline_enrollment.rs index a5c7afb89..ace53a558 100644 --- a/rustfs/tests/connect_offline_enrollment.rs +++ b/rustfs/tests/connect_offline_enrollment.rs @@ -575,6 +575,25 @@ fn every_challenge_boundary_mutation_fails_with_its_frozen_reason() { assert_eq!(covered, 16, "boundary-vectors.json publishes sixteen executable challenge mutations"); } +#[test] +fn malformed_top_level_signature_precedes_malformed_first_link_routing() { + let source = accept_vector_named("challenge signed by a chained signing key under the pinned root"); + let now = unix(field(&source, "evaluationTime")); + let mut challenge_envelope = source["document"].clone(); + challenge_envelope["signature"]["algorithm"] = Value::String("ES384".to_string()); + + let mut challenge = signed_document(&challenge_envelope); + let first_envelope = &mut challenge["trustChain"].as_array_mut().expect("challenge carries a chain")[0]; + let mut first_link = signed_document(first_envelope); + first_link["issuerKeyId"] = Value::String("not-a-key-id".to_string()); + first_envelope["bytes"] = Value::String(encoded_document(&first_link)); + challenge_envelope["bytes"] = Value::String(encoded_document(&challenge)); + + let error = OfflineEnrollment::verify_challenge(&envelope(&challenge_envelope), now) + .expect_err("a malformed top-level signature and first-link issuer must not verify"); + assert_eq!(error.reason(), "SIGNATURE_MALFORMED"); +} + #[test] fn unpinned_root_precedes_a_malformed_chain_shape() { let source = accept_vector_named("challenge signed by a chained signing key under the pinned root");