Skip to main content

structured_pkix/
hybrid.rs

1//! X.509 alternative signatures, independent of native mathematics and trust.
2//!
3//! ITU-T X.509 (2019) §§7.2.2, 7.10.3 and 9.8 define certificate/CRL
4//! extensions. PKCS#10 uses the single-valued attribute convention implemented
5//! by Bouncy Castle, not an extension to RFC2986's normative grammar.
6//! Native verification remains unchanged unless a caller selects this API.
7//! <https://www.itu.int/rec/T-REC-X.509-201910-I/en>
8
9use crate::algorithm::{AlgorithmIdentifier, ml_dsa};
10use crate::error::{CallbackError, CallbackResult, Located};
11use crate::path::{Verified, Verify, WorkingKey};
12use crate::signature::SignatureValue;
13use crate::{Error, ErrorCode, PublicKeyInfo, Result};
14use structured_asn1::memory::Work;
15use structured_asn1::{Oid, ScopedReader, Tag};
16
17mod components;
18mod preimage;
19pub mod request;
20pub use components::{Component, Components};
21pub use preimage::Preimage;
22
23/// X.509 §9.8.2, the subject's alternative SPKI.
24pub const SUBJECT_ALT_PUBLIC_KEY_INFO: Oid<'static> = Oid::from_bytes(&[0x55, 0x1d, 72]);
25/// X.509 §9.8.3, the issuer's alternative signature algorithm.
26pub const ALT_SIGNATURE_ALGORITHM: Oid<'static> = Oid::from_bytes(&[0x55, 0x1d, 73]);
27/// X.509 §9.8.4, the issuer's alternative signature value.
28pub const ALT_SIGNATURE_VALUE: Oid<'static> = Oid::from_bytes(&[0x55, 0x1d, 74]);
29const FIELD: &str = "hybrid";
30
31/// Allocation-free, located conformance diagnostic. Resource errors never enter it.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Fault {
34    code: ErrorCode,
35    field: &'static str,
36    offset: u64,
37}
38impl Fault {
39    /// Recover the original located error without allocating.
40    #[must_use]
41    pub fn error(self) -> Error {
42        Error::new(self.code, self.field, self.offset)
43    }
44    pub(crate) fn of(error: &Error) -> Self {
45        Self {
46            code: error.code(),
47            field: error.field(),
48            offset: error.offset(),
49        }
50    }
51}
52
53/// Alternative outcome, without conflating bad mathematics and bad encoding.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum Alternative {
56    /// The selected verifier completed its mathematical check.
57    Verified(Verified),
58    /// Required source facts are missing or malformed; no alternative callback ran.
59    Nonconforming(Fault),
60}
61
62/// Independent completed signature facts, not a trust or enrollment decision.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct Signatures {
65    /// Native signature result, even when the alternative is missing or invalid.
66    pub native: Verified,
67    /// Alternative signature or located conformance result.
68    pub alternative: Alternative,
69}
70
71/// Caller-selected requirement, fixed before checking any mathematical result.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum Requirement {
74    /// Require the alternative signature independently of native mathematics.
75    Alternative,
76    /// Require both signatures, for example for two-key enrollment.
77    Both,
78}
79impl Requirement {
80    /// Completed mathematical acceptance only; path/usage/trust remain separate.
81    #[must_use]
82    pub fn accepts(self, result: Signatures) -> bool {
83        result.alternative == Alternative::Verified(Verified::Yes)
84            && (self == Self::Alternative || result.native == Verified::Yes)
85    }
86    pub(crate) fn answer(self, result: Signatures) -> Verified {
87        if self.accepts(result) {
88            return Verified::Yes;
89        }
90        if result.alternative == Alternative::Verified(Verified::Unsupported)
91            || (self == Self::Both && result.native == Verified::Unsupported)
92        {
93            Verified::Unsupported
94        } else {
95            Verified::No
96        }
97    }
98}
99
100/// An injected alternative-signature verifier. No algorithm engine is included.
101pub trait AlternativeVerifier<W: Work> {
102    /// Original provider/resource cause, never a boolean mismatch.
103    type Error;
104    /// Verify the exact raw stream. For RFC9881 ML-DSA this is pure mode with
105    /// empty context, not HashML-DSA or a digest passed as the message. Admit
106    /// key work through `key.execution_in(work)` and input through `visit`;
107    /// the provider owns transitive backend allocation/work qualification.
108    fn verify(
109        &self,
110        algorithm: AlgorithmIdentifier<'_>,
111        key: PublicKeyInfo<'_>,
112        preimage: &Preimage<'_>,
113        signature: SignatureValue<'_>,
114        work: &W,
115    ) -> core::result::Result<Verified, Self::Error>;
116}
117
118/// Processing and independent callback errors keep their original causes.
119#[derive(Debug)]
120pub enum CheckError<N, A> {
121    /// Located processing/resource refusal.
122    Processing(Error),
123    /// Native verifier failed to execute.
124    Native(N),
125    /// Alternative verifier failed to execute.
126    Alternative(A),
127}
128impl<N, A> From<Error> for CheckError<N, A> {
129    fn from(error: Error) -> Self {
130        Self::Processing(error)
131    }
132}
133
134impl<N: core::fmt::Display, A: core::fmt::Display> core::fmt::Display for CheckError<N, A> {
135    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136        match self {
137            Self::Processing(error) => error.fmt(f),
138            Self::Native(error) => write!(f, "native verifier: {error}"),
139            Self::Alternative(error) => write!(f, "alternative verifier: {error}"),
140        }
141    }
142}
143#[cfg(feature = "std")]
144impl<N: std::error::Error + 'static, A: std::error::Error + 'static> std::error::Error
145    for CheckError<N, A>
146{
147    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
148        match self {
149            Self::Processing(error) => Some(error),
150            Self::Native(error) => Some(error),
151            Self::Alternative(error) => Some(error),
152        }
153    }
154}
155
156/// Strict alternative signature fields and exact preimage, still not mathematics.
157#[derive(Debug)]
158pub struct Prepared<'a> {
159    algorithm: AlgorithmIdentifier<'a>,
160    signature: SignatureValue<'a>,
161    preimage: Preimage<'a>,
162}
163impl<'a> Prepared<'a> {
164    /// Algorithm protected by the alternative signature.
165    #[must_use]
166    pub fn algorithm(&self) -> AlgorithmIdentifier<'a> {
167        self.algorithm
168    }
169    /// Original alternative signature and producing source.
170    #[must_use]
171    pub fn signature(&self) -> SignatureValue<'a> {
172        self.signature
173    }
174    /// DER stream with original payload ranges and synthetic framing.
175    #[must_use]
176    pub fn preimage(&self) -> &Preimage<'a> {
177        &self.preimage
178    }
179
180    /// Verify with the issuer's authenticated alternative key, not the subject key.
181    pub fn verify<W: Work, V: AlternativeVerifier<W>>(
182        &self,
183        issuer_key: PublicKeyInfo<'_>,
184        verifier: &V,
185        work: &W,
186    ) -> CallbackResult<Verified, V::Error> {
187        // All ML-DSA profile/framing decisions precede the callback. The
188        // provider checks coefficients, key validity and the actual signature.
189        issuer_key
190            .execution_in(work)
191            .charge_work(1)
192            .map_err(|e| Error::resource(e, FIELD, issuer_key.key_offset()))?;
193        if let Some(set) = ml_dsa::resolve(self.algorithm, work)? {
194            let key = crate::key::ml_dsa::read(issuer_key, work)?;
195            if key.parameter_set() != set {
196                return Err(Error::new(
197                    ErrorCode::AlgorithmMismatch,
198                    FIELD,
199                    issuer_key.key_offset(),
200                )
201                .into());
202            }
203            self.signature.ml_dsa_in(set, work)?;
204        }
205        verifier
206            .verify(
207                self.algorithm,
208                issuer_key,
209                &self.preimage,
210                self.signature,
211                work,
212            )
213            .map_err(CallbackError::Callback)
214    }
215}
216
217/// Inspect the original alternative certificate extensions, including absences.
218pub fn inspect_certificate<'a, W: Work>(
219    certificate: &crate::Certificate<'a>,
220    work: &W,
221) -> Result<Components<'a>> {
222    components::extensions(
223        certificate.tbs().extensions(),
224        &certificate.execution_in(work),
225        certificate.span().start,
226    )
227}
228
229/// Prepare the X.509 §7.2.2 preTBSCertificate, requiring all three components.
230pub fn prepare_certificate<'a, W: Work>(
231    certificate: &crate::Certificate<'a>,
232    work: &W,
233) -> Result<Prepared<'a>> {
234    prepare_certificate_key(certificate, work).map(|(prepared, _)| prepared)
235}
236pub(crate) fn prepare_certificate_key<'a, W: Work>(
237    certificate: &crate::Certificate<'a>,
238    work: &W,
239) -> Result<(Prepared<'a>, PublicKeyInfo<'a>)> {
240    let components = components::extensions(
241        certificate.tbs().extensions(),
242        &certificate.execution_in(work),
243        certificate.span().start,
244    )?;
245    prepare_certificate_components(certificate, &components, work)
246}
247
248pub(crate) fn prepare_certificate_components<'a, W: Work>(
249    certificate: &crate::Certificate<'a>,
250    components: &Components<'a>,
251    work: &W,
252) -> Result<(Prepared<'a>, PublicKeyInfo<'a>)> {
253    let key = components.key.require()?;
254    validate_key(key, work)?;
255    let (algorithm, signature, remove) = components.signature_fields()?;
256    let reader = certificate.tbs().reader_scoped(work)?;
257    let preimage = preimage::extensions(reader, certificate.tbs().signature(), 3, remove)?;
258    Ok((
259        Prepared {
260            algorithm,
261            signature,
262            preimage,
263        },
264        key,
265    ))
266}
267
268pub(crate) fn critical_key_understood<W: Work, R: crate::Recognizer<W>>(
269    components: &Components<'_>,
270    recognizer: &R,
271    work: &W,
272) -> CallbackResult<bool, R::Error> {
273    if components.key.critical() != Some(true) {
274        return Ok(true);
275    }
276    let key = components.key.require()?;
277    // X.509(2019)§9.8.2: critical alternative SPKI requires understanding
278    // its public-key algorithm too, not merely the extension's OID.
279    // https://www.itu.int/rec/T-REC-X.509-201910-I/en
280    if ml_dsa::resolve(key.algorithm(), work)?.is_some() {
281        crate::key::ml_dsa::read(key, work)?;
282        return Ok(true);
283    }
284    key.execution_in(work)
285        .charge_work(1)
286        .map_err(|e| Error::resource(e, FIELD, key.key_offset()))?;
287    recognizer
288        .recognizes(SUBJECT_ALT_PUBLIC_KEY_INFO, work)
289        .map_err(CallbackError::Callback)
290}
291
292/// Inspect CRL alternative algorithm/value; CRLs do not carry a subject key.
293pub fn inspect_crl<'a, W: Work>(
294    crl: &crate::crl::CertificateList<'a>,
295    work: &W,
296) -> Result<Components<'a>> {
297    components::extensions(
298        crl.tbs().extensions(),
299        &crl.execution_in(work),
300        crl.span().start,
301    )
302}
303
304/// Prepare the X.509 §7.10.3 preTBSCertList, excluding its native algorithm.
305pub fn prepare_crl<'a, W: Work>(
306    crl: &crate::crl::CertificateList<'a>,
307    work: &W,
308) -> Result<Prepared<'a>> {
309    let components = inspect_crl(crl, work)?;
310    let (algorithm, signature, remove) = components.signature_fields()?;
311    let reader = crl.tbs().reader_scoped(work)?;
312    let preimage = preimage::extensions(reader, crl.tbs().signature(), 0, remove)?;
313    Ok(Prepared {
314        algorithm,
315        signature,
316        preimage,
317    })
318}
319
320fn alternative<W: Work, V: AlternativeVerifier<W>>(
321    prepared: Result<Prepared<'_>>,
322    key: PublicKeyInfo<'_>,
323    verifier: &V,
324    work: &W,
325) -> CallbackResult<Alternative, V::Error> {
326    let result = match prepared {
327        Ok(prepared) => prepared
328            .verify(key, verifier, work)
329            .map(Alternative::Verified),
330        Err(error) => Err(CallbackError::Processing(error)),
331    };
332    match result {
333        Err(CallbackError::Processing(error)) if !terminal(&error) => {
334            Ok(Alternative::Nonconforming(Fault::of(&error)))
335        }
336        other => other,
337    }
338}
339
340pub(super) fn validate_key<W: Work>(key: PublicKeyInfo<'_>, work: &W) -> Result<()> {
341    if ml_dsa::resolve(key.algorithm(), work)?.is_some() {
342        crate::key::ml_dsa::read(key, work)?;
343    }
344    Ok(())
345}
346pub(crate) fn terminal(error: &Error) -> bool {
347    error.is_resource() || error.code() == ErrorCode::ForeignReader
348}
349
350/// Check both mathematical signatures without short-circuiting on native No or Unsupported.
351pub fn check_certificate<W: Work, N: Verify<W>, A: AlternativeVerifier<W>>(
352    certificate: &crate::Certificate<'_>,
353    native_key: WorkingKey<'_>,
354    alternative_key: PublicKeyInfo<'_>,
355    native: &N,
356    verifier: &A,
357    work: &W,
358) -> core::result::Result<Signatures, CheckError<N::Error, A::Error>> {
359    certificate
360        .execution_in(work)
361        .charge_work(1)
362        .map_err(|e| Error::resource(e, FIELD, certificate.span().start))?;
363    native_key
364        .key()
365        .execution_in(work)
366        .charge_work(1)
367        .map_err(|e| Error::resource(e, FIELD, native_key.key().key_offset()))?;
368    let result = native
369        .verify(
370            &certificate.signature_algorithm(),
371            native_key,
372            certificate.signed_bytes(),
373            certificate.signature_value(),
374            work,
375        )
376        .map_err(CheckError::Native)?;
377    let alternative = alternative(
378        prepare_certificate(certificate, work),
379        alternative_key,
380        verifier,
381        work,
382    )
383    .map_err(|e| match e {
384        CallbackError::Processing(e) => CheckError::Processing(e),
385        CallbackError::Callback(e) => CheckError::Alternative(e),
386    })?;
387    Ok(Signatures {
388        native: result,
389        alternative,
390    })
391}
392
393/// Check both CRL signatures with caller-authenticated issuer keys. This does
394/// not replace issuer-name, cRLSign, freshness, scope or revocation processing.
395pub fn check_crl<W: Work, N: Verify<W>, A: AlternativeVerifier<W>>(
396    crl: &crate::crl::CertificateList<'_>,
397    native_key: WorkingKey<'_>,
398    alternative_key: PublicKeyInfo<'_>,
399    native: &N,
400    verifier: &A,
401    work: &W,
402) -> core::result::Result<Signatures, CheckError<N::Error, A::Error>> {
403    crl.execution_in(work)
404        .charge_work(1)
405        .map_err(|e| Error::resource(e, FIELD, crl.span().start))?;
406    native_key
407        .key()
408        .execution_in(work)
409        .charge_work(1)
410        .map_err(|e| Error::resource(e, FIELD, native_key.key().key_offset()))?;
411    let result = native
412        .verify(
413            &crl.signature_algorithm(),
414            native_key,
415            crl.signed_bytes(),
416            crl.signature_value(),
417            work,
418        )
419        .map_err(CheckError::Native)?;
420    let alternative = alternative(prepare_crl(crl, work), alternative_key, verifier, work)
421        .map_err(|e| match e {
422            CallbackError::Processing(e) => CheckError::Processing(e),
423            CallbackError::Callback(e) => CheckError::Alternative(e),
424        })?;
425    Ok(Signatures {
426        native: result,
427        alternative,
428    })
429}
430
431fn required<'a>(
432    reader: &mut ScopedReader<'a, '_>,
433    tag: Tag,
434) -> Result<structured_asn1::Element<'a>> {
435    reader.expect(tag).field(FIELD)
436}
437
438fn identifier_work<W: Work>(kind: Oid<'_>, work: &W, at: u64) -> Result<()> {
439    let units = (kind.as_bytes().len() as u64)
440        .checked_mul(3)
441        .and_then(|n| n.checked_add(1))
442        .ok_or_else(|| {
443            Error::resource(
444                structured_asn1::memory::MemoryError::WorkExceeded,
445                FIELD,
446                at,
447            )
448        })?;
449    work.charge_work(units)
450        .map_err(|e| Error::resource(e, FIELD, at))
451}