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}
143impl<N: core::error::Error + 'static, A: core::error::Error + 'static> core::error::Error
144    for CheckError<N, A>
145{
146    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
147        match self {
148            Self::Processing(error) => Some(error),
149            Self::Native(error) => Some(error),
150            Self::Alternative(error) => Some(error),
151        }
152    }
153}
154
155/// Strict alternative signature fields and exact preimage, still not mathematics.
156#[derive(Debug)]
157pub struct Prepared<'a> {
158    algorithm: AlgorithmIdentifier<'a>,
159    signature: SignatureValue<'a>,
160    preimage: Preimage<'a>,
161}
162impl<'a> Prepared<'a> {
163    /// Algorithm protected by the alternative signature.
164    #[must_use]
165    pub fn algorithm(&self) -> AlgorithmIdentifier<'a> {
166        self.algorithm
167    }
168    /// Original alternative signature and producing source.
169    #[must_use]
170    pub fn signature(&self) -> SignatureValue<'a> {
171        self.signature
172    }
173    /// DER stream with original payload ranges and synthetic framing.
174    #[must_use]
175    pub fn preimage(&self) -> &Preimage<'a> {
176        &self.preimage
177    }
178
179    /// Verify with the issuer's authenticated alternative key, not the subject key.
180    pub fn verify<W: Work, V: AlternativeVerifier<W>>(
181        &self,
182        issuer_key: PublicKeyInfo<'_>,
183        verifier: &V,
184        work: &W,
185    ) -> CallbackResult<Verified, V::Error> {
186        // All ML-DSA profile/framing decisions precede the callback. The
187        // provider checks coefficients, key validity and the actual signature.
188        issuer_key
189            .execution_in(work)
190            .charge_work(1)
191            .map_err(|e| Error::resource(e, FIELD, issuer_key.key_offset()))?;
192        if let Some(set) = ml_dsa::resolve(self.algorithm, work)? {
193            let key = crate::key::ml_dsa::read(issuer_key, work)?;
194            if key.parameter_set() != set {
195                return Err(Error::new(
196                    ErrorCode::AlgorithmMismatch,
197                    FIELD,
198                    issuer_key.key_offset(),
199                )
200                .into());
201            }
202            self.signature.ml_dsa_in(set, work)?;
203        }
204        verifier
205            .verify(
206                self.algorithm,
207                issuer_key,
208                &self.preimage,
209                self.signature,
210                work,
211            )
212            .map_err(CallbackError::Callback)
213    }
214}
215
216/// Inspect the original alternative certificate extensions, including absences.
217pub fn inspect_certificate<'a, W: Work>(
218    certificate: &crate::Certificate<'a>,
219    work: &W,
220) -> Result<Components<'a>> {
221    components::extensions(
222        certificate.tbs().extensions(),
223        &certificate.execution_in(work),
224        certificate.span().start,
225    )
226}
227
228/// Prepare the X.509 §7.2.2 preTBSCertificate, requiring all three components.
229pub fn prepare_certificate<'a, W: Work>(
230    certificate: &crate::Certificate<'a>,
231    work: &W,
232) -> Result<Prepared<'a>> {
233    prepare_certificate_key(certificate, work).map(|(prepared, _)| prepared)
234}
235pub(crate) fn prepare_certificate_key<'a, W: Work>(
236    certificate: &crate::Certificate<'a>,
237    work: &W,
238) -> Result<(Prepared<'a>, PublicKeyInfo<'a>)> {
239    let components = components::extensions(
240        certificate.tbs().extensions(),
241        &certificate.execution_in(work),
242        certificate.span().start,
243    )?;
244    prepare_certificate_components(certificate, &components, work)
245}
246
247pub(crate) fn prepare_certificate_components<'a, W: Work>(
248    certificate: &crate::Certificate<'a>,
249    components: &Components<'a>,
250    work: &W,
251) -> Result<(Prepared<'a>, PublicKeyInfo<'a>)> {
252    let key = components.key.require()?;
253    validate_key(key, work)?;
254    let (algorithm, signature, remove) = components.signature_fields()?;
255    let reader = certificate.tbs().reader_scoped(work)?;
256    let preimage = preimage::extensions(reader, certificate.tbs().signature(), 3, remove)?;
257    Ok((
258        Prepared {
259            algorithm,
260            signature,
261            preimage,
262        },
263        key,
264    ))
265}
266
267pub(crate) fn critical_key_understood<W: Work, R: crate::Recognizer<W>>(
268    components: &Components<'_>,
269    recognizer: &R,
270    work: &W,
271) -> CallbackResult<bool, R::Error> {
272    if components.key.critical() != Some(true) {
273        return Ok(true);
274    }
275    let key = components.key.require()?;
276    // X.509(2019)§9.8.2: critical alternative SPKI requires understanding
277    // its public-key algorithm too, not merely the extension's OID.
278    // https://www.itu.int/rec/T-REC-X.509-201910-I/en
279    if ml_dsa::resolve(key.algorithm(), work)?.is_some() {
280        crate::key::ml_dsa::read(key, work)?;
281        return Ok(true);
282    }
283    key.execution_in(work)
284        .charge_work(1)
285        .map_err(|e| Error::resource(e, FIELD, key.key_offset()))?;
286    recognizer
287        .recognizes(SUBJECT_ALT_PUBLIC_KEY_INFO, work)
288        .map_err(CallbackError::Callback)
289}
290
291/// Inspect CRL alternative algorithm/value; CRLs do not carry a subject key.
292pub fn inspect_crl<'a, W: Work>(
293    crl: &crate::crl::CertificateList<'a>,
294    work: &W,
295) -> Result<Components<'a>> {
296    components::extensions(
297        crl.tbs().extensions(),
298        &crl.execution_in(work),
299        crl.span().start,
300    )
301}
302
303/// Prepare the X.509 §7.10.3 preTBSCertList, excluding its native algorithm.
304pub fn prepare_crl<'a, W: Work>(
305    crl: &crate::crl::CertificateList<'a>,
306    work: &W,
307) -> Result<Prepared<'a>> {
308    let components = inspect_crl(crl, work)?;
309    let (algorithm, signature, remove) = components.signature_fields()?;
310    let reader = crl.tbs().reader_scoped(work)?;
311    let preimage = preimage::extensions(reader, crl.tbs().signature(), 0, remove)?;
312    Ok(Prepared {
313        algorithm,
314        signature,
315        preimage,
316    })
317}
318
319fn alternative<W: Work, V: AlternativeVerifier<W>>(
320    prepared: Result<Prepared<'_>>,
321    key: PublicKeyInfo<'_>,
322    verifier: &V,
323    work: &W,
324) -> CallbackResult<Alternative, V::Error> {
325    let result = match prepared {
326        Ok(prepared) => prepared
327            .verify(key, verifier, work)
328            .map(Alternative::Verified),
329        Err(error) => Err(CallbackError::Processing(error)),
330    };
331    match result {
332        Err(CallbackError::Processing(error)) if !terminal(&error) => {
333            Ok(Alternative::Nonconforming(Fault::of(&error)))
334        }
335        other => other,
336    }
337}
338
339pub(super) fn validate_key<W: Work>(key: PublicKeyInfo<'_>, work: &W) -> Result<()> {
340    if ml_dsa::resolve(key.algorithm(), work)?.is_some() {
341        crate::key::ml_dsa::read(key, work)?;
342    }
343    Ok(())
344}
345pub(crate) fn terminal(error: &Error) -> bool {
346    error.is_resource() || error.code() == ErrorCode::ForeignReader
347}
348
349/// Check both mathematical signatures without short-circuiting on native No or Unsupported.
350pub fn check_certificate<W: Work, N: Verify<W>, A: AlternativeVerifier<W>>(
351    certificate: &crate::Certificate<'_>,
352    native_key: WorkingKey<'_>,
353    alternative_key: PublicKeyInfo<'_>,
354    native: &N,
355    verifier: &A,
356    work: &W,
357) -> core::result::Result<Signatures, CheckError<N::Error, A::Error>> {
358    certificate
359        .execution_in(work)
360        .charge_work(1)
361        .map_err(|e| Error::resource(e, FIELD, certificate.span().start))?;
362    native_key
363        .key()
364        .execution_in(work)
365        .charge_work(1)
366        .map_err(|e| Error::resource(e, FIELD, native_key.key().key_offset()))?;
367    let result = native
368        .verify(
369            &certificate.signature_algorithm(),
370            native_key,
371            certificate.signed_bytes(),
372            certificate.signature_value(),
373            work,
374        )
375        .map_err(CheckError::Native)?;
376    let alternative = alternative(
377        prepare_certificate(certificate, work),
378        alternative_key,
379        verifier,
380        work,
381    )
382    .map_err(|e| match e {
383        CallbackError::Processing(e) => CheckError::Processing(e),
384        CallbackError::Callback(e) => CheckError::Alternative(e),
385    })?;
386    Ok(Signatures {
387        native: result,
388        alternative,
389    })
390}
391
392/// Check both CRL signatures with caller-authenticated issuer keys. This does
393/// not replace issuer-name, cRLSign, freshness, scope or revocation processing.
394pub fn check_crl<W: Work, N: Verify<W>, A: AlternativeVerifier<W>>(
395    crl: &crate::crl::CertificateList<'_>,
396    native_key: WorkingKey<'_>,
397    alternative_key: PublicKeyInfo<'_>,
398    native: &N,
399    verifier: &A,
400    work: &W,
401) -> core::result::Result<Signatures, CheckError<N::Error, A::Error>> {
402    crl.execution_in(work)
403        .charge_work(1)
404        .map_err(|e| Error::resource(e, FIELD, crl.span().start))?;
405    native_key
406        .key()
407        .execution_in(work)
408        .charge_work(1)
409        .map_err(|e| Error::resource(e, FIELD, native_key.key().key_offset()))?;
410    let result = native
411        .verify(
412            &crl.signature_algorithm(),
413            native_key,
414            crl.signed_bytes(),
415            crl.signature_value(),
416            work,
417        )
418        .map_err(CheckError::Native)?;
419    let alternative = alternative(prepare_crl(crl, work), alternative_key, verifier, work)
420        .map_err(|e| match e {
421            CallbackError::Processing(e) => CheckError::Processing(e),
422            CallbackError::Callback(e) => CheckError::Alternative(e),
423        })?;
424    Ok(Signatures {
425        native: result,
426        alternative,
427    })
428}
429
430fn required<'a>(
431    reader: &mut ScopedReader<'a, '_>,
432    tag: Tag,
433) -> Result<structured_asn1::Element<'a>> {
434    reader.expect(tag).field(FIELD)
435}
436
437fn identifier_work<W: Work>(kind: Oid<'_>, work: &W, at: u64) -> Result<()> {
438    let units = (kind.as_bytes().len() as u64)
439        .checked_mul(3)
440        .and_then(|n| n.checked_add(1))
441        .ok_or_else(|| {
442            Error::resource(
443                structured_asn1::memory::MemoryError::WorkExceeded,
444                FIELD,
445                at,
446            )
447        })?;
448    work.charge_work(units)
449        .map_err(|e| Error::resource(e, FIELD, at))
450}