fix(connect): preserve signature error classification

This commit is contained in:
overtrue
2026-09-06 00:22:26 +08:00
parent 0573e619b8
commit 996770cdcd
2 changed files with 65 additions and 4 deletions
+30 -4
View File
@@ -271,6 +271,21 @@ struct SignedDocument {
signature: DocumentSignature,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct UnverifiedSignedDocument {
bytes: String,
signature: UnverifiedDocumentSignature,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct UnverifiedDocumentSignature {
algorithm: Option<serde_json::Value>,
key_id: Option<serde_json::Value>,
value: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DocumentSignature {
@@ -364,7 +379,8 @@ impl OfflineEnrollment {
now_unix: i64,
root: EnrollmentRoot,
) -> Result<VerifiedChallenge, EnrollmentError> {
let envelope: SignedDocument = serde_json::from_slice(document).map_err(|_| EnrollmentError::MalformedDocument)?;
let envelope: UnverifiedSignedDocument =
serde_json::from_slice(document).map_err(|_| EnrollmentError::MalformedDocument)?;
// Decode the exact transmitted octets before reading any routing field.
// Standard padded base64 is canonical in this protocol: accepting an
@@ -385,7 +401,17 @@ impl OfflineEnrollment {
// 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 signature_member = |value: Option<serde_json::Value>| {
value
.and_then(|value| value.as_str().map(str::to_owned))
.ok_or(EnrollmentError::SignatureMalformed)
};
let signature_document = DocumentSignature {
algorithm: signature_member(envelope.signature.algorithm)?,
key_id: signature_member(envelope.signature.key_id)?,
value: signature_member(envelope.signature.value)?,
};
let signature = decode_signature(&signature_document)?;
let first = routing.trust_chain.first().ok_or(EnrollmentError::MalformedDocument)?;
let first_bytes = first
@@ -413,7 +439,7 @@ impl OfflineEnrollment {
// 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
// failure, not an opportunity to ignore the binding.
if envelope.signature.key_id != routing.connect_key_id || !verifies(&connect_key, TAG_CHALLENGE, &bytes, &signature) {
if signature_document.key_id != routing.connect_key_id || !verifies(&connect_key, TAG_CHALLENGE, &bytes, &signature) {
return Err(EnrollmentError::SignatureInvalid);
}
@@ -438,7 +464,7 @@ impl OfflineEnrollment {
issued_at: challenge.issued_at,
expires_at: challenge.expires_at,
connect_key_id: challenge.connect_key_id,
challenge_proof: envelope.signature.value,
challenge_proof: signature_document.value,
})
}
@@ -357,6 +357,41 @@ fn every_challenge_accept_vector_verifies_and_exposes_the_signed_fields() {
);
}
#[test]
fn malformed_top_level_signature_members_keep_the_frozen_reason() {
let vector = accept_vector_named("challenge signed by a chained signing key under the pinned root");
let now = unix(field(&vector, "evaluationTime"));
for (member, malformed) in [
("algorithm", serde_json::json!(1)),
("keyId", serde_json::Value::Null),
("value", serde_json::json!([])),
] {
let mut document = vector["document"].clone();
document["signature"][member] = malformed;
let error = OfflineEnrollment::verify_challenge(&envelope(&document), now)
.expect_err(&format!("a malformed top-level signature {member} must be refused"));
assert_eq!(
error.reason(),
"SIGNATURE_MALFORMED",
"a malformed top-level signature {member} keeps the frozen classifier"
);
}
}
#[test]
fn duplicate_top_level_signature_members_are_refused() {
let vector = accept_vector_named("challenge signed by a chained signing key under the pinned root");
let document = String::from_utf8(envelope(&vector["document"])).expect("envelope is UTF-8 JSON");
let duplicate = document.replacen("\"algorithm\":\"ES256\"", "\"algorithm\":\"ES384\",\"algorithm\":\"ES256\"", 1);
assert_ne!(duplicate, document, "the accepted vector carries the expected algorithm");
let error = OfflineEnrollment::verify_challenge(duplicate.as_bytes(), unix(field(&vector, "evaluationTime")))
.expect_err("a duplicate top-level signature member must be refused");
assert_eq!(error.reason(), "DOCUMENT_MALFORMED");
}
#[cfg(feature = "offline-enrollment-e2e-root")]
#[test]
fn e2e_root_is_fixed_and_disjoint_from_the_hosted_root() {