oxedyne/fe2o3/fe2o3_net/src/acme/trust.rs
10.0 KiB, 23 runs
created by r1870400018:9527, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Pinned Let's Encrypt root CA trust anchors for the ACME client. |
| 2 | //! |
| 3 | //! The ACME client needs to verify the TLS certificate of the CA it talks |
| 4 | //! to (e.g. `acme-v02.api.letsencrypt.org`). Rather than pull in the |
| 5 | //! `webpki-roots` crate -- which carries the entire Mozilla root programme, |
| 6 | //! hundreds of CAs, most of which are irrelevant to our use case -- we |
| 7 | //! embed only the two Let's Encrypt root anchors that actually sign the |
| 8 | //! LE API endpoints today: |
| 9 | //! |
| 10 | //! - **ISRG Root X1** (RSA 4096), valid through 4 June 2035. Serial |
| 11 | //! `172886928669790476064670243504169061120`. SHA-256 fingerprint |
| 12 | //! `96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6`. |
| 13 | //! - **ISRG Root X2** (ECDSA P-384), valid through 17 September 2040. |
| 14 | //! Serial `87493402998870891108772069816698636114`. SHA-256 fingerprint |
| 15 | //! `69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70`. |
| 16 | //! |
| 17 | //! The base64 bodies below are the exact bytes between the `-----BEGIN |
| 18 | //! CERTIFICATE-----` and `-----END CERTIFICATE-----` markers on Let's |
| 19 | //! Encrypt's published PEM files, retained in PEM line-break form for |
| 20 | //! readability and stripped of whitespace at runtime before base64 |
| 21 | //! decoding. |
| 22 | //! |
| 23 | //! When either root reaches end of life, add the replacement here and |
| 24 | //! leave the old one in place long enough to cover the rollover window. |
| 25 | //! Both anchors are advertised to `rustls` at every ACME startup, so |
| 26 | //! having multiple active is harmless. |
| 27 | //! |
| 28 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 29 | //! Anthropic Claude |
| 30 | |
| 31 | use oxedyne_fe2o3_core::prelude::*; |
| 32 | |
| 33 | use std::sync::Arc; |
| 34 | |
| 35 | use base64; |
| 36 | use tokio_rustls::rustls::{ |
| 37 | pki_types::CertificateDer, |
| 38 | ClientConfig, |
| 39 | RootCertStore, |
| 40 | }; |
| 41 | |
| 42 | |
| 43 | // ISRG Root X1, DER body encoded as base64. Valid through 2035-06-04. |
| 44 | const ISRG_ROOT_X1_B64: &str = "\ |
| 45 | MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\ |
| 46 | TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ |
| 47 | cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\ |
| 48 | WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\ |
| 49 | ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\ |
| 50 | MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\ |
| 51 | h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\ |
| 52 | 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\ |
| 53 | A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\ |
| 54 | T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\ |
| 55 | B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\ |
| 56 | B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\ |
| 57 | KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\ |
| 58 | OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\ |
| 59 | jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\ |
| 60 | qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\ |
| 61 | rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\ |
| 62 | HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\ |
| 63 | hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\ |
| 64 | ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\ |
| 65 | 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\ |
| 66 | NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\ |
| 67 | ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\ |
| 68 | TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\ |
| 69 | jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\ |
| 70 | oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\ |
| 71 | 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\ |
| 72 | mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\ |
| 73 | emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc="; |
| 74 | |
| 75 | // ISRG Root X2, DER body encoded as base64. Valid through 2040-09-17. |
| 76 | const ISRG_ROOT_X2_B64: &str = "\ |
| 77 | MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw\ |
| 78 | CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg\ |
| 79 | R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00\ |
| 80 | MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT\ |
| 81 | ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw\ |
| 82 | EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW\ |
| 83 | +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9\ |
| 84 | ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T\ |
| 85 | AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI\ |
| 86 | zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW\ |
| 87 | tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1\ |
| 88 | /q4AaOeMSQ+2b1tbFfLn"; |
| 89 | |
| 90 | |
| 91 | // ┌───────────────────────────────────────────────────────────────────────────┐ |
| 92 | // │ CLIENT CONFIG BUILDER │ |
| 93 | // └───────────────────────────────────────────────────────────────────────────┘ |
| 94 | |
| 95 | /// Trusts only the two roots pinned above, no other CA, and is handed straight |
| 96 | /// to [`crate::http::client::https_request`] for an ACME directory URL. Built |
| 97 | /// once at service startup and reused for every request. |
| 98 | pub fn letsencrypt_client_config() -> Outcome<Arc<ClientConfig>> { |
| 99 | let mut store = RootCertStore::empty(); |
| 100 | for (label, b64) in [ |
| 101 | ("ISRG Root X1", ISRG_ROOT_X1_B64), |
| 102 | ("ISRG Root X2", ISRG_ROOT_X2_B64), |
| 103 | ] { |
| 104 | let der = res!(decode_pem_body(label, b64)); |
| 105 | let anchor = CertificateDer::from(der); |
| 106 | if let Err(e) = store.add(anchor) { |
| 107 | return Err(err!( |
| 108 | "rustls refused to add the pinned root {:?}: {:?}.", label, e; |
| 109 | Init, Invalid)); |
| 110 | } |
| 111 | } |
| 112 | // rustls's builder panics when it cannot resolve a crypto provider from |
| 113 | // process-global state. Install one if the application has not. |
| 114 | crate::tls::ensure_crypto_provider(); |
| 115 | let config = ClientConfig::builder() |
| 116 | .with_root_certificates(store) |
| 117 | .with_no_client_auth(); |
| 118 | Ok(Arc::new(config)) |
| 119 | } |
| 120 | |
| 121 | /// Whitespace is stripped first, so a body may be kept in PEM line-break form. |
| 122 | /// `label` only points the error at the right constant. |
| 123 | fn decode_pem_body(label: &str, body: &str) -> Outcome<Vec<u8>> { |
| 124 | let cleaned: String = body.chars().filter(|c| !c.is_whitespace()).collect(); |
| 125 | match base64::decode(&cleaned) { |
| 126 | Ok(v) => Ok(v), |
| 127 | Err(e) => Err(err!(e, |
| 128 | "Failed to base64-decode the embedded {:?} certificate body.", label; |
| 129 | Decode, Invalid, Input)), |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | |
| 134 | // ┌───────────────────────────────────────────────────────────────────────────┐ |
| 135 | // │ TESTS │ |
| 136 | // └───────────────────────────────────────────────────────────────────────────┘ |
| 137 | |
| 138 | #[cfg(test)] |
| 139 | mod tests { |
| 140 | use super::*; |
| 141 | |
| 142 | /// Both embedded base64 bodies must decode to non-empty DER. |
| 143 | #[test] |
| 144 | fn test_decode_embedded_roots() -> Outcome<()> { |
| 145 | let x1 = res!(decode_pem_body("ISRG Root X1", ISRG_ROOT_X1_B64)); |
| 146 | let x2 = res!(decode_pem_body("ISRG Root X2", ISRG_ROOT_X2_B64)); |
| 147 | // X1 is an RSA-4096 self-signed root; a realistic length is around |
| 148 | // 1.4-1.6 KB. X2 is ECDSA P-384 and closer to 0.5 KB. |
| 149 | if x1.len() < 1000 || x1.len() > 2500 { |
| 150 | return Err(err!( |
| 151 | "ISRG Root X1 decoded to {} bytes; expected roughly 1.4-1.6 KB.", |
| 152 | x1.len(); |
| 153 | Test, Mismatch)); |
| 154 | } |
| 155 | if x2.len() < 300 || x2.len() > 1000 { |
| 156 | return Err(err!( |
| 157 | "ISRG Root X2 decoded to {} bytes; expected roughly 0.5 KB.", |
| 158 | x2.len(); |
| 159 | Test, Mismatch)); |
| 160 | } |
| 161 | // X.509 certificates always begin with the SEQUENCE tag 0x30. |
| 162 | if x1[0] != 0x30 { |
| 163 | return Err(err!( |
| 164 | "ISRG Root X1 does not begin with the X.509 SEQUENCE tag 0x30."; |
| 165 | Test, Mismatch)); |
| 166 | } |
| 167 | if x2[0] != 0x30 { |
| 168 | return Err(err!( |
| 169 | "ISRG Root X2 does not begin with the X.509 SEQUENCE tag 0x30."; |
| 170 | Test, Mismatch)); |
| 171 | } |
| 172 | Ok(()) |
| 173 | } |
| 174 | |
| 175 | /// Building the ClientConfig must succeed, must return a fresh `Arc` |
| 176 | /// on each call, and rustls must have accepted both roots into the |
| 177 | /// trust store without complaint. |
| 178 | #[test] |
| 179 | fn test_build_letsencrypt_client_config() -> Outcome<()> { |
| 180 | let config_a = res!(letsencrypt_client_config()); |
| 181 | let config_b = res!(letsencrypt_client_config()); |
| 182 | // The two Arcs should point at different allocations; we do not |
| 183 | // cache anything. |
| 184 | if Arc::ptr_eq(&config_a, &config_b) { |
| 185 | return Err(err!( |
| 186 | "letsencrypt_client_config must return a freshly-built \ |
| 187 | ClientConfig on each call; got two Arcs pointing at the \ |
| 188 | same allocation."; |
| 189 | Test, Mismatch)); |
| 190 | } |
| 191 | Ok(()) |
| 192 | } |
| 193 | |
| 194 | /// The cleaner strips whitespace deterministically: given the same |
| 195 | /// input with extra spacing, we must decode to the same DER. |
| 196 | #[test] |
| 197 | fn test_decode_pem_body_tolerates_whitespace() -> Outcome<()> { |
| 198 | let without = res!(decode_pem_body("ISRG Root X2", ISRG_ROOT_X2_B64)); |
| 199 | let spaced = res!(decode_pem_body( |
| 200 | "ISRG Root X2", |
| 201 | &fmt!("\n {} \n ", ISRG_ROOT_X2_B64), |
| 202 | )); |
| 203 | if without != spaced { |
| 204 | return Err(err!( |
| 205 | "decode_pem_body produced different output when extra \ |
| 206 | whitespace was inserted."; |
| 207 | Test, Mismatch)); |
| 208 | } |
| 209 | Ok(()) |
| 210 | } |
| 211 | } |