From d28adee416395805b4e85ef2d0b981bd61ea77b9 Mon Sep 17 00:00:00 2001 From: overtrue Date: Sat, 5 Sep 2026 15:48:35 +0800 Subject: [PATCH] fix(connect): preserve signature precedence across chain parsing --- rustfs/src/connect/offline/enrollment.rs | 29 ++++++++++++++-------- rustfs/tests/connect_offline_enrollment.rs | 19 ++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/rustfs/src/connect/offline/enrollment.rs b/rustfs/src/connect/offline/enrollment.rs index b767d272c..7975c3955 100644 --- a/rustfs/src/connect/offline/enrollment.rs +++ b/rustfs/src/connect/offline/enrollment.rs @@ -286,7 +286,7 @@ struct DocumentSignature { struct ChallengeRouting { connect_key_id: String, issued_at: String, - trust_chain: Vec, + trust_chain: Vec, } #[derive(Deserialize)] @@ -382,26 +382,33 @@ impl OfflineEnrollment { 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. + // parsing any trust-link envelope or routing fields. Otherwise a + // malformed 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_bytes = first + .get("bytes") + .and_then(serde_json::Value::as_str) + .ok_or(EnrollmentError::MalformedDocument) + .and_then(decode_document_bytes)?; let first_routing: TrustLinkRouting = serde_json::from_slice(&first_bytes).map_err(|_| EnrollmentError::MalformedDocument)?; if !is_key_id(&first_routing.issuer_key_id) { return Err(EnrollmentError::MalformedDocument); } + // The pinned-root decision precedes full chain-envelope validation. + if first_routing.issuer_key_id != root.key_id { + return Err(EnrollmentError::EnrollmentRootUnknown); + } + let trust_chain: Vec = serde_json::from_value(serde_json::Value::Array(routing.trust_chain)) + .map_err(|_| EnrollmentError::TrustChainInvalid)?; + // Steps 3 to 5. - let connect_key = verify_trust_chain( - &routing.trust_chain, - &routing.connect_key_id, - &first_routing.issuer_key_id, - issued_at, - root, - )?; + let connect_key = + verify_trust_chain(&trust_chain, &routing.connect_key_id, &first_routing.issuer_key_id, issued_at, root)?; // Step 6. The detached signature must name the same chained key whose // public key verifies it. A different well-formed key id is a signature diff --git a/rustfs/tests/connect_offline_enrollment.rs b/rustfs/tests/connect_offline_enrollment.rs index ace53a558..70cd68a88 100644 --- a/rustfs/tests/connect_offline_enrollment.rs +++ b/rustfs/tests/connect_offline_enrollment.rs @@ -594,6 +594,25 @@ fn malformed_top_level_signature_precedes_malformed_first_link_routing() { assert_eq!(error.reason(), "SIGNATURE_MALFORMED"); } +#[test] +fn malformed_top_level_signature_precedes_malformed_second_link_envelope() { + 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); + challenge["trustChain"].as_array_mut().expect("challenge carries a chain")[1] + .as_object_mut() + .expect("trust link envelope is an object") + .remove("signature"); + 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 second-link envelope 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");