Skip to main content

x509/
lib.rs

1//! *Pure-Rust X.509 certificate serialization*
2//!
3//! `x509` is a crate providing serialization APIs for X.509 v3 ([RFC 5280]) certificates,
4//! implemented using the `cookie-factory` combinatorial serializer framework.
5//!
6//! [RFC 5280]: https://tools.ietf.org/html/rfc5280
7
8use cookie_factory::{GenResult, WriteContext};
9use std::io::Write;
10
11pub mod der;
12
13/// A trait for objects which represent ASN.1 `AlgorithmIdentifier`s.
14pub trait AlgorithmIdentifier {
15    type AlgorithmOid: der::Oid;
16
17    /// Returns the object identifier for this `AlgorithmIdentifier`.
18    fn algorithm(&self) -> Self::AlgorithmOid;
19
20    /// Writes the parameters for this `AlgorithmIdentifier`, if any.
21    fn parameters<W: Write>(&self, w: WriteContext<W>) -> GenResult<W>;
22}
23
24/// A trait for objects which represent ASN.1 `SubjectPublicKeyInfo`s.
25pub trait SubjectPublicKeyInfo {
26    type AlgorithmId: AlgorithmIdentifier;
27    type SubjectPublicKey: AsRef<[u8]>;
28
29    /// Returns the [`AlgorithmIdentifier`] for this public key.
30    fn algorithm_id(&self) -> Self::AlgorithmId;
31
32    /// Returns the encoded public key.
33    fn public_key(&self) -> Self::SubjectPublicKey;
34}
35
36#[derive(Clone)]
37enum RdnType {
38    Country,
39    Organization,
40    OrganizationalUnit,
41    CommonName,
42}
43
44/// An X.509 RelativeDistinguishedName.
45#[derive(Clone)]
46pub struct RelativeDistinguishedName<'a> {
47    typ: RdnType,
48    value: &'a str,
49}
50
51impl<'a> RelativeDistinguishedName<'a> {
52    /// Constructs a Country RDN.
53    ///
54    /// # Panics
55    /// Panics if `value.len() > 64`.
56    pub fn country(value: &'a str) -> Self {
57        assert!(value.len() <= 64);
58
59        RelativeDistinguishedName {
60            typ: RdnType::Country,
61            value,
62        }
63    }
64
65    /// Constructs an Organization RDN.
66    ///
67    /// # Panics
68    /// Panics if `value.len() > 64`.
69    pub fn organization(value: &'a str) -> Self {
70        assert!(value.len() <= 64);
71
72        RelativeDistinguishedName {
73            typ: RdnType::Organization,
74            value,
75        }
76    }
77
78    /// Constructs an OrganizationalUnit RDN.
79    ///
80    /// # Panics
81    /// Panics if `value.len() > 64`.
82    pub fn organizational_unit(value: &'a str) -> Self {
83        assert!(value.len() <= 64);
84
85        RelativeDistinguishedName {
86            typ: RdnType::OrganizationalUnit,
87            value,
88        }
89    }
90
91    /// Constructs a CommonName RDN.
92    ///
93    /// # Panics
94    /// Panics if `value.len() > 64`.
95    pub fn common_name(value: &'a str) -> Self {
96        assert!(value.len() <= 64);
97
98        RelativeDistinguishedName {
99            typ: RdnType::CommonName,
100            value,
101        }
102    }
103}
104
105/// A certificate extension.
106pub struct Extension<'a, O: der::Oid + 'a> {
107    /// An OID that specifies the format and definitions of the extension.
108    oid: O,
109    /// Whether the information in the extension is important.
110    ///
111    /// ```text
112    /// Each extension in a certificate may be designated as critical or non-critical. A
113    /// certificate using system MUST reject the certificate if it encounters a critical
114    /// extension it does not recognize; however, a non-critical extension may be ignored
115    /// if it is not recognized.
116    /// ```
117    critical: bool,
118    /// The DER encoding of an ASN.1 value corresponding to the extension type identified
119    /// by `oid`.
120    value: &'a [u8],
121}
122
123impl<'a, O: der::Oid + 'a> Extension<'a, O> {
124    /// Constructs an extension.
125    ///
126    /// If this extension is not recognized by a certificate-using system, it will be
127    /// ignored.
128    ///
129    /// `oid` is an OID that specifies the format and definitions of the extension.
130    ///
131    /// `value` is the DER encoding of an ASN.1 value corresponding to the extension type
132    /// identified by `oid`.
133    pub fn regular(oid: O, value: &'a [u8]) -> Self {
134        Extension {
135            oid,
136            critical: false,
137            value,
138        }
139    }
140
141    /// Constructs a critical extension.
142    ///
143    /// If this extension is not recognized by a certificate-using system, the certificate
144    /// will be rejected.
145    ///
146    /// `oid` is an OID that specifies the format and definitions of the extension.
147    ///
148    /// `value` is the DER encoding of an ASN.1 value corresponding to the extension type
149    /// identified by `oid`.
150    pub fn critical(oid: O, value: &'a [u8]) -> Self {
151        Extension {
152            oid,
153            critical: true,
154            value,
155        }
156    }
157}
158
159/// X.509 serialization APIs.
160pub mod write {
161    use chrono::{DateTime, Datelike, TimeZone, Utc};
162    use cookie_factory::{
163        combinator::{cond, slice},
164        multi::all,
165        sequence::pair,
166        SerializeFn, WriteContext,
167    };
168    use std::io::Write;
169
170    use crate::{Extension, RdnType, RelativeDistinguishedName};
171
172    use super::{
173        der::{write::*, Oid},
174        AlgorithmIdentifier, SubjectPublicKeyInfo,
175    };
176
177    /// X.509 versions that we care about.
178    #[derive(Clone, Copy)]
179    enum Version {
180        V3,
181    }
182
183    impl From<Version> for usize {
184        fn from(version: Version) -> usize {
185            match version {
186                Version::V3 => 2,
187            }
188        }
189    }
190
191    /// Object identifiers used internally by X.509.
192    enum InternalOid {
193        Country,
194        Organization,
195        OrganizationalUnit,
196        CommonName,
197    }
198
199    impl AsRef<[u64]> for InternalOid {
200        fn as_ref(&self) -> &[u64] {
201            match self {
202                InternalOid::Country => &[2, 5, 4, 6],
203                InternalOid::Organization => &[2, 5, 4, 10],
204                InternalOid::OrganizationalUnit => &[2, 5, 4, 11],
205                InternalOid::CommonName => &[2, 5, 4, 3],
206            }
207        }
208    }
209
210    impl Oid for InternalOid {}
211
212    impl<'a> RelativeDistinguishedName<'a> {
213        fn oid(&self) -> InternalOid {
214            match self.typ {
215                RdnType::Country => InternalOid::Country,
216                RdnType::CommonName => InternalOid::CommonName,
217                RdnType::Organization => InternalOid::Organization,
218                RdnType::OrganizationalUnit => InternalOid::OrganizationalUnit,
219            }
220        }
221    }
222
223    /// From [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.1):
224    /// ```text
225    /// TBSCertificate  ::=  SEQUENCE  {
226    ///      version         [0]  EXPLICIT Version DEFAULT v1,
227    ///      ...
228    ///      }
229    ///
230    /// Version  ::=  INTEGER  {  v1(0), v2(1), v3(2)  }
231    /// ```
232    fn version<W: Write>(version: Version) -> impl SerializeFn<W> {
233        // TODO: Omit version if V1, once x509-parser correctly handles this.
234        der_explicit(0, der_integer_usize(version.into()))
235    }
236
237    /// From [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.1.1.2):
238    /// ```text
239    /// AlgorithmIdentifier  ::=  SEQUENCE  {
240    ///      algorithm               OBJECT IDENTIFIER,
241    ///      parameters              ANY DEFINED BY algorithm OPTIONAL  }
242    /// ```
243    pub fn algorithm_identifier<'a, W: Write + 'a, Alg: AlgorithmIdentifier>(
244        algorithm_id: &'a Alg,
245    ) -> impl SerializeFn<W> + 'a {
246        der_sequence((
247            der_oid(algorithm_id.algorithm()),
248            move |w: WriteContext<Vec<u8>>| algorithm_id.parameters(w),
249        ))
250    }
251
252    /// Encodes an X.509 RelativeDistinguishedName.
253    ///
254    /// From [RFC 5280 section 4.1.2.4](https://tools.ietf.org/html/rfc5280#section-4.1.2.4):
255    /// ```text
256    /// RelativeDistinguishedName ::=
257    ///   SET SIZE (1..MAX) OF AttributeTypeAndValue
258    ///
259    /// AttributeTypeAndValue ::= SEQUENCE {
260    ///   type     AttributeType,
261    ///   value    AttributeValue }
262    ///
263    /// AttributeType ::= OBJECT IDENTIFIER
264    ///
265    /// AttributeValue ::= ANY -- DEFINED BY AttributeType
266    /// ```
267    ///
268    /// From [RFC 5280 appendix A.1](https://tools.ietf.org/html/rfc5280#appendix-A.1):
269    /// ```text
270    /// X520CommonName ::= CHOICE {
271    ///      teletexString     TeletexString   (SIZE (1..ub-common-name)),
272    ///      printableString   PrintableString (SIZE (1..ub-common-name)),
273    ///      universalString   UniversalString (SIZE (1..ub-common-name)),
274    ///      utf8String        UTF8String      (SIZE (1..ub-common-name)),
275    ///      bmpString         BMPString       (SIZE (1..ub-common-name)) }
276    ///
277    /// ub-common-name INTEGER ::= 64
278    /// ```
279    fn relative_distinguished_name<'a, W: Write + 'a>(
280        rdn: &'a RelativeDistinguishedName<'a>,
281    ) -> impl SerializeFn<W> + 'a {
282        der_set((der_sequence((
283            der_oid(rdn.oid()),
284            der_utf8_string(&rdn.value),
285        )),))
286    }
287
288    /// Encodes an X.509 Name.
289    ///
290    /// From [RFC 5280 section 4.1.2.4](https://tools.ietf.org/html/rfc5280#section-4.1.2.4):
291    /// ```text
292    /// Name ::= CHOICE { -- only one possibility for now --
293    ///   rdnSequence  RDNSequence }
294    ///
295    /// RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
296    /// ```
297    fn name<'a, W: Write + 'a>(
298        name: &'a [RelativeDistinguishedName<'a>],
299    ) -> impl SerializeFn<W> + 'a {
300        der_sequence((all(name.iter().map(relative_distinguished_name)),))
301    }
302
303    /// From [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.1):
304    /// ```text
305    /// Time ::= CHOICE {
306    ///      utcTime        UTCTime,
307    ///      generalTime    GeneralizedTime }
308    ///
309    /// CAs conforming to this profile MUST always encode certificate
310    /// validity dates through the year 2049 as UTCTime; certificate validity
311    /// dates in 2050 or later MUST be encoded as GeneralizedTime.
312    /// ```
313    fn time<W: Write>(t: DateTime<Utc>) -> impl SerializeFn<W> {
314        pair(
315            cond(t.year() < 2050, der_utc_time(t)),
316            cond(t.year() >= 2050, der_generalized_time(t)),
317        )
318    }
319
320    /// From [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.1):
321    /// ```text
322    /// Validity ::= SEQUENCE {
323    ///      notBefore      Time,
324    ///      notAfter       Time }
325    ///
326    /// To indicate that a certificate has no well-defined expiration date,
327    /// the notAfter SHOULD be assigned the GeneralizedTime value of
328    /// 99991231235959Z.
329    /// ```
330    fn validity<W: Write>(
331        not_before: DateTime<Utc>,
332        not_after: Option<DateTime<Utc>>,
333    ) -> impl SerializeFn<W> {
334        der_sequence((
335            time(not_before),
336            time(not_after.unwrap_or_else(|| Utc.ymd(9999, 12, 31).and_hms(23, 59, 59))),
337        ))
338    }
339
340    /// Encodes a `PublicKeyInfo` as an ASN.1 `SubjectPublicKeyInfo` using DER.
341    ///
342    /// From [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.1):
343    /// ```text
344    /// SubjectPublicKeyInfo  ::=  SEQUENCE  {
345    ///      algorithm            AlgorithmIdentifier,
346    ///      subjectPublicKey     BIT STRING  }
347    /// ```
348    fn subject_public_key_info<'a, W: Write + 'a, PKI: SubjectPublicKeyInfo>(
349        subject_pki: &'a PKI,
350    ) -> impl SerializeFn<W> + 'a {
351        move |w: WriteContext<W>| {
352            der_sequence((
353                algorithm_identifier(&subject_pki.algorithm_id()),
354                der_bit_string(subject_pki.public_key().as_ref()),
355            ))(w)
356        }
357    }
358
359    /// From [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.1):
360    /// ```text
361    /// Extension  ::=  SEQUENCE  {
362    ///      extnID      OBJECT IDENTIFIER,
363    ///      critical    BOOLEAN DEFAULT FALSE,
364    ///      extnValue   OCTET STRING
365    ///                  -- contains the DER encoding of an ASN.1 value
366    ///                  -- corresponding to the extension type identified
367    ///                  -- by extnID
368    ///      }
369    /// ```
370    fn extension<'a, W: Write + 'a, O: Oid + 'a>(
371        extension: &'a Extension<'a, O>,
372    ) -> impl SerializeFn<W> + 'a {
373        der_sequence((
374            der_oid(&extension.oid),
375            der_default(der_boolean, extension.critical, false),
376            der_octet_string(extension.value),
377        ))
378    }
379
380    /// From [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.1):
381    /// ```text
382    /// TBSCertificate  ::=  SEQUENCE  {
383    ///      ...
384    ///      extensions      [3]  EXPLICIT Extensions OPTIONAL
385    ///                           -- If present, version MUST be v3
386    ///      }
387    ///
388    /// Extensions  ::=  SEQUENCE SIZE (1..MAX) OF Extension
389    /// ```
390    fn extensions<'a, W: Write + 'a, O: Oid + 'a>(
391        exts: &'a [Extension<'a, O>],
392    ) -> impl SerializeFn<W> + 'a {
393        cond(
394            !exts.is_empty(),
395            der_explicit(3, der_sequence((all(exts.iter().map(extension)),))),
396        )
397    }
398
399    /// Encodes a version 1 X.509 `TBSCertificate` using DER.
400    ///
401    /// `extensions` is optional; if empty, no extensions section will be serialized. Due
402    /// to the need for an `O: Oid` type parameter, users who do not have any extensions
403    /// should use the following workaround:
404    ///
405    /// ```ignore
406    /// let exts: &[Extension<'_, &[u64]>] = &[];
407    /// x509::write::tbs_certificate(
408    ///     serial_number,
409    ///     signature,
410    ///     issuer,
411    ///     not_before,
412    ///     not_after,
413    ///     subject,
414    ///     subject_pki,
415    ///     exts,
416    /// );
417    /// ```
418    ///
419    /// From [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.1):
420    /// ```text
421    /// TBSCertificate  ::=  SEQUENCE  {
422    ///      version         [0]  EXPLICIT Version DEFAULT v1,
423    ///      serialNumber         CertificateSerialNumber,
424    ///      signature            AlgorithmIdentifier,
425    ///      issuer               Name,
426    ///      validity             Validity,
427    ///      subject              Name,
428    ///      subjectPublicKeyInfo SubjectPublicKeyInfo,
429    ///      issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
430    ///                           -- If present, version MUST be v2 or v3
431    ///      subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
432    ///                           -- If present, version MUST be v2 or v3
433    ///      extensions      [3]  EXPLICIT Extensions OPTIONAL
434    ///                           -- If present, version MUST be v3
435    ///      }
436    ///
437    /// CertificateSerialNumber  ::=  INTEGER
438    ///
439    /// Certificate users MUST be able to handle serialNumber values up to 20 octets.
440    /// Conforming CAs MUST NOT use serialNumber values longer than 20 octets.
441    /// ```
442    ///
443    /// # Panics
444    ///
445    /// Panics if:
446    /// - `serial_number.len() > 20`
447    pub fn tbs_certificate<'a, W: Write + 'a, Alg, PKI, O: Oid + 'a>(
448        serial_number: &'a [u8],
449        signature: &'a Alg,
450        issuer: &'a [RelativeDistinguishedName<'a>],
451        not_before: DateTime<Utc>,
452        not_after: Option<DateTime<Utc>>,
453        subject: &'a [RelativeDistinguishedName<'a>],
454        subject_pki: &'a PKI,
455        exts: &'a [Extension<'a, O>],
456    ) -> impl SerializeFn<W> + 'a
457    where
458        Alg: AlgorithmIdentifier,
459        PKI: SubjectPublicKeyInfo,
460    {
461        assert!(serial_number.len() <= 20);
462
463        der_sequence((
464            version(Version::V3),
465            der_integer(serial_number),
466            algorithm_identifier(signature),
467            name(issuer),
468            validity(not_before, not_after),
469            name(subject),
470            subject_public_key_info(subject_pki),
471            extensions(exts),
472        ))
473    }
474
475    /// Encodes an X.509 certificate using DER.
476    ///
477    /// From [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.1):
478    /// ```text
479    /// Certificate  ::=  SEQUENCE  {
480    ///      tbsCertificate       TBSCertificate,
481    ///      signatureAlgorithm   AlgorithmIdentifier,
482    ///      signatureValue       BIT STRING  }
483    /// ```
484    ///
485    /// Use [`tbs_certificate`] to serialize the certificate itself, then sign it and call
486    /// this function with the serialized `TBSCertificate` and signature.
487    pub fn certificate<'a, W: Write + 'a, Alg: AlgorithmIdentifier>(
488        cert: &'a [u8],
489        signature_algorithm: &'a Alg,
490        signature: &'a [u8],
491    ) -> impl SerializeFn<W> + 'a {
492        der_sequence((
493            slice(cert),
494            algorithm_identifier(signature_algorithm),
495            der_bit_string(signature),
496        ))
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use chrono::Utc;
503
504    use crate::{
505        write, AlgorithmIdentifier, Extension, RelativeDistinguishedName, SubjectPublicKeyInfo,
506    };
507
508    struct MockAlgorithmId;
509
510    impl AlgorithmIdentifier for MockAlgorithmId {
511        type AlgorithmOid = &'static [u64];
512
513        fn algorithm(&self) -> Self::AlgorithmOid {
514            &[1, 1, 1, 1]
515        }
516
517        fn parameters<W: std::io::Write>(
518            &self,
519            w: cookie_factory::WriteContext<W>,
520        ) -> cookie_factory::GenResult<W> {
521            Ok(w)
522        }
523    }
524
525    struct MockPublicKeyInfo;
526
527    impl SubjectPublicKeyInfo for MockPublicKeyInfo {
528        type AlgorithmId = MockAlgorithmId;
529        type SubjectPublicKey = Vec<u8>;
530
531        fn algorithm_id(&self) -> Self::AlgorithmId {
532            MockAlgorithmId
533        }
534
535        fn public_key(&self) -> Self::SubjectPublicKey {
536            vec![]
537        }
538    }
539
540    #[test]
541    fn names() {
542        const COUNTRY: &str = "NZ";
543        const ORGANIZATION: &str = "ACME";
544        const ORGANIZATIONAL_UNIT: &str = "Road Runner";
545        const COMMON_NAME: &str = "Test-in-a-Box";
546
547        let subject = &[
548            RelativeDistinguishedName::country(COUNTRY),
549            RelativeDistinguishedName::organization(ORGANIZATION),
550            RelativeDistinguishedName::organizational_unit(ORGANIZATIONAL_UNIT),
551            RelativeDistinguishedName::common_name(COMMON_NAME),
552        ];
553        let exts: &[Extension<'_, &[u64]>] = &[];
554
555        let mut tbs_cert = vec![];
556        cookie_factory::gen(
557            write::tbs_certificate(
558                &[],
559                &MockAlgorithmId,
560                &[],
561                Utc::now(),
562                None,
563                subject,
564                &MockPublicKeyInfo,
565                exts,
566            ),
567            &mut tbs_cert,
568        )
569        .unwrap();
570
571        let mut data = vec![];
572        cookie_factory::gen(
573            write::certificate(&tbs_cert, &MockAlgorithmId, &[]),
574            &mut data,
575        )
576        .unwrap();
577
578        let (_, cert) = x509_parser::parse_x509_certificate(&data).unwrap();
579
580        assert_eq!(
581            cert.subject()
582                .iter_country()
583                .map(|c| c.as_str())
584                .collect::<Result<Vec<_>, _>>(),
585            Ok(vec![COUNTRY])
586        );
587        assert_eq!(
588            cert.subject()
589                .iter_organization()
590                .map(|c| c.as_str())
591                .collect::<Result<Vec<_>, _>>(),
592            Ok(vec![ORGANIZATION])
593        );
594        assert_eq!(
595            cert.subject()
596                .iter_organizational_unit()
597                .map(|c| c.as_str())
598                .collect::<Result<Vec<_>, _>>(),
599            Ok(vec![ORGANIZATIONAL_UNIT])
600        );
601        assert_eq!(
602            cert.subject()
603                .iter_common_name()
604                .map(|c| c.as_str())
605                .collect::<Result<Vec<_>, _>>(),
606            Ok(vec![COMMON_NAME])
607        );
608    }
609
610    #[test]
611    fn extensions() {
612        let signature = MockAlgorithmId;
613        let not_before = Utc::now();
614        let subject_pki = MockPublicKeyInfo;
615        let exts = &[
616            Extension::regular(&[1u64, 2, 3, 4][..], &[1, 2, 3]),
617            Extension::critical(&[1u64, 4, 5, 6][..], &[7, 7, 7]),
618        ];
619
620        let mut tbs_cert = vec![];
621        cookie_factory::gen(
622            write::tbs_certificate(
623                &[],
624                &signature,
625                &[],
626                not_before,
627                None,
628                &[],
629                &subject_pki,
630                exts,
631            ),
632            &mut tbs_cert,
633        )
634        .unwrap();
635
636        let mut data = vec![];
637        cookie_factory::gen(
638            write::certificate(&tbs_cert, &MockAlgorithmId, &[]),
639            &mut data,
640        )
641        .unwrap();
642
643        let (_, cert) = x509_parser::parse_x509_certificate(&data).unwrap();
644
645        assert_eq!(
646            cert.validity().not_before.timestamp(),
647            not_before.timestamp()
648        );
649
650        for ext in exts {
651            let oid = x509_parser::der_parser::oid::Oid::from(ext.oid).unwrap();
652            if let Some(extension) = cert.extensions().get(&oid) {
653                assert_eq!(extension.critical, ext.critical);
654                assert_eq!(extension.value, ext.value);
655            } else {
656                panic!();
657            }
658        }
659    }
660}