Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
The goal of this library is to provide a simple and easy-to-use
interface for generating key pairs, certificates, signing and verifying messages, and encrypting and decrypting messages using post-quantum cryptographic algorithms.

A secondary goal is to provide a set of cryptographic algorithms that are compatible with existing X.509, PKIX, and CMS data structures and protocols and to support the efforts of the [LAMPS Working Group](https://datatracker.ietf.org/wg/lamps/about/) in the IETF especially the [draft-ietf-lamps-pq-composite-sigs](https://datatracker.ietf.org/doc/draft-ietf-lamps-pq-composite-sigs/) and [draft-ietf-lamps-pq-composite-kem](https://datatracker.ietf.org/doc/draft-ietf-lamps-pq-composite-kem/) drafts.
A secondary goal is to provide a set of cryptographic algorithms that are compatible with existing X.509, PKIX, and CMS data structures and protocols and to support the efforts of the [LAMPS Working Group](https://datatracker.ietf.org/wg/lamps/about/) in the IETF especially the [draft-ietf-lamps-pq-composite-sigs](https://datatracker.ietf.org/doc/draft-ietf-lamps-pq-composite-sigs/) (composite signatures, tracking **draft-19**) and [draft-ietf-lamps-pq-composite-kem](https://datatracker.ietf.org/doc/draft-ietf-lamps-pq-composite-kem/) (composite KEM, tracking **draft-15**) drafts. Composite algorithms use the production OID arc `1.3.6.1.5.5.7.6.37`–`.66`.

## Warning

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
¿~3ƒ#Þö ƒÒΟç‡ôúkez$ ùêÉ<é¢áÿä
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ߍλÕK¼Bª÷_}Vþ
?ƒ§¦‰ÒÔSsã‚t
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.-?*&G%{Éó„ÎÅd—bLëä@ûÉÔ8[yÇ
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
35 changes: 18 additions & 17 deletions src/asn1/asn_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
};

// Change the alias to use `QuantCryptError`.
#[allow(dead_code)]
type Result<T> = std::result::Result<T, errors::QuantCryptError>;

/// Convert an OID string to a DER encoded byte array
Expand All @@ -25,6 +26,11 @@ type Result<T> = std::result::Result<T, errors::QuantCryptError>;
/// # Errors
///
/// `QuantCryptError::InvalidOid` will be returned if the OID is invalid
///
/// Retained as a general-purpose utility (and exercised by the unit test
/// below); the composite constructions now use fixed per-algorithm Labels
/// rather than DER-encoded OIDs as domain separators.
#[allow(dead_code)]
pub fn oid_to_der(oid: &str) -> Result<Vec<u8>> {
let oid = ObjectIdentifier::new_unwrap(oid)
.to_der()
Expand Down Expand Up @@ -116,26 +122,21 @@ mod tests {

#[test]
fn test_oid_to_der() {
// This tests the Domain separator encoding:
//https://lamps-wg.github.io/draft-composite-kem/draft-ietf-lamps-pq-composite-kem.html#name-algorithm-identifiers

let oid_tests = vec![
("2.16.840.1.114027.80.5.2.21", "060B6086480186FA6B50050215"),
("2.16.840.1.114027.80.5.2.22", "060B6086480186FA6B50050216"),
("2.16.840.1.114027.80.5.2.23", "060B6086480186FA6B50050217"),
("2.16.840.1.114027.80.5.2.24", "060B6086480186FA6B50050218"),
("2.16.840.1.114027.80.5.2.25", "060B6086480186FA6B50050219"),
("2.16.840.1.114027.80.5.2.26", "060B6086480186FA6B5005021A"),
("2.16.840.1.114027.80.5.2.27", "060B6086480186FA6B5005021B"),
("2.16.840.1.114027.80.5.2.28", "060B6086480186FA6B5005021C"),
("2.16.840.1.114027.80.5.2.29", "060B6086480186FA6B5005021D"),
("2.16.840.1.114027.80.8.1.1", "060B6086480186FA6B50080101"),
use der::Decode;

// Round-trip a representative set of production composite OIDs
// (draft-ietf-lamps-pq-composite-sigs-19 / -kem-15).
let oids = vec![
"1.3.6.1.5.5.7.6.37", // MLDSA44-RSA2048-PSS-SHA256
"1.3.6.1.5.5.7.6.54", // MLDSA87-ECDSA-P521-SHA512
"1.3.6.1.5.5.7.6.55", // MLKEM768-RSA2048
"1.3.6.1.5.5.7.6.66", // MLKEM1024-ECDH-P521
];

for (oid, hex_string) in oid_tests {
for oid in oids {
let der = oid_to_der(oid).unwrap();
let expected_der = hex::decode(hex_string).unwrap();
assert_eq!(expected_der, der);
let decoded = ObjectIdentifier::from_der(&der).unwrap();
assert_eq!(decoded.to_string(), oid);
}
}
}
274 changes: 267 additions & 7 deletions src/asn1/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Result<T> = std::result::Result<T, QuantCryptError>;
/// # Example
/// ```
/// use quantcrypt::certificates::Certificate;
/// let cert_path = "test/data/MlDsa44EcdsaP256Sha256-2.16.840.1.114027.80.8.1.43_ta.der";
/// let cert_path = "test/data/bc_artifacts_certs_r4/ml-dsa-44-2.16.840.1.101.3.4.3.17_ta.der";
/// let cert = Certificate::from_file(cert_path).unwrap();
/// assert!(cert.verify_self_signed().unwrap());
/// ```
Expand Down Expand Up @@ -541,21 +541,22 @@ mod tests {
fn test_bc_cert_artifacts() {
let base_folder_path = "test/data/bc_artifacts_certs_r4/";

// Check all the certificates that start with MLDSA
// Verify the pure ML-DSA and SLH-DSA certificates. The R4 composite
// certificates (uppercase "MLDSA"/"HashMLDSA", old Entrust-arc OIDs
// 2.16.840.1.114027.80.8.1.x) use a superseded composite draft and are
// intentionally skipped now that the library tracks
// draft-ietf-lamps-pq-composite-sigs-19. Cross-provider composite
// interop should be validated against R5 artifacts.
let files = std::fs::read_dir(base_folder_path).unwrap();

let ml_dsa_composites_prefix = "MLDSA";
let ml_dsa_pure_prefix = "ml-dsa";
let slh_dsa_prefix = "slh-dsa";
for file in files {
let file = file.unwrap();
let path = file.path();
let path = path.to_str().unwrap();

if !path.contains(ml_dsa_composites_prefix)
&& !path.contains(slh_dsa_prefix)
&& !path.contains(ml_dsa_pure_prefix)
{
if !path.contains(slh_dsa_prefix) && !path.contains(ml_dsa_pure_prefix) {
continue;
}

Expand Down Expand Up @@ -608,4 +609,263 @@ mod tests {
println!("Verified: {}", path);
}
}

/// Cross-provider interop gate for the draft-19 composite signatures.
///
/// Loads the self-signed trust-anchor certificates produced by the IETF
/// Hackathon composite-signatures *reference implementation* (R5 artifacts,
/// production OID arc 1.3.6.1.5.5.7.6.37-.54) and verifies each signature.
/// A pass proves that our Prefix / Label / pre-hash construction is
/// byte-for-byte compatible with the reference implementation for every one
/// of the 18 composite ML-DSA algorithms (plus the pure ML-DSA anchors).
#[test]
fn test_r5_composite_sig_interop() {
let base = "test/data/r5_interop/composite-sigs-ref-impl/";
let files = std::fs::read_dir(base).unwrap();
let mut verified = 0usize;
for file in files {
let path = file.unwrap().path();
let name = path.file_name().unwrap().to_str().unwrap();
// Only self-signed trust anchors carry a verifiable signature here.
if !name.ends_with("_ta.der") {
continue;
}
let cert = crate::certificates::Certificate::from_file(path.to_str().unwrap()).unwrap();
assert!(
cert.verify_self_signed().unwrap(),
"reference-impl cert failed to verify: {name}"
);
verified += 1;
println!("Verified R5 ref-impl TA: {name}");
}
// 18 composite algorithms + 3 pure ML-DSA anchors.
assert!(
verified >= 18,
"expected >=18 verified reference-impl anchors, got {verified}"
);
}

/// Cross-provider interop check for the draft-15 composite KEM public-key
/// encoding. Loads the composite KEM end-entity certificates from the IETF
/// Hackathon composite-KEM *reference implementation* (R5 artifacts,
/// production OID arc 1.3.6.1.5.5.7.6.55-.66), parses each `mlkemPK ||
/// tradPK` public key, and performs an encapsulation. A pass proves our
/// composite public-key deserialization and encapsulation path are
/// compatible with the reference implementation's published keys.
#[test]
fn test_r5_composite_kem_interop() {
let base = "test/data/r5_interop/composite-kem-ref-impl/";
let files = std::fs::read_dir(base).unwrap();
let mut encapsulated = 0usize;
for file in files {
let path = file.unwrap().path();
let name = path.file_name().unwrap().to_str().unwrap();
if !name.ends_with("_ee.der") || !name.contains("1.3.6.1.5.5.7.6.") {
continue;
}
let cert = crate::certificates::Certificate::from_file(path.to_str().unwrap()).unwrap();
let pk = cert.get_public_key().unwrap();
pk.encap()
.unwrap_or_else(|e| panic!("encap failed for {name}: {e:?}"));
encapsulated += 1;
println!("Encapsulated to R5 ref-impl composite KEM: {name}");
}
assert!(
encapsulated >= 12,
"expected >=12 composite KEM encapsulations, got {encapsulated}"
);
}

/// Cross-provider interop for draft-19 composite *private* keys.
///
/// Loads each composite signature private key produced by the reference
/// implementation (PKCS#8 `mldsaSeed || tradSK`, production OID arc), signs a
/// message with it, and verifies the signature against the matching trust
/// anchor's public key. A pass proves our ML-DSA seed expansion and
/// traditional-key decoding reconstruct exactly the reference key pair.
#[test]
fn test_r5_composite_sig_privkey_interop() {
let base = "test/data/r5_interop/composite-sigs-ref-impl/";
let msg = b"quantcrypt composite private-key interop test";
let mut count = 0usize;
for file in std::fs::read_dir(base).unwrap() {
let path = file.unwrap().path();
let name = path.file_name().unwrap().to_str().unwrap().to_string();
if !name.ends_with("_priv.der") || !name.contains("1.3.6.1.5.5.7.6.") {
continue;
}
let prefix = name.strip_suffix("_priv.der").unwrap();
let sk = crate::keys::PrivateKey::from_file(path.to_str().unwrap())
.unwrap_or_else(|e| panic!("load sig priv {name}: {e:?}"));
let ta = crate::certificates::Certificate::from_file(&format!("{base}{prefix}_ta.der"))
.unwrap();
let pk = ta.get_public_key().unwrap();
let sig = sk
.sign(msg)
.unwrap_or_else(|e| panic!("sign with {name}: {e:?}"));
assert!(
pk.verify(msg, &sig).unwrap(),
"signature from ref-impl private key {name} failed to verify"
);
count += 1;
}
assert!(
count >= 18,
"expected >=18 composite sig private-key interop, got {count}"
);
}

/// Cross-provider interop for draft-15 composite *private* keys, using the
/// reference implementation's full decapsulation vectors.
///
/// For each composite KEM private key (PKCS#8 `mlkemSeed || tradSK`), loads
/// it, decapsulates the reference `_ciphertext.bin`, and asserts the derived
/// shared secret equals the reference `_ss.bin`. This validates, end-to-end
/// against the reference implementation, our ML-KEM seed expansion, the
/// traditional-key decoding, the tradPK recomputation, and the SHA3-256 KEM
/// combiner.
#[test]
fn test_r5_composite_kem_privkey_interop() {
let base = "test/data/r5_interop/composite-kem-ref-impl/";
let mut count = 0usize;
for file in std::fs::read_dir(base).unwrap() {
let path = file.unwrap().path();
let name = path.file_name().unwrap().to_str().unwrap().to_string();
if !name.ends_with("_priv.der") || !name.contains("1.3.6.1.5.5.7.6.") {
continue;
}
let prefix = name.strip_suffix("_priv.der").unwrap();
let ct = std::fs::read(format!("{base}{prefix}_ciphertext.bin")).unwrap();
let expected_ss = std::fs::read(format!("{base}{prefix}_ss.bin")).unwrap();
let sk = crate::keys::PrivateKey::from_file(path.to_str().unwrap())
.unwrap_or_else(|e| panic!("load kem priv {name}: {e:?}"));
let ss = sk
.decap(&ct)
.unwrap_or_else(|e| panic!("decap with {name}: {e:?}"));
assert_eq!(
ss, expected_ss,
"decapsulated shared secret mismatch for {name}"
);
count += 1;
}
assert!(
count >= 12,
"expected >=12 composite KEM decap vectors, got {count}"
);
}

/// Cross-provider interop for the draft-19/-15 composite algorithms against
/// GREEN R5 artifacts submitted by independent third parties (Bouncy Castle,
/// Entrust, Crypto4A, CryptoNext, leancrypto) — not our own submission and
/// not only the reference implementation. For each provider we verify every
/// composite signature self-signed cert (`*_ta.der`); load every composite
/// signature private key, sign, and verify the signature against that
/// provider's trust-anchor public key; and load every composite KEM private
/// key, decapsulate that provider's `*_ciphertext.bin`, and assert the
/// result equals its `*_ss.bin`. Any load/verify/decap failure fails the
/// test (no silent skipping).
#[test]
fn test_r5_cross_provider_interop() {
let base = "test/data/r5_interop/providers";
// Minimum composite artifacts expected per provider (0 => provider dir
// may be absent; leancrypto only submitted 3 composite signatures).
let providers = [
("bc", 18, 12),
("entrust", 18, 12),
("crypto4a", 18, 12),
("cryptonext", 18, 12),
("leancrypto", 3, 0),
];
let mut total_sig = 0usize;
let mut total_kem = 0usize;
for (prov, min_sig, min_kem) in providers {
let dir = format!("{base}/{prov}");
let files: Vec<std::path::PathBuf> = match std::fs::read_dir(&dir) {
Ok(rd) => rd.filter_map(|e| e.ok()).map(|e| e.path()).collect(),
Err(_) => panic!("missing provider corpus: {dir}"),
};
let find = |suffix: &str, oid: &str| -> Option<std::path::PathBuf> {
files
.iter()
.find(|p| {
let n = p.file_name().unwrap().to_str().unwrap();
n.ends_with(suffix) && n.contains(oid)
})
.cloned()
};
let msg = b"cross-provider composite interop";

// Signatures (.37-.54): verify TA, and sign+verify with the priv key.
let mut sig_ok = 0;
for oidn in 37..=54 {
let oid = format!("1.3.6.1.5.5.7.6.{oidn}_");
let Some(ta_p) = find("_ta.der", &oid) else {
continue;
};
let ta = crate::certificates::Certificate::from_file(ta_p.to_str().unwrap())
.unwrap_or_else(|e| panic!("{prov} load TA .{oidn}: {e:?}"));
assert!(
ta.verify_self_signed().unwrap(),
"{prov}: TA .{oidn} failed self-verify"
);
if let Some(priv_p) = find("_priv.der", &oid) {
let sk = crate::keys::PrivateKey::from_file(priv_p.to_str().unwrap())
.unwrap_or_else(|e| panic!("{prov} load sig priv .{oidn}: {e:?}"));
let sig = sk
.sign(msg)
.unwrap_or_else(|e| panic!("{prov} sign .{oidn}: {e:?}"));
assert!(
ta.get_public_key().unwrap().verify(msg, &sig).unwrap(),
"{prov}: signature by priv .{oidn} did not verify against its TA"
);
}
sig_ok += 1;
}
assert!(
sig_ok >= min_sig,
"{prov}: expected >={min_sig} composite sig certs, got {sig_ok}"
);
total_sig += sig_ok;

// KEM (.55-.66): decap the provider ciphertext, compare to its ss.
let mut kem_ok = 0;
for oidn in 55..=66 {
let oid = format!("1.3.6.1.5.5.7.6.{oidn}_");
let (Some(priv_p), Some(ct_p), Some(ss_p)) = (
find("_priv.der", &oid),
find("_ciphertext.bin", &oid),
find("_ss.bin", &oid),
) else {
continue;
};
let sk = crate::keys::PrivateKey::from_file(priv_p.to_str().unwrap())
.unwrap_or_else(|e| panic!("{prov} load kem priv .{oidn}: {e:?}"));
let ct = std::fs::read(&ct_p).unwrap();
let ss = sk
.decap(&ct)
.unwrap_or_else(|e| panic!("{prov} decap .{oidn}: {e:?}"));
assert_eq!(
ss,
std::fs::read(&ss_p).unwrap(),
"{prov}: KEM .{oidn} shared secret mismatch"
);
kem_ok += 1;
}
assert!(
kem_ok >= min_kem,
"{prov}: expected >={min_kem} composite KEM vectors, got {kem_ok}"
);
total_kem += kem_ok;
println!("{prov}: {sig_ok} sig + {kem_ok} kem composite artifacts verified");
}
// 4 full providers x (18 sig + 12 kem) + leancrypto 3 sig = 75 sig, 48 kem.
assert!(
total_sig >= 75,
"expected >=75 cross-provider sig checks, got {total_sig}"
);
assert!(
total_kem >= 48,
"expected >=48 cross-provider kem checks, got {total_kem}"
);
}
}
Loading
Loading