purecrypto 0.1.1

A pure-Rust cryptography toolkit with no foreign-code dependencies, from constant-time primitives up to keys, X.509 and TLS.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! A registry of digital-signature algorithms, and a whitelist policy
//! controlling which algorithms a verifier accepts.
//!
//! `purecrypto`'s X.509 chain validation and TLS 1.3 `CertificateVerify` paths
//! used to each carry a hand-rolled `match` on the algorithm identifier (an
//! OID for X.509, a `SignatureScheme` code point for TLS). The two switches
//! duplicated dispatch logic and only handled the subset they were wired for.
//!
//! This module replaces both with a single static table — [`ALGORITHMS`] — of
//! [`SignatureAlgorithm`] trait objects. Each entry knows
//!   * a stable string id (e.g. `"ecdsa-secp256r1-sha256"`) for policy
//!     whitelisting,
//!   * the X.509 `AlgorithmIdentifier` OIDs it matches (a single algorithm
//!     may match several),
//!   * the TLS 1.3 `SignatureScheme` code points it implements (often empty),
//!   * a `verify(spki, message, signature)` method that parses the
//!     `SubjectPublicKeyInfo` DER, recovers the key, and verifies.
//!
//! The slice is small (≈10–20 entries) and linear scans cost a few nanoseconds
//! — dwarfed by the actual asymmetric verification. There is no `HashMap`, no
//! `OnceLock`, no init order: the registry is `&'static` and works in
//! `no_std`.
//!
//! # Whitelist policy
//!
//! [`SignaturePolicy`] (requires `alloc`) enforces a strict **whitelist**:
//! adding an algorithm to [`ALGORITHMS`] does NOT auto-permit it; the caller
//! has to add the id explicitly. The shipped default
//! [`SignaturePolicy::modern`] permits exactly the modern IANA-blessed set —
//! RSA-PSS-RSAE / RSA-PKCS1 with SHA-256/384, ECDSA with matched-curve /
//! matched-hash pairs over P-256/P-384/P-521, and Ed25519 — with RSA keys
//! ≥ 2048 bits.

use crate::x509::Error;

// The module is gated behind `x509` at the crate root: it returns
// `x509::Error` and the per-primitive impls re-use the SPKI parsers in
// `src/x509/pubkey.rs`. Without `x509`, none of these types exist.

/// A signature algorithm purecrypto can verify.
///
/// Implementors are zero-sized types in `src/{rsa,ec,mldsa,slhdsa}/registry.rs`
/// that delegate to the primitive's existing `verify` method after parsing the
/// `SubjectPublicKeyInfo` to recover the key.
pub trait SignatureAlgorithm: Sync + 'static {
    /// Stable identifier for whitelisting (e.g. `"ecdsa-secp256r1-sha256"`).
    fn id(&self) -> &'static str;

    /// X.509 signature `AlgorithmIdentifier` OIDs that map to this entry. A
    /// single algorithm may match multiple OIDs (legacy aliases); the slice
    /// is non-empty for any algorithm reachable from an X.509 chain.
    fn x509_oids(&self) -> &'static [&'static [u64]];

    /// TLS 1.3 `SignatureScheme` code points (RFC 8446 §4.2.3) for this
    /// entry. May be empty (e.g. SLH-DSA, only useful in chains).
    fn tls_schemes(&self) -> &'static [u16];

    /// Verifies `signature` over `message` under `spki` (the full
    /// `SubjectPublicKeyInfo` DER, so curve / key parameters travel with the
    /// key).
    fn verify(&self, spki: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Error>;

    /// For policy decisions: RSA modulus length in bits. `None` for non-RSA
    /// algorithms.
    fn rsa_modulus_bits(&self, _spki: &[u8]) -> Option<u32> {
        None
    }
}

/// All signature algorithms purecrypto knows. Lookups are linear; the slice
/// is small so this is cheap.
pub static ALGORITHMS: &[&'static dyn SignatureAlgorithm] = &[
    // Legacy SHA-1 / RSA — opt-in only.
    #[cfg(all(feature = "rsa", feature = "alloc"))]
    &crate::rsa::registry::Pkcs1Sha1,
    #[cfg(all(feature = "rsa", feature = "alloc"))]
    &crate::rsa::registry::Pkcs1Sha256,
    #[cfg(all(feature = "rsa", feature = "alloc"))]
    &crate::rsa::registry::Pkcs1Sha384,
    #[cfg(all(feature = "rsa", feature = "alloc"))]
    &crate::rsa::registry::Pkcs1Sha512,
    #[cfg(all(feature = "rsa", feature = "alloc"))]
    &crate::rsa::registry::PssRsaeSha256,
    #[cfg(all(feature = "rsa", feature = "alloc"))]
    &crate::rsa::registry::PssRsaeSha384,
    #[cfg(all(feature = "rsa", feature = "alloc"))]
    &crate::rsa::registry::PssRsaeSha512,
    // RSA-PSS with a PSS-key-restricted SPKI (`id-RSASSA-PSS`).
    #[cfg(all(feature = "rsa", feature = "alloc"))]
    &crate::rsa::registry::PssPssSha256,
    // OID-keyed ECDSA entries (X.509 chain dispatch).
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaSha256AnyCurve,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaSha384AnyCurve,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaSha512AnyCurve,
    // Strict curve/hash-pair ECDSA entries (TLS scheme dispatch, fine-grained
    // policy whitelisting). Matched-pair entries carry an IANA TLS scheme;
    // cross-hash and secp256k1 entries have none and are policy-only.
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaP256Sha256,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaP384Sha384,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaP521Sha512,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaP256Sha384,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaP256Sha512,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaP384Sha256,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaP384Sha512,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaP521Sha256,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaP521Sha384,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaSecp256k1Sha256,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaSecp256k1Sha384,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::EcdsaSecp256k1Sha512,
    #[cfg(all(feature = "ec", feature = "alloc"))]
    &crate::ec::registry::Ed25519,
    #[cfg(all(feature = "mldsa", feature = "alloc"))]
    &crate::mldsa::registry::MlDsa44,
    #[cfg(all(feature = "mldsa", feature = "alloc"))]
    &crate::mldsa::registry::MlDsa65,
    #[cfg(all(feature = "mldsa", feature = "alloc"))]
    &crate::mldsa::registry::MlDsa87,
    // SLH-DSA (FIPS 205) × 12 parameter sets. None are on `modern()`;
    // explicit opt-in (signatures are 7–50 KB).
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaSha2128s,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaSha2128f,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaSha2192s,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaSha2192f,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaSha2256s,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaSha2256f,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaShake128s,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaShake128f,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaShake192s,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaShake192f,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaShake256s,
    #[cfg(all(feature = "slhdsa", feature = "alloc"))]
    &crate::slhdsa::registry::SlhDsaShake256f,
];

/// Looks up a registry entry by X.509 `AlgorithmIdentifier` OID arcs.
pub fn find_by_oid(oid: &[u64]) -> Option<&'static dyn SignatureAlgorithm> {
    for algo in ALGORITHMS {
        for entry in algo.x509_oids() {
            if *entry == oid {
                return Some(*algo);
            }
        }
    }
    None
}

/// Looks up a registry entry by TLS 1.3 `SignatureScheme` code point.
pub fn find_by_tls_scheme(scheme: u16) -> Option<&'static dyn SignatureAlgorithm> {
    for algo in ALGORITHMS {
        for entry in algo.tls_schemes() {
            if *entry == scheme {
                return Some(*algo);
            }
        }
    }
    None
}

/// Looks up a registry entry by its stable identifier.
pub fn find_by_id(id: &str) -> Option<&'static dyn SignatureAlgorithm> {
    for algo in ALGORITHMS {
        if algo.id() == id {
            return Some(*algo);
        }
    }
    None
}

#[cfg(feature = "alloc")]
mod policy {
    use super::{SignatureAlgorithm, find_by_id};
    use alloc::vec::Vec;

    /// Compares two `&dyn SignatureAlgorithm` references for logical equality.
    /// Pointer-identity is unreliable here: every registry entry is a
    /// zero-sized type, and Rust does not guarantee distinct ZSTs have
    /// distinct data-pointers. Using `id()` (a stable, unique string) is
    /// both portable and matches the user-visible whitelist key.
    fn algo_eq(a: &dyn SignatureAlgorithm, b: &dyn SignatureAlgorithm) -> bool {
        a.id() == b.id()
    }

    /// Whitelist policy controlling which signature algorithms a verifier
    /// accepts. Adding an algorithm to [`super::ALGORITHMS`] does NOT
    /// auto-permit it; the caller must explicitly add it here with
    /// [`Self::permit`].
    ///
    /// The shipped default — [`SignaturePolicy::modern`] — accepts exactly the
    /// modern IANA-blessed set: RSA-PKCS1 / RSA-PSS-RSAE with SHA-256/384/512,
    /// ECDSA with matched curve/hash pairs over P-256/P-384/P-521, and
    /// Ed25519. RSA keys must be at least 2048 bits.
    #[derive(Clone)]
    pub struct SignaturePolicy {
        permitted: Vec<&'static dyn SignatureAlgorithm>,
        /// Minimum acceptable RSA modulus length, in bits.
        pub min_rsa_bits: u32,
    }

    impl SignaturePolicy {
        /// The shipped default whitelist: modern IANA-blessed signature
        /// algorithms, RSA ≥ 2048 bits.
        ///
        /// Permitted ids:
        ///   * `rsa-pkcs1-sha256`, `rsa-pkcs1-sha384`
        ///   * `rsa-pss-rsae-sha256`, `rsa-pss-rsae-sha384`, `rsa-pss-rsae-sha512`
        ///   * `ecdsa-secp256r1-sha256`, `ecdsa-secp384r1-sha384`,
        ///     `ecdsa-secp521r1-sha512`
        ///   * `ed25519`
        ///   * `ml-dsa-44`, `ml-dsa-65`, `ml-dsa-87` (NIST FIPS 204)
        ///
        /// Everything else in [`super::ALGORITHMS`] (SHA-1 RSA, secp256k1,
        /// cross-hash ECDSA, SLH-DSA, …) is one-line opt-in via
        /// [`Self::permit`].
        pub fn modern() -> Self {
            let permitted_ids = [
                "rsa-pkcs1-sha256",
                "rsa-pkcs1-sha384",
                "rsa-pss-rsae-sha256",
                "rsa-pss-rsae-sha384",
                "rsa-pss-rsae-sha512",
                // X.509-chain dispatch entries (OID-keyed; any supported curve).
                // The matched-pair entries below pin the curve for TLS 1.3
                // CertificateVerify (one per IANA scheme code point).
                "ecdsa-with-sha256",
                "ecdsa-with-sha384",
                "ecdsa-with-sha512",
                "ecdsa-secp256r1-sha256",
                "ecdsa-secp384r1-sha384",
                "ecdsa-secp521r1-sha512",
                "ed25519",
                "ml-dsa-44",
                "ml-dsa-65",
                "ml-dsa-87",
            ];
            let mut permitted = Vec::new();
            for id in permitted_ids {
                if let Some(algo) = find_by_id(id) {
                    permitted.push(algo);
                }
            }
            SignaturePolicy {
                permitted,
                min_rsa_bits: 2048,
            }
        }

        /// An empty policy — accepts nothing. Build it up by chaining
        /// [`SignaturePolicy::permit`].
        pub fn empty() -> Self {
            SignaturePolicy {
                permitted: Vec::new(),
                min_rsa_bits: 2048,
            }
        }

        /// Adds an algorithm by id, looking it up in [`super::ALGORITHMS`].
        /// Ignores unknown ids and duplicates.
        pub fn permit(mut self, id: &str) -> Self {
            if let Some(algo) = find_by_id(id)
                && !self.permitted.iter().any(|a| algo_eq(*a, algo))
            {
                self.permitted.push(algo);
            }
            self
        }

        /// Overrides the RSA-modulus-bit floor.
        pub fn with_min_rsa_bits(mut self, bits: u32) -> Self {
            self.min_rsa_bits = bits;
            self
        }

        /// `true` if `algo` is on the whitelist and `spki`'s parameters meet
        /// any extra constraints (today only the `min_rsa_bits` check).
        pub fn permits(&self, algo: &dyn SignatureAlgorithm, spki: &[u8]) -> bool {
            if !self.permitted.iter().any(|a| algo_eq(*a, algo)) {
                return false;
            }
            if let Some(bits) = algo.rsa_modulus_bits(spki)
                && bits < self.min_rsa_bits
            {
                return false;
            }
            true
        }
    }

    impl Default for SignaturePolicy {
        fn default() -> Self {
            Self::modern()
        }
    }
}

#[cfg(feature = "alloc")]
pub use policy::SignaturePolicy;

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(all(feature = "rsa", feature = "ec", feature = "alloc"))]
    #[test]
    fn registry_has_modern_entries() {
        assert!(find_by_id("rsa-pkcs1-sha256").is_some());
        assert!(find_by_id("rsa-pss-rsae-sha256").is_some());
        assert!(find_by_id("ecdsa-secp256r1-sha256").is_some());
        assert!(find_by_id("ecdsa-secp384r1-sha384").is_some());
        assert!(find_by_id("ecdsa-secp521r1-sha512").is_some());
        assert!(find_by_id("ed25519").is_some());
    }

    #[cfg(all(feature = "rsa", feature = "ec", feature = "alloc"))]
    #[test]
    fn lookup_by_oid_and_scheme() {
        // X.509 OID for ecdsa-with-SHA256 dispatches through the OID-keyed
        // any-curve entry (the strict pair entries have no X.509 OIDs).
        let algo = find_by_oid(&[1, 2, 840, 10045, 4, 3, 2]).expect("ecdsa-with-SHA256");
        assert_eq!(algo.id(), "ecdsa-with-sha256");
        // TLS scheme for ecdsa_secp256r1_sha256 dispatches through the strict
        // pair entry.
        let algo = find_by_tls_scheme(0x0403).expect("ecdsa_secp256r1_sha256");
        assert_eq!(algo.id(), "ecdsa-secp256r1-sha256");
        // TLS scheme for rsa_pss_rsae_sha256.
        let algo = find_by_tls_scheme(0x0804).expect("rsa_pss_rsae_sha256");
        assert_eq!(algo.id(), "rsa-pss-rsae-sha256");
    }

    #[cfg(all(feature = "rsa", feature = "ec", feature = "alloc"))]
    #[test]
    fn modern_policy_permits_default_set() {
        let policy = SignaturePolicy::modern();
        for id in [
            "rsa-pkcs1-sha256",
            "rsa-pkcs1-sha384",
            "rsa-pss-rsae-sha256",
            "rsa-pss-rsae-sha384",
            "rsa-pss-rsae-sha512",
            "ecdsa-secp256r1-sha256",
            "ecdsa-secp384r1-sha384",
            "ecdsa-secp521r1-sha512",
            "ed25519",
        ] {
            let algo = find_by_id(id).unwrap();
            assert!(policy.permits(algo, &[]), "modern() should permit {id}");
        }
    }

    #[cfg(all(feature = "ec", feature = "alloc"))]
    #[test]
    fn empty_policy_permits_nothing_until_opt_in() {
        let algo = find_by_id("ed25519").unwrap();
        let policy = SignaturePolicy::empty();
        assert!(!policy.permits(algo, &[]));
        let policy = policy.permit("ed25519");
        assert!(policy.permits(algo, &[]));
    }

    #[cfg(all(feature = "rsa", feature = "alloc"))]
    #[test]
    fn min_rsa_bits_floor_rejects_small_keys() {
        use crate::x509::AnyPublicKey;
        let key = crate::test_util::rsa_test_key_a();
        let pk = key.public_key();
        let mut n = [0u8; 256];
        pk.modulus().write_be_bytes(&mut n);
        let mut e = [0u8; 256];
        pk.exponent().write_be_bytes(&mut e);
        let boxed = crate::rsa::BoxedRsaPublicKey::new(
            crate::bignum::BoxedUint::from_be_bytes(&n),
            crate::bignum::BoxedUint::from_be_bytes(&e),
        );
        let spki = AnyPublicKey::Rsa(boxed).to_spki_der();

        let algo = find_by_id("rsa-pkcs1-sha256").unwrap();
        // 2048-bit key permitted under default min.
        assert!(SignaturePolicy::modern().permits(algo, &spki));
        // Asking for ≥ 4096 bits rejects a 2048-bit key.
        let strict = SignaturePolicy::modern().with_min_rsa_bits(4096);
        assert!(!strict.permits(algo, &spki));
    }
}