firmware_uefi/service/nvram/spec_services/
auth_var_crypto.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Cryptographic operations to validate authenticated variables
5
6#![cfg(feature = "auth-var-verify-crypto")]
7
8use super::ParsedAuthVar;
9use thiserror::Error;
10use uefi_nvram_specvars::signature_list;
11use zerocopy::IntoBytes;
12
13/// Errors that occur due to various formatting issues in the crypto objects.
14#[derive(Debug, Error)]
15pub enum FormatError {
16    #[error("parsing signature list from auth_var_data")]
17    SignatureList(#[from] signature_list::ParseError),
18    #[error("decoding x509 cert from signature list")]
19    SignatureListX509(#[source] openssl::error::ErrorStack),
20
21    #[error("parsing auth var's pkcs7_data as pkcs#7 DER")]
22    AuthVarPkcs7Der(#[source] openssl::error::ErrorStack),
23    #[error("could not reconstruct signedData header for auth var's pkcs#7 data: {0}")]
24    AuthVarPkcs7DerHeader(der::Error),
25}
26
27impl FormatError {
28    /// Whether the error is due to malformed data in the signature lists
29    pub fn key_var_error(&self) -> bool {
30        match self {
31            FormatError::SignatureList(_) | FormatError::SignatureListX509(_) => true,
32            FormatError::AuthVarPkcs7Der(_) | FormatError::AuthVarPkcs7DerHeader(_) => false,
33        }
34    }
35}
36
37/// Authenticate the variable against the certs in the provided signature_lists,
38/// returning `true` if the auth was successful.
39pub fn authenticate_variable(
40    signature_lists: &[u8],
41    var: ParsedAuthVar<'_>,
42) -> Result<bool, FormatError> {
43    let ParsedAuthVar {
44        name,
45        vendor,
46        attr,
47        timestamp,
48        pkcs7_data,
49        var_data,
50    } = var;
51
52    // stage 1 - parse the pkcs7_data into an openssl Pkcs7 object
53    let var_pkcs7 = match openssl::pkcs7::Pkcs7::from_der(pkcs7_data) {
54        Ok(pkcs7) => pkcs7,
55        Err(_) => {
56            // From UEFI spec 8.2.2 Using the EFI_VARIABLE_AUTHENTICATION_2 descriptor
57            //
58            // > Construct a DER-encoded SignedData structure per PKCS#7 version 1.5
59            // > (RFC 2315), which shall be supported **both with and without**
60            // > a DER-encoded ContentInfo structure per PKCS#7 version 1.5 [..]
61            //
62            // (emphasis mine)
63            //
64            // Yes, you read that right.
65            //
66            // The UEFI spec explicitly allows _malformed_ PKCS#7 payloads that
67            // are missing a ContentInfo header. _sigh_
68
69            // stage 1.5 - if parsing fails the first time, construct an appropriate
70            // ContentInfo header and retry parsing the payload as a PKCS#7 DER
71            let buf = pkcs7_details::encapsulate_in_content_info(pkcs7_data)
72                .map_err(FormatError::AuthVarPkcs7DerHeader)?;
73            match openssl::pkcs7::Pkcs7::from_der(&buf) {
74                Ok(pkcs7) => pkcs7,
75                // ...but if that also fails, there's nothing else we can do
76                Err(e) => return Err(FormatError::AuthVarPkcs7Der(e)),
77            }
78        }
79    };
80
81    // stage 2 - extract and parse all the x509 certs from the signature list(s)
82    //           into openssl x509 objects
83    let certs = {
84        let mut parsed_certs = Vec::new();
85        let lists = signature_list::ParseSignatureLists::new(signature_lists);
86        for list in lists {
87            let list = list?;
88            // we only care about x509 certs in the signature lists
89            if let signature_list::ParseSignatureList::X509(certs) = list {
90                for cert in certs {
91                    let cert = cert?;
92                    let cert = openssl::x509::X509::from_der(&cert.data.0)
93                        .map_err(FormatError::SignatureListX509)?;
94                    parsed_certs.push(cert);
95                }
96            }
97        }
98        parsed_certs
99    };
100
101    // stage 3 - construct the "data to verify" buffer
102    //
103    // See bullet point 2. in UEFI spec 8.2.2
104    let mut verify_buf = Vec::new();
105    verify_buf.extend(name.as_bytes_without_nul());
106    verify_buf.extend(vendor.as_bytes());
107    verify_buf.extend(attr.as_bytes());
108    verify_buf.extend(timestamp.as_bytes());
109    verify_buf.extend(var_data);
110
111    // stage 4 - package those raw certs into an openssl X509Store object
112    let store = {
113        let mut store = openssl::x509::store::X509StoreBuilder::new().unwrap();
114
115        // unlike the HCL / worker process implementations, which manually
116        // compare certs to perform the verification, we leverage openssl's
117        // built-in functionality to do this for us.
118        //
119        // first, we throw all our trusted certs into a X509Store:
120        for cert in certs {
121            store.add_cert(cert).unwrap();
122        }
123
124        // then, we set some extra flags to work around the particular
125        // idiosyncrasies of how these certs are constructed...
126
127        // PARTIAL_CHAIN rationale: the certs in the EFI_SIGNATURE_LIST are not
128        // root certs, and we don't have a full cert chain available. Instead,
129        // we want to terminate the chain verification at whatever certs are
130        // present from the EFI_SIGNATURE_LISTs.
131        //
132        // NO_CHECK_TIME rationale: when testing this feature, we noticed that
133        // the UEFI signing key expired a long time ago. The existing
134        // implementations didn't care about this, and allowed the verification
135        // to succeed regardless.
136        let store_flags = openssl::x509::verify::X509VerifyFlags::PARTIAL_CHAIN
137            | openssl::x509::verify::X509VerifyFlags::NO_CHECK_TIME;
138        store.set_flags(store_flags).unwrap();
139
140        // X509Purpose::Any rationale: openssl expects the trusted certs to have
141        // certain capabilities that ours do not. Omitting this call will result
142        // in the verify operation failing with "Verify error:unsupported
143        // certificate purpose"
144        store
145            .set_purpose(openssl::x509::X509PurposeId::ANY)
146            .unwrap();
147
148        store.build()
149    };
150
151    // stage 5 - actually perform the verification
152    match var_pkcs7.verify(
153        // `certs` should be nullable (i.e: represented using an optional).
154        // This is an oversight in the openssl-rs API, so instead, we use an
155        // empty stack...
156        &openssl::stack::Stack::new().unwrap(),
157        &store,
158        Some(&verify_buf),
159        None,
160        openssl::pkcs7::Pkcs7Flags::empty(),
161    ) {
162        Ok(()) => Ok(true),
163        Err(e) => {
164            tracing::trace!(
165                error = &e as &dyn std::error::Error,
166                "could not verify auth var"
167            );
168            Ok(false)
169        }
170    }
171}
172
173mod pkcs7_details {
174    use der::Encode;
175    use der::Sequence;
176    use der::TagMode;
177    use der::TagNumber;
178    use der::asn1::AnyRef;
179    use der::asn1::ContextSpecific;
180    use der::asn1::ObjectIdentifier;
181
182    #[derive(Copy, Clone, Debug, Eq, PartialEq, Sequence)]
183    struct ContentInfo<'a> {
184        pub content_type: ObjectIdentifier,
185        pub content: ContextSpecific<AnyRef<'a>>,
186    }
187
188    /// Construct a ASN.1 `ContentInfo` header with `ContentType = signedData`
189    /// as specified by the PKCS#7 RFC2315.
190    ///
191    /// See https://datatracker.ietf.org/doc/html/rfc2315#section-7
192    ///
193    /// ```text
194    /// ContentInfo ::= SEQUENCE {
195    ///   contentType ContentType,
196    ///   content
197    ///     [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL }
198    /// ```
199    pub fn encapsulate_in_content_info(content: &[u8]) -> der::Result<Vec<u8>> {
200        // constant pulled from https://datatracker.ietf.org/doc/html/rfc2315#section-14
201        const PKCS_7_SIGNED_DATA_OID: ObjectIdentifier =
202            ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.2");
203
204        let content_info = ContentInfo {
205            content_type: PKCS_7_SIGNED_DATA_OID,
206            content: ContextSpecific {
207                tag_number: TagNumber::new(0),
208                value: AnyRef::try_from(content)?,
209                tag_mode: TagMode::Explicit,
210            },
211        };
212
213        Encode::to_der(&content_info)
214    }
215}