Skip to main content

rama_crypto/
crl.rs

1//! Generic X.509 v2 CRL builder (TLS-backend agnostic).
2//!
3//! DER-encodes a `CertificateList` (RFC 5280 §5.1) signed by its issuer.
4//! Hashing and signing are supplied by the caller, so this module pulls in no
5//! crypto backend — pure `yasna` assembly, mirroring [`crate::ocsp`].
6//!
7//! Primary use: a MITM proxy hosting a CA-signed CRL whose distribution point
8//! it stamps onto re-signed leaves, so revocation-strict clients (notably
9//! libcurl + schannel, which resolves revocation from the cert's own CDP and
10//! ignores stapled OCSP) accept the leaf.
11
12use std::time::SystemTime;
13
14use rama_core::error::{BoxError, ErrorContext};
15use yasna::{
16    Tag,
17    models::{GeneralizedTime, ObjectIdentifier, UTCTime},
18};
19
20fn oid_ecdsa_sha256() -> ObjectIdentifier {
21    ObjectIdentifier::from_slice(&[1, 2, 840, 10045, 4, 3, 2])
22}
23fn oid_rsa_sha256() -> ObjectIdentifier {
24    ObjectIdentifier::from_slice(&[1, 2, 840, 113549, 1, 1, 11])
25}
26fn oid_authority_key_id() -> ObjectIdentifier {
27    ObjectIdentifier::from_slice(&[2, 5, 29, 35])
28}
29fn oid_crl_number() -> ObjectIdentifier {
30    ObjectIdentifier::from_slice(&[2, 5, 29, 20])
31}
32
33/// Signature algorithm the caller used to sign the `tbsCertList`. It is encoded
34/// both inside the signed `tbsCertList` and in the outer `signatureAlgorithm`,
35/// so the caller commits to it before signing.
36#[derive(Debug, Clone, Copy)]
37pub enum CrlSignatureAlgorithm {
38    /// `ecdsa-with-SHA256` (1.2.840.10045.4.3.2) — parameters absent.
39    EcdsaSha256,
40    /// `sha256WithRSAEncryption` (1.2.840.113549.1.1.11) — NULL parameters.
41    RsaSha256,
42}
43
44/// A single revoked certificate entry.
45#[derive(Debug, Clone, Copy)]
46pub struct RevokedEntry<'a> {
47    /// Revoked serial as a big-endian unsigned magnitude.
48    pub serial: &'a [u8],
49    /// When the certificate was revoked.
50    pub revocation_date: SystemTime,
51}
52
53/// Inputs for [`build_crl`]. All identity fields are caller-supplied so this
54/// crate needs no hash/key backend.
55pub struct CrlParams<'a> {
56    /// DER of the issuer's subject `Name` (the full `SEQUENCE` TLV).
57    pub issuer_name_der: &'a [u8],
58    /// CA `keyIdentifier` bytes, emitted as the CRL `authorityKeyIdentifier`.
59    pub authority_key_id: &'a [u8],
60    /// `thisUpdate`.
61    pub this_update: SystemTime,
62    /// `nextUpdate` — governs how long a client caches the CRL.
63    pub next_update: SystemTime,
64    /// Monotonic `cRLNumber`.
65    pub crl_number: u64,
66    /// Revoked entries; empty omits the `revokedCertificates` field entirely.
67    pub revoked: &'a [RevokedEntry<'a>],
68}
69
70/// Build a DER-encoded v2 `CertificateList`.
71///
72/// `sign_tbs` signs the `tbsCertList` DER with the issuer key. The public
73/// surface takes only `std` time types; `time::OffsetDateTime` is an internal
74/// detail of the `Time` encoding.
75pub fn build_crl(
76    params: &CrlParams<'_>,
77    alg: CrlSignatureAlgorithm,
78    sign_tbs: impl FnOnce(&[u8]) -> Result<Vec<u8>, BoxError>,
79) -> Result<Vec<u8>, BoxError> {
80    // Times are fallible to encode, so resolve them before the infallible
81    // `yasna` writer closures.
82    let this_update = x509_time(params.this_update)?;
83    let next_update = x509_time(params.next_update)?;
84    let revoked = params
85        .revoked
86        .iter()
87        .map(|e| Ok::<_, BoxError>((e.serial, x509_time(e.revocation_date)?)))
88        .collect::<Result<Vec<_>, _>>()?;
89
90    // extnValue payloads (the OCTET STRING contents) for the v2 CRL extensions.
91    let aki_value = yasna::construct_der(|w| {
92        w.write_sequence(|w| {
93            w.next()
94                .write_tagged_implicit(Tag::context(0), |w| w.write_bytes(params.authority_key_id));
95        });
96    });
97    let crl_number_value = yasna::construct_der(|w| w.write_u64(params.crl_number));
98
99    let tbs_der = yasna::construct_der(|w| {
100        w.write_sequence(|w| {
101            // version v2 (present because crlExtensions are present)
102            w.next().write_i64(1);
103            // signature AlgorithmIdentifier (must match the outer one)
104            write_alg(w.next(), alg);
105            // issuer Name (raw DER)
106            w.next().write_der(params.issuer_name_der);
107            write_time(w.next(), &this_update);
108            write_time(w.next(), &next_update);
109            // revokedCertificates ::= SEQUENCE OF SEQUENCE { serial, date }
110            if !revoked.is_empty() {
111                w.next().write_sequence_of(|w| {
112                    for (serial, date) in &revoked {
113                        w.next().write_sequence(|w| {
114                            w.next().write_bigint_bytes(serial, true);
115                            write_time(w.next(), date);
116                        });
117                    }
118                });
119            }
120            // crlExtensions [0] EXPLICIT Extensions
121            w.next().write_tagged(Tag::context(0), |w| {
122                w.write_sequence(|w| {
123                    w.next().write_sequence(|w| {
124                        w.next().write_oid(&oid_authority_key_id());
125                        w.next().write_bytes(&aki_value);
126                    });
127                    w.next().write_sequence(|w| {
128                        w.next().write_oid(&oid_crl_number());
129                        w.next().write_bytes(&crl_number_value);
130                    });
131                });
132            });
133        });
134    });
135
136    let signature = sign_tbs(&tbs_der)?;
137
138    Ok(yasna::construct_der(|w| {
139        w.write_sequence(|w| {
140            w.next().write_der(&tbs_der);
141            write_alg(w.next(), alg);
142            w.next().write_bitvec_bytes(&signature, signature.len() * 8);
143        });
144    }))
145}
146
147/// DER of a `CRLDistributionPoints` extension value with a single
148/// `fullName` URI distribution point, for embedding as the `2.5.29.31`
149/// extension on a re-signed leaf.
150#[must_use]
151pub fn crl_distribution_point_der(uri: &str) -> Vec<u8> {
152    yasna::construct_der(|w| {
153        w.write_sequence(|w| {
154            w.next().write_sequence(|w| {
155                // distributionPoint [0] EXPLICIT DistributionPointName
156                w.next().write_tagged(Tag::context(0), |w| {
157                    // fullName [0] IMPLICIT GeneralNames
158                    w.write_tagged_implicit(Tag::context(0), |w| {
159                        w.write_sequence(|w| {
160                            // uniformResourceIdentifier [6] IA5String
161                            w.next().write_tagged_implicit(Tag::context(6), |w| {
162                                w.write_bytes(uri.as_bytes());
163                            });
164                        });
165                    });
166                });
167            });
168        });
169    })
170}
171
172fn write_alg(w: yasna::DERWriter<'_>, alg: CrlSignatureAlgorithm) {
173    w.write_sequence(|w| match alg {
174        CrlSignatureAlgorithm::EcdsaSha256 => {
175            w.next().write_oid(&oid_ecdsa_sha256());
176        }
177        CrlSignatureAlgorithm::RsaSha256 => {
178            w.next().write_oid(&oid_rsa_sha256());
179            w.next().write_null();
180        }
181    });
182}
183
184/// RFC 5280 `Time`: UTCTime through 2049, GeneralizedTime from 2050.
185enum X509Time {
186    Utc(UTCTime),
187    General(GeneralizedTime),
188}
189
190fn x509_time(t: SystemTime) -> Result<X509Time, BoxError> {
191    let secs = t
192        .duration_since(SystemTime::UNIX_EPOCH)
193        .context("crl: timestamp before unix epoch")?
194        .as_secs();
195    let odt = time::OffsetDateTime::from_unix_timestamp(secs as i64)
196        .map_err(|e| BoxError::from(format!("crl: invalid timestamp: {e}")))?;
197    Ok(if odt.year() < 2050 {
198        X509Time::Utc(UTCTime::from_datetime(odt))
199    } else {
200        X509Time::General(GeneralizedTime::from_datetime(odt))
201    })
202}
203
204fn write_time(w: yasna::DERWriter<'_>, t: &X509Time) {
205    match t {
206        X509Time::Utc(u) => w.write_utctime(u),
207        X509Time::General(g) => w.write_generalized_time(g),
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use std::time::Duration;
215
216    /// ~2027-01-15, comfortably in the UTCTime range.
217    const T0: u64 = 1_800_000_000;
218
219    fn params<'a>(issuer: &'a [u8], revoked: &'a [RevokedEntry<'a>]) -> CrlParams<'a> {
220        CrlParams {
221            issuer_name_der: issuer,
222            authority_key_id: &[0xAB; 20],
223            this_update: SystemTime::UNIX_EPOCH + Duration::from_secs(T0),
224            next_update: SystemTime::UNIX_EPOCH + Duration::from_secs(T0 + 7 * 86_400),
225            crl_number: 1,
226            revoked,
227        }
228    }
229
230    /// A `good`/empty CRL is a well-formed `CertificateList` whose embedded
231    /// `tbsCertList` round-trips byte-for-byte with what the caller signed.
232    #[test]
233    fn builds_wellformed_empty_crl() {
234        let issuer = yasna::construct_der(|w| w.write_sequence(|_| {}));
235        let mut signed_tbs: Vec<u8> = Vec::new();
236        let der = build_crl(
237            &params(&issuer, &[]),
238            CrlSignatureAlgorithm::EcdsaSha256,
239            |tbs| {
240                signed_tbs = tbs.to_vec();
241                Ok(vec![0xDE, 0xAD, 0xBE, 0xEF])
242            },
243        )
244        .expect("build crl");
245
246        assert!(
247            !signed_tbs.is_empty(),
248            "tbsCertList was handed to the signer"
249        );
250
251        yasna::parse_der(&der, |r| {
252            r.read_sequence(|r| {
253                let tbs = r.next().read_der()?;
254                assert_eq!(tbs, signed_tbs, "embedded tbs == signed tbs");
255                r.next().read_sequence(|r| {
256                    let oid = r.next().read_oid()?;
257                    assert_eq!(oid, oid_ecdsa_sha256());
258                    Ok(())
259                })?;
260                let (sig, bits) = r.next().read_bitvec_bytes()?;
261                assert_eq!(sig, vec![0xDE, 0xAD, 0xBE, 0xEF]);
262                assert_eq!(bits, 32);
263                Ok(())
264            })
265        })
266        .expect("parse CertificateList");
267    }
268
269    /// A revoked entry lands in `revokedCertificates` with its serial intact.
270    #[test]
271    fn revoked_serial_present() {
272        let issuer = yasna::construct_der(|w| w.write_sequence(|_| {}));
273        let revoked = [RevokedEntry {
274            serial: &[0x12, 0x34, 0x56],
275            revocation_date: SystemTime::UNIX_EPOCH + Duration::from_secs(T0),
276        }];
277        let der = build_crl(
278            &params(&issuer, &revoked),
279            CrlSignatureAlgorithm::RsaSha256,
280            |_| Ok(vec![0x00]),
281        )
282        .expect("build crl");
283
284        let serials = yasna::parse_der(&der, |r| {
285            r.read_sequence(|r| {
286                let serials = r.next().read_sequence(|r| {
287                    let _version = r.next().read_i64()?;
288                    let _alg = r.next().read_der()?;
289                    let _issuer = r.next().read_der()?;
290                    let _this = r.next().read_der()?;
291                    let _next = r.next().read_der()?;
292                    let mut serials: Vec<Vec<u8>> = Vec::new();
293                    r.next().read_sequence_of(|r| {
294                        r.read_sequence(|r| {
295                            let (serial, _pos) = r.next().read_bigint_bytes()?;
296                            let _date = r.next().read_der()?;
297                            serials.push(serial);
298                            Ok(())
299                        })
300                    })?;
301                    let _exts = r.next().read_der()?;
302                    Ok(serials)
303                })?;
304                // signatureAlgorithm + signature complete the CertificateList.
305                let _alg = r.next().read_der()?;
306                let _sig = r.next().read_bitvec_bytes()?;
307                Ok(serials)
308            })
309        })
310        .expect("parse tbsCertList");
311
312        assert_eq!(serials, vec![vec![0x12, 0x34, 0x56]]);
313    }
314
315    #[test]
316    fn crl_distribution_point_is_a_sequence_carrying_the_uri() {
317        let uri = "http://127.0.0.1:9999/abc.crl";
318        let der = crl_distribution_point_der(uri);
319        yasna::parse_der(&der, |r| {
320            r.read_sequence(|r| {
321                let _dp = r.next().read_der()?;
322                Ok(())
323            })
324        })
325        .expect("CRLDistributionPoints is a SEQUENCE");
326        assert!(
327            der.windows(uri.len()).any(|w| w == uri.as_bytes()),
328            "URI present in the distribution point"
329        );
330    }
331}