Skip to main content

xml_sec/provider/
key_association.rs

1//! Selected-provider evidence, separate from key authorization and trust.
2
3use crate::xmldsig::{DsigError, SignatureAlgorithm, SignatureContext, VerifyingKey};
4use structured_asn1::memory::MemoryError;
5use structured_pkix::path::WorkingKey;
6
7/// Public-key association established by the originally selected provider.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum KeyAssociation {
10    /// The expected effective public key is the successful verification key.
11    Same,
12    /// Public-key semantics establish a different key.
13    Mismatch,
14    /// Available public evidence cannot establish either conclusion.
15    Unproven,
16}
17
18/// Borrowed input to one original signature verification, never a second pass.
19#[derive(Clone, Copy)]
20pub struct SignatureVerificationInput<'a> {
21    /// Originally parsed method and effective parameters.
22    pub algorithm: SignatureAlgorithm,
23    /// Originally parsed domain separation.
24    pub context: &'a SignatureContext,
25    /// Original canonical SignedInfo bytes.
26    pub data: &'a [u8],
27    /// Original decoded SignatureValue.
28    pub signature: &'a [u8],
29}
30
31/// Provider observations; only a successful leaf can supply key evidence.
32pub enum KeyVerificationEvent<'a> {
33    /// Admit another candidate dispatch; a previous terminal refusal stays terminal.
34    BeforeCandidate {
35        /// Nested composite depth, bounded by the hard candidate ceiling.
36        depth: usize,
37    },
38    /// This exact leaf returned a successful cryptographic verification.
39    Verified(&'a dyn VerifyingKey),
40}
41
42pub(super) fn observed_leaf<F, O>(
43    key: &dyn VerifyingKey,
44    observer: &mut O,
45    verify_leaf: &F,
46) -> Result<bool, DsigError>
47where
48    F: Fn(&dyn VerifyingKey) -> Result<bool, DsigError>,
49    O: FnMut(KeyVerificationEvent<'_>) -> Result<(), DsigError> + ?Sized,
50{
51    let mut admitted = 0;
52    let mut observer_refused = false;
53    let result = observed_at_depth(
54        key,
55        &mut |event| {
56            if observer_refused {
57                return Err(MemoryError::WorkExceeded.into());
58            }
59            let result = observer(event);
60            if matches!(
61                result,
62                Err(DsigError::SharedExecution(MemoryError::WorkExceeded))
63            ) {
64                observer_refused = true;
65            }
66            result
67        },
68        verify_leaf,
69        0,
70        &mut admitted,
71    );
72    // A composite may recover from a visitor error, but cannot recover capacity
73    // after a terminal hard refusal and report an unchecked later success.
74    if observer_refused || admitted > crate::hard_limits::KEY_CANDIDATE_CEILING {
75        return Err(MemoryError::WorkExceeded.into());
76    }
77    result
78}
79
80fn observed_at_depth<F, O>(
81    key: &dyn VerifyingKey,
82    observer: &mut O,
83    verify_leaf: &F,
84    depth: usize,
85    admitted: &mut usize,
86) -> Result<bool, DsigError>
87where
88    F: Fn(&dyn VerifyingKey) -> Result<bool, DsigError>,
89    O: FnMut(KeyVerificationEvent<'_>) -> Result<(), DsigError> + ?Sized,
90{
91    if depth == crate::hard_limits::KEY_CANDIDATE_CEILING
92        || *admitted >= crate::hard_limits::KEY_CANDIDATE_CEILING
93    {
94        *admitted = crate::hard_limits::KEY_CANDIDATE_CEILING + 1;
95        return Err(MemoryError::WorkExceeded.into());
96    }
97    *admitted += 1;
98    observer(KeyVerificationEvent::BeforeCandidate { depth })?;
99    if let Some(result) = key.verify_candidate_keys(&mut |candidate| {
100        observed_at_depth(candidate, observer, verify_leaf, depth + 1, admitted)
101    })? {
102        return Ok(result);
103    }
104    let verified = verify_leaf(key)?;
105    if verified {
106        observer(KeyVerificationEvent::Verified(key))?;
107    }
108    Ok(verified)
109}
110
111#[cfg(any(feature = "aws-lc-fips", feature = "pkcs11"))]
112pub(super) fn compare(
113    actual: &[u8],
114    expected: WorkingKey<'_>,
115    charge: &dyn Fn(u64) -> Result<(), MemoryError>,
116) -> Result<KeyAssociation, DsigError> {
117    compare_with_points(actual, expected, charge, false)
118}
119
120pub(super) fn compare_rustcrypto(
121    actual: &[u8],
122    expected: WorkingKey<'_>,
123    charge: &dyn Fn(u64) -> Result<(), MemoryError>,
124) -> Result<KeyAssociation, DsigError> {
125    compare_with_points(actual, expected, charge, true)
126}
127
128fn compare_with_points(
129    actual: &[u8],
130    expected: WorkingKey<'_>,
131    charge: &dyn Fn(u64) -> Result<(), MemoryError>,
132    rustcrypto_points: bool,
133) -> Result<KeyAssociation, DsigError> {
134    use der::{Decode as _, Tagged as _};
135    // Borrowed DER decoding and comparisons are linear in the supplied source;
136    // admit their complete byte scans before entering either codec.
137    let size = actual
138        .len()
139        .checked_add(expected.key().as_bytes().len())
140        .and_then(|size| size.checked_add(expected.parameters().map_or(0, <[u8]>::len)))
141        .and_then(|size| size.checked_mul(8))
142        .and_then(|size| size.checked_add(256))
143        .and_then(|size| u64::try_from(size).ok())
144        .ok_or(MemoryError::WorkExceeded)?;
145    charge(size)?;
146    let Ok(actual) = pkcs8::SubjectPublicKeyInfoRef::from_der(actual) else {
147        return Ok(KeyAssociation::Unproven);
148    };
149    let Some(bits) = actual.subject_public_key.as_bytes() else {
150        return Ok(KeyAssociation::Unproven);
151    };
152    let Some(expected_bits) = expected.key().key_octets() else {
153        return Ok(KeyAssociation::Unproven);
154    };
155    let algorithm = expected.key().algorithm();
156    let rsa_pss = der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.10");
157    // RFC 4055 §3.1: PSS parameters are absent or RSASSA-PSS-params, never
158    // NULL. PSS does not inherit parameters; retain the original field rather
159    // than the generic WorkingKey projection which treats NULL as absent.
160    // https://www.rfc-editor.org/rfc/rfc4055#section-3.1
161    let effective_parameters = if algorithm.algorithm().as_bytes() == rsa_pss.as_bytes() {
162        algorithm.parameters()
163    } else {
164        expected.parameters()
165    };
166    let parameters = match effective_parameters {
167        Some(bytes) => match der::asn1::AnyRef::from_der(bytes) {
168            Ok(value) => Some(value),
169            Err(_) => return Ok(KeyAssociation::Unproven),
170        },
171        None => None,
172    };
173    let expected_oid = algorithm.algorithm().as_bytes();
174    let actual_rsa =
175        actual.algorithm.oid == pkcs1::ALGORITHM_OID || actual.algorithm.oid == rsa_pss;
176    let expected_rsa =
177        expected_oid == pkcs1::ALGORITHM_OID.as_bytes() || expected_oid == rsa_pss.as_bytes();
178    if actual_rsa && expected_rsa {
179        // RFC 3279 §2.3.1: RSA key identity consists of n and e. Omitted
180        // versus NULL parameters are tolerated by the existing RSA codec.
181        // https://www.rfc-editor.org/rfc/rfc3279#section-2.3.1
182        if (actual.algorithm.oid == pkcs1::ALGORITHM_OID
183            && actual
184                .algorithm
185                .parameters
186                .is_some_and(|value| !value.is_null()))
187            || (expected_oid == pkcs1::ALGORITHM_OID.as_bytes()
188                && parameters.is_some_and(|value| !value.is_null()))
189            || (actual.algorithm.oid == rsa_pss
190                && actual
191                    .algorithm
192                    .parameters
193                    .is_some_and(|value| value.tag() != der::Tag::Sequence))
194            || (expected_oid == rsa_pss.as_bytes()
195                && parameters.is_some_and(|value| value.tag() != der::Tag::Sequence))
196        {
197            return Ok(KeyAssociation::Unproven);
198        }
199        let components = rsa_components(bits, expected_bits);
200        // RFC 4055 §3.3 constrains PSS use independently of n/e. Comparing
201        // components across rsaEncryption/PSS can prove a different key, but
202        // equal components do not prove equal effective algorithm restrictions.
203        // This Same/Mismatch/Unproven classification is product reporting.
204        // https://www.rfc-editor.org/rfc/rfc4055#section-3.3
205        return Ok(
206            if components == KeyAssociation::Same
207                && (actual.algorithm.oid.as_bytes() != expected_oid
208                    || (actual.algorithm.oid == rsa_pss
209                        && actual.algorithm.parameters != parameters))
210            {
211                KeyAssociation::Unproven
212            } else {
213                components
214            },
215        );
216    }
217    if actual.algorithm.oid.as_bytes() != expected_oid {
218        return Ok(KeyAssociation::Unproven);
219    }
220    let dsa = der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10040.4.1");
221    if actual.algorithm.oid == dsa {
222        // RFC 3279 §2.3.2: y alone is not a DSA key; the effective p/q/g
223        // may come from the checked path rather than the subject's SPKI.
224        // https://www.rfc-editor.org/rfc/rfc3279#section-2.3.2
225        let (Some(actual_params), Some(expected_params)) =
226            (actual.algorithm.parameters, parameters)
227        else {
228            return Ok(KeyAssociation::Unproven);
229        };
230        let (Ok(actual_params), Ok(expected_params), Ok(actual_y), Ok(expected_y)) = (
231            actual_params.decode_as::<crate::xmldsig::signature::BorrowedDsaPublicParameters<'_>>(),
232            expected_params
233                .decode_as::<crate::xmldsig::signature::BorrowedDsaPublicParameters<'_>>(),
234            der::asn1::UintRef::from_der(bits),
235            der::asn1::UintRef::from_der(expected_bits),
236        ) else {
237            return Ok(KeyAssociation::Unproven);
238        };
239        return Ok(
240            if actual_y == expected_y
241                && actual_params.p == expected_params.p
242                && actual_params.q == expected_params.q
243                && actual_params.g == expected_params.g
244            {
245                KeyAssociation::Same
246            } else {
247                KeyAssociation::Mismatch
248            },
249        );
250    }
251    #[cfg(feature = "experimental-pq")]
252    if rustcrypto_points
253        && let Some(algorithm) = crate::xmldsig::PqAlgorithm::from_oid(actual.algorithm.oid)
254    {
255        // RFC 9881 §§2,4 / RFC 9909 §§3,5 require absent parameters and
256        // fixed-width raw public encodings. Validate both before byte equality;
257        // identical malformed encodings do not establish key association.
258        // https://www.rfc-editor.org/rfc/rfc9881.html#section-4
259        // https://www.rfc-editor.org/rfc/rfc9909.html#section-5
260        return Ok(
261            if actual.algorithm.parameters.is_some()
262                || expected.key().algorithm().parameters().is_some()
263                || parameters.is_some()
264                || !valid_pq_public_encoding(algorithm, bits)
265                || !valid_pq_public_encoding(algorithm, expected_bits)
266            {
267                KeyAssociation::Unproven
268            } else if bits == expected_bits {
269                KeyAssociation::Same
270            } else {
271                KeyAssociation::Mismatch
272            },
273        );
274    }
275    if rustcrypto_points
276        && (actual.algorithm.oid == ed25519_dalek::pkcs8::ALGORITHM_OID
277            || actual.algorithm.oid == ed448_goldilocks::ALGORITHM_OID)
278    {
279        // RFC 8410 §§3–4 requires absent parameters and encoded public-key
280        // octets; RFC 8032 §§5.1.3/5.2.3 defines their point encoding. Decode
281        // only inside the selected RustCrypto provider, never as native fallback.
282        // https://www.rfc-editor.org/rfc/rfc8410#section-4
283        // https://www.rfc-editor.org/rfc/rfc8032#section-5.1.3
284        // WorkingKey's generic effective projection treats NULL as absent;
285        // the original encoding is still governed by RFC 8410 §3.
286        // https://www.rfc-editor.org/rfc/rfc8410#section-3
287        if actual.algorithm.parameters.is_some()
288            || expected.key().algorithm().parameters().is_some()
289            || parameters.is_some()
290        {
291            return Ok(KeyAssociation::Unproven);
292        }
293        macro_rules! compare_edwards {
294            ($key:ty, $length:expr, $width:expr, $point:ident, $encoded:expr) => {{
295                let (Ok(left), Ok(right)) = (
296                    <&[u8; $length]>::try_from(bits),
297                    <&[u8; $length]>::try_from(expected_bits),
298                ) else {
299                    return Ok(KeyAssociation::Unproven);
300                };
301                charge($width * $width * 8)?;
302                let Ok(left) = <$key>::from_bytes(left) else {
303                    return Ok(KeyAssociation::Unproven);
304                };
305                let $point = left.to_edwards();
306                let left = $encoded;
307                // Point decoders may accept noncanonical encodings. Such wire
308                // aliases are not validated RFC 8032 public-key encodings.
309                if left.as_slice() != bits {
310                    KeyAssociation::Unproven
311                } else if bits == expected_bits {
312                    // Reuse the first decoded point for identical input.
313                    KeyAssociation::Same
314                } else {
315                    charge($width * $width * 8)?;
316                    let Ok(right) = <$key>::from_bytes(right) else {
317                        return Ok(KeyAssociation::Unproven);
318                    };
319                    let $point = right.to_edwards();
320                    let right = $encoded;
321                    if right.as_slice() != expected_bits {
322                        KeyAssociation::Unproven
323                    } else {
324                        KeyAssociation::Mismatch
325                    }
326                }
327            }};
328        }
329        return Ok(
330            if actual.algorithm.oid == ed25519_dalek::pkcs8::ALGORITHM_OID {
331                compare_edwards!(
332                    ed25519_dalek::VerifyingKey,
333                    32,
334                    256,
335                    point,
336                    point.compress().to_bytes()
337                )
338            } else {
339                compare_edwards!(
340                    ed448_goldilocks::VerifyingKey,
341                    57,
342                    448,
343                    point,
344                    point.to_affine().compress().0
345                )
346            },
347        );
348    }
349    if actual.algorithm.parameters != parameters {
350        // RFC 5280 §6.1.3 preserves effective working parameters. Method
351        // compatibility alone does not prove equal PSS restrictions or groups.
352        // https://www.rfc-editor.org/rfc/rfc5280#section-6.1.3
353        return Ok(KeyAssociation::Unproven);
354    }
355    // Exact original algorithm, effective parameters and public octets prove
356    // association without re-encoding, fingerprints or another signature check.
357    if bits == expected_bits {
358        return Ok(KeyAssociation::Same);
359    }
360    if rustcrypto_points && actual.algorithm.oid == p256::elliptic_curve::ALGORITHM_OID {
361        let Some(curve) =
362            parameters.and_then(|value| value.decode_as::<der::asn1::ObjectIdentifier>().ok())
363        else {
364            return Ok(KeyAssociation::Unproven);
365        };
366        // RFC 5480 §2.2 permits compressed/uncompressed points. Only the
367        // selected RustCrypto provider uses its own point decoder here.
368        // https://www.rfc-editor.org/rfc/rfc5480#section-2.2
369        macro_rules! compare_points {
370            ($curve:ty, $bits:expr) => {{
371                // Two fixed-width point decodings/comparison have a fixed
372                // field-arithmetic admission, independent of XML length.
373                charge($bits * $bits * 16)?;
374                match (
375                    <$curve>::from_sec1_bytes(bits),
376                    <$curve>::from_sec1_bytes(expected_bits),
377                ) {
378                    (Ok(left), Ok(right)) => {
379                        if left == right {
380                            KeyAssociation::Same
381                        } else {
382                            KeyAssociation::Mismatch
383                        }
384                    }
385                    _ => KeyAssociation::Unproven,
386                }
387            }};
388        }
389        return Ok(
390            if curve == der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7") {
391                compare_points!(p256::PublicKey, 256)
392            } else if curve == der::asn1::ObjectIdentifier::new_unwrap("1.3.132.0.34") {
393                compare_points!(p384::PublicKey, 384)
394            } else if curve == der::asn1::ObjectIdentifier::new_unwrap("1.3.132.0.35") {
395                compare_points!(p521::PublicKey, 521)
396            } else {
397                KeyAssociation::Unproven
398            },
399        );
400    }
401    Ok(KeyAssociation::Unproven)
402}
403
404#[cfg(feature = "experimental-pq")]
405fn valid_pq_public_encoding(algorithm: crate::xmldsig::PqAlgorithm, bytes: &[u8]) -> bool {
406    use crate::rustcrypto_ml_dsa as ml_dsa;
407    use crate::xmldsig::PqAlgorithm;
408    // FIPS 204 §7.2 Algorithms 22–23: pkDecode is a bijection over the
409    // fixed-size rho/t1 encoding (every packed 10-bit coefficient is valid).
410    // Admission uses the selected backend's borrowed encoded-key type, not
411    // matrix expansion, key cloning or a second signature verification.
412    // https://doi.org/10.6028/NIST.FIPS.204
413    fn ml<P: ml_dsa::MlDsaParams>(bytes: &[u8]) -> bool {
414        <&ml_dsa::EncodedVerifyingKey<P>>::try_from(bytes).is_ok()
415    }
416    // FIPS 205 §9.1 / RFC 9909 §5: every seed||root of width 2*n is a
417    // public encoding; backend-associated width validation needs no copy.
418    // https://www.rfc-editor.org/rfc/rfc9909.html#section-5
419    fn slh<P: slh_dsa::ParameterSet>(bytes: &[u8]) -> bool {
420        <&hybrid_array::Array<u8, P::VkLen>>::try_from(bytes).is_ok()
421    }
422    match algorithm {
423        PqAlgorithm::MlDsa44 => ml::<ml_dsa::MlDsa44>(bytes),
424        PqAlgorithm::MlDsa65 => ml::<ml_dsa::MlDsa65>(bytes),
425        PqAlgorithm::MlDsa87 => ml::<ml_dsa::MlDsa87>(bytes),
426        PqAlgorithm::SlhDsaSha2_128f => slh::<slh_dsa::Sha2_128f>(bytes),
427        PqAlgorithm::SlhDsaSha2_128s => slh::<slh_dsa::Sha2_128s>(bytes),
428        PqAlgorithm::SlhDsaSha2_192f => slh::<slh_dsa::Sha2_192f>(bytes),
429        PqAlgorithm::SlhDsaSha2_192s => slh::<slh_dsa::Sha2_192s>(bytes),
430        PqAlgorithm::SlhDsaSha2_256f => slh::<slh_dsa::Sha2_256f>(bytes),
431        PqAlgorithm::SlhDsaSha2_256s => slh::<slh_dsa::Sha2_256s>(bytes),
432    }
433}
434
435fn rsa_components(actual: &[u8], expected: &[u8]) -> KeyAssociation {
436    use der::Decode as _;
437    let (Ok(actual), Ok(expected)) = (
438        pkcs1::RsaPublicKeyRef::from_der(actual),
439        pkcs1::RsaPublicKeyRef::from_der(expected),
440    ) else {
441        return KeyAssociation::Unproven;
442    };
443    if actual.modulus == expected.modulus && actual.public_exponent == expected.public_exponent {
444        KeyAssociation::Same
445    } else {
446        KeyAssociation::Mismatch
447    }
448}