Skip to main content

pq_oid/
lib.rs

1//! pq-oid - Type-safe OID constants for post-quantum algorithms
2//!
3//! This crate provides OID (Object Identifier) constants and utilities for
4//! post-quantum cryptographic algorithms as defined in FIPS 203, 204, and 205.
5//!
6//! # Features
7//!
8//! - Type-safe enums for ML-KEM, ML-DSA, and SLH-DSA algorithm families
9//! - Ergonomic conversions via `FromStr`, `TryFrom<&str>`, and `Display`
10//! - Direct access to algorithm properties (key sizes, security levels, OIDs)
11//! - DER encoding/decoding of OIDs
12//! - JOSE and COSE mappings for ML-DSA
13//!
14//! # Quick Start
15//!
16//! ```rust
17//! use pq_oid::{MlKem, MlDsa, SlhDsa, Algorithm};
18//! use std::str::FromStr;
19//!
20//! // Parse from string
21//! let kem: MlKem = "ML-KEM-512".parse().unwrap();
22//! assert_eq!(kem.oid(), "2.16.840.1.101.3.4.4.1");
23//! assert_eq!(kem.public_key_size(), 800);
24//!
25//! // Or use try_into
26//! let dsa: MlDsa = "ML-DSA-65".try_into().unwrap();
27//! assert_eq!(dsa.jose(), "ML-DSA-65");
28//! assert_eq!(dsa.cose(), -49);
29//!
30//! // Convert back to string
31//! let name: &str = kem.as_ref();
32//! assert_eq!(name, "ML-KEM-512");
33//!
34//! // Unified algorithm type
35//! let alg: Algorithm = MlKem::Kem512.into();
36//! assert_eq!(alg.family(), pq_oid::AlgorithmFamily::MlKem);
37//! ```
38//!
39//! # Algorithm Families
40//!
41//! ## ML-KEM (FIPS 203)
42//!
43//! Module-Lattice-Based Key-Encapsulation Mechanism:
44//! - [`MlKem::Kem512`] - NIST Level 1
45//! - [`MlKem::Kem768`] - NIST Level 3
46//! - [`MlKem::Kem1024`] - NIST Level 5
47//!
48//! ## ML-DSA (FIPS 204)
49//!
50//! Module-Lattice-Based Digital Signature Algorithm:
51//! - [`MlDsa::Dsa44`] - NIST Level 2
52//! - [`MlDsa::Dsa65`] - NIST Level 3
53//! - [`MlDsa::Dsa87`] - NIST Level 5
54//!
55//! ## SLH-DSA (FIPS 205)
56//!
57//! Stateless Hash-Based Digital Signature Algorithm with SHA2 and SHAKE variants
58//! in both "small" (s) and "fast" (f) modes.
59
60// =============================================================================
61// NOTE: Future enhancements to consider
62// =============================================================================
63// - `serde` feature: Add optional Serialize/Deserialize derives
64// - `zeroize` feature: Add optional secure memory wiping for key material
65// - `const-oid` interop: Integration with RustCrypto's const-oid crate
66// - `AlgorithmOid` trait: Common trait for OID operations across types
67// - `Default` impls: Default implementations for MlKem/MlDsa
68//
69// See: https://github.com/multivmlabs/post-quantum-packages/pull/4
70// =============================================================================
71
72mod encoding;
73mod error;
74mod types;
75
76// Re-export main types
77pub use error::{Error, Result};
78pub use types::{
79    Algorithm, AlgorithmFamily, AlgorithmInfo, AlgorithmSizes, AlgorithmType, HashFunction, MlDsa,
80    MlKem, SecurityLevel, SlhDsa, SlhDsaMode,
81};
82
83// Re-export encoding functions
84pub use encoding::{decode_oid, encode_oid, encode_oid_to};
85
86/// OID constants for all algorithms.
87///
88/// These are provided for convenience when you need the raw OID strings.
89/// DER-encoded bytes (`*_BYTES`) are also provided for ASN.1 embedding.
90pub mod oid {
91    // ML-KEM OIDs (FIPS 203)
92    pub const ML_KEM_512: &str = "2.16.840.1.101.3.4.4.1";
93    pub const ML_KEM_768: &str = "2.16.840.1.101.3.4.4.2";
94    pub const ML_KEM_1024: &str = "2.16.840.1.101.3.4.4.3";
95
96    // ML-KEM DER-encoded bytes
97    pub const ML_KEM_512_BYTES: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x01];
98    pub const ML_KEM_768_BYTES: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x02];
99    pub const ML_KEM_1024_BYTES: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x03];
100
101    // ML-DSA OIDs (FIPS 204)
102    pub const ML_DSA_44: &str = "2.16.840.1.101.3.4.3.17";
103    pub const ML_DSA_65: &str = "2.16.840.1.101.3.4.3.18";
104    pub const ML_DSA_87: &str = "2.16.840.1.101.3.4.3.19";
105
106    // ML-DSA DER-encoded bytes
107    pub const ML_DSA_44_BYTES: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11];
108    pub const ML_DSA_65_BYTES: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12];
109    pub const ML_DSA_87_BYTES: &[u8] = &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13];
110
111    // SLH-DSA SHA2 OIDs (FIPS 205)
112    pub const SLH_DSA_SHA2_128S: &str = "2.16.840.1.101.3.4.3.20";
113    pub const SLH_DSA_SHA2_128F: &str = "2.16.840.1.101.3.4.3.21";
114    pub const SLH_DSA_SHA2_192S: &str = "2.16.840.1.101.3.4.3.22";
115    pub const SLH_DSA_SHA2_192F: &str = "2.16.840.1.101.3.4.3.23";
116    pub const SLH_DSA_SHA2_256S: &str = "2.16.840.1.101.3.4.3.24";
117    pub const SLH_DSA_SHA2_256F: &str = "2.16.840.1.101.3.4.3.25";
118
119    // SLH-DSA SHA2 DER-encoded bytes
120    pub const SLH_DSA_SHA2_128S_BYTES: &[u8] =
121        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x14];
122    pub const SLH_DSA_SHA2_128F_BYTES: &[u8] =
123        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x15];
124    pub const SLH_DSA_SHA2_192S_BYTES: &[u8] =
125        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x16];
126    pub const SLH_DSA_SHA2_192F_BYTES: &[u8] =
127        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x17];
128    pub const SLH_DSA_SHA2_256S_BYTES: &[u8] =
129        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x18];
130    pub const SLH_DSA_SHA2_256F_BYTES: &[u8] =
131        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x19];
132
133    // SLH-DSA SHAKE OIDs (FIPS 205)
134    pub const SLH_DSA_SHAKE_128S: &str = "2.16.840.1.101.3.4.3.26";
135    pub const SLH_DSA_SHAKE_128F: &str = "2.16.840.1.101.3.4.3.27";
136    pub const SLH_DSA_SHAKE_192S: &str = "2.16.840.1.101.3.4.3.28";
137    pub const SLH_DSA_SHAKE_192F: &str = "2.16.840.1.101.3.4.3.29";
138    pub const SLH_DSA_SHAKE_256S: &str = "2.16.840.1.101.3.4.3.30";
139    pub const SLH_DSA_SHAKE_256F: &str = "2.16.840.1.101.3.4.3.31";
140
141    // SLH-DSA SHAKE DER-encoded bytes
142    pub const SLH_DSA_SHAKE_128S_BYTES: &[u8] =
143        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x1a];
144    pub const SLH_DSA_SHAKE_128F_BYTES: &[u8] =
145        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x1b];
146    pub const SLH_DSA_SHAKE_192S_BYTES: &[u8] =
147        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x1c];
148    pub const SLH_DSA_SHAKE_192F_BYTES: &[u8] =
149        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x1d];
150    pub const SLH_DSA_SHAKE_256S_BYTES: &[u8] =
151        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x1e];
152    pub const SLH_DSA_SHAKE_256F_BYTES: &[u8] =
153        &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x1f];
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use std::str::FromStr;
160
161    #[test]
162    fn test_ml_kem_ergonomics() {
163        // Parse from string
164        let alg: MlKem = "ML-KEM-512".parse().unwrap();
165        assert_eq!(alg, MlKem::Kem512);
166
167        // try_into works
168        let alg: MlKem = "ML-KEM-768".try_into().unwrap();
169        assert_eq!(alg, MlKem::Kem768);
170
171        // Get properties directly
172        assert_eq!(MlKem::Kem512.oid(), oid::ML_KEM_512);
173        assert_eq!(MlKem::Kem512.public_key_size(), 800);
174        assert_eq!(MlKem::Kem512.ciphertext_size(), 768);
175
176        // Convert back to string
177        assert_eq!(MlKem::Kem512.to_string(), "ML-KEM-512");
178        assert_eq!(MlKem::Kem512.as_ref(), "ML-KEM-512");
179    }
180
181    #[test]
182    fn test_ml_dsa_ergonomics() {
183        let alg: MlDsa = "ML-DSA-65".parse().unwrap();
184        assert_eq!(alg.oid(), oid::ML_DSA_65);
185        assert_eq!(alg.jose(), "ML-DSA-65");
186        assert_eq!(alg.cose(), -49);
187        assert_eq!(alg.signature_size(), 3309);
188    }
189
190    #[test]
191    fn test_ml_dsa_jose_cose_roundtrip() {
192        for dsa in MlDsa::ALL {
193            // JOSE roundtrip
194            let jose = dsa.jose();
195            let recovered = MlDsa::from_jose(jose).unwrap();
196            assert_eq!(*dsa, recovered);
197
198            // COSE roundtrip
199            let cose = dsa.cose();
200            let recovered = MlDsa::from_cose(cose).unwrap();
201            assert_eq!(*dsa, recovered);
202        }
203    }
204
205    #[test]
206    fn test_slh_dsa_properties() {
207        let alg: SlhDsa = "SLH-DSA-SHA2-128s".parse().unwrap();
208        assert_eq!(alg.hash_function(), HashFunction::Sha2);
209        assert_eq!(alg.mode(), SlhDsaMode::Small);
210        assert_eq!(alg.security_level(), SecurityLevel::Level1);
211
212        let alg: SlhDsa = "SLH-DSA-SHAKE-256f".parse().unwrap();
213        assert_eq!(alg.hash_function(), HashFunction::Shake);
214        assert_eq!(alg.mode(), SlhDsaMode::Fast);
215        assert_eq!(alg.security_level(), SecurityLevel::Level5);
216    }
217
218    #[test]
219    fn test_algorithm_unified() {
220        // Parse any algorithm
221        let alg: Algorithm = "ML-KEM-512".parse().unwrap();
222        assert_eq!(alg.family(), AlgorithmFamily::MlKem);
223        assert_eq!(alg.algorithm_type(), AlgorithmType::Kem);
224
225        let alg: Algorithm = "ML-DSA-44".parse().unwrap();
226        assert_eq!(alg.family(), AlgorithmFamily::MlDsa);
227        assert_eq!(alg.algorithm_type(), AlgorithmType::Sign);
228
229        // Convert from specific to unified
230        let alg: Algorithm = MlKem::Kem768.into();
231        assert_eq!(alg.oid(), oid::ML_KEM_768);
232
233        // Cast back
234        assert!(alg.as_ml_kem().is_some());
235        assert!(alg.as_ml_dsa().is_none());
236    }
237
238    #[test]
239    fn test_algorithm_iteration() {
240        assert_eq!(Algorithm::all().count(), 18);
241        assert_eq!(Algorithm::kems().count(), 3);
242        assert_eq!(Algorithm::signatures().count(), 15);
243    }
244
245    #[test]
246    fn test_from_oid() {
247        let alg = MlKem::from_oid("2.16.840.1.101.3.4.4.1").unwrap();
248        assert_eq!(alg, MlKem::Kem512);
249
250        let alg = Algorithm::from_oid("2.16.840.1.101.3.4.3.17").unwrap();
251        assert_eq!(alg, Algorithm::MlDsa(MlDsa::Dsa44));
252    }
253
254    #[test]
255    fn test_oid_bytes_roundtrip() {
256        for alg in Algorithm::all() {
257            let oid = alg.oid();
258            let bytes = encode_oid(oid).unwrap();
259            let decoded = decode_oid(&bytes).unwrap();
260            assert_eq!(oid, decoded);
261        }
262    }
263
264    #[test]
265    fn test_algorithm_info() {
266        let info = MlKem::Kem512.info();
267        assert_eq!(info.name, "ML-KEM-512");
268        assert_eq!(info.oid, oid::ML_KEM_512);
269        assert_eq!(info.algorithm_type, AlgorithmType::Kem);
270        assert_eq!(info.family, AlgorithmFamily::MlKem);
271        assert_eq!(info.public_key_size, 800);
272        assert_eq!(
273            info.sizes,
274            AlgorithmSizes::Kem {
275                ciphertext_size: 768,
276                shared_secret_size: 32
277            }
278        );
279    }
280
281    #[test]
282    fn test_error_handling() {
283        assert!(MlKem::from_str("invalid").is_err());
284        assert!(MlDsa::from_jose("invalid").is_err());
285        assert!(MlDsa::from_cose(-100).is_none());
286        assert!(Algorithm::from_oid("1.2.3.4").is_err());
287    }
288
289    #[test]
290    fn test_oid_bytes_constants() {
291        // Verify ALL pre-computed bytes match runtime encoding
292
293        // ML-KEM
294        assert_eq!(encode_oid(oid::ML_KEM_512).unwrap(), oid::ML_KEM_512_BYTES);
295        assert_eq!(encode_oid(oid::ML_KEM_768).unwrap(), oid::ML_KEM_768_BYTES);
296        assert_eq!(
297            encode_oid(oid::ML_KEM_1024).unwrap(),
298            oid::ML_KEM_1024_BYTES
299        );
300
301        // ML-DSA
302        assert_eq!(encode_oid(oid::ML_DSA_44).unwrap(), oid::ML_DSA_44_BYTES);
303        assert_eq!(encode_oid(oid::ML_DSA_65).unwrap(), oid::ML_DSA_65_BYTES);
304        assert_eq!(encode_oid(oid::ML_DSA_87).unwrap(), oid::ML_DSA_87_BYTES);
305
306        // SLH-DSA SHA2
307        assert_eq!(
308            encode_oid(oid::SLH_DSA_SHA2_128S).unwrap(),
309            oid::SLH_DSA_SHA2_128S_BYTES
310        );
311        assert_eq!(
312            encode_oid(oid::SLH_DSA_SHA2_128F).unwrap(),
313            oid::SLH_DSA_SHA2_128F_BYTES
314        );
315        assert_eq!(
316            encode_oid(oid::SLH_DSA_SHA2_192S).unwrap(),
317            oid::SLH_DSA_SHA2_192S_BYTES
318        );
319        assert_eq!(
320            encode_oid(oid::SLH_DSA_SHA2_192F).unwrap(),
321            oid::SLH_DSA_SHA2_192F_BYTES
322        );
323        assert_eq!(
324            encode_oid(oid::SLH_DSA_SHA2_256S).unwrap(),
325            oid::SLH_DSA_SHA2_256S_BYTES
326        );
327        assert_eq!(
328            encode_oid(oid::SLH_DSA_SHA2_256F).unwrap(),
329            oid::SLH_DSA_SHA2_256F_BYTES
330        );
331
332        // SLH-DSA SHAKE
333        assert_eq!(
334            encode_oid(oid::SLH_DSA_SHAKE_128S).unwrap(),
335            oid::SLH_DSA_SHAKE_128S_BYTES
336        );
337        assert_eq!(
338            encode_oid(oid::SLH_DSA_SHAKE_128F).unwrap(),
339            oid::SLH_DSA_SHAKE_128F_BYTES
340        );
341        assert_eq!(
342            encode_oid(oid::SLH_DSA_SHAKE_192S).unwrap(),
343            oid::SLH_DSA_SHAKE_192S_BYTES
344        );
345        assert_eq!(
346            encode_oid(oid::SLH_DSA_SHAKE_192F).unwrap(),
347            oid::SLH_DSA_SHAKE_192F_BYTES
348        );
349        assert_eq!(
350            encode_oid(oid::SLH_DSA_SHAKE_256S).unwrap(),
351            oid::SLH_DSA_SHAKE_256S_BYTES
352        );
353        assert_eq!(
354            encode_oid(oid::SLH_DSA_SHAKE_256F).unwrap(),
355            oid::SLH_DSA_SHAKE_256F_BYTES
356        );
357    }
358}