From 7fd82dc1dead08962e49899f1976f3872510b085 Mon Sep 17 00:00:00 2001 From: Mark Karpeles Date: Sat, 24 Oct 2026 06:32:30 +0900 Subject: [PATCH 1/2] Add a blocklist covering disabled roots and compromised intermediates Ship a blocklist alongside the trust store so callers can reject a certificate wherever it comes from, including a root supplied by the OS store. It holds every disabled root (roots/**/*.disabled) and the 16 compromised eMudhra intermediate certificates (blocked/), keyed by the SHA-256 of the certificate and of its SubjectPublicKeyInfo. - blocked(), blocked_by_spki(), blocked_by_cert(): static lookups, no runtime hashing - optional `check` feature: blocked_by_der() and check_chain() parse and hash DER chains; still no_std, no alloc, no dependencies - build.rs fingerprints each entry with a shared core-only SHA-256, requires a `# Blocked:` reason header, checks published fingerprint headers, and fails if a blocked key is also a trusted root's key - docs: README, CURATION.md, CHANGELOG-roots.md; CI covers the feature Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 + CHANGELOG-roots.md | 14 ++ CURATION.md | 23 +++ Cargo.toml | 8 + README.md | 46 ++++- .../EM_DV_TLS_CA_G3B.Sectigo_AAA.pem | 28 +++ .../EM_DV_TLS_CA_G3B.emSign.pem | 26 +++ .../EM_DV_TLS_CA_G3C.Sectigo_AAA.pem | 28 +++ .../EM_DV_TLS_CA_G3C.emSign.pem | 26 +++ .../EM_EV_TLS_CA_G3B.Sectigo_AAA.pem | 28 +++ .../EM_EV_TLS_CA_G3B.emSign.pem | 26 +++ .../EM_EV_TLS_CA_G3C.Sectigo_AAA.pem | 28 +++ .../EM_EV_TLS_CA_G3C.emSign.pem | 26 +++ .../EM_OV_CS_CA_G3B.Sectigo_AAA.pem | 28 +++ .../EM_OV_CS_CA_G3B.emSign.pem | 25 +++ .../EM_OV_CS_CA_G3C.Sectigo_AAA.pem | 28 +++ .../EM_OV_CS_CA_G3C.emSign.pem | 25 +++ .../EM_OV_TLS_CA_G3B.Sectigo_AAA.pem | 28 +++ .../EM_OV_TLS_CA_G3B.emSign.pem | 26 +++ .../EM_OV_TLS_CA_G3C.Sectigo_AAA.pem | 28 +++ .../EM_OV_TLS_CA_G3C.emSign.pem | 26 +++ .../2026-10-emudhra-intermediates/README.md | 22 +++ blocked/README.md | 13 ++ build.rs | 149 +++++++++++++-- build_support/mod.rs | 3 + .../Buypass/Buypass_Class_2_Root_CA.disabled | 1 + .../Buypass/Buypass_Class_3_Root_CA.disabled | 1 + roots/eMudhra/emSign_ECC_Root_CA_G3.disabled | 1 + src/check.rs | 123 +++++++++++++ src/lib.rs | 125 +++++++++++++ src/sha256.rs | 130 +++++++++++++ tests/blocked.rs | 171 ++++++++++++++++++ 32 files changed, 1251 insertions(+), 15 deletions(-) create mode 100644 blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3B.Sectigo_AAA.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3B.emSign.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3C.Sectigo_AAA.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3C.emSign.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3B.Sectigo_AAA.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3B.emSign.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3C.Sectigo_AAA.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3C.emSign.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3B.Sectigo_AAA.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3B.emSign.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3C.Sectigo_AAA.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3C.emSign.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3B.Sectigo_AAA.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3B.emSign.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3C.Sectigo_AAA.pem create mode 100644 blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3C.emSign.pem create mode 100644 blocked/2026-10-emudhra-intermediates/README.md create mode 100644 blocked/README.md create mode 100644 src/check.rs create mode 100644 src/sha256.rs create mode 100644 tests/blocked.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2ffc0a..ae5c897 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,8 +21,12 @@ jobs: run: cargo fmt --all -- --check - name: Clippy run: cargo clippy --all-targets -- -D warnings + - name: Clippy (check feature) + run: cargo clippy --all-targets --features check -- -D warnings - name: Test (includes openssl subject-hash cross-check) run: cargo test --all-targets + - name: Test (check feature) + run: cargo test --all-targets --features check - name: Verify curated roots against curation rules run: cargo run --example cacrt-tool -- verify @@ -38,6 +42,8 @@ jobs: # allocator. If it referenced std or alloc, this would fail to compile. - name: Build library for thumbv7em-none-eabi run: cargo build --lib --target thumbv7em-none-eabi + - name: Build library for thumbv7em-none-eabi (check feature) + run: cargo build --lib --target thumbv7em-none-eabi --features check msrv: name: Minimum supported Rust version diff --git a/CHANGELOG-roots.md b/CHANGELOG-roots.md index 0c8974e..a460fba 100644 --- a/CHANGELOG-roots.md +++ b/CHANGELOG-roots.md @@ -35,6 +35,20 @@ roots serve no purpose as trust anchors. The certificate files and the company `README.md` (with full rationale and sources) are kept in `roots/Buypass/`. See . +## 2026-10-24 — blocklist: disabled roots and eMudhra intermediates + +Added a blocklist that ships in the crate (`cacrt::blocked()`, see `CURATION.md` +"Blocking intermediates"). It starts with 19 certificates: + +- the 3 disabled roots: **emSign ECC Root CA - G3**, **Buypass Class 2 Root CA** + and **Buypass Class 3 Root CA**; +- the 16 compromised eMudhra intermediate certificates (8 keys, each issued by + emSign Root TLS/CS CA - G3 and cross-signed by Sectigo AAA Certificate + Services), in `blocked/2026-10-emudhra-intermediates/`. + +Callers that check every certificate in their chain now reject these even when +their anchor comes from an OS store that still trusts them. + ## 2026-10-21 — disabled emSign ECC Root CA - G3 (intermediate key compromise) Disabled **emSign ECC Root CA - G3** (eMudhra Technologies Limited, SHA-256 diff --git a/CURATION.md b/CURATION.md index 77c667b..3dbc817 100644 --- a/CURATION.md +++ b/CURATION.md @@ -85,10 +85,33 @@ trail intact (we can always see what was once trusted and why it was dropped) without deleting history. Record every such change in `CHANGELOG-roots.md`, and add a status banner to the company's `README.md`. +A disabled root is also **blocked**: it goes on the blocklist the crate ships +(`cacrt::blocked()`), so callers that check their whole chain reject it even when +it comes from another trust store. Every `.disabled` file must carry a +`# Blocked: ` header line; the build fails without one. + Typical reasons to disable rather than keep a root: the CA has stopped issuing TLS certificates and all of its end-entity certificates have since expired (so the anchor can no longer serve any live chain), or a root program has distrusted it. +## Blocking intermediates + +Some incidents call for distrusting specific CA certificates rather than (or +before) a whole root, for example an intermediate whose private key is +compromised. Put each such certificate as a PEM file under +`blocked/-/`, with a `# Blocked: ` header and a +`README.md` for the incident. Include **every** certificate known to carry the +key, cross-signs included. Never commit private keys there: `blocked/` ships in +the crate. Evidence that includes keys belongs in `evidence/`, which does not ship. + +The build fingerprints each file (SHA-256 of the certificate and of its +`SubjectPublicKeyInfo`), checks any `# SHA256 fingerprint:` / `# SPKI SHA256:` +header against the certificate, and fails if a blocked key is also the key of a +trusted root. + +Blocking an intermediate protects only callers that check the blocklist. When a +root program removes the root itself, disable the root as well. + ## Auditability Every addition or removal of a root is recorded in diff --git a/Cargo.toml b/Cargo.toml index c41eb24..0167f0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,5 +32,13 @@ unsafe_code = "forbid" [lib] path = "src/lib.rs" +[features] +# Runtime blocklist checks on DER certificates (`check_chain`, `blocked_by_der`): +# adds a tiny DER walker and SHA-256. Still no_std, no alloc, no dependencies. +check = [] + +[package.metadata.docs.rs] +all-features = true + # The library itself needs nothing. build.rs and examples use only std + the # hand-rolled helpers under build_support/, so there are no dependencies at all. diff --git a/README.md b/README.md index cae8cb3..0bda627 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,47 @@ for candidate in cacrt::find_by_subject(issuer_der_name) { Each `Cert` exposes `der()`, `subject_der()`, `subject_hash()`, `seq()`, `label()`, and `hash_name()` (a `Display` value — no allocation needed). +## Blocklist: check the whole chain + +Leaving a CA out of the store is not enough when your software also trusts +another store, such as the operating system's. So `cacrt` also ships a +**blocklist** of certificates that must never be trusted, wherever they come +from: + +- every root `cacrt` has disabled (`roots/**/*.disabled`), and +- compromised intermediates (`blocked/`), such as the eMudhra intermediates + whose private keys were trivially small integers. + +Entries are keyed by the SHA-256 of the certificate **and** of its +`SubjectPublicKeyInfo`. Matching on the key catches every certificate that +carries it: cross-signs, reissues and copies we have never seen. + +After your verifier has built and accepted a chain, **check every certificate in +it, leaf to root, including a root that came from the OS store.** A root that +`cacrt` disabled then stays rejected on systems that still trust it. + +```rust +// Zero-cost lookups: hash with your own crypto, compare against static data. +if let Some(b) = cacrt::blocked_by_spki(&spki_sha256) { + return Err(format!("blocked: {} ({})", b.label(), b.reason())); +} + +// Or, with the `check` feature, pass the DER chain and let cacrt parse and hash. +cacrt::check_chain(&[leaf_der, intermediate_der, root_der])?; +``` + +| Function | Purpose | +|---|---| +| `cacrt::blocked() -> &'static [Blocked]` | Every blocked certificate, with `kind()`, `label()`, `reason()` and both hashes. | +| `cacrt::blocked_by_spki(&[u8; 32]) -> Option<&Blocked>` | Is this **key** blocked? (SHA-256 of the DER SPKI.) | +| `cacrt::blocked_by_cert(&[u8; 32]) -> Option<&Blocked>` | Is this **certificate** blocked? (SHA-256 of its DER.) | +| `cacrt::blocked_by_der(&[u8])` *(feature `check`)* | Both checks on one DER certificate. | +| `cacrt::check_chain(&[&[u8]])` *(feature `check`)* | Check every certificate of a chain and report the first blocked one. | + +The blocklist only helps verifiers that call it. Generic TLS stacks (rustls, +OpenSSL) know nothing about it. Hook it in after normal verification, for +example from a rustls `ServerCertVerifier` wrapper. + ## The `062cdee6.0` naming This is exactly OpenSSL's hash-directory filename (`c_rehash` / `openssl @@ -97,8 +138,9 @@ cargo run --example cacrt-tool -- hash path/to/cert.pem ## `no_std` / no-`alloc` guarantee The library is `#![no_std]`, `#![forbid(unsafe_code)]`, and never references -`alloc`. CI builds it for `thumbv7em-none-eabi` (a bare-metal target with no std -and no default allocator) to prove it. +`alloc`, with or without the `check` feature. CI builds both for +`thumbv7em-none-eabi` (a bare-metal target with no std and no default allocator) +to prove it. ## License diff --git a/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3B.Sectigo_AAA.pem b/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3B.Sectigo_AAA.pem new file mode 100644 index 0000000..3a02f73 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3B.Sectigo_AAA.pem @@ -0,0 +1,28 @@ +# Label: EM DV TLS CA - G3B (issued by AAA Certificate Services) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE +# SPKI SHA256: EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAw +ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y +ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW +MRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAATnntnASpdzxHNeUREdqFrCvV5kqzdLgOEsvCA47B5PDjuBL8mFnh5nr7BO +L4vJf3SFumWqkR7opORbYtXMA9Vr5nizw4y03JN8okYOI4RTyJKTXSnUT5HhVga2 +PM7atGijggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd +BgNVHQ4EFgQU4h4v5xwZOQ5VS2LBICRXNSSKJRQwDgYDVR0PAQH/BAQDAgEGMBIG +A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC +MDEGA1UdIAQqMCgwCAYGZ4EMAQIBMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h +AQJkMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB +QUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF +BQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB +AQBzQ1IoqpdotM6gFrR4Q3YrKifOLeaj5XXunldpVE/vZn2TxC2y0s8oYiLR5ICE +wqVXSB7N6BM1Mp+Nif7Jbhb2IswAfSR4Iik8+B0iRmi7lYoNlycCgHjmsjOmZaxM +QkPmjXAJbG5f4V41OsvJZmS7lI3r0IuvEDVX3GAZ13on6IImSs9ybNpHQ7NCqkMQ +7wH4uAO1dtpFycgJSdHaCOhHjrox2BTYGLdpUrYwY3EOAjlZ8UGgsxs7mG+xquMl +bN1LFURwfnkVMP3T/DBIZ/ys2+lNFyxK3FlArrV4/000uTnVB1lO8oGORYdXoT5V +GUyRCEIDhF5TBUuKF4E3kf1h +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3B.emSign.pem b/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3B.emSign.pem new file mode 100644 index 0000000..6719761 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3B.emSign.pem @@ -0,0 +1,26 @@ +# Label: EM DV TLS CA - G3B (issued by emSign Root TLS CA - G3) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493 +# SPKI SHA256: EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDaDCCAu6gAwIBAgIQAKSu5njyM2K0wORgt3LyFDAKBggqhkjOPQQDAzBWMQsw +CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTU1 +NDA4WhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11 +ZGhyYSBCVjEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE557ZwEqXc8RzXlERHahawr1eZKs3S4DhLLwgOOweTw47gS/J +hZ4eZ6+wTi+LyX90hbplqpEe6KTkW2LVzAPVa+Z4s8OMtNyTfKJGDiOEU8iSk10p +1E+R4VYGtjzO2rRoo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L +WUIgkFQwHQYDVR0OBBYEFOIeL+ccGTkOVUtiwSAkVzUkiiUUMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB +BAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY +aHR0cDovL29jc3AtYi5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw +b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQi5wN2MwNwYDVR0f +BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j +cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E +AwMDaAAwZQIwLmsGXi6oFMJ2klkcEMXMug6gvxQWZWkewiDWVzQyl2L6z6DuQfPO +Ibh1grvdULDCAjEAxZvLlxUavG69J6wnSjC8XBbqNIQsJ6CE2QdNOe84Ptxb6VG8 +gt2TlfJbHii+KNvK +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3C.Sectigo_AAA.pem b/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3C.Sectigo_AAA.pem new file mode 100644 index 0000000..795b1db --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3C.Sectigo_AAA.pem @@ -0,0 +1,28 @@ +# Label: EM DV TLS CA - G3C (issued by AAA Certificate Services) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72 +# SPKI SHA256: 49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIRAJHzR/Glt9OZYrleDuTimdEwDQYJKoZIhvcNAQELBQAw +ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y +ODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu +YzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE +ACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFypDPjYw8C +vRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpVbMyLpQVh +3xsqRB0Bo4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw +HQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMA4GA1UdDwEB/wQEAwIBBjAS +BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD +AjAxBgNVHSAEKjAoMAgGBmeBDAECATANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO +IQECZDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B +QUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB +BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC +AQEAr7b6Mx11TIJgVzHt6EH5qGNWeiiqoMrp8E5/k8s+jvupx1n083pZ5XOzUzND +wEUe0W4C4FIGUHNQ4XDwqOw0yK42xqm2dvCKgsko2UvDlT0haZVtTCeWMQOe5J/J +ziaMfZwsZNfZ3aaI6XPtWDZtwY8AaNfyaeCpbtm5GcGnyNWk4jSNpbtpo6NjfXuP +CFOm3rTjJ4zytP7Tso6iMm0L01cYFOlTmoPv4515R2r3SPnKDtijoB/D/vgxzojv +DafgyG9afX8+laO9NbMMz4sqC1vZx3984i/sBNFiugm3Vvdq5OdJRjuGC1QILIs6 +dg/K3i8VQqRpbfaM9kIlMzZ3dQ== +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3C.emSign.pem b/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3C.emSign.pem new file mode 100644 index 0000000..f619e25 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_DV_TLS_CA_G3C.emSign.pem @@ -0,0 +1,26 @@ +# Label: EM DV TLS CA - G3C (issued by emSign Root TLS CA - G3) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19 +# SPKI SHA256: 49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDaDCCAu6gAwIBAgIPWIsuPCu4zaE+GhIJJUMhMAoGCCqGSM49BAMDMFYxCzAJ +BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk +MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM1 +MDlaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk +aHJhIEluYzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFy +pDPjYw8CvRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpV +bMyLpQVh3xsqRB0Bo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L +WUIgkFQwHQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB +BAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY +aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw +b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQy5wN2MwNwYDVR0f +BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j +cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E +AwMDaAAwZQIxAMEN2HWMocbuWdy1d33pH6JpMvP/V/DIS5PtZbv1Q7TXEbxrcyxg +yuddOJscYDJJlAIwB4sH1BpbScDzkdD4tJBSwrG/1Mx82vb8YaySehNuu1+1oXNd +AEKwaJ0mVzScy2+6 +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3B.Sectigo_AAA.pem b/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3B.Sectigo_AAA.pem new file mode 100644 index 0000000..2b6a9cd --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3B.Sectigo_AAA.pem @@ -0,0 +1,28 @@ +# Label: EM EV TLS CA - G3B (issued by AAA Certificate Services) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512 +# SPKI SHA256: E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDzTCCArWgAwIBAgIRANj4aF5PK9nYJAKoWhwetzEwDQYJKoZIhvcNAQELBQAw +ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y +ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW +MRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQl26haZNOX2gsQUunqVG5Ie+/qnJ2daKDF6tz+73Wmm7XW/AH0K/vKbrQL +zWTAdn42/9Bdl6gTxaoMbJUw55z18IZkaPE4BkIKpchRhAseJSVA55DktlTDhS7W +SvHoZX2jggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd +BgNVHQ4EFgQUPtwg2YlGmId7hz/YgUeyXBXYRwYwDgYDVR0PAQH/BAQDAgEGMBIG +A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC +MDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB +AngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB +Q2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF +BzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB +AJNFuAFW9Gnw/8+kCO4KIlZdnM1Sh2u+8cK0EYAHhXpb5FlGvYkcARigo5oS7VoM +7FOYlTueV6yICZKNAYawZPJPhNCGK+rp3cQBEz6d0n8OZEYZov9lb3ll8ORgSDuf +FVIW6Gagg/UnOamLBTOR8eD5V73SCme/B2cHUDw+m5E8cc+0Ujzz3Cn3ex5JrhXk +2T3s8PoVzEmhTgGboFfcWO1EYhPPrYwdxPYRQIXAXUXhLEOVNL9ASbZJLFI8E1IM +m7NwxVN2s38jWpaCUaaF0tg7hfiy7oe13rzJfHsdqldJVTTUTPWVx8TC4ju0VLet +bAlwhwFT6AP8uIVMGUHNw24= +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3B.emSign.pem b/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3B.emSign.pem new file mode 100644 index 0000000..a28cf01 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3B.emSign.pem @@ -0,0 +1,26 @@ +# Label: EM EV TLS CA - G3B (issued by emSign Root TLS CA - G3) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838 +# SPKI SHA256: E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDaDCCAu2gAwIBAgIQAMfEHv6S2CXwXq6EGkqCwTAKBggqhkjOPQQDAzBWMQsw +CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTYw +MzMxWhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11 +ZGhyYSBCVjEbMBkGA1UEAwwSRU0gRVYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAEJduoWmTTl9oLEFLp6lRuSHvv6pydnWigxerc/u91ppu11vwB +9Cv7ym60C81kwHZ+Nv/QXZeoE8WqDGyVMOec9fCGZGjxOAZCCqXIUYQLHiUlQOeQ +5LZUw4Uu1krx6GV9o4IBlTCCAZEwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L +WUIgkFQwHQYDVR0OBBYEFD7cINmJRpiHe4c/2IFHslwV2EcGMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjBcBgNVHSAEVTBTMDkGCysGAQQBg44hAQABMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB +BAGDjiEBAngwBwYFZ4EMAQEwdQYIKwYBBQUHAQEEaTBnMCQGCCsGAQUFBzABhhho +dHRwOi8vb2NzcC1iLmVtc2lnbi5jb20wPwYIKwYBBQUHMAKGM2h0dHA6Ly9yZXBv +c2l0b3J5LmVtc2lnbi5jb20vY2VydHMvRU1FVlRMU0NBRzNCLnA3YzA3BgNVHR8E +MDAuMCygKqAohiZodHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3R0bHNjYWczLmNy +bDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQD +AwNpADBmAjEA9Jq999oaOUKhFwopw33iCmt0OHIn3gjT43GfuOHR/YckXdwoNdDA +lIEhz0NgRO3/AjEAvY/a3OMvktIZLYjn+WLKmp0QhrIMV+e6EpS7j4GVeOKouNp6 +MN/P+ByGeiRMghzB +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3C.Sectigo_AAA.pem b/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3C.Sectigo_AAA.pem new file mode 100644 index 0000000..57298f2 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3C.Sectigo_AAA.pem @@ -0,0 +1,28 @@ +# Label: EM EV TLS CA - G3C (issued by AAA Certificate Services) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F +# SPKI SHA256: 5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDzTCCArWgAwIBAgIQBfB4dS3s56G7hZXTAxZxcTANBgkqhkiG9w0BAQsFADB7 +MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE +AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTI1MDUxNDAwMDAwMFoXDTI4 +MTIzMTIzNTk1OVowQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5j +MRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0MwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAQtVugppvuCxkpGl9lgr5UekGeukfkm00MG2kioqut4abamfm0nQm4EQqoD +s24ZiN/2NlFO4c8H9bpLP/KgZkWufTHVhmrTGqPSRusLMXfumct1ZsUWznSAimaP +kfDEf8qjggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd +BgNVHQ4EFgQUpiVegKlNt/OWfBvHkd+SvO5L618wDgYDVR0PAQH/BAQDAgEGMBIG +A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC +MDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB +AngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB +Q2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF +BzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB +AIFfHiaMSjnrdZygRYV4ekRBmG7ybcFk4hGQWMMyMFi0cAEk2+dGfz5bl8IaIbLq +z+uVJzhOgVEX7WqIfgbWfFsfPYHYjPHBmy7SlZ3BwA86IQ6qF/etpX2/qL9A5yRN +N3nVowUpaqV6H3aCpkAh3wR2ZNkvAbLDvPz3J0l+sEWjWv+ZISOV4R+hXDTdPXV0 +1lKxnbwqEsdo/sE2QkCDKwFrZh/0uKjWspydJj+jSaDLTrGNx50Dgmn7VEcD1ZI5 +FXjknVUiO25YlmS1dtVZSBbyUKSjlrP/yDwPcZunoAsmCAAtCKsFLviLaymzvsKI +eSSDGxpSySp2VCxompGDk8g= +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3C.emSign.pem b/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3C.emSign.pem new file mode 100644 index 0000000..d4e04c3 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_EV_TLS_CA_G3C.emSign.pem @@ -0,0 +1,26 @@ +# Label: EM EV TLS CA - G3C (issued by emSign Root TLS CA - G3) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3 +# SPKI SHA256: 5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDaDCCAu6gAwIBAgIQAIAJkopkEHYBrwX6LxnxCTAKBggqhkjOPQQDAzBWMQsw +CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTUz +OTMxWhcNNDAwNDI4MTgzMDAwWjBAMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZU11 +ZGhyYSBJbmMxGzAZBgNVBAMMEkVNIEVWIFRMUyBDQSAtIEczQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABC1W6Cmm+4LGSkaX2WCvlR6QZ66R+SbTQwbaSKiq63hptqZ+ +bSdCbgRCqgOzbhmI3/Y2UU7hzwf1uks/8qBmRa59MdWGatMao9JG6wsxd+6Zy3Vm +xRbOdICKZo+R8MR/yqOCAZUwggGRMB8GA1UdIwQYMBaAFOE6qyF88eNNVK7Ucrf9 +i1lCIJBUMB0GA1UdDgQWBBSmJV6AqU2385Z8G8eR35K87kvrXzAdBgNVHSUEFjAU +BggrBgEFBQcDAQYIKwYBBQUHAwIwXAYDVR0gBFUwUzA5BgsrBgEEAYOOIQEAATAq +MCgGCCsGAQUFBwIBFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMA0GCysG +AQQBg44hAQJ4MAcGBWeBDAEBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY +aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw +b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRVZUTFNDQUczQy5wN2MwNwYDVR0f +BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j +cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E +AwMDaAAwZQIwBy1wTnzH4lXDTy2PD91ysLjvnlVN3JbJmc1BIMEU8TOLkYVHibMc +9HeyonikFSfmAjEA6uE3QRxlrZTz/1/lXPTEMUhoauAtycagtlNFp79kjhvNa2/f +Aal1GNxsOutLnr0Y +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3B.Sectigo_AAA.pem b/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3B.Sectigo_AAA.pem new file mode 100644 index 0000000..bfe5580 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3B.Sectigo_AAA.pem @@ -0,0 +1,28 @@ +# Label: EM OV CS CA - G3B (issued by AAA Certificate Services) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8 +# SPKI SHA256: EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIQQeMrc6wYXWzg10gLrPhOaTANBgkqhkiG9w0BAQsFADB7 +MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE +AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTI1MDUxNDAwMDAwMFoXDTI4 +MTIzMTIzNTk1OVowPjELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYx +GjAYBgNVBAMMEUVNIE9WIENTIENBIC0gRzNCMHYwEAYHKoZIzj0CAQYFK4EEACID +YgAEqrsPsGZqz88B+tUr/P1MK/LLPDSX3nOVDlgZth/mlQmHQcjEkUObNKBDHK/+ +W/m0wcjorgAB+fTcitgsWfNjnpbNbD1NsM7+wmw9YlF7N3YTcnQ/jordd7EAZPKr +5eBQo4IBLDCCASgwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD +VR0OBBYEFHn66OzP2w11jaRY1p+27dx8Sd7mMA4GA1UdDwEB/wQEAwIBBjASBgNV +HRMBAf8ECDAGAQH/AgEAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDIGA1UdIAQrMCkw +CAYGZ4EMAQQBMA0GCysGAQQBg44hAQABMA4GDCsGAQQBg44hAQKBSDBDBgNVHR8E +PDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9BQUFDZXJ0aWZpY2F0 +ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6 +Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAIh0OzWsdBYhY +7+yxPGyosRL+uUSN6/Mrd30fBuougs5FVWQ3cRgLhImnUUgXADY9buugWhMmfT3c +yU5T1MFXZXClCILm5mfg+oqm/ikUWJl3Ayf2GvYf2O++6vzqvkn418nI5Lpdt0O7 +Li524YS/3CZHDA+zZQ7rrHpZXgQ9Hmp8AUQ4FneaGjNanFoYxiZxwCyW5z4u7AoK +0bmOVqKtcDFoVNz5++G9TYDJdhWOXmKKgzqRo1MK0PjazNT7MGHqknP93SqMVJ1g +mX/qNWxYteAjlvFanQxBNX/+TRxDjuFsEHO/7BvqSpIIck0UEEfizhN8zNpWLbcl +EqVgl0g39A== +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3B.emSign.pem b/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3B.emSign.pem new file mode 100644 index 0000000..57a31f4 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3B.emSign.pem @@ -0,0 +1,25 @@ +# Label: EM OV CS CA - G3B (issued by emSign Root CS CA - G3) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4 +# SPKI SHA256: EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDWzCCAuCgAwIBAgIPeBiTP4+FtGGaKzX7qD0KMAoGCCqGSM49BAMDMFUxCzAJ +BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk +MR8wHQYDVQQDDBZlbVNpZ24gUm9vdCBDUyBDQSAtIEczMB4XDTI1MDQzMDE2MDgy +N1oXDTQwMDQyODE4MzAwMFowPjELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRo +cmEgQlYxGjAYBgNVBAMMEUVNIE9WIENTIENBIC0gRzNCMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEqrsPsGZqz88B+tUr/P1MK/LLPDSX3nOVDlgZth/mlQmHQcjEkUOb +NKBDHK/+W/m0wcjorgAB+fTcitgsWfNjnpbNbD1NsM7+wmw9YlF7N3YTcnQ/jord +d7EAZPKr5eBQo4IBizCCAYcwHwYDVR0jBBgwFoAUaHQD+IXKjVRQVx0W4MjeRQ2r +zS0wHQYDVR0OBBYEFHn66OzP2w11jaRY1p+27dx8Sd7mMBMGA1UdJQQMMAoGCCsG +AQUFBwMDMF4GA1UdIARXMFUwOQYLKwYBBAGDjiEBAAEwKjAoBggrBgEFBQcCARYc +aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbTAOBgwrBgEEAYOOIQECgUgwCAYG +Z4EMAQQBMHQGCCsGAQUFBwEBBGgwZjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3At +Yi5lbXNpZ24uY29tMD4GCCsGAQUFBzAChjJodHRwOi8vcmVwb3NpdG9yeS5lbXNp +Z24uY29tL2NlcnRzL0VNT1ZDU0NBRzNCLnA3YzA2BgNVHR8ELzAtMCugKaAnhiVo +dHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3Rjc2NhZzMuY3JsMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMAoGCCqGSM49BAMDA2kAMGYCMQC/zr03 +YEELsNKqzUtH2dVFi4BaKFdyrr/6G1dIWp8KAVgsw2/u4lyyboUDaC9jDvICMQC9 +HTxku84tNX9dmHOtH7aC2Nu/JM0jTf6KjZAvovCJzzv1IFWSZpVLZibynMLXOrM= +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3C.Sectigo_AAA.pem b/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3C.Sectigo_AAA.pem new file mode 100644 index 0000000..99afdf7 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3C.Sectigo_AAA.pem @@ -0,0 +1,28 @@ +# Label: EM OV CS CA - G3C (issued by AAA Certificate Services) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD +# SPKI SHA256: 92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIRANal72E7OQ8y75zfNJ93KEEwDQYJKoZIhvcNAQELBQAw +ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y +ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu +YzEaMBgGA1UEAwwRRU0gT1YgQ1MgQ0EgLSBHM0MwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAS58OBjRwKmnKaq3UQ8/WLEG5uksgkkaX15dOdpGhJvuWr7PjTC/TvF9IMG +HyZdwqWENjRzI1EENrKPBPOGI2MGeYDiUD4CUy1lRiNacWWiWaHvSW90G1/e53hy +COS4+u+jggEsMIIBKDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd +BgNVHQ4EFgQU7m9zKY1vv6ezG8OVNPYVVkR4ZrcwDgYDVR0PAQH/BAQDAgEGMBIG +A1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwMgYDVR0gBCsw +KTAIBgZngQwBBAEwDQYLKwYBBAGDjiEBAAEwDgYMKwYBBAGDjiEBAoFIMEMGA1Ud +HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj +YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0 +cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBgGbZ+NDkn +2Fr5dCVs/51+6cT5iEPP1stglX4TI2UaNh9LmPdltxXw3crV8gK5L8i+d+ScGibF +y0obtBHNTldXDgLnAHIzY3n+pceyKj7O2ae9CYnyuu5RBifeI3XYKowWE+oXnVGF +Oy0CvCJ/JsdP6aYgoCa9hGnAYicYQQL6MOMQZ5PuhcSM8t4giIzrpgrEkUP93GYE +emDG5Z8BYwKs80lLeQNBnwg/VHJ3iEF/o84RXYwrlrKqJ+33C094o6LNikMJCqOK +/WYHZSR8Xyhf8GXFAj5pZuI6Bcpug2V/E7i42j8OBhc25aNYDqK+B0qmb90sjNUB +g3tJeSIb/599 +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3C.emSign.pem b/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3C.emSign.pem new file mode 100644 index 0000000..6d5831b --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_OV_CS_CA_G3C.emSign.pem @@ -0,0 +1,25 @@ +# Label: EM OV CS CA - G3C (issued by emSign Root CS CA - G3) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1 +# SPKI SHA256: 92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDWzCCAuGgAwIBAgIPVEt/kiwxriIKSpIpFvbmMAoGCCqGSM49BAMDMFUxCzAJ +BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk +MR8wHQYDVQQDDBZlbVNpZ24gUm9vdCBDUyBDQSAtIEczMB4XDTI1MDQzMDE2Mzg0 +MFoXDTQwMDQyODE4MzAwMFowPzELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRo +cmEgSW5jMRowGAYDVQQDDBFFTSBPViBDUyBDQSAtIEczQzB2MBAGByqGSM49AgEG +BSuBBAAiA2IABLnw4GNHAqacpqrdRDz9YsQbm6SyCSRpfXl052kaEm+5avs+NML9 +O8X0gwYfJl3CpYQ2NHMjUQQ2so8E84YjYwZ5gOJQPgJTLWVGI1pxZaJZoe9Jb3Qb +X97neHII5Lj676OCAYswggGHMB8GA1UdIwQYMBaAFGh0A/iFyo1UUFcdFuDI3kUN +q80tMB0GA1UdDgQWBBTub3MpjW+/p7Mbw5U09hVWRHhmtzATBgNVHSUEDDAKBggr +BgEFBQcDAzBeBgNVHSAEVzBVMDkGCysGAQQBg44hAQABMCowKAYIKwYBBQUHAgEW +HGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDgYMKwYBBAGDjiEBAoFIMAgG +BmeBDAEEATB0BggrBgEFBQcBAQRoMGYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3Nw +LWMuZW1zaWduLmNvbTA+BggrBgEFBQcwAoYyaHR0cDovL3JlcG9zaXRvcnkuZW1z +aWduLmNvbS9jZXJ0cy9FTU9WQ1NDQUczQy5wN2MwNgYDVR0fBC8wLTAroCmgJ4Yl +aHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290Y3NjYWczLmNybDAOBgNVHQ8BAf8E +BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQDAwNoADBlAjEA8yPg +8sntOhLwV2QXJ5LEaKSpo/fnE3uOgTadK73fDhyjiqr0Io9GVuQTYqBPbQ2TAjBg +euApmBS486+iei03lRrp139dPw6k28jjGNvWsAU7A1N86J1zPJVDEyVH+nMAWC0= +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3B.Sectigo_AAA.pem b/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3B.Sectigo_AAA.pem new file mode 100644 index 0000000..697fb8a --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3B.Sectigo_AAA.pem @@ -0,0 +1,28 @@ +# Label: EM OV TLS CA - G3B (issued by AAA Certificate Services) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699 +# SPKI SHA256: 43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDzjCCAragAwIBAgIRAOlCI73pL5FOWrmsq/2sNxAwDQYJKoZIhvcNAQELBQAw +ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y +ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW +MRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK56Ks1xwy +Ko42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDftDIVOTMEv +TPhkbQ2jggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd +BgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowDgYDVR0PAQH/BAQDAgEGMBIG +A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC +MDEGA1UdIAQqMCgwCAYGZ4EMAQICMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h +AQJuMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB +QUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF +BQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB +AQALRoOSvkzRa+tN5Rje9W5GiXd/qsicd9N6Gnh0SWC7VyWH+HycIqAnFCMYZznj +CeGusomFYHE42SHWr85JDIbgzy6y3dO4i9n9w0Bx7yrXWBbDzFS8VgA3+EY4Cg7y +mtoC3RPOJVtaXXPZ+68X804g1Mj+3vCSHj2lNPVgco0OzSIOJ/KMg1R9MLBkiKzb +bERAGZHejY0HBhYd76IINZN8z6bBNah2UdFYxsOisLynPQUkol6gRLum024/FzVl +CCDfnUusrkr+mAfPAvD/Vb8bhpA7jYtepuU+2mIdShj5lLlvAwZphn+ypWycuo2x +0k9mldz2dqI9nhCiY+TTIOOR +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3B.emSign.pem b/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3B.emSign.pem new file mode 100644 index 0000000..d6789c8 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3B.emSign.pem @@ -0,0 +1,26 @@ +# Label: EM OV TLS CA - G3B (issued by emSign Root TLS CA - G3) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B +# SPKI SHA256: 43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDZjCCAu2gAwIBAgIPbK2jZ/scEACIEg6iAtVoMAoGCCqGSM49BAMDMFYxCzAJ +BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk +MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNjAw +MTJaFw00MDA0MjgxODMwMDBaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVk +aHJhIEJWMRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIB +BgUrgQQAIgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK +56Ks1xwyKo42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDft +DIVOTMEvTPhkbQ2jggGWMIIBkjAfBgNVHSMEGDAWgBThOqshfPHjTVSu1HK3/YtZ +QiCQVDAdBgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowHQYDVR0lBBYwFAYI +KwYBBQUHAwEGCCsGAQUFBwMCMF0GA1UdIARWMFQwOQYLKwYBBAGDjiEBAAEwKjAo +BggrBgEFBQcCARYcaHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbTANBgsrBgEE +AYOOIQECbjAIBgZngQwBAgIwdQYIKwYBBQUHAQEEaTBnMCQGCCsGAQUFBzABhhho +dHRwOi8vb2NzcC1iLmVtc2lnbi5jb20wPwYIKwYBBQUHMAKGM2h0dHA6Ly9yZXBv +c2l0b3J5LmVtc2lnbi5jb20vY2VydHMvRU1PVlRMU0NBRzNCLnA3YzA3BgNVHR8E +MDAuMCygKqAohiZodHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3R0bHNjYWczLmNy +bDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQD +AwNnADBkAjBAk+5BizdJhrED3wb7F2Gm7bWI60dlNd075nghT2TrR4NWKZtJZcbc +28xxIY/NONICMF6mPdsjosaqmvEMWqKcBh4vsIvXSKnQj5o8zrFB4oKS+accjYTe +8Dz3bQjauvy9uw== +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3C.Sectigo_AAA.pem b/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3C.Sectigo_AAA.pem new file mode 100644 index 0000000..a5f006b --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3C.Sectigo_AAA.pem @@ -0,0 +1,28 @@ +# Label: EM OV TLS CA - G3C (issued by AAA Certificate Services) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: 7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489 +# SPKI SHA256: 63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDzzCCAregAwIBAgIRAI1aSBGTnvRH13ERzdmEmukwDQYJKoZIhvcNAQELBQAw +ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV +BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y +ODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu +YzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE +ACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZfjpRIOHv +1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+N7ulaVSf +HAKycO1no4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw +HQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMA4GA1UdDwEB/wQEAwIBBjAS +BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD +AjAxBgNVHSAEKjAoMAgGBmeBDAECAjANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO +IQECbjBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B +QUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB +BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC +AQEAHlbhZpjhkPEAQVhPHRtBDaHqiisVD20pQ1TUeSx3l62Qe7NQ0b7vWQy7DN0i +KOu7BGpP5EnIjvXbz9FhpfPJ4FhKEt3tQ51rqXz2oD8vRaryvExWPobDUz3j7HN6 +OzBsVMIe3PD7AAkpky0x8zMSi/ep5R9OMzybmNptbtppyuB5ytgq+h7lQdRIP+CQ +TaJX3qTgfMqzntYre5GljmEB5hU2/rSn8zoSU2APkJidlZFpghtgbD4I2VRHZvCw +FFr/SrFM9pwrgbJNZ1MtyTSVgI5NlNb0aMfLR+/r6jJt3ROeY2yh2QQ2dx+Yqgdb +uL5ChFWOQU+ErSDnUq7yr+pjBQ== +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3C.emSign.pem b/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3C.emSign.pem new file mode 100644 index 0000000..5a68f80 --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/EM_OV_TLS_CA_G3C.emSign.pem @@ -0,0 +1,26 @@ +# Label: EM OV TLS CA - G3C (issued by emSign Root TLS CA - G3) +# Blocked: eMudhra intermediate private key compromise, 2026-10-17 (trivially small P-384 private keys); see CHANGELOG-roots.md 2026-10-21 +# SHA256 fingerprint: CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F +# SPKI SHA256: 63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8 +# Source: https://emudhra.we-have-a-problem.com/impacted-intermediates.json +-----BEGIN CERTIFICATE----- +MIIDaDCCAu6gAwIBAgIPKWkmwiejoVJjkrGUgqUEMAoGCCqGSM49BAMDMFYxCzAJ +BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk +MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM3 +NDFaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk +aHJhIEluYzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZ +fjpRIOHv1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+ +N7ulaVSfHAKycO1no4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L +WUIgkFQwHQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow +KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB +BAGDjiEBAm4wCAYGZ4EMAQICMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY +aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw +b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNT1ZUTFNDQUczQy5wN2MwNwYDVR0f +BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j +cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E +AwMDaAAwZQIxAMSZt2xsPeumRdeSMSQ3CbzIZeQ/q7GDCWfeE1Pq+mhgA6gRM44c +NVF3xJ+wsxJk+wIwFJbaDMokD8Ei6eNOiA8yUMYvfzQyngmrhZQMdPCK1SewrR79 +4rSb9vGqxZzelQth +-----END CERTIFICATE----- diff --git a/blocked/2026-10-emudhra-intermediates/README.md b/blocked/2026-10-emudhra-intermediates/README.md new file mode 100644 index 0000000..5dce53d --- /dev/null +++ b/blocked/2026-10-emudhra-intermediates/README.md @@ -0,0 +1,22 @@ +# eMudhra intermediate key compromise (October 2026) + +The 8 eMudhra intermediate CA keys below have trivially small P-384 private +keys (d ≤ 255) and are public. Each key was certified twice: once by its emSign +G3 root and once by Sectigo's AAA Certificate Services, so all 16 certificates +are here. Blocking by key (SPKI SHA-256) also catches any copy not listed. + +| Intermediate | SPKI SHA-256 | +|---|---| +| EM OV TLS CA - G3C | `63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8` | +| EM EV TLS CA - G3C | `5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0` | +| EM OV CS CA - G3B | `EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6` | +| EM EV TLS CA - G3B | `E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41` | +| EM OV TLS CA - G3B | `43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B` | +| EM DV TLS CA - G3B | `EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105` | +| EM DV TLS CA - G3C | `49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F` | +| EM OV CS CA - G3C | `92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30` | + +Source: . +The private keys and a verification script are archived (outside the crate) in +[`evidence/2026-10-emudhra-intermediates/`](../../evidence/2026-10-emudhra-intermediates/). +Rationale: [`CHANGELOG-roots.md`](../../CHANGELOG-roots.md), 2026-10-21. diff --git a/blocked/README.md b/blocked/README.md new file mode 100644 index 0000000..d9a3d22 --- /dev/null +++ b/blocked/README.md @@ -0,0 +1,13 @@ +# Blocked certificates + +CA certificates that must never be trusted, wherever they are found. The build +fingerprints every `*.pem` here, together with every disabled root +(`roots/**/*.disabled`), into the blocklist the crate exposes as +`cacrt::blocked()`. See [`CURATION.md`](../CURATION.md) "Blocking intermediates". + +| Directory | What | Certificates | +|---|---|---| +| [`2026-10-emudhra-intermediates/`](2026-10-emudhra-intermediates/) | eMudhra intermediates with trivially small private keys | 16 (8 keys) | + +Each file needs a `# Blocked: ` header. Never put private keys here: +this directory ships in the crate. diff --git a/build.rs b/build.rs index 9b582f6..8871173 100644 --- a/build.rs +++ b/build.rs @@ -3,13 +3,15 @@ //! Reads the curated `roots/*.pem` set, computes each certificate's OpenSSL //! subject hash, validates it against the curation rules, and emits a sorted, //! indexed static table to `$OUT_DIR/certs.rs` (plus the raw DER blobs under -//! `$OUT_DIR/der/`). The library `include!`s that file; nothing here ends up in -//! the runtime, which stays `#![no_std]` with no `alloc`. +//! `$OUT_DIR/der/`). It also fingerprints every blocked certificate (disabled +//! `roots/**/*.disabled` plus `blocked/**/*.pem`) into `$OUT_DIR/blocked.rs`. +//! The library `include!`s both files; nothing here ends up in the runtime, +//! which stays `#![no_std]` with no `alloc`. #[path = "build_support/mod.rs"] mod build_support; -use build_support::{curation, der, pem, subject_hash}; +use build_support::{curation, der, pem, sha256::sha256, subject_hash}; use std::fmt::Write as _; use std::path::Path; @@ -20,13 +22,24 @@ struct Entry { label: String, } +struct BlockedEntry { + is_root: bool, + spki_sha256: [u8; 32], + cert_sha256: [u8; 32], + label: String, + reason: String, +} + fn main() { let manifest = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"); let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR"); let roots = Path::new(&manifest).join("roots"); + let blocked_dir = Path::new(&manifest).join("blocked"); - // Rebuild when the root set or the generators change. + // Rebuild when the root set, the blocklist or the generators change. println!("cargo:rerun-if-changed=roots"); + println!("cargo:rerun-if-changed=blocked"); + println!("cargo:rerun-if-changed=src/sha256.rs"); println!("cargo:rerun-if-changed=build_support"); println!("cargo:rerun-if-changed=build.rs"); @@ -35,7 +48,7 @@ fn main() { // Roots are organized one folder per operating company, so walk recursively. let mut pems = Vec::new(); - collect_pems(&roots, &mut pems); + collect_files(&roots, "pem", &mut pems); pems.sort(); for path in &pems { println!("cargo:rerun-if-changed={}", path.display()); @@ -108,23 +121,128 @@ fn main() { writeln!(src, "];").unwrap(); std::fs::write(Path::new(&out_dir).join("certs.rs"), src).expect("write certs.rs"); + + write_blocklist(&roots, &blocked_dir, &entries, &out_dir); } -/// Recursively collect every `*.pem` under `dir` (roots are nested one folder +/// Fingerprint every blocked certificate and emit `$OUT_DIR/blocked.rs`. +/// +/// Sources are the disabled roots (`roots/**/*.disabled`) and the blocked +/// intermediates (`blocked/**/*.pem`). Each file must carry a `# Blocked:` +/// header giving the reason, and any `# SHA256 fingerprint:` / `# SPKI SHA256:` +/// header must match what we compute. A trusted root sharing a key with a +/// blocked entry fails the build: the two lists would contradict each other. +fn write_blocklist(roots: &Path, blocked_dir: &Path, trusted: &[Entry], out_dir: &str) { + let mut files = Vec::new(); + collect_files(roots, "disabled", &mut files); + if blocked_dir.is_dir() { + collect_files(blocked_dir, "pem", &mut files); + } + files.sort(); + + let mut blocked = Vec::new(); + for path in &files { + println!("cargo:rerun-if-changed={}", path.display()); + let file = path.display().to_string(); + let text = std::fs::read_to_string(path).unwrap_or_else(|e| panic!("{file}: {e}")); + let der = pem::read_one_certificate(&text).unwrap_or_else(|e| panic!("{file}: {e}")); + let tbs = der::tbs_certificate(&der).unwrap_or_else(|e| panic!("{file}: {e}")); + let field = |f| der::tbs_field(tbs, f).unwrap_or_else(|e| panic!("{file}: {e}")); + let spki = field(der::TbsField::Spki); + let is_root = field(der::TbsField::Subject) == field(der::TbsField::Issuer); + + let reason = parse_header(&text, "# Blocked:") + .unwrap_or_else(|| panic!("{file}: missing `# Blocked: ` header")); + let spki_sha256 = sha256(spki); + let cert_sha256 = sha256(&der); + for (header, digest) in [ + ("# SHA256 fingerprint:", &cert_sha256), + ("# SPKI SHA256:", &spki_sha256), + ] { + if let Some(want) = parse_header(&text, header) { + assert!( + want.eq_ignore_ascii_case(&hex(digest)), + "{file}: `{header}` header does not match the certificate" + ); + } + } + for t in trusted { + let t_tbs = der::tbs_certificate(&t.der).expect("trusted root re-parse"); + let t_spki = der::tbs_field(t_tbs, der::TbsField::Spki).expect("trusted root SPKI"); + assert!( + sha256(t_spki) != spki_sha256, + "{file}: blocked key is also trusted via root {:?}", + t.label + ); + } + + let label = + parse_label(&text).unwrap_or_else(|| stem(path.file_name().unwrap().to_str().unwrap())); + blocked.push(BlockedEntry { + is_root, + spki_sha256, + cert_sha256, + label, + reason, + }); + } + + // Sorted by (SPKI, cert) so the table is deterministic and binary-searchable + // by key; certificates sharing a key (cross-signs) end up adjacent. + blocked.sort_by_key(|b| (b.spki_sha256, b.cert_sha256)); + for w in blocked.windows(2) { + assert!( + w[0].cert_sha256 != w[1].cert_sha256 || w[0].spki_sha256 != w[1].spki_sha256, + "certificate {:?} is blocked twice", + w[0].label + ); + } + + let mut src = String::new(); + src.push_str( + "// @generated by build.rs from roots/**/*.disabled and blocked/**/*.pem — do not edit.\n", + ); + writeln!(src, "pub(crate) static BLOCKED: &[Blocked] = &[").unwrap(); + for b in &blocked { + writeln!( + src, + " Blocked::new({}, {:?}, {:?}, {:?}, {:?}),", + if b.is_root { + "BlockedKind::Root" + } else { + "BlockedKind::Intermediate" + }, + b.spki_sha256, + b.cert_sha256, + b.label, + b.reason + ) + .unwrap(); + } + writeln!(src, "];").unwrap(); + std::fs::write(Path::new(out_dir).join("blocked.rs"), src).expect("write blocked.rs"); +} + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02X}")).collect() +} + +/// Recursively collect every `*.` under `dir` (roots are nested one folder /// per company). Emits `rerun-if-changed` for each directory so that adding or /// removing a root triggers a rebuild. -fn collect_pems(dir: &Path, out: &mut Vec) { +fn collect_files(dir: &Path, ext: &str, out: &mut Vec) { println!("cargo:rerun-if-changed={}", dir.display()); let entries = std::fs::read_dir(dir).unwrap_or_else(|e| panic!("cannot read {}: {e}", dir.display())); for ent in entries { let path = ent.expect("dir entry").path(); if path.is_dir() { - collect_pems(&path, out); - } else if path.extension().and_then(|s| s.to_str()) == Some("pem") { - // Only *.pem is compiled in. A retired root is renamed to - // `*.disabled` so it stays in the repo (audit trail) but is excluded - // from the store — see CURATION.md "Disabling a root". + collect_files(&path, ext, out); + } else if path.extension().and_then(|s| s.to_str()) == Some(ext) { + // Only roots/**/*.pem is compiled into the store. A retired root is + // renamed to `*.disabled` so it stays in the repo (audit trail), is + // excluded from the store and lands on the blocklist instead — see + // CURATION.md "Disabling a root". out.push(path); } } @@ -132,8 +250,13 @@ fn collect_pems(dir: &Path, out: &mut Vec) { /// Pull the friendly name out of the `# Label: ...` PEM header comment. fn parse_label(text: &str) -> Option { + parse_header(text, "# Label:") +} + +/// The trimmed, non-empty value of the first ` value` header line. +fn parse_header(text: &str, prefix: &str) -> Option { text.lines() - .find_map(|l| l.trim_start().strip_prefix("# Label:")) + .find_map(|l| l.trim_start().strip_prefix(prefix)) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) } diff --git a/build_support/mod.rs b/build_support/mod.rs index e094d98..76efb81 100644 --- a/build_support/mod.rs +++ b/build_support/mod.rs @@ -14,6 +14,9 @@ pub mod der; pub mod octal; pub mod pem; pub mod sha1; +// Shared with the library's optional runtime `check` feature. +#[path = "../src/sha256.rs"] +pub mod sha256; /// Compute the OpenSSL subject hash of a DER-encoded certificate. /// diff --git a/roots/Buypass/Buypass_Class_2_Root_CA.disabled b/roots/Buypass/Buypass_Class_2_Root_CA.disabled index 18e2b3e..1977e9c 100644 --- a/roots/Buypass/Buypass_Class_2_Root_CA.disabled +++ b/roots/Buypass/Buypass_Class_2_Root_CA.disabled @@ -1,4 +1,5 @@ # Label: Buypass Class 2 Root CA +# Blocked: root disabled 2026-06-04, Buypass ended public TLS issuance (all certificates expired by 2026-04-15); see CHANGELOG-roots.md 2026-06-04 # OpenSSL subject hash: 9734f1e6 # SHA1 fingerprint: 49:0A:75:74:DE:87:0A:47:FE:58:EE:F6:C7:6B:EB:C6:0B:12:40:99 # Source: Mozilla NSS certdata.txt (CKT_NSS_TRUSTED_DELEGATOR, server auth) diff --git a/roots/Buypass/Buypass_Class_3_Root_CA.disabled b/roots/Buypass/Buypass_Class_3_Root_CA.disabled index cfe5e38..71075fc 100644 --- a/roots/Buypass/Buypass_Class_3_Root_CA.disabled +++ b/roots/Buypass/Buypass_Class_3_Root_CA.disabled @@ -1,4 +1,5 @@ # Label: Buypass Class 3 Root CA +# Blocked: root disabled 2026-06-04, Buypass ended public TLS issuance (all certificates expired by 2026-04-15); see CHANGELOG-roots.md 2026-06-04 # OpenSSL subject hash: a6a76f04 # SHA1 fingerprint: DA:FA:F7:FA:66:84:EC:06:8F:14:50:BD:C7:C2:81:A5:BC:A9:64:57 # Source: Mozilla NSS certdata.txt (CKT_NSS_TRUSTED_DELEGATOR, server auth) diff --git a/roots/eMudhra/emSign_ECC_Root_CA_G3.disabled b/roots/eMudhra/emSign_ECC_Root_CA_G3.disabled index 631b03d..df93674 100644 --- a/roots/eMudhra/emSign_ECC_Root_CA_G3.disabled +++ b/roots/eMudhra/emSign_ECC_Root_CA_G3.disabled @@ -1,4 +1,5 @@ # Label: emSign ECC Root CA - G3 +# Blocked: root disabled 2026-10-21 after eMudhra intermediate private key compromise; see CHANGELOG-roots.md 2026-10-21 # OpenSSL subject hash: 14bc7599 # SHA1 fingerprint: 30:43:FA:4F:F2:57:DC:A0:C3:80:EE:2E:58:EA:78:B2:3F:E6:BB:C1 # Source: Mozilla NSS certdata.txt (CKT_NSS_TRUSTED_DELEGATOR, server auth) diff --git a/src/check.rs b/src/check.rs new file mode 100644 index 0000000..47ca590 --- /dev/null +++ b/src/check.rs @@ -0,0 +1,123 @@ +//! Optional (`check` feature) runtime blocklist checks on DER certificates. +//! +//! A tiny, allocation-free DER walk finds the `SubjectPublicKeyInfo`, and the +//! certificate and its key are hashed with the bundled SHA-256. Nothing here +//! verifies signatures or builds paths: run it on a chain your verifier has +//! already accepted. + +use crate::sha256::sha256; +use crate::{blocked_by_cert, blocked_by_spki, Blocked}; + +/// The certificate is not DER this parser can read. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ParseError; + +impl core::fmt::Display for ParseError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("malformed DER certificate") + } +} + +/// Why [`check_chain`] rejected a chain. `index` is the position in the slice. +#[derive(Clone, Copy, Debug)] +pub enum ChainError { + /// The certificate at `index` (or its key) is on the blocklist. + Blocked { + /// Position of the certificate in the chain. + index: usize, + /// The matching blocklist entry. + entry: &'static Blocked, + }, + /// The certificate at `index` could not be parsed. + Malformed { + /// Position of the certificate in the chain. + index: usize, + }, +} + +impl core::fmt::Display for ChainError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ChainError::Blocked { index, entry } => write!( + f, + "certificate {index} is blocked: {} ({})", + entry.label(), + entry.reason() + ), + ChainError::Malformed { index } => write!(f, "certificate {index} is malformed"), + } + } +} + +/// The blocklist entry matching this DER certificate, by its key or by the +/// certificate itself. +pub fn blocked_by_der(cert_der: &[u8]) -> Result, ParseError> { + let spki = spki(cert_der)?; + Ok(blocked_by_spki(&sha256(spki)).or_else(|| blocked_by_cert(&sha256(cert_der)))) +} + +/// Check every DER certificate of a chain (leaf, intermediates **and root**) +/// against the blocklist. Fails on the first blocked or unparsable certificate. +/// +/// Pass the root too, even when it came from another trust store: that is how +/// a root `cacrt` has disabled stays rejected on systems that still trust it. +pub fn check_chain(chain: &[&[u8]]) -> Result<(), ChainError> { + for (index, der) in chain.iter().enumerate() { + match blocked_by_der(der) { + Ok(None) => {} + Ok(Some(entry)) => return Err(ChainError::Blocked { index, entry }), + Err(ParseError) => return Err(ChainError::Malformed { index }), + } + } + Ok(()) +} + +/// The raw `SubjectPublicKeyInfo` TLV of a DER `Certificate`. +fn spki(cert: &[u8]) -> Result<&[u8], ParseError> { + let (outer, rest) = read_tlv(cert, 0x30)?; + if !rest.is_empty() { + return Err(ParseError); + } + let (tbs, _) = read_tlv(outer, 0x30)?; + let mut rest = tbs; + // Skip the optional `[0] EXPLICIT Version`. + if rest.first() == Some(&0xA0) { + rest = read_tlv(rest, 0xA0)?.1; + } + // serialNumber, signature, issuer, validity, subject. + for tag in [0x02, 0x30, 0x30, 0x30, 0x30] { + rest = read_tlv(rest, tag)?.1; + } + let (content, after) = read_tlv(rest, 0x30)?; + let len = rest.len() - after.len(); + debug_assert!(content.len() < len); + Ok(&rest[..len]) +} + +/// Read one definite-length TLV with the expected tag, returning its content +/// and the bytes after it. +fn read_tlv(input: &[u8], tag: u8) -> Result<(&[u8], &[u8]), ParseError> { + let (&t, rest) = input.split_first().ok_or(ParseError)?; + if t != tag { + return Err(ParseError); + } + let (&first, mut rest) = rest.split_first().ok_or(ParseError)?; + let len = if first < 0x80 { + first as usize + } else { + let n = (first & 0x7f) as usize; + if n == 0 || n > 4 || rest.len() < n { + return Err(ParseError); + } + let mut len = 0usize; + for &b in &rest[..n] { + len = (len << 8) | b as usize; + } + rest = &rest[n..]; + len + }; + if rest.len() < len { + return Err(ParseError); + } + Ok(rest.split_at(len)) +} diff --git a/src/lib.rs b/src/lib.rs index 2993bde..108108c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,32 @@ //! //! To find the issuer of a certificate without hashing at runtime, match the //! issuer's raw DER `Name` against each CA's subject with [`find_by_subject`]. +//! +//! # Blocklist +//! +//! Some certificates must never be trusted, even when another trust store (the +//! operating system's, say) still contains them: intermediates whose private +//! keys were compromised, and roots that `cacrt` has disabled. [`blocked`] lists +//! them, keyed by the SHA-256 of the certificate and of its +//! `SubjectPublicKeyInfo`. +//! +//! Your verifier must check **every certificate in the chain, including the +//! root**, against it, after building and verifying the path. Checking the root +//! is what protects you when it comes from another store: a root `cacrt` has +//! disabled is still blocked, even if the OS still trusts it. Match on the key +//! ([`blocked_by_spki`]) rather than only the certificate: a compromised key +//! is usually certified more than once (cross-signs, reissues). +//! +//! ``` +//! # let spki_sha256 = [0u8; 32]; +//! // SHA-256 of the DER `SubjectPublicKeyInfo`, computed with your own crypto. +//! if let Some(b) = cacrt::blocked_by_spki(&spki_sha256) { +//! panic!("blocked: {} ({})", b.label(), b.reason()); +//! } +//! ``` +//! +//! With the `check` feature, `check_chain` and `blocked_by_der` parse and hash +//! the DER for you (still `no_std`, no `alloc`). #![no_std] #![forbid(unsafe_code)] @@ -134,6 +160,16 @@ impl core::fmt::Debug for HashName { // Generated: `pub(crate) static CERTS: &[Cert]`, sorted by (subject_hash, seq). include!(concat!(env!("OUT_DIR"), "/certs.rs")); +// Generated: `pub(crate) static BLOCKED: &[Blocked]`, sorted by (SPKI, cert) SHA-256. +include!(concat!(env!("OUT_DIR"), "/blocked.rs")); + +#[cfg(feature = "check")] +mod check; +#[cfg(feature = "check")] +mod sha256; +#[cfg(feature = "check")] +pub use check::{blocked_by_der, check_chain, ChainError, ParseError}; + /// All embedded CA certificates, sorted by subject hash then sequence number. pub fn all() -> &'static [Cert] { CERTS @@ -171,3 +207,92 @@ pub fn lookup(name: &str) -> Option<&'static Cert> { pub fn find_by_subject<'a>(subject_der: &'a [u8]) -> impl Iterator + 'a { CERTS.iter().filter(move |c| c.subject == subject_der) } + +/// Whether a blocked certificate is a root (self-issued) or an intermediate. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BlockedKind { + /// A self-issued root `cacrt` has disabled (`roots/**/*.disabled`). + Root, + /// A CA certificate that is not self-issued, e.g. a compromised intermediate. + Intermediate, +} + +/// A certificate that must not be trusted, wherever it is found. +pub struct Blocked { + kind: BlockedKind, + spki_sha256: [u8; 32], + cert_sha256: [u8; 32], + label: &'static str, + reason: &'static str, +} + +impl Blocked { + /// Construct an entry. Only called by generated code in `blocked.rs`. + const fn new( + kind: BlockedKind, + spki_sha256: [u8; 32], + cert_sha256: [u8; 32], + label: &'static str, + reason: &'static str, + ) -> Self { + Blocked { + kind, + spki_sha256, + cert_sha256, + label, + reason, + } + } + + /// Root or intermediate. + pub const fn kind(&self) -> BlockedKind { + self.kind + } + + /// SHA-256 of the certificate's DER `SubjectPublicKeyInfo`. + pub const fn spki_sha256(&self) -> &[u8; 32] { + &self.spki_sha256 + } + + /// SHA-256 of the full DER certificate (its usual fingerprint). + pub const fn cert_sha256(&self) -> &[u8; 32] { + &self.cert_sha256 + } + + /// A human-readable label for the certificate. + pub const fn label(&self) -> &'static str { + self.label + } + + /// Why the certificate is blocked, with a pointer to `CHANGELOG-roots.md`. + pub const fn reason(&self) -> &'static str { + self.reason + } +} + +impl core::fmt::Debug for Blocked { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Blocked") + .field("kind", &self.kind) + .field("label", &self.label) + .finish() + } +} + +/// Every blocked certificate, sorted by SPKI SHA-256 then certificate SHA-256. +pub fn blocked() -> &'static [Blocked] { + BLOCKED +} + +/// The first blocked entry whose `SubjectPublicKeyInfo` has this SHA-256, if +/// any. A match means the key itself is distrusted, so every certificate +/// carrying it must be rejected, including ones not listed in [`blocked`]. +pub fn blocked_by_spki(spki_sha256: &[u8; 32]) -> Option<&'static Blocked> { + let i = BLOCKED.partition_point(|b| b.spki_sha256 < *spki_sha256); + BLOCKED.get(i).filter(|b| b.spki_sha256 == *spki_sha256) +} + +/// The blocked entry for the certificate whose DER has this SHA-256, if any. +pub fn blocked_by_cert(cert_sha256: &[u8; 32]) -> Option<&'static Blocked> { + BLOCKED.iter().find(|b| b.cert_sha256 == *cert_sha256) +} diff --git a/src/sha256.rs b/src/sha256.rs new file mode 100644 index 0000000..2c513db --- /dev/null +++ b/src/sha256.rs @@ -0,0 +1,130 @@ +//! Minimal SHA-256 (FIPS 180-4), `core`-only so the same file serves both the +//! `no_std` library (the optional `check` feature) and the host-side build +//! helpers, which include it via `#[path]`. Used to fingerprint certificates and +//! their `SubjectPublicKeyInfo` for the blocklist. + +const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +]; + +/// SHA-256 digest of `data`. +pub fn sha256(data: &[u8]) -> [u8; 32] { + let mut h: [u32; 8] = [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, + 0x5be0cd19, + ]; + + let bit_len = (data.len() as u64).wrapping_mul(8); + + // Process all full 64-byte blocks, then a padded final block. + let mut chunks = data.chunks_exact(64); + for block in &mut chunks { + process(&mut h, block); + } + + // Padding: 0x80, then zeros, then 64-bit big-endian length. + let rem = chunks.remainder(); + let mut tail = [0u8; 128]; + tail[..rem.len()].copy_from_slice(rem); + tail[rem.len()] = 0x80; + let tail_len = if rem.len() + 1 + 8 <= 64 { 64 } else { 128 }; + tail[tail_len - 8..tail_len].copy_from_slice(&bit_len.to_be_bytes()); + for block in tail[..tail_len].chunks_exact(64) { + process(&mut h, block); + } + + let mut out = [0u8; 32]; + for (i, word) in h.iter().enumerate() { + out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes()); + } + out +} + +fn process(h: &mut [u32; 8], block: &[u8]) { + let mut w = [0u32; 64]; + for i in 0..16 { + w[i] = u32::from_be_bytes([ + block[i * 4], + block[i * 4 + 1], + block[i * 4 + 2], + block[i * 4 + 3], + ]); + } + for i in 16..64 { + let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3); + let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + .wrapping_add(s0) + .wrapping_add(w[i - 7]) + .wrapping_add(s1); + } + + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = *h; + for i in 0..64 { + let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let ch = (e & f) ^ ((!e) & g); + let t1 = hh + .wrapping_add(s1) + .wrapping_add(ch) + .wrapping_add(K[i]) + .wrapping_add(w[i]); + let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); + let maj = (a & b) ^ (a & c) ^ (b & c); + let t2 = s0.wrapping_add(maj); + hh = g; + g = f; + f = e; + e = d.wrapping_add(t1); + d = c; + c = b; + b = a; + a = t1.wrapping_add(t2); + } + + for (x, v) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) { + *x = x.wrapping_add(v); + } +} + +#[cfg(test)] +mod tests { + // The library is `no_std`; pull in std explicitly for the test helpers. + extern crate std; + use super::sha256; + use std::{format, string::String, vec}; + + fn hex(d: &[u8]) -> String { + d.iter().map(|b| format!("{b:02x}")).collect() + } + + #[test] + fn known_vectors() { + assert_eq!( + hex(&sha256(b"")), + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ); + assert_eq!( + hex(&sha256(b"abc")), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); + assert_eq!( + hex(&sha256( + b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + )), + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1" + ); + // Spans multiple blocks + exercises padding into a second block. + let long = vec![b'a'; 1000]; + assert_eq!( + hex(&sha256(&long)), + "41edece42d63e8d9bf515a9ba6932e1c20cbc9f5a5d134645adb5db1b9737ea3" + ); + } +} diff --git a/tests/blocked.rs b/tests/blocked.rs new file mode 100644 index 0000000..9b436ff --- /dev/null +++ b/tests/blocked.rs @@ -0,0 +1,171 @@ +//! Blocklist behavior: the disabled roots and the compromised eMudhra +//! intermediates are listed, lookups by key and by certificate hash work, and +//! (with the `check` feature) whole DER chains are checked, root included. + +use cacrt::BlockedKind; + +fn unhex(s: &str) -> [u8; 32] { + let mut out = [0u8; 32]; + for (i, b) in out.iter_mut().enumerate() { + *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap(); + } + out +} + +/// SPKI SHA-256 of the eight compromised eMudhra intermediate keys, as +/// published in the incident's `impacted-intermediates.json`. +const EMUDHRA_SPKI: [&str; 8] = [ + "EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105", // EM DV TLS CA - G3B + "43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B", // EM OV TLS CA - G3B + "E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41", // EM EV TLS CA - G3B + "49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F", // EM DV TLS CA - G3C + "63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8", // EM OV TLS CA - G3C + "5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0", // EM EV TLS CA - G3C + "EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6", // EM OV CS CA - G3B + "92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30", // EM OV CS CA - G3C +]; + +#[test] +fn blocklist_is_sorted_and_complete() { + let all = cacrt::blocked(); + for w in all.windows(2) { + let a = (w[0].spki_sha256(), w[0].cert_sha256()); + let b = (w[1].spki_sha256(), w[1].cert_sha256()); + assert!(a < b, "not strictly ordered"); + } + let roots = all.iter().filter(|b| b.kind() == BlockedKind::Root).count(); + let inter = all + .iter() + .filter(|b| b.kind() == BlockedKind::Intermediate) + .count(); + // 3 disabled roots (emSign ECC G3, Buypass Class 2 and 3); 16 eMudhra + // intermediates (8 keys, each issued by eMudhra and cross-signed by Sectigo). + assert_eq!((roots, inter), (3, 16)); + for b in all { + assert!(!b.reason().is_empty(), "{} has no reason", b.label()); + } +} + +#[test] +fn compromised_emudhra_keys_are_blocked() { + for spki in EMUDHRA_SPKI { + let b = + cacrt::blocked_by_spki(&unhex(spki)).unwrap_or_else(|| panic!("{spki} not blocked")); + assert_eq!(b.kind(), BlockedKind::Intermediate); + let certs = cacrt::blocked() + .iter() + .filter(|e| *e.spki_sha256() == unhex(spki)) + .count(); + assert_eq!( + certs, 2, + "{spki}: expected the eMudhra cert and the Sectigo cross-sign" + ); + } +} + +#[test] +fn disabled_root_is_blocked_by_key_and_cert() { + // emSign ECC Root CA - G3. + let spki = unhex("8D417DB2DD8BF5E3084D1E3F196D583849D81BDD4C00C70B9D39369E96B8C782"); + let cert = unhex("86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B"); + let b = cacrt::blocked_by_spki(&spki).expect("emSign ECC Root CA - G3 key blocked"); + assert_eq!(b.kind(), BlockedKind::Root); + assert_eq!(b.label(), "emSign ECC Root CA - G3"); + assert!(core::ptr::eq(b, cacrt::blocked_by_cert(&cert).unwrap())); +} + +#[test] +fn unknown_hashes_are_not_blocked() { + assert!(cacrt::blocked_by_spki(&[0; 32]).is_none()); + assert!(cacrt::blocked_by_spki(&[0xff; 32]).is_none()); + assert!(cacrt::blocked_by_cert(&[0; 32]).is_none()); +} + +// PEM decoding for the DER-level tests (`pem` reaches `base64` via `super::`). +#[cfg(feature = "check")] +#[path = "../build_support/base64.rs"] +mod base64; +#[cfg(feature = "check")] +#[path = "../build_support/pem.rs"] +mod pem; + +#[cfg(feature = "check")] +mod check { + use super::pem; + use std::path::{Path, PathBuf}; + + fn files(dir: &Path, ext: &str, out: &mut Vec) { + for e in std::fs::read_dir(dir).unwrap() { + let path = e.unwrap().path(); + if path.is_dir() { + files(&path, ext, out); + } else if path.extension().and_then(|s| s.to_str()) == Some(ext) { + out.push(path); + } + } + } + + fn der(path: &Path) -> Vec { + pem::read_one_certificate(&std::fs::read_to_string(path).unwrap()).unwrap() + } + + fn blocked_files() -> Vec { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let mut v = Vec::new(); + files(&root.join("roots"), "disabled", &mut v); + files(&root.join("blocked"), "pem", &mut v); + assert_eq!(v.len(), 19); + v + } + + #[test] + fn every_blocked_file_is_caught_from_der() { + for path in blocked_files() { + let d = der(&path); + assert!( + cacrt::blocked_by_der(&d).unwrap().is_some(), + "{} not blocked", + path.display() + ); + } + } + + #[test] + fn trusted_roots_pass() { + let chain: Vec<&[u8]> = cacrt::all().iter().map(|c| c.der()).collect(); + cacrt::check_chain(&chain).expect("no embedded root may be blocked"); + } + + #[test] + fn chain_with_os_supplied_disabled_root_is_rejected() { + // A chain whose anchor came from an OS store that still trusts emSign + // ECC Root CA - G3: the trusted root before it passes, the root fails. + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let disabled = der(&root.join("roots/eMudhra/emSign_ECC_Root_CA_G3.disabled")); + let ok = cacrt::lookup("062cdee6.0").unwrap().der(); + match cacrt::check_chain(&[ok, &disabled]) { + Err(cacrt::ChainError::Blocked { index: 1, entry }) => { + assert_eq!(entry.label(), "emSign ECC Root CA - G3") + } + other => panic!("unexpected {other:?}"), + } + } + + #[test] + fn malformed_der_is_reported() { + let ok = cacrt::lookup("062cdee6.0").unwrap().der(); + let truncated = &ok[..ok.len() - 1]; + for bad in [ + &[][..], + &[0x30, 0x00][..], + truncated, + &[0x30, 0x85, 1, 1, 1, 1, 1][..], + ] { + assert!(cacrt::blocked_by_der(bad).is_err()); + } + assert!(matches!( + cacrt::check_chain(&[ok, truncated]), + Err(cacrt::ChainError::Malformed { index: 1 }) + )); + } +} From a6190cc4e9988afe4becde3fa08389e5213ec3eb Mon Sep 17 00:00:00 2001 From: Mark Karpeles Date: Sat, 24 Oct 2026 06:35:59 +0900 Subject: [PATCH 2/2] Drop the `check` feature: callers are TLS libraries that already parse DER Remove check_chain()/blocked_by_der() and the runtime DER walker. The library is back to static data and comparisons; SHA-256 moves to build_support as a build-time helper only. The DER-level tests now play the caller's role, hashing with the build helpers and querying blocked_by_spki()/blocked_by_cert(). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 -- Cargo.toml | 8 -- README.md | 17 ++--- build.rs | 1 - build_support/mod.rs | 2 - {src => build_support}/sha256.rs | 14 +--- src/check.rs | 123 ------------------------------- src/lib.rs | 12 +-- tests/blocked.rs | 82 ++++++++------------- 9 files changed, 42 insertions(+), 223 deletions(-) rename {src => build_support}/sha256.rs (88%) delete mode 100644 src/check.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae5c897..e2ffc0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,12 +21,8 @@ jobs: run: cargo fmt --all -- --check - name: Clippy run: cargo clippy --all-targets -- -D warnings - - name: Clippy (check feature) - run: cargo clippy --all-targets --features check -- -D warnings - name: Test (includes openssl subject-hash cross-check) run: cargo test --all-targets - - name: Test (check feature) - run: cargo test --all-targets --features check - name: Verify curated roots against curation rules run: cargo run --example cacrt-tool -- verify @@ -42,8 +38,6 @@ jobs: # allocator. If it referenced std or alloc, this would fail to compile. - name: Build library for thumbv7em-none-eabi run: cargo build --lib --target thumbv7em-none-eabi - - name: Build library for thumbv7em-none-eabi (check feature) - run: cargo build --lib --target thumbv7em-none-eabi --features check msrv: name: Minimum supported Rust version diff --git a/Cargo.toml b/Cargo.toml index 0167f0f..c41eb24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,13 +32,5 @@ unsafe_code = "forbid" [lib] path = "src/lib.rs" -[features] -# Runtime blocklist checks on DER certificates (`check_chain`, `blocked_by_der`): -# adds a tiny DER walker and SHA-256. Still no_std, no alloc, no dependencies. -check = [] - -[package.metadata.docs.rs] -all-features = true - # The library itself needs nothing. build.rs and examples use only std + the # hand-rolled helpers under build_support/, so there are no dependencies at all. diff --git a/README.md b/README.md index 0bda627..8150c92 100644 --- a/README.md +++ b/README.md @@ -76,13 +76,13 @@ it, leaf to root, including a root that came from the OS store.** A root that `cacrt` disabled then stays rejected on systems that still trust it. ```rust -// Zero-cost lookups: hash with your own crypto, compare against static data. -if let Some(b) = cacrt::blocked_by_spki(&spki_sha256) { +// For each certificate in the verified chain, root included. Your TLS library +// already parses the DER; hash the SubjectPublicKeyInfo and the certificate. +if let Some(b) = cacrt::blocked_by_spki(&spki_sha256) + .or_else(|| cacrt::blocked_by_cert(&cert_sha256)) +{ return Err(format!("blocked: {} ({})", b.label(), b.reason())); } - -// Or, with the `check` feature, pass the DER chain and let cacrt parse and hash. -cacrt::check_chain(&[leaf_der, intermediate_der, root_der])?; ``` | Function | Purpose | @@ -90,8 +90,6 @@ cacrt::check_chain(&[leaf_der, intermediate_der, root_der])?; | `cacrt::blocked() -> &'static [Blocked]` | Every blocked certificate, with `kind()`, `label()`, `reason()` and both hashes. | | `cacrt::blocked_by_spki(&[u8; 32]) -> Option<&Blocked>` | Is this **key** blocked? (SHA-256 of the DER SPKI.) | | `cacrt::blocked_by_cert(&[u8; 32]) -> Option<&Blocked>` | Is this **certificate** blocked? (SHA-256 of its DER.) | -| `cacrt::blocked_by_der(&[u8])` *(feature `check`)* | Both checks on one DER certificate. | -| `cacrt::check_chain(&[&[u8]])` *(feature `check`)* | Check every certificate of a chain and report the first blocked one. | The blocklist only helps verifiers that call it. Generic TLS stacks (rustls, OpenSSL) know nothing about it. Hook it in after normal verification, for @@ -138,9 +136,8 @@ cargo run --example cacrt-tool -- hash path/to/cert.pem ## `no_std` / no-`alloc` guarantee The library is `#![no_std]`, `#![forbid(unsafe_code)]`, and never references -`alloc`, with or without the `check` feature. CI builds both for -`thumbv7em-none-eabi` (a bare-metal target with no std and no default allocator) -to prove it. +`alloc`. CI builds it for `thumbv7em-none-eabi` (a bare-metal target with no std +and no default allocator) to prove it. ## License diff --git a/build.rs b/build.rs index 8871173..d820f73 100644 --- a/build.rs +++ b/build.rs @@ -39,7 +39,6 @@ fn main() { // Rebuild when the root set, the blocklist or the generators change. println!("cargo:rerun-if-changed=roots"); println!("cargo:rerun-if-changed=blocked"); - println!("cargo:rerun-if-changed=src/sha256.rs"); println!("cargo:rerun-if-changed=build_support"); println!("cargo:rerun-if-changed=build.rs"); diff --git a/build_support/mod.rs b/build_support/mod.rs index 76efb81..20034f4 100644 --- a/build_support/mod.rs +++ b/build_support/mod.rs @@ -14,8 +14,6 @@ pub mod der; pub mod octal; pub mod pem; pub mod sha1; -// Shared with the library's optional runtime `check` feature. -#[path = "../src/sha256.rs"] pub mod sha256; /// Compute the OpenSSL subject hash of a DER-encoded certificate. diff --git a/src/sha256.rs b/build_support/sha256.rs similarity index 88% rename from src/sha256.rs rename to build_support/sha256.rs index 2c513db..47774b7 100644 --- a/src/sha256.rs +++ b/build_support/sha256.rs @@ -1,6 +1,4 @@ -//! Minimal SHA-256 (FIPS 180-4), `core`-only so the same file serves both the -//! `no_std` library (the optional `check` feature) and the host-side build -//! helpers, which include it via `#[path]`. Used to fingerprint certificates and +//! Minimal SHA-256 (FIPS 180-4), used to fingerprint blocked certificates and //! their `SubjectPublicKeyInfo` for the blocklist. const K: [u32; 64] = [ @@ -50,12 +48,7 @@ pub fn sha256(data: &[u8]) -> [u8; 32] { fn process(h: &mut [u32; 8], block: &[u8]) { let mut w = [0u32; 64]; for i in 0..16 { - w[i] = u32::from_be_bytes([ - block[i * 4], - block[i * 4 + 1], - block[i * 4 + 2], - block[i * 4 + 3], - ]); + w[i] = u32::from_be_bytes(block[i * 4..i * 4 + 4].try_into().unwrap()); } for i in 16..64 { let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3); @@ -95,10 +88,7 @@ fn process(h: &mut [u32; 8], block: &[u8]) { #[cfg(test)] mod tests { - // The library is `no_std`; pull in std explicitly for the test helpers. - extern crate std; use super::sha256; - use std::{format, string::String, vec}; fn hex(d: &[u8]) -> String { d.iter().map(|b| format!("{b:02x}")).collect() diff --git a/src/check.rs b/src/check.rs deleted file mode 100644 index 47ca590..0000000 --- a/src/check.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Optional (`check` feature) runtime blocklist checks on DER certificates. -//! -//! A tiny, allocation-free DER walk finds the `SubjectPublicKeyInfo`, and the -//! certificate and its key are hashed with the bundled SHA-256. Nothing here -//! verifies signatures or builds paths: run it on a chain your verifier has -//! already accepted. - -use crate::sha256::sha256; -use crate::{blocked_by_cert, blocked_by_spki, Blocked}; - -/// The certificate is not DER this parser can read. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ParseError; - -impl core::fmt::Display for ParseError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str("malformed DER certificate") - } -} - -/// Why [`check_chain`] rejected a chain. `index` is the position in the slice. -#[derive(Clone, Copy, Debug)] -pub enum ChainError { - /// The certificate at `index` (or its key) is on the blocklist. - Blocked { - /// Position of the certificate in the chain. - index: usize, - /// The matching blocklist entry. - entry: &'static Blocked, - }, - /// The certificate at `index` could not be parsed. - Malformed { - /// Position of the certificate in the chain. - index: usize, - }, -} - -impl core::fmt::Display for ChainError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - ChainError::Blocked { index, entry } => write!( - f, - "certificate {index} is blocked: {} ({})", - entry.label(), - entry.reason() - ), - ChainError::Malformed { index } => write!(f, "certificate {index} is malformed"), - } - } -} - -/// The blocklist entry matching this DER certificate, by its key or by the -/// certificate itself. -pub fn blocked_by_der(cert_der: &[u8]) -> Result, ParseError> { - let spki = spki(cert_der)?; - Ok(blocked_by_spki(&sha256(spki)).or_else(|| blocked_by_cert(&sha256(cert_der)))) -} - -/// Check every DER certificate of a chain (leaf, intermediates **and root**) -/// against the blocklist. Fails on the first blocked or unparsable certificate. -/// -/// Pass the root too, even when it came from another trust store: that is how -/// a root `cacrt` has disabled stays rejected on systems that still trust it. -pub fn check_chain(chain: &[&[u8]]) -> Result<(), ChainError> { - for (index, der) in chain.iter().enumerate() { - match blocked_by_der(der) { - Ok(None) => {} - Ok(Some(entry)) => return Err(ChainError::Blocked { index, entry }), - Err(ParseError) => return Err(ChainError::Malformed { index }), - } - } - Ok(()) -} - -/// The raw `SubjectPublicKeyInfo` TLV of a DER `Certificate`. -fn spki(cert: &[u8]) -> Result<&[u8], ParseError> { - let (outer, rest) = read_tlv(cert, 0x30)?; - if !rest.is_empty() { - return Err(ParseError); - } - let (tbs, _) = read_tlv(outer, 0x30)?; - let mut rest = tbs; - // Skip the optional `[0] EXPLICIT Version`. - if rest.first() == Some(&0xA0) { - rest = read_tlv(rest, 0xA0)?.1; - } - // serialNumber, signature, issuer, validity, subject. - for tag in [0x02, 0x30, 0x30, 0x30, 0x30] { - rest = read_tlv(rest, tag)?.1; - } - let (content, after) = read_tlv(rest, 0x30)?; - let len = rest.len() - after.len(); - debug_assert!(content.len() < len); - Ok(&rest[..len]) -} - -/// Read one definite-length TLV with the expected tag, returning its content -/// and the bytes after it. -fn read_tlv(input: &[u8], tag: u8) -> Result<(&[u8], &[u8]), ParseError> { - let (&t, rest) = input.split_first().ok_or(ParseError)?; - if t != tag { - return Err(ParseError); - } - let (&first, mut rest) = rest.split_first().ok_or(ParseError)?; - let len = if first < 0x80 { - first as usize - } else { - let n = (first & 0x7f) as usize; - if n == 0 || n > 4 || rest.len() < n { - return Err(ParseError); - } - let mut len = 0usize; - for &b in &rest[..n] { - len = (len << 8) | b as usize; - } - rest = &rest[n..]; - len - }; - if rest.len() < len { - return Err(ParseError); - } - Ok(rest.split_at(len)) -} diff --git a/src/lib.rs b/src/lib.rs index 108108c..1092b1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,14 +50,11 @@ //! //! ``` //! # let spki_sha256 = [0u8; 32]; -//! // SHA-256 of the DER `SubjectPublicKeyInfo`, computed with your own crypto. +//! // SHA-256 of the DER `SubjectPublicKeyInfo`, computed by your TLS library. //! if let Some(b) = cacrt::blocked_by_spki(&spki_sha256) { //! panic!("blocked: {} ({})", b.label(), b.reason()); //! } //! ``` -//! -//! With the `check` feature, `check_chain` and `blocked_by_der` parse and hash -//! the DER for you (still `no_std`, no `alloc`). #![no_std] #![forbid(unsafe_code)] @@ -163,13 +160,6 @@ include!(concat!(env!("OUT_DIR"), "/certs.rs")); // Generated: `pub(crate) static BLOCKED: &[Blocked]`, sorted by (SPKI, cert) SHA-256. include!(concat!(env!("OUT_DIR"), "/blocked.rs")); -#[cfg(feature = "check")] -mod check; -#[cfg(feature = "check")] -mod sha256; -#[cfg(feature = "check")] -pub use check::{blocked_by_der, check_chain, ChainError, ParseError}; - /// All embedded CA certificates, sorted by subject hash then sequence number. pub fn all() -> &'static [Cert] { CERTS diff --git a/tests/blocked.rs b/tests/blocked.rs index 9b436ff..5882969 100644 --- a/tests/blocked.rs +++ b/tests/blocked.rs @@ -81,17 +81,13 @@ fn unknown_hashes_are_not_blocked() { assert!(cacrt::blocked_by_cert(&[0; 32]).is_none()); } -// PEM decoding for the DER-level tests (`pem` reaches `base64` via `super::`). -#[cfg(feature = "check")] -#[path = "../build_support/base64.rs"] -mod base64; -#[cfg(feature = "check")] -#[path = "../build_support/pem.rs"] -mod pem; +// The tests below play the caller's role: parse and hash the DER themselves +// (with the build helpers), then query the lookups the way a TLS library would. +#[path = "../build_support/mod.rs"] +mod build_support; -#[cfg(feature = "check")] -mod check { - use super::pem; +mod from_der { + use super::build_support::{der, pem, sha256::sha256}; use std::path::{Path, PathBuf}; fn files(dir: &Path, ext: &str, out: &mut Vec) { @@ -109,63 +105,49 @@ mod check { pem::read_one_certificate(&std::fs::read_to_string(path).unwrap()).unwrap() } - fn blocked_files() -> Vec { + /// What a verifier does for one certificate of its chain. + fn lookup(cert: &[u8]) -> Option<&'static cacrt::Blocked> { + let tbs = der::tbs_certificate(cert).unwrap(); + let spki = der::tbs_field(tbs, der::TbsField::Spki).unwrap(); + cacrt::blocked_by_spki(&sha256(spki)).or_else(|| cacrt::blocked_by_cert(&sha256(cert))) + } + + #[test] + fn every_blocked_file_is_caught() { let root = Path::new(env!("CARGO_MANIFEST_DIR")); let mut v = Vec::new(); files(&root.join("roots"), "disabled", &mut v); files(&root.join("blocked"), "pem", &mut v); assert_eq!(v.len(), 19); - v - } - - #[test] - fn every_blocked_file_is_caught_from_der() { - for path in blocked_files() { + for path in v { let d = der(&path); + let b = lookup(&d).unwrap_or_else(|| panic!("{} not blocked", path.display())); assert!( - cacrt::blocked_by_der(&d).unwrap().is_some(), - "{} not blocked", + cacrt::blocked_by_cert(&sha256(&d)).is_some(), + "{}: cert hash not listed", path.display() ); + assert_eq!(*b.spki_sha256(), { + let tbs = der::tbs_certificate(&d).unwrap(); + sha256(der::tbs_field(tbs, der::TbsField::Spki).unwrap()) + }); } } #[test] - fn trusted_roots_pass() { - let chain: Vec<&[u8]> = cacrt::all().iter().map(|c| c.der()).collect(); - cacrt::check_chain(&chain).expect("no embedded root may be blocked"); + fn no_trusted_root_is_blocked() { + for ca in cacrt::all() { + assert!(lookup(ca.der()).is_none(), "{} is blocked", ca.label()); + } } #[test] - fn chain_with_os_supplied_disabled_root_is_rejected() { + fn os_supplied_disabled_root_is_rejected() { // A chain whose anchor came from an OS store that still trusts emSign - // ECC Root CA - G3: the trusted root before it passes, the root fails. + // ECC Root CA - G3: checking the root, not just the intermediates, is + // what catches it. let root = Path::new(env!("CARGO_MANIFEST_DIR")); - let disabled = der(&root.join("roots/eMudhra/emSign_ECC_Root_CA_G3.disabled")); - let ok = cacrt::lookup("062cdee6.0").unwrap().der(); - match cacrt::check_chain(&[ok, &disabled]) { - Err(cacrt::ChainError::Blocked { index: 1, entry }) => { - assert_eq!(entry.label(), "emSign ECC Root CA - G3") - } - other => panic!("unexpected {other:?}"), - } - } - - #[test] - fn malformed_der_is_reported() { - let ok = cacrt::lookup("062cdee6.0").unwrap().der(); - let truncated = &ok[..ok.len() - 1]; - for bad in [ - &[][..], - &[0x30, 0x00][..], - truncated, - &[0x30, 0x85, 1, 1, 1, 1, 1][..], - ] { - assert!(cacrt::blocked_by_der(bad).is_err()); - } - assert!(matches!( - cacrt::check_chain(&[ok, truncated]), - Err(cacrt::ChainError::Malformed { index: 1 }) - )); + let anchor = der(&root.join("roots/eMudhra/emSign_ECC_Root_CA_G3.disabled")); + assert_eq!(lookup(&anchor).unwrap().label(), "emSign ECC Root CA - G3"); } }