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}