Skip to main content

firmware_uefi/service/nvram/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! UEFI Nvram variable services subsystem.
5//!
6//! Special care has been taken to keep all Hyper-V specific interfaces and
7//! extensions in a separate layer from the underlying UEFI spec mandated
8//! functionality.
9//!
10//! e.g: things like injecting various nvram vars related to secure boot, boot
11//! order, etc... are not part of the UEFI spec, and are therefore implemented
12//! _outside_ of the [`spec_services`] module.
13
14pub use spec_services::NvramError;
15pub use spec_services::NvramResult;
16pub use spec_services::NvramServicesExt;
17pub use spec_services::NvramSpecServices;
18
19use crate::UefiDevice;
20use cvm_tracing::CVM_ALLOWED;
21use cvm_tracing::CVM_CONFIDENTIAL;
22use firmware_uefi_custom_vars::BaseTemplate;
23use firmware_uefi_custom_vars::BaseTemplateVars;
24use firmware_uefi_custom_vars::FinalVars;
25use firmware_uefi_custom_vars::Signature;
26use firmware_uefi_custom_vars::UefiVarsDeltaJson;
27use firmware_uefi_resources::platform::VsmConfig;
28use guestmem::GuestMemoryError;
29use guid::Guid;
30use inspect::Inspect;
31use std::borrow::Cow;
32use std::collections::BTreeSet;
33use std::fmt::Debug;
34use std::mem::size_of;
35use std::mem::size_of_val;
36use thiserror::Error;
37use uefi_nvram_specvars::signature_list::ParseError as SignatureListParseError;
38use uefi_nvram_specvars::signature_list::ParseSignatureList;
39use uefi_nvram_specvars::signature_list::ParseSignatureLists;
40use uefi_nvram_storage::VmmNvramStorage;
41use uefi_specs::uefi::common::EfiStatus;
42use uefi_specs::uefi::nvram::EFI_VARIABLE_AUTHENTICATION_2;
43use uefi_specs::uefi::nvram::EfiVariableAttributes;
44use uefi_specs::uefi::nvram::signature_list::EFI_SIGNATURE_DATA;
45use uefi_specs::uefi::nvram::vars;
46use uefi_specs::uefi::signing::EFI_CERT_TYPE_PKCS7_GUID;
47use uefi_specs::uefi::signing::WIN_CERT_TYPE_EFI_GUID;
48use uefi_specs::uefi::signing::WIN_CERTIFICATE_UEFI_GUID;
49use zerocopy::FromBytes;
50use zerocopy::IntoBytes;
51
52#[cfg(feature = "fuzzing")]
53pub mod spec_services;
54#[cfg(not(feature = "fuzzing"))]
55mod spec_services;
56
57const WIN_CERT_REVISION_2_0: u16 = 0x0200;
58
59/// A Secure Boot signature's type, owner, and payload used for set comparison.
60#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
61enum SignatureValue {
62    X509(EFI_SIGNATURE_DATA, Vec<u8>),
63    Sha256(EFI_SIGNATURE_DATA, Vec<u8>),
64}
65
66/// Unique Secure Boot signatures contained in a variable or base template.
67type SignatureSet = BTreeSet<SignatureValue>;
68
69/// Secure Boot telemetry schema mirrored by legacy HCL's `UefiNvramStore.cpp`.
70struct SecureBootVariableReport<'a> {
71    name: &'a ucs2::Ucs2LeSlice,
72    in_nvram: bool,
73    has_custom_uefi: bool,
74    loaded_bytes: usize,
75    loaded_entries: usize,
76    template_entries: usize,
77    missing_entries: usize,
78}
79
80impl Debug for SecureBootVariableReport<'_> {
81    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        let Self {
83            name,
84            in_nvram,
85            has_custom_uefi,
86            loaded_bytes,
87            loaded_entries,
88            template_entries,
89            missing_entries,
90        } = self;
91
92        formatter
93            .debug_struct("SecureBootVariable")
94            .field("name", name)
95            .field("in_nvram", in_nvram)
96            .field("has_custom_uefi", has_custom_uefi)
97            .field("loaded_bytes", &format_args!("{loaded_bytes:#x}"))
98            .field("loaded_entries", &format_args!("{loaded_entries:#x}"))
99            .field("template_entries", &format_args!("{template_entries:#x}"))
100            .field("missing_entries", &format_args!("{missing_entries:#x}"))
101            .finish()
102    }
103}
104
105#[derive(Debug, Error)]
106pub enum NvramSetupError {
107    #[error("could not query backing nvram storage")]
108    BadNvramStorage(#[source] uefi_nvram_storage::NvramStorageError),
109    #[error("failed to apply custom UEFI template variables")]
110    ApplyCustomTemplate(#[source] firmware_uefi_custom_vars::ApplyDeltaError),
111    #[error("failed to load built-in UEFI template")]
112    LoadBaseTemplate(#[source] hyperv_uefi_custom_vars_json::ParseJsonError),
113    #[error("failed to load custom UEFI variable JSON")]
114    LoadCustomUefiJson(#[source] hyperv_uefi_custom_vars_json::ParseJsonError),
115    #[error("could not inject pre-boot var '{0}': {1:?}")]
116    InjectPreBootVar(
117        Cow<'static, ucs2::Ucs2LeSlice>,
118        EfiStatus,
119        #[source] Option<NvramError>,
120    ),
121    #[error("could not inject signature var '{0}': {1:?}")]
122    InjectSigVar(
123        Cow<'static, ucs2::Ucs2LeSlice>,
124        EfiStatus,
125        #[source] Option<NvramError>,
126    ),
127    #[error("could not inject UEFI var '{0}': {1:?}")]
128    InjectUefiVar(String, EfiStatus, #[source] Option<NvramError>),
129    #[error("UEFI variable name is not valid UCS-2")]
130    UefiVarNotUcs2,
131}
132
133/// Implements Hyper-V specific nvram service interfaces, extensions, and
134/// functionality, deferring to the underlying [`NvramSpecServices`] object to
135/// implement any UEFI spec mandated nvram service functionality.
136#[derive(Inspect)]
137pub struct NvramServices {
138    // Runtime glue
139    #[inspect(skip)]
140    vsm_config: Option<Box<dyn VsmConfig>>,
141
142    // Sub-emulators
143    #[inspect(flatten)]
144    services: NvramSpecServices<Box<dyn VmmNvramStorage>>,
145}
146
147impl NvramServices {
148    pub async fn new(
149        nvram_storage: Box<dyn VmmNvramStorage>,
150        base_template: Option<BaseTemplate>,
151        custom_uefi_json: Option<UefiVarsDeltaJson>,
152        secure_boot_enabled: bool,
153        vsm_config: Option<Box<dyn VsmConfig>>,
154        is_restoring: bool,
155    ) -> Result<NvramServices, NvramSetupError> {
156        let mut nvram = NvramServices {
157            services: NvramSpecServices::new(nvram_storage),
158            vsm_config,
159        };
160
161        if !is_restoring {
162            let is_nvram_empty = nvram
163                .services
164                .is_empty()
165                .await
166                .map_err(NvramSetupError::BadNvramStorage)?;
167            let base_template = match base_template
168                .filter(|_| is_nvram_empty || secure_boot_enabled)
169                .map(|template| {
170                    hyperv_uefi_custom_vars_json::parse_template_json(template.json.as_bytes())
171                })
172                .transpose()
173            {
174                Ok(vars) => vars,
175                Err(error) if !is_nvram_empty => {
176                    tracing::warn!(
177                        CVM_ALLOWED,
178                        error = &error as &dyn std::error::Error,
179                        "secure boot configuration report skipped: failed to parse baseline template"
180                    );
181                    None
182                }
183                Err(error) => return Err(NvramSetupError::LoadBaseTemplate(error)),
184            };
185
186            // Top-level configuration reads the VMGS CUSTOM_UEFI entry on every boot and
187            // populates this value when the entry is non-empty, matching legacy HCL. The delta
188            // itself is only applied below when NVRAM is empty.
189            let has_custom_uefi = custom_uefi_json
190                .as_ref()
191                .is_some_and(|json| !json.as_bytes().is_empty());
192
193            if is_nvram_empty {
194                nvram
195                    .inject_initial_vars(base_template.as_ref(), custom_uefi_json)
196                    .await?;
197            }
198            nvram.inject_hyperv_vars().await?;
199            nvram.setup_secure_boot(secure_boot_enabled).await?;
200
201            if secure_boot_enabled {
202                nvram
203                    .report_secure_boot_configuration(base_template.as_ref(), has_custom_uefi)
204                    .await;
205            }
206        }
207
208        nvram.services.prepare_for_boot();
209
210        Ok(nvram)
211    }
212
213    pub fn reset(&mut self) {
214        self.services.reset();
215        self.services.prepare_for_boot();
216    }
217
218    /// Inject hard-coded and configured UEFI variables into empty NVRAM.
219    async fn inject_initial_vars(
220        &mut self,
221        base_template_vars: Option<&BaseTemplateVars>,
222        custom_uefi_json: Option<UefiVarsDeltaJson>,
223    ) -> Result<(), NvramSetupError> {
224        let custom_template_delta = custom_uefi_json
225            .map(|json| hyperv_uefi_custom_vars_json::parse_delta_json(json.as_bytes()))
226            .transpose()
227            .map_err(NvramSetupError::LoadCustomUefiJson)?;
228        let final_vars = FinalVars::resolve(base_template_vars.cloned(), custom_template_delta)
229            .map_err(NvramSetupError::ApplyCustomTemplate)?;
230
231        tracing::info!("No NVRAM variables (first boot). Loading in initial NVRAM values.");
232
233        // Windows uses CurrentPolicy to protect secure boot policy
234        tracing::trace!("Injecting 'CurrentPolicy'");
235        {
236            use uefi_specs::hyperv::nvram::vars::CURRENT_POLICY;
237
238            let (vendor, name) = CURRENT_POLICY();
239            const CURRENT_POLICY_AUTHENTICATED_MARKER: u8 = 0x02;
240            let data = [CURRENT_POLICY_AUTHENTICATED_MARKER];
241            let attr = EfiVariableAttributes::DEFAULT_ATTRIBUTES_TIME_BASED_AUTH;
242
243            // because this variable is set with time based auth, it needs a
244            // `EFI_VARIABLE_AUTHENTICATION_2`. fortunately, we are still in
245            // pre-boot, which means it suffices to use a dummy header.
246            let data = {
247                let mut v = Vec::new();
248                v.extend(EFI_VARIABLE_AUTHENTICATION_2::DUMMY.as_bytes());
249                v.extend(data);
250                v
251            };
252
253            self.services
254                .set_variable_ucs2(vendor, name, attr.into(), data.clone())
255                .await
256                .map_err(|(status, err)| {
257                    NvramSetupError::InjectPreBootVar(name.into(), status, err)
258                })?;
259        }
260
261        tracing::trace!("Updating 'SetupMode'");
262        {
263            use uefi_specs::uefi::nvram::vars::SETUP_MODE;
264            let (_, name) = SETUP_MODE();
265
266            self.services.update_setup_mode().await.map_err(|e| {
267                NvramSetupError::InjectPreBootVar(
268                    name.into(),
269                    EfiStatus::DEVICE_ERROR,
270                    Some(NvramError::NvramStorage(e)),
271                )
272            })?
273        }
274
275        self.inject_final_vars(final_vars).await?;
276
277        Ok(())
278    }
279
280    /// Compare one loaded Secure Boot variable with its base-template signatures.
281    async fn get_secure_boot_variable_report<'a>(
282        &mut self,
283        (vendor, name): (Guid, &'a ucs2::Ucs2LeSlice),
284        template_signatures: &[Signature],
285        has_custom_uefi: bool,
286    ) -> Option<SecureBootVariableReport<'a>> {
287        let base_signatures = base_signature_set(template_signatures);
288        if base_signatures.is_empty() {
289            tracing::warn!(
290                CVM_ALLOWED,
291                %name,
292                "secure boot variable omitted from configuration report: baseline contains no signatures"
293            );
294            return None;
295        }
296
297        match self.services.get_variable_ucs2(vendor, name).await {
298            // The variable exists in NVRAM but is empty
299            Ok((_, data)) if data.is_empty() => Some(SecureBootVariableReport {
300                name,
301                in_nvram: true,
302                has_custom_uefi,
303                loaded_bytes: 0,
304                loaded_entries: 0,
305                template_entries: base_signatures.len(),
306                missing_entries: base_signatures.len(),
307            }),
308            // The variable exists in NVRAM and has data
309            Ok((_, data)) => match collect_signature_set(&data) {
310                // The data is valid
311                Ok(loaded_signatures) => Some(SecureBootVariableReport {
312                    name,
313                    in_nvram: true,
314                    has_custom_uefi,
315                    loaded_bytes: data.len(),
316                    loaded_entries: loaded_signatures.len(),
317                    template_entries: base_signatures.len(),
318                    missing_entries: base_signatures.difference(&loaded_signatures).count(),
319                }),
320                // The data cannot be parsed
321                Err(error) => {
322                    tracing::warn!(
323                        CVM_CONFIDENTIAL,
324                        %name,
325                        error = &error as &dyn std::error::Error,
326                        "secure boot variable omitted from configuration report: failed to parse variable"
327                    );
328                    None
329                }
330            },
331            // The variable does not exist in NVRAM
332            Err((EfiStatus::NOT_FOUND, _)) => Some(SecureBootVariableReport {
333                name,
334                in_nvram: false,
335                has_custom_uefi,
336                loaded_bytes: 0,
337                loaded_entries: 0,
338                template_entries: base_signatures.len(),
339                missing_entries: base_signatures.len(),
340            }),
341            // The variable exists in NVRAM but cannot be read for some other reason
342            Err((status, error)) => {
343                tracing::warn!(
344                    CVM_CONFIDENTIAL,
345                    %name,
346                    ?status,
347                    ?error,
348                    "secure boot variable omitted from configuration report: failed to read variable"
349                );
350                None
351            }
352        }
353    }
354
355    /// Report the loaded Secure Boot configuration against the selected base template.
356    async fn report_secure_boot_configuration(
357        &mut self,
358        base_template: Option<&BaseTemplateVars>,
359        has_custom_uefi: bool,
360    ) {
361        let Some(signatures) = base_template.and_then(BaseTemplateVars::signatures) else {
362            tracing::warn!(
363                CVM_ALLOWED,
364                "secure boot configuration report skipped: baseline signatures are unavailable"
365            );
366            return;
367        };
368
369        // Report each secure boot variable's loaded configuration against the baseline template
370        for (variable, template_signatures) in [
371            (vars::PK(), std::slice::from_ref(&signatures.pk)),
372            (vars::KEK(), signatures.kek.as_slice()),
373            (vars::DB(), signatures.db.as_slice()),
374            (vars::DBX(), signatures.dbx.as_slice()),
375        ] {
376            if let Some(variable_report) = self
377                .get_secure_boot_variable_report(variable, template_signatures, has_custom_uefi)
378                .await
379            {
380                tracing::info!(CVM_ALLOWED, "{variable_report:?}");
381            }
382        }
383    }
384
385    async fn inject_hyperv_vars(&mut self) -> Result<(), NvramSetupError> {
386        // Always inject these, in case the vmgs file was first booted RS1.86
387        tracing::trace!("Injecting 'OsLoaderIndications'");
388        {
389            use uefi_specs::hyperv::nvram::vars::OS_LOADER_INDICATIONS;
390
391            let (vendor, name) = OS_LOADER_INDICATIONS();
392            let data = 0u32.as_bytes();
393            let attr = EfiVariableAttributes::new().with_bootservice_access(true);
394
395            self.services
396                .set_variable_ucs2(vendor, name, attr.into(), data.to_vec())
397                .await
398                .map_err(|(status, err)| {
399                    NvramSetupError::InjectPreBootVar(name.into(), status, err)
400                })?;
401        }
402
403        tracing::trace!("Injecting 'OsLoaderIndicationsSupported'");
404        {
405            use uefi_specs::hyperv::nvram::vars::OS_LOADER_INDICATIONS_SUPPORTED;
406
407            let (vendor, name) = OS_LOADER_INDICATIONS_SUPPORTED();
408            // All VM versions capable of running the HCL support VSM
409            let data = 1u32.as_bytes();
410            let attr = EfiVariableAttributes::new().with_bootservice_access(true);
411
412            self.services
413                .set_variable_ucs2(vendor, name, attr.into(), data.to_vec())
414                .await
415                .map_err(|(status, err)| {
416                    NvramSetupError::InjectPreBootVar(name.into(), status, err)
417                })?;
418        }
419
420        Ok(())
421    }
422
423    async fn inject_final_vars(&mut self, final_vars: FinalVars) -> Result<(), NvramSetupError> {
424        use firmware_uefi_custom_vars::Sha256Digest;
425        use firmware_uefi_custom_vars::Signature;
426        use firmware_uefi_custom_vars::UefiVar;
427        use firmware_uefi_custom_vars::X509Cert;
428        use uefi_nvram_specvars::signature_list::SignatureData;
429        use uefi_nvram_specvars::signature_list::SignatureList;
430        use uefi_specs::hyperv::nvram::vars::MSFT_SECURE_BOOT_PRODUCTION_GUID;
431        use uefi_specs::uefi::nvram::EFI_VARIABLE_AUTHENTICATION_2;
432
433        let firmware_uefi_custom_vars::UefiVars {
434            signatures,
435            non_signature_vars,
436        } = final_vars.into_uefi_vars();
437
438        // Inject non-signature vars first, as some may require an auth bypass.
439        for (name, UefiVar { guid, attr, value }) in non_signature_vars {
440            tracing::trace!(%name, "Injecting UEFI var");
441
442            // the value might need to be prepended with an auth header,
443            // depending on what auth mode the variable is using.
444            let value = {
445                let attr = EfiVariableAttributes::from(attr);
446                if attr.contains_unsupported_bits() {
447                    return Err(NvramSetupError::InjectUefiVar(
448                        name,
449                        EfiStatus::INVALID_PARAMETER,
450                        Some(NvramError::AttributeNonSpec),
451                    ));
452                }
453
454                if attr.time_based_authenticated_write_access() {
455                    let mut new_value = Vec::new();
456                    // a dummy header needs to be present, even through no
457                    // actual validation will be performed while nvram is still
458                    // in SetupMode (i.e: until `pk` is injected).
459                    new_value.extend(EFI_VARIABLE_AUTHENTICATION_2::DUMMY.as_bytes());
460                    new_value.extend(value);
461                    new_value
462                } else {
463                    value
464                }
465            };
466
467            self.services
468                .set_variable(guid, &name, attr, value)
469                .await
470                .map_err(|(status, err)| NvramSetupError::InjectUefiVar(name, status, err))?;
471        }
472
473        // inject structured signature vars
474        if let Some(sigs) = signatures {
475            use uefi_specs::linux::nvram::vars as linux_vars;
476            use uefi_specs::uefi::nvram::vars as uefi_vars;
477
478            // `dbDefault` is a read-only copy of the initial `db`
479            let dbdefault_sig = sigs.db.clone();
480
481            // for each of the signatures, construct the variable payload
482            // (in the form of a signature list), and inject it into nvram.
483            #[rustfmt::skip]
484            let sigs_loop = [
485                (uefi_vars::KEK(),        sigs.kek,      EfiVariableAttributes::DEFAULT_ATTRIBUTES_TIME_BASED_AUTH),
486                (uefi_vars::DB(),         sigs.db,       EfiVariableAttributes::DEFAULT_ATTRIBUTES_TIME_BASED_AUTH),
487                (uefi_vars::DBX(),        sigs.dbx,      EfiVariableAttributes::DEFAULT_ATTRIBUTES_TIME_BASED_AUTH),
488                // Two notes:
489                //
490                // 1. Why the `vec![]`? Well, while there can only ever be a
491                //    single PK, it still ends up getting stored in a signature
492                //    _list_, so we may as well reuse the existing logic (rather
493                //    than having a special cased block just for PK).
494                //
495                // 2. pk _must_ be injected after kek, db, and dbx, as once pk
496                //    is injected, nvram switches out of SetupMode, and requires
497                //    non-dummy auth var headers to update those vars.
498                (uefi_vars::PK(),         vec![sigs.pk], EfiVariableAttributes::DEFAULT_ATTRIBUTES_TIME_BASED_AUTH),
499                (uefi_vars::DBDEFAULT(),  dbdefault_sig, EfiVariableAttributes::DEFAULT_ATTRIBUTES_VOLATILE),
500                (linux_vars::MOK_LIST(),  sigs.moklist,  EfiVariableAttributes::DEFAULT_ATTRIBUTES),
501                (linux_vars::MOK_LISTX(), sigs.moklistx, EfiVariableAttributes::DEFAULT_ATTRIBUTES),
502            ];
503
504            for ((vendor, name), sigs, attr) in sigs_loop {
505                tracing::trace!(?name, "Injecting");
506
507                let mut var_data: Vec<u8> = Vec::new();
508
509                for sig in sigs {
510                    match sig {
511                        Signature::X509(certs) => {
512                            // x509 is weird, since every cert in the array
513                            // actually ends up as a _separate_ signature list!
514                            for X509Cert(data) in certs {
515                                let sig_list = SignatureList::X509(SignatureData::new_x509(
516                                    MSFT_SECURE_BOOT_PRODUCTION_GUID,
517                                    Cow::Owned(data),
518                                ));
519                                sig_list.extend_as_spec_signature_list(&mut var_data);
520                            }
521                        }
522                        Signature::Sha256(digests) => {
523                            let sig_list = SignatureList::Sha256(
524                                digests
525                                    .into_iter()
526                                    .map(|Sha256Digest(data)| {
527                                        SignatureData::new_sha256(
528                                            MSFT_SECURE_BOOT_PRODUCTION_GUID,
529                                            Cow::Owned(data),
530                                        )
531                                    })
532                                    .collect(),
533                            );
534                            sig_list.extend_as_spec_signature_list(&mut var_data);
535                        }
536                    }
537                }
538
539                if var_data.is_empty() {
540                    continue;
541                }
542
543                if attr.time_based_authenticated_write_access() {
544                    // a dummy header needs to be present, even through no
545                    // actual validation will be performed while nvram is still
546                    // in SetupMode (i.e: until `pk` is injected).
547                    let mut authenticated_var_data =
548                        EFI_VARIABLE_AUTHENTICATION_2::DUMMY.as_bytes().to_vec();
549                    authenticated_var_data.extend(var_data);
550                    var_data = authenticated_var_data;
551                }
552
553                self.services
554                    .set_variable_ucs2(vendor, name, attr.into(), var_data)
555                    .await
556                    .map_err(|(status, err)| {
557                        NvramSetupError::InjectSigVar(name.into(), status, err)
558                    })?;
559            }
560        }
561
562        Ok(())
563    }
564
565    /// Inject secure boot configuration vars.
566    async fn setup_secure_boot(&mut self, enabled: bool) -> Result<(), NvramSetupError> {
567        tracing::info!(enabled, "configuring secure boot");
568
569        let data = if enabled { [0x01] } else { [0x00] };
570
571        tracing::trace!("Injecting 'SecureBoot'");
572        {
573            use uefi_specs::uefi::nvram::vars::SECURE_BOOT;
574
575            let (vendor, name) = SECURE_BOOT();
576
577            // Older versions of OpenHCL (and Hyper-V, closed-source HCL, etc. ) may have created
578            // a SecureBoot variable with the NV attribute, which doesn't match the UEFI spec.
579            // Delete this variable (if it exists).
580            let delete_attr = EfiVariableAttributes::new();
581            let _ = self
582                .services
583                .set_variable_ucs2(vendor, name, delete_attr.into(), data.to_vec())
584                .await;
585
586            // TODO: For compatibility with older OpenHCL images that cannot handle a volatile
587            // variable, we still need to create with NV for now.  Once the above variable
588            // deletion code is deployed everywhere, replace with:
589            // let attr = EfiVariableAttributes::DEFAULT_ATTRIBUTES_VOLATILE;
590            let attr = EfiVariableAttributes::DEFAULT_ATTRIBUTES;
591            self.services
592                .set_variable_ucs2(vendor, name, attr.into(), data.to_vec())
593                .await
594                .map_err(|(status, err)| {
595                    NvramSetupError::InjectPreBootVar(name.into(), status, err)
596                })?;
597        }
598
599        tracing::trace!("Injecting 'SecureBootEnabled'");
600        {
601            use uefi_specs::hyperv::nvram::vars::SECURE_BOOT_ENABLE;
602
603            let (vendor, name) = SECURE_BOOT_ENABLE();
604            let attr = EfiVariableAttributes::DEFAULT_ATTRIBUTES;
605
606            self.services
607                .set_variable_ucs2(vendor, name, attr.into(), data.to_vec())
608                .await
609                .map_err(|(status, err)| {
610                    NvramSetupError::InjectPreBootVar(name.into(), status, err)
611                })?;
612        }
613
614        Ok(())
615    }
616}
617
618/// Convert typed base-template signatures into their injected signature identities.
619fn base_signature_set<'a>(signatures: impl IntoIterator<Item = &'a Signature>) -> SignatureSet {
620    use uefi_specs::hyperv::nvram::vars::MSFT_SECURE_BOOT_PRODUCTION_GUID;
621
622    let header = EFI_SIGNATURE_DATA {
623        signature_owner: MSFT_SECURE_BOOT_PRODUCTION_GUID,
624    };
625    let mut values = SignatureSet::new();
626    for signature in signatures {
627        match signature {
628            Signature::X509(certs) => values.extend(
629                certs
630                    .iter()
631                    .map(|cert| SignatureValue::X509(header, cert.0.clone())),
632            ),
633            Signature::Sha256(digests) => values.extend(
634                digests
635                    .iter()
636                    .map(|digest| SignatureValue::Sha256(header, digest.0.to_vec())),
637            ),
638        }
639    }
640    values
641}
642
643/// Parse a serialized Secure Boot variable into unique signature identities.
644fn collect_signature_set(data: &[u8]) -> Result<SignatureSet, SignatureListParseError> {
645    let mut signatures = SignatureSet::new();
646
647    for list in ParseSignatureLists::new(signature_list_payload(data)) {
648        match list? {
649            ParseSignatureList::X509(certs) => {
650                for cert in certs {
651                    let cert = cert?;
652                    signatures.insert(SignatureValue::X509(
653                        cert.header,
654                        cert.data.0.as_ref().to_vec(),
655                    ));
656                }
657            }
658            ParseSignatureList::Sha256(digests) => {
659                for digest in digests {
660                    let digest = digest?;
661                    signatures.insert(SignatureValue::Sha256(
662                        digest.header,
663                        digest.data.0.as_ref().to_vec(),
664                    ));
665                }
666            }
667        }
668    }
669
670    Ok(signatures)
671}
672
673/// Return the signature-list payload, skipping a valid authentication header.
674///
675/// Malformed or unrecognized headers are treated as part of the payload so the
676/// signature-list parser can report the underlying format error.
677fn signature_list_payload(data: &[u8]) -> &[u8] {
678    let Ok((auth, _)) = EFI_VARIABLE_AUTHENTICATION_2::read_from_prefix(data) else {
679        return data;
680    };
681
682    if auth.auth_info.header.revision != WIN_CERT_REVISION_2_0
683        || auth.auth_info.header.certificate_type != WIN_CERT_TYPE_EFI_GUID
684        || auth.auth_info.cert_type != EFI_CERT_TYPE_PKCS7_GUID
685    {
686        return data;
687    }
688
689    let cert_len = auth.auth_info.header.length as usize;
690    if cert_len < size_of::<WIN_CERTIFICATE_UEFI_GUID>() {
691        return data;
692    }
693
694    let Some(auth_len) = size_of_val(&auth.timestamp).checked_add(cert_len) else {
695        return data;
696    };
697    data.get(auth_len..).unwrap_or(data)
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703    use firmware_uefi_custom_vars::ApplyDeltaError;
704    use pal_async::async_test;
705    use ucs2::Ucs2LeSlice;
706    use uefi_nvram_specvars::signature_list::SignatureData;
707    use uefi_nvram_specvars::signature_list::SignatureList;
708    use uefi_nvram_storage::EFI_TIME;
709    use uefi_nvram_storage::NvramStorage;
710    use uefi_nvram_storage::in_memory::InMemoryNvram;
711    use wchar::wchz;
712
713    const TEST_SIGNATURE_OWNER: Guid = guid::guid!("00000000-0000-0000-0000-000000000001");
714
715    fn x509_variable(certs: &[&'static [u8]]) -> Vec<u8> {
716        let mut data = Vec::new();
717        for cert in certs {
718            SignatureList::X509(SignatureData::new_x509(
719                TEST_SIGNATURE_OWNER,
720                Cow::Borrowed(*cert),
721            ))
722            .extend_as_spec_signature_list(&mut data);
723        }
724        data
725    }
726
727    fn append_without_base_json() -> Vec<u8> {
728        br#"{
729            "type": "Microsoft.Compute/disks",
730            "properties": {
731                "uefiSettings": {
732                    "signatureMode": "Append",
733                    "signatures": {}
734                }
735            }
736        }"#
737        .to_vec()
738    }
739
740    fn invalid_base_template() -> BaseTemplate {
741        BaseTemplate {
742            json: b"not json".to_vec().into(),
743        }
744    }
745
746    fn nvram_services(storage: InMemoryNvram) -> NvramServices {
747        let storage: Box<dyn VmmNvramStorage> = Box::new(storage);
748        NvramServices {
749            vsm_config: None,
750            services: NvramSpecServices::new(storage),
751        }
752    }
753
754    #[test]
755    fn authenticated_and_raw_signature_payloads_parse_identically() {
756        let raw = x509_variable(&[b"cert1"]);
757        let mut authenticated = EFI_VARIABLE_AUTHENTICATION_2::DUMMY.as_bytes().to_vec();
758        authenticated.extend_from_slice(&raw);
759
760        assert_eq!(
761            collect_signature_set(&authenticated).unwrap(),
762            collect_signature_set(&raw).unwrap()
763        );
764    }
765
766    #[test]
767    fn signature_set_difference_ignores_extra_loaded_signatures() {
768        let base = collect_signature_set(&x509_variable(&[b"cert1", b"cert2"])).unwrap();
769        let loaded = collect_signature_set(&x509_variable(&[b"cert1", b"cert3"])).unwrap();
770
771        assert_eq!(base.difference(&loaded).count(), 1);
772    }
773
774    #[test]
775    fn secure_boot_variable_report_fits_get_trace_limit() {
776        const GET_TRACE_MESSAGE_MAX_BYTES: usize = 256;
777
778        let report = SecureBootVariableReport {
779            name: vars::KEK().1,
780            in_nvram: false,
781            has_custom_uefi: false,
782            loaded_bytes: usize::MAX,
783            loaded_entries: usize::MAX,
784            template_entries: usize::MAX,
785            missing_entries: usize::MAX,
786        };
787        let message = format!("{report:?}");
788
789        assert!(
790            message.len() <= GET_TRACE_MESSAGE_MAX_BYTES,
791            "Secure Boot variable report is {} bytes: {message}",
792            message.len()
793        );
794    }
795
796    #[async_test]
797    async fn missing_secure_boot_variable_is_reported_as_not_present() {
798        let mut nvram = nvram_services(InMemoryNvram::new());
799        let template_signatures = [Signature::X509(vec![firmware_uefi_custom_vars::X509Cert(
800            b"cert1".to_vec(),
801        )])];
802
803        let report = nvram
804            .get_secure_boot_variable_report(vars::DB(), &template_signatures, true)
805            .await
806            .unwrap();
807
808        assert!(!report.in_nvram);
809        assert!(report.has_custom_uefi);
810        assert_eq!(report.loaded_bytes, 0);
811        assert_eq!(report.loaded_entries, 0);
812        assert_eq!(report.template_entries, 1);
813        assert_eq!(report.missing_entries, 1);
814    }
815
816    #[async_test]
817    async fn empty_secure_boot_variable_reports_all_entries_missing() {
818        let (vendor, name) = vars::DB();
819        let mut storage = InMemoryNvram::new();
820        storage
821            .set_variable(name, vendor, 0, Vec::new(), EFI_TIME::default())
822            .await
823            .unwrap();
824        let mut nvram = nvram_services(storage);
825        let template_signatures = [Signature::X509(vec![
826            firmware_uefi_custom_vars::X509Cert(b"cert1".to_vec()),
827            firmware_uefi_custom_vars::X509Cert(b"cert2".to_vec()),
828        ])];
829
830        let report = nvram
831            .get_secure_boot_variable_report((vendor, name), &template_signatures, false)
832            .await
833            .unwrap();
834
835        assert!(report.in_nvram);
836        assert!(!report.has_custom_uefi);
837        assert_eq!(report.loaded_bytes, 0);
838        assert_eq!(report.loaded_entries, 0);
839        assert_eq!(report.template_entries, 2);
840        assert_eq!(report.missing_entries, 2);
841    }
842
843    #[async_test]
844    async fn populated_secure_boot_variable_reports_signature_counts() {
845        use uefi_specs::hyperv::nvram::vars::MSFT_SECURE_BOOT_PRODUCTION_GUID;
846
847        let (vendor, name) = vars::DB();
848        let mut data = Vec::new();
849        for cert in [b"cert1".as_slice(), b"cert3".as_slice()] {
850            SignatureList::X509(SignatureData::new_x509(
851                MSFT_SECURE_BOOT_PRODUCTION_GUID,
852                Cow::Borrowed(cert),
853            ))
854            .extend_as_spec_signature_list(&mut data);
855        }
856        let loaded_bytes = data.len();
857        let mut storage = InMemoryNvram::new();
858        storage
859            .set_variable(name, vendor, 0, data, EFI_TIME::default())
860            .await
861            .unwrap();
862        let mut nvram = nvram_services(storage);
863        let template_signatures = [Signature::X509(vec![
864            firmware_uefi_custom_vars::X509Cert(b"cert1".to_vec()),
865            firmware_uefi_custom_vars::X509Cert(b"cert2".to_vec()),
866        ])];
867
868        let report = nvram
869            .get_secure_boot_variable_report((vendor, name), &template_signatures, false)
870            .await
871            .unwrap();
872
873        assert_eq!(report.loaded_bytes, loaded_bytes);
874        assert_eq!(report.loaded_entries, 2);
875        assert_eq!(report.template_entries, 2);
876        assert_eq!(report.missing_entries, 1);
877    }
878
879    #[async_test]
880    async fn invalid_templates_are_ignored_after_first_boot_without_secure_boot() {
881        let mut storage = InMemoryNvram::new();
882        let name = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "existing").as_bytes()).unwrap();
883        storage
884            .set_variable(name, Guid::default(), 0, vec![1], EFI_TIME::default())
885            .await
886            .unwrap();
887
888        NvramServices::new(
889            Box::new(storage),
890            Some(invalid_base_template()),
891            Some(b"not json".to_vec().into()),
892            false,
893            None,
894            false,
895        )
896        .await
897        .unwrap();
898    }
899
900    #[async_test]
901    async fn invalid_base_template_is_ignored_after_first_boot_with_secure_boot() {
902        let mut storage = InMemoryNvram::new();
903        let name = Ucs2LeSlice::from_slice_with_nul(wchz!(u16, "existing").as_bytes()).unwrap();
904        storage
905            .set_variable(name, Guid::default(), 0, vec![1], EFI_TIME::default())
906            .await
907            .unwrap();
908
909        NvramServices::new(
910            Box::new(storage),
911            Some(invalid_base_template()),
912            None,
913            true,
914            None,
915            false,
916        )
917        .await
918        .unwrap();
919    }
920
921    #[async_test]
922    async fn invalid_base_template_fails_on_first_boot() {
923        assert!(matches!(
924            NvramServices::new(
925                Box::new(InMemoryNvram::new()),
926                Some(invalid_base_template()),
927                None,
928                false,
929                None,
930                false,
931            )
932            .await,
933            Err(NvramSetupError::LoadBaseTemplate(_))
934        ));
935    }
936
937    #[async_test]
938    async fn invalid_delta_fails_on_first_boot() {
939        let mut nvram = nvram_services(InMemoryNvram::new());
940
941        assert!(matches!(
942            nvram
943                .inject_initial_vars(None, Some(append_without_base_json().into()))
944                .await,
945            Err(NvramSetupError::ApplyCustomTemplate(
946                ApplyDeltaError::AppendWithoutBase
947            ))
948        ));
949    }
950
951    #[async_test]
952    async fn malformed_json_fails_on_first_boot() {
953        let mut nvram = nvram_services(InMemoryNvram::new());
954
955        assert!(matches!(
956            nvram
957                .inject_initial_vars(None, Some(b"not json".to_vec().into()))
958                .await,
959            Err(NvramSetupError::LoadCustomUefiJson(_))
960        ));
961    }
962
963    #[async_test]
964    async fn deferred_base_template_parses_on_first_boot() {
965        let mut nvram = nvram_services(InMemoryNvram::new());
966        let base_template = firmware_uefi_resources::x64_secure_boot_templates::microsoft_windows();
967        let base_template_vars =
968            hyperv_uefi_custom_vars_json::parse_template_json(base_template.json.as_bytes())
969                .unwrap();
970
971        nvram
972            .inject_initial_vars(Some(&base_template_vars), None)
973            .await
974            .unwrap();
975
976        let (vendor, name) = vars::PK();
977        assert!(nvram.services.get_variable_ucs2(vendor, name).await.is_ok());
978    }
979
980    #[async_test]
981    async fn omitted_dbx_is_not_injected_on_first_boot() {
982        let custom_vars = br#"{
983            "type": "Microsoft.Compute/disks",
984            "properties": {
985                "uefiSettings": {
986                    "signatureMode": "Replace",
987                    "signatures": {
988                        "PK": { "type": "x509", "value": ["Jw=="] },
989                        "KEK": [{ "type": "x509", "value": ["Jw=="] }],
990                        "db": [{ "type": "x509", "value": ["Jw=="] }]
991                    }
992                }
993            }
994        }"#;
995        let mut nvram = nvram_services(InMemoryNvram::new());
996
997        nvram
998            .inject_initial_vars(None, Some(custom_vars.to_vec().into()))
999            .await
1000            .unwrap();
1001
1002        let (pk_vendor, pk_name) = vars::PK();
1003        assert!(
1004            nvram
1005                .services
1006                .get_variable_ucs2(pk_vendor, pk_name)
1007                .await
1008                .is_ok()
1009        );
1010
1011        let (dbx_vendor, dbx_name) = vars::DBX();
1012        assert!(matches!(
1013            nvram.services.get_variable_ucs2(dbx_vendor, dbx_name).await,
1014            Err((EfiStatus::NOT_FOUND, _))
1015        ));
1016    }
1017}
1018
1019impl UefiDevice {
1020    pub(crate) async fn nvram_handle_command(&mut self, desc_addr: u64) {
1021        use uefi_specs::hyperv::nvram::NvramCommandDescriptor;
1022
1023        let mut desc: NvramCommandDescriptor = match self.gm.read_plain(desc_addr) {
1024            Ok(desc) => desc,
1025            Err(err) => {
1026                tracelimit::warn_ratelimited!(
1027                    error = &err as &dyn std::error::Error,
1028                    "Could not read NvramCommandDescriptor from guest memory",
1029                );
1030                return;
1031            }
1032        };
1033
1034        let status = match self.handle_nvram_command_inner(desc_addr, desc).await {
1035            Ok(status) => status,
1036            Err(err) => {
1037                tracelimit::warn_ratelimited!(
1038                    error = &err as &dyn std::error::Error,
1039                    "Guest memory error while handling nvram command"
1040                );
1041                EfiStatus::DEVICE_ERROR
1042            }
1043        };
1044
1045        // write back status into guest memory
1046        desc.status = status.into();
1047
1048        if let Err(err) = self.gm.write_plain(desc_addr, &desc) {
1049            tracelimit::warn_ratelimited!(
1050                error = &err as &dyn std::error::Error,
1051                "Could not write NvramCommandDescriptor into guest memory",
1052            );
1053        }
1054    }
1055
1056    async fn handle_nvram_command_inner(
1057        &mut self,
1058        desc_addr: u64,
1059        desc: uefi_specs::hyperv::nvram::NvramCommandDescriptor,
1060    ) -> Result<EfiStatus, GuestMemoryError> {
1061        use uefi_specs::hyperv::nvram::NvramCommand;
1062        use uefi_specs::hyperv::nvram::NvramVariableCommand;
1063
1064        let command_addr = desc_addr + size_of_val(&desc) as u64;
1065
1066        let (status, err) = match desc.command {
1067            NvramCommand::GET_VARIABLE => {
1068                let mut command: NvramVariableCommand = self.gm.read_plain(command_addr)?;
1069
1070                let name = if command.name_address.get() != 0 {
1071                    let mut buf = vec![0; command.name_bytes as usize];
1072                    self.gm
1073                        .read_at(command.name_address.into(), buf.as_mut_slice())?;
1074                    Some(buf)
1075                } else {
1076                    None
1077                };
1078
1079                let NvramResult(data, status, err) = self
1080                    .service
1081                    .nvram
1082                    .services
1083                    .uefi_get_variable(
1084                        name.as_deref(),
1085                        command.vendor_guid,
1086                        &mut command.attributes,
1087                        &mut command.data_bytes,
1088                        command.data_address.get() == 0,
1089                    )
1090                    .await;
1091
1092                // writeback updated command struct
1093                self.gm.write_plain(command_addr, &command)?;
1094
1095                // write any data to provided guest memory location
1096                // (bounds checking is performed within `nvram.get_variable`)
1097                if let Some(data) = data {
1098                    self.gm
1099                        .write_at(command.data_address.get(), data.as_bytes())?;
1100                }
1101
1102                (status, err)
1103            }
1104            NvramCommand::SET_VARIABLE => {
1105                let command: NvramVariableCommand = self.gm.read_plain(command_addr)?;
1106
1107                let name = if command.name_address.get() != 0 {
1108                    let mut buf = vec![0; command.name_bytes as usize];
1109                    self.gm
1110                        .read_at(command.name_address.into(), buf.as_mut_slice())?;
1111                    Some(buf)
1112                } else {
1113                    None
1114                };
1115
1116                let data = if command.data_address.get() != 0 {
1117                    let mut buf = vec![0; command.data_bytes as usize];
1118                    self.gm
1119                        .read_at(command.data_address.into(), buf.as_mut_slice())?;
1120                    Some(buf)
1121                } else {
1122                    None
1123                };
1124
1125                let NvramResult((), status, err) = self
1126                    .service
1127                    .nvram
1128                    .services
1129                    .uefi_set_variable(
1130                        name.as_deref(),
1131                        command.vendor_guid,
1132                        command.attributes,
1133                        command.data_bytes,
1134                        data,
1135                    )
1136                    .await;
1137
1138                (status, err)
1139            }
1140            NvramCommand::GET_FIRST_VARIABLE_NAME | NvramCommand::GET_NEXT_VARIABLE_NAME => {
1141                let mut command: NvramVariableCommand = self.gm.read_plain(command_addr)?;
1142
1143                let name = if desc.command == NvramCommand::GET_NEXT_VARIABLE_NAME {
1144                    if command.name_address.get() != 0 {
1145                        let mut buf = vec![0; command.name_bytes as usize];
1146                        self.gm
1147                            .read_at(command.name_address.into(), buf.as_mut_slice())?;
1148                        Some(buf)
1149                    } else {
1150                        None
1151                    }
1152                } else {
1153                    // If the command is GET_FIRST_VARIABLE_NAME, then we should
1154                    // ignore the name provided in the NvramVariableCommand
1155                    // struct, and just pass along a empty UTF-16 string to
1156                    // `get_next_variable`, which will fetch the first variable
1157                    // name (as specified by the official UEFI spec)
1158                    Some(vec![0, 0])
1159                };
1160
1161                let NvramResult(data, status, err) = self
1162                    .service
1163                    .nvram
1164                    .services
1165                    .uefi_get_next_variable(
1166                        &mut command.name_bytes,
1167                        name.as_deref(),
1168                        command.vendor_guid,
1169                    )
1170                    .await;
1171
1172                // write new name data to provided guest memory location
1173                // (bounds checking is performed within `nvram.get_next_variable`)
1174                if let Some((name, vendor)) = data {
1175                    command.vendor_guid = vendor;
1176
1177                    self.gm
1178                        .write_at(command.name_address.get(), name.as_bytes())?;
1179                }
1180
1181                // writeback updated command struct
1182                self.gm.write_at(command_addr, command.as_bytes())?;
1183
1184                (status, err)
1185            }
1186            NvramCommand::QUERY_INFO => (EfiStatus::UNSUPPORTED, None),
1187            NvramCommand::SIGNAL_RUNTIME => {
1188                use uefi_specs::hyperv::nvram::NvramSignalRuntimeCommand;
1189                let command: NvramSignalRuntimeCommand = self.gm.read_plain(command_addr)?;
1190
1191                if !command.flags.vsm_aware() {
1192                    if let Some(vsm) = &self.service.nvram.vsm_config {
1193                        tracelimit::info_ratelimited!("Revoking guest vsm");
1194                        vsm.revoke_guest_vsm()
1195                    }
1196                }
1197                self.service.nvram.services.exit_boot_services();
1198
1199                (EfiStatus::SUCCESS, None)
1200            }
1201            NvramCommand::DEBUG_STRING => {
1202                let command: uefi_specs::hyperv::nvram::NvramDebugStringCommand =
1203                    self.gm.read_plain(command_addr)?;
1204
1205                let mut data = vec![0u16; command.len as usize / 2];
1206                self.gm
1207                    .read_at(command.address.into(), data.as_mut_bytes())?;
1208
1209                tracing::trace!(
1210                    target: "uefi-nvram-guest-debug",
1211                    data = %String::from_utf16_lossy(&data),
1212                    "nvram guest debug",
1213                );
1214                (EfiStatus::SUCCESS, None)
1215            }
1216            command => {
1217                tracelimit::warn_ratelimited!(?command, "unknown nvram command");
1218                (EfiStatus::UNSUPPORTED, None)
1219            }
1220        };
1221
1222        // log any errors which may have occurred
1223        if let Some(err) = err {
1224            let err: &(dyn std::error::Error + 'static) = &err;
1225            tracelimit::warn_ratelimited!(
1226                command = ?desc.command,
1227                ?status,
1228                error = err,
1229                "nvram error"
1230            )
1231        }
1232
1233        if status != EfiStatus::SUCCESS {
1234            tracing::trace!(?status, "nvram status");
1235        }
1236
1237        Ok(status)
1238    }
1239}
1240
1241mod save_restore {
1242    use super::*;
1243    use vmcore::save_restore::RestoreError;
1244    use vmcore::save_restore::SaveError;
1245    use vmcore::save_restore::SaveRestore;
1246
1247    mod state {
1248        use crate::service::nvram::NvramSpecServices;
1249        use mesh::payload::Protobuf;
1250        use uefi_nvram_storage::VmmNvramStorage;
1251        use vmcore::save_restore::SaveRestore;
1252
1253        #[derive(Protobuf)]
1254        #[mesh(package = "firmware.uefi.nvram")]
1255        pub struct SavedState {
1256            #[mesh(1)]
1257            pub services: <NvramSpecServices<Box<dyn VmmNvramStorage>> as SaveRestore>::SavedState,
1258        }
1259    }
1260
1261    impl SaveRestore for NvramServices {
1262        type SavedState = state::SavedState;
1263
1264        fn save(&mut self) -> Result<Self::SavedState, SaveError> {
1265            let NvramServices {
1266                vsm_config: _,
1267                services,
1268            } = self;
1269
1270            let saved_state = state::SavedState {
1271                services: services.save()?,
1272            };
1273
1274            Ok(saved_state)
1275        }
1276
1277        fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
1278            let state::SavedState { services } = state;
1279
1280            self.services.restore(services)?;
1281
1282            Ok(())
1283        }
1284    }
1285}