windows_ctl/
lib.rs

1//! A crate for parsing Windows Certificate Trust Lists (CTLs).
2//!
3//! Certificate Trust Lists are how Windows distributes the metadata needed
4//! to bootstrap trusted certificate stores.
5
6#![deny(rustdoc::broken_intra_doc_links)]
7#![deny(missing_docs)]
8#![allow(clippy::redundant_field_names)]
9#![forbid(unsafe_code)]
10
11use std::io::{Read, Seek};
12
13use der::asn1::{Any, ObjectIdentifier, OctetString, OctetStringRef, Uint};
14use der::{Decode, Enumerated, Sequence};
15use itertools::Itertools;
16use pkcs7::{ContentInfo, ContentType};
17#[cfg(feature = "serde")]
18use serde::ser::SerializeStruct;
19#[cfg(feature = "serde")]
20use serde::{ser, Serialize};
21use spki::AlgorithmIdentifier;
22use thiserror::Error;
23use x509_cert::attr::Attributes;
24use x509_cert::ext::pkix::ExtendedKeyUsage;
25use x509_cert::time::Time;
26
27/// The object identifier for [`CertificateTrustList`].
28pub const MS_CERT_TRUST_LIST_OID: ObjectIdentifier =
29    ObjectIdentifier::new_unwrap("1.3.6.1.4.1.311.10.1");
30
31/// The OID for an attribute containing `ExtendedKeyUsage` identifiers.
32pub const MS_CERT_PROP_ID_METAEKUS_OID: ObjectIdentifier =
33    ObjectIdentifier::new_unwrap("1.3.6.1.4.1.311.10.11.9");
34
35/// Possible errors while parsing a certificate trust list.
36#[derive(Debug, Error)]
37pub enum CtlError {
38    /// I/O errors.
39    #[error("I/O error")]
40    Io(#[from] std::io::Error),
41
42    /// Invalid DER.
43    #[error("bad DER encoding")]
44    Der(#[from] der::Error),
45
46    /// Valid PKCS#7, but the wrong `content-type`.
47    #[error("bad PKCS#7 content-type: expected SignedData, got {0:?}")]
48    ContentType(ContentType),
49
50    /// Valid PKCS#7, but no encapsulated `signed-data`.
51    #[error("missing SignedData encapsulated content")]
52    MissingSignedData,
53
54    /// Valid PKCS#7 with `signed-data`, but not a `CertificateTrustList`.
55    #[error("bad SignedData ContentType: expected {MS_CERT_TRUST_LIST_OID}, got {0}")]
56    Content(ObjectIdentifier),
57
58    /// Valid PKCS#7 that claims to have a `CertificateTrustList`, but not present.
59    #[error("missing SignedData inner content")]
60    MissingSignedDataContent,
61}
62
63/// ```asn1
64/// SubjectIdentifier ::= OCTETSTRING
65/// ```
66pub type SubjectIdentifier = OctetString;
67
68/// Completely undocumented by MS.
69///
70/// As best I can tell this is:
71///
72/// ```asn1
73/// MetaEku ::= SEQUENCE OF OBJECT IDENTIFIER
74/// ```
75pub type MetaEku = Vec<ObjectIdentifier>;
76
77/// Represents a single entry in the certificate trust list.
78///
79/// From MS-CAESO:
80///
81/// ```asn1
82/// TrustedSubject ::= SEQUENCE {
83///   subjectIdentifier SubjectIdentifier,
84///   subjectAttributes Attributes OPTIONAL
85/// }
86/// ```
87#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
88pub struct TrustedSubject {
89    identifier: SubjectIdentifier,
90    /// Any X.509 attributes attached to this [`TrustedSubject`].
91    pub attributes: Option<Attributes>,
92}
93
94impl TrustedSubject {
95    /// Returns the certificate's ID, as bytes.
96    pub fn cert_id(&self) -> &[u8] {
97        self.identifier.as_bytes()
98    }
99
100    /// Returns an iterator over all Extended Key Usages (EKUs) listed
101    /// in this `TrustedSubject`.
102    pub fn extended_key_usages(
103        &self,
104    ) -> impl Iterator<Item = Result<ObjectIdentifier, der::Error>> + '_ {
105        // Option<Attributes>
106        //   -> Iterator<Attribute>
107        //   -> attributes that list EKUs
108        //   -> all values for those attributes
109        //   -> each value is an OCTET STRING
110        //   -> ...which in turn contains DER for a MetaEKU...
111        //   -> ...which in turn is a list of OIDs
112        self.attributes
113            .iter()
114            .flat_map(|attrs| attrs.iter())
115            .filter(|attr| attr.oid == MS_CERT_PROP_ID_METAEKUS_OID)
116            .flat_map(|attr| attr.values.iter())
117            .flat_map(|value| {
118                value
119                    .decode_as::<OctetStringRef>()
120                    .map(|o| MetaEku::from_der(o.as_bytes()))
121            })
122            .flatten_ok()
123    }
124}
125
126#[cfg(feature = "serde")]
127impl Serialize for TrustedSubject {
128    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
129    where
130        S: serde::Serializer,
131    {
132        let eku_oids = self
133            .extended_key_usages()
134            .collect::<Result<Vec<_>, _>>()
135            .map_err(|e| ser::Error::custom(format!("EKU collection failed: {e}")))?
136            .iter()
137            .map(ToString::to_string)
138            .collect::<Vec<_>>();
139
140        let mut s = serializer.serialize_struct("TrustedSubject", 2)?;
141        s.serialize_field("identifier", &hex::encode(self.identifier.as_bytes()))?;
142        s.serialize_field("ekus", &eku_oids)?;
143        s.end()
144    }
145}
146
147/// Version identifier for CertificateTrustList.
148///
149/// ```asn1
150/// CTLVersion ::= INTEGER {v1(0)}
151/// ```
152#[derive(Clone, Debug, Copy, PartialEq, Eq, Enumerated)]
153#[asn1(type = "INTEGER")]
154#[repr(u8)]
155#[derive(Default)]
156pub enum CtlVersion {
157    /// CtlVersion 1 (default)
158    #[default]
159    V1 = 0,
160}
161
162/// NOTE: MS calls X.509's [`ExtendedKeyUsage`] "`EnhancedKeyUsage`".
163///
164/// ```asn1
165/// SubjectUsage ::= EnhancedKeyUsage
166/// ```
167pub type SubjectUsage = ExtendedKeyUsage;
168
169/// ```asn1
170/// ListIdentifier ::= OCTETSTRING
171/// ```
172pub type ListIdentifier = OctetString;
173
174/// ```asn1
175/// TrustedSubjects ::= SEQUENCE OF TrustedSubject
176/// ```
177pub type TrustedSubjects = Vec<TrustedSubject>;
178
179/// The certificate trust list.
180///
181/// From [MS-CAESO], pages 47-48:
182///
183/// ```asn1
184/// CertificateTrustList ::= SEQUENCE {
185///   version CTLVersion DEFAULT v1,
186///   subjectUsage SubjectUsage,
187///   listIdentifier ListIdentifier OPTIONAL,
188///   sequenceNumber HUGEINTEGER OPTIONAL,
189///   ctlThisUpdate ChoiceOfTime,
190///   ctlNextUpdate ChoiceOfTime OPTIONAL,
191///   subjectAlgorithm AlgorithmIdentifier,
192///   trustedSubjects TrustedSubjects OPTIONAL,
193///   ctlExtensions [0] EXPLICIT Extensions OPTIONAL
194/// }
195/// ```
196///
197/// [MS-CAESO]: https://yossarian.net/junk/hard_to_find/ms-caeso-v20090709.pdf
198#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
199pub struct CertificateTrustList {
200    /// This trust list's version. The default version is 1.
201    #[asn1(default = "Default::default")]
202    pub version: CtlVersion,
203
204    /// X.509-style usage.
205    pub subject_usage: SubjectUsage,
206
207    /// See [MS-CAESO](https://yossarian.net/junk/hard_to_find/ms-caeso-v20090709.pdf) page 48.
208    pub list_identifier: Option<ListIdentifier>,
209
210    /// Some kind of sequence number; purpose unknown.
211    pub sequence_number: Option<Uint>,
212
213    // NOTE: MS doesn't bother to document `ChoiceOfTime`, but experimentally
214    // it's the same thing as an X.509 `Time` (See <https://www.rfc-editor.org/rfc/rfc5280#section-4.1>)
215    /// X.509-style time for when this CTL was produced/released.
216    pub this_update: Time,
217
218    /// X.509-style time for when the next CTL will be produced/released.
219    pub next_update: Option<Time>,
220
221    /// Presumably the digest algorithm used to compute each [`TrustedSubjects`]'s identifier.
222    pub subject_algorithm: AlgorithmIdentifier<Any>,
223
224    /// The list of trusted subjects in this CTL.
225    pub trusted_subjects: Option<TrustedSubjects>,
226
227    // TODO: this should really be `x509_cert::ext::Extensions`
228    // but that's a borrowed type and this struct is owning.
229    /// Any X.509 style extensions.
230    #[asn1(context_specific = "0", optional = "true", tag_mode = "EXPLICIT")]
231    pub ctl_extensions: Option<Any>,
232}
233
234impl CertificateTrustList {
235    /// Load a `CertificateTrustList` from the given source, which is expected to be a DER-encoded
236    /// PKCS#7 stream.
237    pub fn from_der<R: Read + Seek>(mut source: R) -> Result<Self, CtlError> {
238        // TODO: Micro-optimize: could pre-allocate `der` here using the stream's
239        // size (since we have the `Seek` bound).
240        let mut der = vec![];
241        source.read_to_end(&mut der)?;
242
243        let body = ContentInfo::from_der(&der)?;
244        let signed_data = match body {
245            ContentInfo::SignedData(signed_data) => signed_data,
246            _ => return Err(CtlError::ContentType(body.content_type())),
247        };
248
249        // Our actual SignedData content should be a MS-specific `certTrustList`.
250        if signed_data.encap_content_info.e_content_type != MS_CERT_TRUST_LIST_OID {
251            return Err(CtlError::Content(
252                signed_data.encap_content_info.e_content_type,
253            ));
254        }
255
256        let Some(content) = signed_data.encap_content_info.e_content else {
257            return Err(CtlError::MissingSignedDataContent);
258        };
259
260        Ok(content.decode_as()?)
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_metaeku() {
270        // SEQUENCE
271        //   OBJECT IDENTIFIER x 3
272        let metaeku = b"\x30\x1E\x06\x08\x2B\x06\x01\x05\x05\x07\x03\x02\x06\x08\x2B\x06\x01\x05\x05\x07\x03\x04\x06\x08\x2B\x06\x01\x05\x05\x07\x03\x01";
273        let res = MetaEku::from_der(metaeku).unwrap();
274
275        assert_eq!(res.len(), 3);
276        assert_eq!(res[0], ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.2"));
277        assert_eq!(res[1], ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.4"));
278        assert_eq!(res[2], ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.3.1"));
279    }
280}