diff --git a/Cargo.lock b/Cargo.lock index 6349b80..3883368 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,7 +346,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attest-measure" version = "0.0.1" -source = "git+https://github.com/easy-tee/attest.git?rev=8206cd19d9dcb1978d85a3d8dece06a3ee7a1206#8206cd19d9dcb1978d85a3d8dece06a3ee7a1206" +source = "git+https://github.com/easy-tee/attest.git?rev=12f1e29f6ea63ecc2f80f39c2c1f1172720bcf24#12f1e29f6ea63ecc2f80f39c2c1f1172720bcf24" dependencies = [ "attest-types", "authenticode", @@ -371,7 +371,7 @@ dependencies = [ [[package]] name = "attest-types" version = "0.0.1" -source = "git+https://github.com/easy-tee/attest.git?rev=8206cd19d9dcb1978d85a3d8dece06a3ee7a1206#8206cd19d9dcb1978d85a3d8dece06a3ee7a1206" +source = "git+https://github.com/easy-tee/attest.git?rev=12f1e29f6ea63ecc2f80f39c2c1f1172720bcf24#12f1e29f6ea63ecc2f80f39c2c1f1172720bcf24" dependencies = [ "parity-scale-codec", "serde", diff --git a/crates/attestation/Cargo.toml b/crates/attestation/Cargo.toml index 2a51288..bb0ba33 100644 --- a/crates/attestation/Cargo.toml +++ b/crates/attestation/Cargo.toml @@ -13,8 +13,8 @@ pccs = { workspace = true } mock-tdx = { workspace = true, optional = true } tokio = { workspace = true, features = ["fs", "rt", "rt-multi-thread"] } tokio-rustls = { workspace = true, default-features = false } -attest-types = { git = "https://github.com/easy-tee/attest.git", rev = "8206cd19d9dcb1978d85a3d8dece06a3ee7a1206" } -attest-measure = {git = "https://github.com/easy-tee/attest.git", rev = "8206cd19d9dcb1978d85a3d8dece06a3ee7a1206" } +attest-types = { git = "https://github.com/easy-tee/attest.git", rev = "12f1e29f6ea63ecc2f80f39c2c1f1172720bcf24" } +attest-measure = { git = "https://github.com/easy-tee/attest.git", rev = "12f1e29f6ea63ecc2f80f39c2c1f1172720bcf24" } anyhow = "1.0.100" pem-rfc7468 = { version = "0.7.0", features = ["std"] } @@ -51,6 +51,10 @@ rustls = { workspace = true, default-features = false, features = ["aws_lc_rs"] serde-saphyr = "0.0.22" +[build-dependencies] +attest-measure = { git = "https://github.com/easy-tee/attest.git", rev = "12f1e29f6ea63ecc2f80f39c2c1f1172720bcf24" } +serde_json = "1.0.145" + [features] default = [] diff --git a/crates/attestation/README.md b/crates/attestation/README.md index 2fe0194..213f4bb 100644 --- a/crates/attestation/README.md +++ b/crates/attestation/README.md @@ -262,21 +262,24 @@ The `measurements` format above specifies register values, so any change to platform-injected values (firmware, RAM size, disk count, ACPI tables) changes the expected register values even when the OS image is unchanged. -The `dcap_image_hashes` alternative allows you to specify the OS image's +The `dcap_image_hashes` alternative allows you to specify the OS image's boot-component hashes instead, and the verifier reconstructs the expected -register values from those hashes plus platform metadata fetched attest -verification time. The same policy record then matches the same OS images -across platform variants. +register values from those hashes plus platform metadata available at +attestation verification time. The same policy record then matches the same OS +images across platform variants. This can be done with the `attest measure` CLI from [Easy-TEE/attest](https://github.com/Easy-TEE/attest) which outputs five -hex-encoded SHA-384 values: +hex-encoded SHA-384 values and, for images using a recent systemd EFI stub, one +additional optional value: - `uki_authenticode` - authenticode hash of the UKI (unified kernel image) - `kernel_authenticode` - authenticode hash of the kernel binary - `cmdline_hash` - hash of the kernel command line - `initrd_hash` - hash of the initramfs - `gpt_disk_guid_hash` - hash derived from GPT partition GUIDs +- `pe_sections` - optional accumulated hash of the UKI PE sections measured by + recent systemd EFI stubs Example: @@ -284,7 +287,7 @@ Example: [ { "measurement_id": "flashbox-l1-v1.0.0", - "attestation_type": "gcp-tdx", + "attestation_type": "dcap-tdx", "dcap_image_hashes": { "uki_authenticode": "fcaceb6d87694746ba2d93a87ef4209f2a7629b7f400097b93241e80b9ec3e1e80f9a4cd8028e6a83f297ea5de8d9abc", "kernel_authenticode": "b6c5133268aa8b440509f3d53ee855a5cd3aeb6441eb109a9f27f14c43bce3e2383856df4af876501ceeb4c9a3b15f0c", @@ -298,17 +301,21 @@ Example: #### Supported attestation types for portable measurements -Portable policies currently only work with the `"gcp-tdx"` attestation type. -For GCP, the verifier fetches the platform firmware blob from Google's metadata -service (keyed by MRTD) and combines it with the image hashes to reconstruct the -expected registers. A `dcap_image_hashes` record with any other attestation type -is rejected when parsing from JSON. +Portable policies work with the `"dcap-tdx"` and `"gcp-tdx"` attestation types. +`"dcap-tdx"` accepts DCAP evidence from any platform, including GCP and +bare-metal TDX, while `"gcp-tdx"` restricts the record to GCP. For bare-metal +DCAP TDX, MRTD selects a bundled trusted OVMF firmware blob. The verifier uses +that blob and the platform metadata to reconstruct and check MRTD and RTMR0, in +addition to the image-dependent RTMR1 and RTMR2. An unknown bare-metal MRTD is +rejected. The accepted firmware assets and their provenance are documented in +[`assets/ovmf`](assets/ovmf/README.md). For GCP, the verifier instead fetches a +Google-endorsed platform firmware blob keyed by MRTD. The JSON object emitted directly by `attest measure portable` is also accepted as a measurement policy. Its optional `azure` PCR values and its `dcap` image -hashes are converted into Azure TDX and GCP TDX policy records respectively. -It can be supplied on its own or as an element of a policy array, including an -array mixed with records in the policy format described above: +hashes are converted into an Azure TDX record and a generic DCAP record +respectively. It can be supplied on its own or as an element of a policy array, +including an array mixed with records in the policy format described above: ```JSON { diff --git a/crates/attestation/assets/ovmf/COPYRIGHT b/crates/attestation/assets/ovmf/COPYRIGHT new file mode 100644 index 0000000..60366c5 --- /dev/null +++ b/crates/attestation/assets/ovmf/COPYRIGHT @@ -0,0 +1,375 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: edk2 +Source: git://github.com/tianocore/edk2.git, http://www.openssl.org/source/, + with .efi binary files removed from the source at package generation time. + See get-orig-source in debian/rules for details. + +Files: * +Copyright: 2019, TianoCore and contributors. All rights reserved. +License: BSD-2-Clause-Patent + +Files: BaseTools/Source/C/GenFw/elf*.h + BaseTools/Source/Python/sitecustomize.py EmulatorPkg/* + MdeModulePkg/Library/PeiDebugPrintHobLib/PeiDebugPrintHobLib.c + MdeModulePkg/Universal/Variable/RuntimeDxe/VariableDxe.c + MdePkg/Include/* MdePkg/Library/* +Copyright: 1996-1998 John D. Polstra + 2004-2013, Intel Corporation + 2006, Tristan Gingold + 2008-2012, Apple Inc. + 2011-2013, ARM Limited + 2013, Red Hat, Inc. +License: BSD-2-Clause-Patent + +Files: MdePkg/Library/MipiSysTLib/mipisyst/* +Copyright: 2018, MIPI Alliance, Inc. +License: BSD-3-Clause + +Files: MdePkg/Library/MipiSysTLib/mipisyst/library/include/mipi_syst/api.h +Copyright: 2018-2023, MIPI Alliance, Inc. +License: BSD-3-Clause + +Files: MdePkg/Library/MipiSysTLib/mipisyst/library/include/mipi_syst/message.h +Copyright: 2018-2023, MIPI Alliance, Inc. +License: BSD-3-Clause + +Files: MdePkg/Library/MipiSysTLib/mipisyst/library/include/mipi_syst.h.in +Copyright: 2018-2023, MIPI Alliance, Inc. +License: BSD-3-Clause + +Files: MdePkg/Library/MipiSysTLib/mipisyst/library/src/mipi_syst_api.c +Copyright: 2018-2023, MIPI Alliance, Inc. +License: BSD-3-Clause + +Files: MdePkg/Library/MipiSysTLib/mipisyst/library/test/unit/mipi_syst_sbd_test.cpp +Copyright: 2018-2023, MIPI Alliance, Inc. +License: BSD-3-Clause + +Files: MdePkg/Library/MipiSysTLib/mipisyst/library/src/mipi_syst_writer.c +Copyright: 2018-2023, MIPI Alliance, Inc. +License: BSD-3-Clause + +Files: OvmfPkg/* +Copyright: 2004-2019, Intel Corporation + 2008-2009, Apple Inc. + 2011, Andrei Warkentin + 2011-2012, Bei Guan + 2012-2013, Red Hat, Inc + 2013, ARM Ltd. +License: BSD-2-Clause-Patent + +Files: ArmPkg/* ArmPlatformPkg/* BaseTools/Source/C/Common/*PeCoff*.c + BaseTools/Source/C/GenFv/GenFvInternalLib.c + BaseTools/Source/C/GenFw/Elf64Convert.c + EmbeddedPkg/* +Copyright: 2011-2013, ARM Limited + 2008-2010, Apple Inc. + 2004-2013, Intel Corporation + 2009, Hewlett-Packard Company + 2011, Hewlett-Packard Corporation + 2003-2008 University of Illinois at Urbana-Champaign +License: BSD-2-Clause-Patent + +Files: SecurityPkg/DeviceSecurity/SpdmLib/libspdm/* +Copyright: 2021-2024, Contributing Member(s) of Distributed Management Task Force, Inc.. All rights reserved. +License: BSD-3-Clause + +Files: CryptoPkg/Library/OpensslLib/openssl/* +Copyright: 1998-2019 The OpenSSL Project + 1995-1998 Eric A. Young, Tim J. Hudson +License: OpenSSL + +Files: debian/tests/shell.py +Copyright: 2019 Canonical Ltd. +License: GPL-3 + +Files: debian/tests/secvar_update.py +Copyright: 2026 Cannonical Ltd. +License: GPL-2+ + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +Comment: + On Debian systems, the complete text of the Apache License, Version 2 + can be found in "/usr/share/common-licenses/Apache-2.0". + +License: BSD-2-Clause-Patent + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + . + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + . + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + . + Subject to the terms and conditions of this license, each copyright holder + and contributor hereby grants to those receiving rights under this license + a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except for failure to satisfy the conditions of this license) patent + license to make, have made, use, offer to sell, sell, import, and otherwise + transfer this software, where such license applies only to those patent + claims, already acquired or hereafter acquired, licensable by such copyright + holder or contributor that are necessarily infringed by: + . + (a) their Contribution(s) (the licensed copyrights of copyright holders and + non-copyrightable additions of contributors, in source or binary form) + alone; or + . + (b) combination of their Contribution(s) with the work of authorship to + which such Contribution(s) was added by such copyright holder or + contributor, if, at the time the Contribution is added, such addition + causes such combination to be necessarily infringed. The patent license + shall not apply to any other combinations which include the + Contribution. + . + Except as expressly stated above, no rights or licenses from any copyright + holder or contributor is granted under this license, whether expressly, by + implication, estoppel or otherwise. + . + DISCLAIMER + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +License: BSD-3-Clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + . + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + . + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + . + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +License: BSD-4-Clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by the NetBSD + Foundation, Inc. and its contributors. + 4. Neither the name of The NetBSD Foundation nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +License: GPL-2+ + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this package; if not, see . +Comment: + On Debian systems, the full text of the GNU General Public License + version 2 can be found in the file '/usr/share/common-licenses/GPL-2'. + +License: GPL-3 + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . + . + On Debian and Debian-based systems, the full text of the GNU General + Public License version 3 can be found in the file + `/usr/share/common-licenses/GPL-3'. + +License: OpenSSL + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + . + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + . + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + . + 3. All advertising materials mentioning features or use of this + software must display the following acknowledgment: + "This product includes software developed by the OpenSSL Project + for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + . + 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + endorse or promote products derived from this software without + prior written permission. For written permission, please contact + openssl-core@openssl.org. + . + 5. Products derived from this software may not be called "OpenSSL" + nor may "OpenSSL" appear in their names without prior written + permission of the OpenSSL Project. + . + 6. Redistributions of any form whatsoever must retain the following + acknowledgment: + "This product includes software developed by the OpenSSL Project + for use in the OpenSSL Toolkit (http://www.openssl.org/)" + . + THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + ==================================================================== + . + This product includes cryptographic software written by Eric Young + (eay@cryptsoft.com). This product includes software written by Tim + Hudson (tjh@cryptsoft.com). + . + This library is free for commercial and non-commercial use as long as + the following conditions are aheared to. The following conditions + apply to all code found in this distribution, be it the RC4, RSA, + lhash, DES, etc., code; not just the SSL code. The SSL documentation + included with this distribution is covered by the same copyright terms + except that the holder is Tim Hudson (tjh@cryptsoft.com). + . + Copyright remains Eric Young's, and as such any Copyright notices in + the code are not to be removed. + If this package is used in a product, Eric Young should be given attribution + as the author of the parts of the library used. + This can be in the form of a textual message at program startup or + in documentation (online or textual) provided with the package. + . + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + "This product includes cryptographic software written by + Eric Young (eay@cryptsoft.com)" + The word 'cryptographic' can be left out if the rouines from the library + being used are not cryptographic related :-). + 4. If you include any Windows specific code (or a derivative thereof) from + the apps directory (application code) you must include an acknowledgement: + "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + . + THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + . + The licence and distribution terms for any publically available version or + derivative of this code cannot be changed. i.e. this code cannot simply be + copied and put under another distribution licence + [including the GNU Public Licence.] + +License: ISC + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + . + THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS + ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE + CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR + PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + SOFTWARE. + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. diff --git a/crates/attestation/assets/ovmf/OVMF.inteltdx.2025.02-8ubuntu3.2.fd b/crates/attestation/assets/ovmf/OVMF.inteltdx.2025.02-8ubuntu3.2.fd new file mode 100644 index 0000000..7142ce1 Binary files /dev/null and b/crates/attestation/assets/ovmf/OVMF.inteltdx.2025.02-8ubuntu3.2.fd differ diff --git a/crates/attestation/assets/ovmf/README.md b/crates/attestation/assets/ovmf/README.md new file mode 100644 index 0000000..57b46f9 --- /dev/null +++ b/crates/attestation/assets/ovmf/README.md @@ -0,0 +1,47 @@ +# Trusted OVMF firmware + +Every `.fd` file in this directory is a verifier trust root for self-hosted TDX +portable measurement policies. Its filename identifies it for build errors and +provenance documentation. + +At build time, `build.rs` parses each complete firmware blob and checks its +computed MRTD for uniqueness. It emits a compact description containing only +MRTD, the configuration firmware volume hash, and the HOB template. Production +binaries embed that generated description, not the complete blob. + +## Ubuntu OVMF.inteltdx.fd 2025.02-8ubuntu3.2 + +- Asset: `OVMF.inteltdx.2025.02-8ubuntu3.2.fd` +- Ubuntu package: `ovmf-inteltdx` version `2025.02-8ubuntu3.2` +- Package URL: + +- Package SHA-256: + `5667e225cc5dedc7d14e9e572e19ed6c138179d79d7a6e9e3630f4beeb67a71b` +- Firmware path in package: `/usr/share/ovmf/OVMF.inteltdx.fd` +- Firmware SHA-256: + `c4cb73edc27a378abbb55add5f3f326a76ce59fc046fe390fb50dac55b70a6da` +- MRTD: + `ea216f26cc4ef0571dce29f0111c6dadfddcd86d032dbf990258456fe4d27799e8b72777fc05f532b1ebd597289d924d` +- Package copyright and license information: [`COPYRIGHT`](COPYRIGHT) + +## Fedora OVMF.inteltdx.fd 20260213-6.fc44 + +- Asset: `edk2-ovmf-20260213-6.fc44.noarch.OVMF.inteltdx.fd` +- Fedora package: `edk2-ovmf` version `20260213-6.fc44` +- Package URL: + +- Package SHA-256: + `0ab52ddd63208ca18073fb6357a3433d972f889d3f4d8da16049385949aa34e6` +- Firmware path in package: `/usr/share/edk2/ovmf/OVMF.inteltdx.fd` +- Firmware SHA-256: + `5f89870e13794d708e50ede0f7112c44e361773c751cb47c6819ece56552306a` +- MRTD: + `1b4be140773d8dad689732c1997ef06d89f435ea5b4c9de4115077ccd95ddfee94ac1a5bfd3e119b5b265e5b7a7d1617` +- Package license expression: Apache-2.0 AND (BSD-2-Clause OR + GPL-2.0-or-later) AND BSD-2-Clause-Patent AND BSD-4-Clause AND ISC AND + LicenseRef-Fedora-Public-Domain + +To add another accepted firmware, place the exact `.fd` provenance asset in +this directory and document its source and checksums below. The build discovers +it automatically and fails if the blob cannot be parsed or its computed MRTD +is already present. diff --git a/crates/attestation/assets/ovmf/edk2-ovmf-20260213-6.fc44.noarch.OVMF.inteltdx.fd b/crates/attestation/assets/ovmf/edk2-ovmf-20260213-6.fc44.noarch.OVMF.inteltdx.fd new file mode 100644 index 0000000..96e8410 Binary files /dev/null and b/crates/attestation/assets/ovmf/edk2-ovmf-20260213-6.fc44.noarch.OVMF.inteltdx.fd differ diff --git a/crates/attestation/build.rs b/crates/attestation/build.rs new file mode 100644 index 0000000..82362d1 --- /dev/null +++ b/crates/attestation/build.rs @@ -0,0 +1,44 @@ +//! Reads pinned trusted OVMF firmware to get [DcapFirmware] from them +//! without embedding the whole firmware blob in the binary. +use std::{collections::HashSet, env, fs, path::PathBuf}; + +use attest_measure::dcap::DcapFirmware; + +const FIRMWARE_DIR: &str = "assets/ovmf"; +const GENERATED_FIRMWARE: &str = "trusted-firmware.json"; + +fn main() { + println!("cargo:rerun-if-changed={FIRMWARE_DIR}"); + + let mut paths = fs::read_dir(FIRMWARE_DIR) + .expect("failed to read trusted firmware directory") + .map(|entry| entry.expect("failed to read trusted firmware directory entry").path()) + .filter(|path| path.extension().is_some_and(|extension| extension == "fd")) + .collect::>(); + paths.sort(); + assert!(!paths.is_empty(), "trusted firmware directory contains no .fd files"); + + let mut seen_mrtds = HashSet::new(); + let firmware = paths + .iter() + .map(|path| { + let name = path + .file_name() + .expect("trusted firmware path must have a filename") + .to_string_lossy(); + println!("cargo:rerun-if-changed={}", path.display()); + + let blob = fs::read(path) + .unwrap_or_else(|err| panic!("failed to read trusted firmware {name}: {err}")); + let firmware = DcapFirmware::from_blob(&blob, false) + .unwrap_or_else(|err| panic!("failed to parse trusted firmware {name}: {err}")); + assert!(seen_mrtds.insert(firmware.mrtd), "duplicate trusted MRTD for {name}"); + firmware + }) + .collect::>(); + + let output = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR must be set")) + .join(GENERATED_FIRMWARE); + fs::write(output, serde_json::to_vec(&firmware).expect("failed to serialize firmware")) + .expect("failed to write generated trusted firmware"); +} diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index 832822f..914d4c0 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -358,7 +358,11 @@ mod tests { .unwrap(); assert_eq!(async_measurements, sync_measurements); - measurement_policy.check_measurement(&async_measurements, None).unwrap(); + let platform_metadata = + crate::mock_platform_metadata(crate::AttestationType::DcapTdx).unwrap(); + measurement_policy + .check_measurement(&async_measurements, Some(&platform_metadata)) + .unwrap(); } // This specifically tests a quote which has outdated TCB level from Azure diff --git a/crates/attestation/src/gcp/firmware.rs b/crates/attestation/src/gcp/firmware.rs index 4aa56fe..b6d37ec 100644 --- a/crates/attestation/src/gcp/firmware.rs +++ b/crates/attestation/src/gcp/firmware.rs @@ -77,7 +77,6 @@ mod tests { use super::GcpFirmwareCache; use crate::{ - AttestationType, PlatformMetadata, dcap::{get_quote_input_data, verify_dcap_attestation_with_given_timestamp}, measurements::{ExpectedMeasurements, MeasurementPolicy, MeasurementRecord}, @@ -114,6 +113,7 @@ mod tests { gpt_disk_guid_hash: decode_dcap_hash( "488fa3f08aae01c1a46b497319e8a7d3b7335c9ff4f4d7fe6a3dd62c844b03de22157c0303be58f10e3152687778e68d", ), + pe_sections: None, } } @@ -134,6 +134,8 @@ mod tests { "0bb0afa008873bdc20dee0f741da7896c2bfeee94ae52e9bdbf94bc87c32d04a4b1f1d824490f1dae574ff6d4e4bb0b3", ), }), + dm_verity_boot: false, + smbios_handoff: None, } } @@ -167,7 +169,8 @@ mod tests { let measurement_policy = MeasurementPolicy { accepted_measurements: vec![MeasurementRecord { measurement_id: "gcp-tdx-portable-image-hashes".to_string(), - attestation_type: AttestationType::GcpTdx, + // The generic DCAP policy type accepts GCP DCAP evidence. + attestation_type: crate::AttestationType::DcapTdx, measurements: ExpectedMeasurements::Image(gcp_portable_image_hashes()), }], }; diff --git a/crates/attestation/src/gcp/provenance.rs b/crates/attestation/src/gcp/provenance.rs index 56f7786..166fc03 100644 --- a/crates/attestation/src/gcp/provenance.rs +++ b/crates/attestation/src/gcp/provenance.rs @@ -254,7 +254,7 @@ pub enum GcpProvenanceError { #[cfg(test)] mod tests { use std::{ - io::{Read as _, Write as _}, + io::Write as _, net::SocketAddr, sync::mpsc, thread, diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index cb67f63..dab9108 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -8,6 +8,7 @@ pub mod azure; pub mod dcap; mod gcp; pub mod measurements; +mod trusted_firmware; #[cfg(test)] use std::sync::OnceLock; use std::{ @@ -721,6 +722,8 @@ pub fn mock_platform_metadata( ram_bytes: 0, num_disks: 0, acpi: None, + dm_verity_boot: false, + smbios_handoff: None, }) } diff --git a/crates/attestation/src/measurements.rs b/crates/attestation/src/measurements.rs index 8dd7e9c..1839509 100644 --- a/crates/attestation/src/measurements.rs +++ b/crates/attestation/src/measurements.rs @@ -20,6 +20,7 @@ use crate::{ AttestationType, dcap::DcapVerificationError, gcp::{GcpFirmwareCache, fetch_firmware}, + trusted_firmware::firmware_for_mrtd, }; /// Represents the measurement register types in a TDX quote @@ -211,6 +212,7 @@ impl fmt::Debug for AzureHexDebug<'_> { } /// Expected measurement values for policy enforcement +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq)] pub enum ExpectedMeasurements { Image(DcapImageHashes), @@ -436,20 +438,21 @@ impl MeasurementPolicy { platform_metadata: Option<&PlatformMetadata>, known_gcp_firmware: Option<&GcpFirmwareCache>, ) -> Result<(), AttestationError> { - let attestation_type = platform_metadata - .map(|metadata| metadata.attestation_type.into()) - .unwrap_or_else(|| match measurements { - MultiMeasurements::Dcap(_) => AttestationType::DcapTdx, - MultiMeasurements::Azure(_) => AttestationType::AzureTdx, - MultiMeasurements::NoAttestation => AttestationType::None, - }); + let actual_attestation_type = match measurements { + MultiMeasurements::NoAttestation => Some(AttestationType::None), + MultiMeasurements::Dcap(_) | MultiMeasurements::Azure(_) => { + platform_metadata.map(|metadata| metadata.attestation_type.into()) + } + }; + + let Some(actual_attestation_type) = actual_attestation_type else { + return Err(AttestationError::MeasurementsNotAccepted); + }; if self.accepted_measurements.iter().any(|measurement_record| match measurements { + _ if !measurement_record.attestation_type.accepts(actual_attestation_type) => false, MultiMeasurements::Dcap(dcap_measurements) => match &measurement_record.measurements { ExpectedMeasurements::Dcap(expected) => { - if !measurement_record.attestation_type.accepts(attestation_type) { - return false; - } // All measurements in our policy must be given and must match for (k, v) in expected.iter() { let actual_value = dcap_measurements.get(k); @@ -459,21 +462,15 @@ impl MeasurementPolicy { } true } - ExpectedMeasurements::Image(image_hashes) => { - measurement_record.attestation_type.accepts(attestation_type) && - compare_portable_dcap_measurement( - image_hashes, - dcap_measurements, - platform_metadata, - known_gcp_firmware, - ) - } + ExpectedMeasurements::Image(image_hashes) => compare_portable_dcap_measurement( + image_hashes, + dcap_measurements, + platform_metadata, + known_gcp_firmware, + ), ExpectedMeasurements::Azure(_) | ExpectedMeasurements::NoAttestation => false, }, MultiMeasurements::Azure(azure_measurements) => { - if !measurement_record.attestation_type.accepts(attestation_type) { - return false; - } if let ExpectedMeasurements::Azure(expected) = &measurement_record.measurements { for (k, v) in expected.iter() { match azure_measurements.get(k) { @@ -486,11 +483,7 @@ impl MeasurementPolicy { false } MultiMeasurements::NoAttestation => { - measurement_record.attestation_type.accepts(attestation_type) && - matches!( - measurement_record.measurements, - ExpectedMeasurements::NoAttestation - ) + matches!(measurement_record.measurements, ExpectedMeasurements::NoAttestation) } }) { Ok(()) @@ -628,7 +621,7 @@ impl MeasurementPolicy { measurement_policy.push(MeasurementRecord { measurement_id: String::new(), - attestation_type: AttestationType::GcpTdx, + attestation_type: AttestationType::DcapTdx, measurements: ExpectedMeasurements::Image(portable.dcap), }); } @@ -690,12 +683,10 @@ impl MeasurementPolicy { } }, (None, Some(image_hashes)) => match attestation_type { - // Currently only GCP is supported for portable measurement policy - but support - // for other types is planned - AttestationType::GcpTdx => ExpectedMeasurements::Image(image_hashes), - AttestationType::DcapTdx | - AttestationType::None | - AttestationType::AzureTdx => { + AttestationType::DcapTdx | AttestationType::GcpTdx => { + ExpectedMeasurements::Image(image_hashes) + } + AttestationType::None | AttestationType::AzureTdx => { return Err( MeasurementFormatError::DcapImageHashesUnsupportedAttestationType( record.attestation_type, @@ -750,7 +741,8 @@ pub(crate) fn compare_portable_dcap_measurement( return false; }; - // On GCP, fetch the firmware associated with the MRTD + // Trusted firmware is needed to reconstruct MRTD and RTMR0. GCP firmware + // is fetched with a signed endorsement; self-hosted firmware is bundled. let firmware = match platform_metadata.attestation_type { ImageAttestationType::GcpTdx => { let mrtd = dcap_measurements.get(&DcapMeasurementRegister::MRTD); @@ -770,13 +762,18 @@ pub(crate) fn compare_portable_dcap_measurement( } } } - // These may be supported in the future but currently regarded as too - // experimental to work with 'portable' measurement policies ImageAttestationType::SelfHostedTdx => { - warn!( - "Attempting to match portable measurement policy with bare metal TDX - not yet supported" - ); - return false; + let mrtd = *dcap_measurements.get(&DcapMeasurementRegister::MRTD); + match firmware_for_mrtd(mrtd) { + Some(firmware) => Some(firmware), + None => { + warn!( + "Could not match image hash measurement - self-hosted MRTD {} is not trusted", + hex::encode(mrtd) + ); + return false; + } + } } ImageAttestationType::AzureTdx => { warn!( @@ -796,23 +793,15 @@ pub(crate) fn compare_portable_dcap_measurement( } }; - if let Some(expected_mrtd) = expected_measurements.mrtd { - if dcap_measurements.get(&DcapMeasurementRegister::MRTD) != &expected_mrtd { - return false; - } - } else { - // This will only be the case with SelfHostedTdx which currently would - // already bail with the check above + if expected_measurements.mrtd.is_some_and(|expected_mrtd| { + dcap_measurements.get(&DcapMeasurementRegister::MRTD) != &expected_mrtd + }) { return false; } - if let Some(expected_rtmr0) = expected_measurements.rtmr0 { - if dcap_measurements.get(&DcapMeasurementRegister::RTMR0) != &expected_rtmr0 { - return false; - } - } else { - // This will only be the case with SelfHostedTdx which currently would - // already bail with the check above + if expected_measurements.rtmr0.is_some_and(|expected_rtmr0| { + dcap_measurements.get(&DcapMeasurementRegister::RTMR0) != &expected_rtmr0 + }) { return false; } @@ -879,11 +868,23 @@ mod tests { use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; use super::*; + use crate::trusted_firmware::any_trusted_firmware; fn test_dcap_measurements(mrtd: [u8; 48], rtmr0: [u8; 48]) -> MultiMeasurements { MultiMeasurements::Dcap(DcapMeasurements::new(mrtd, rtmr0, [0u8; 48], [0u8; 48], [0u8; 48])) } + fn self_hosted_platform_metadata() -> PlatformMetadata { + PlatformMetadata { + attestation_type: ImageAttestationType::SelfHostedTdx, + ram_bytes: 0, + num_disks: 0, + acpi: None, + dm_verity_boot: false, + smbios_handoff: None, + } + } + /// MRTD from the pinned GCP firmware snapshot test asset const GCP_FIRMWARE_MRTD: &str = "feb7486608382c1ff0e15b4648ddc0acea6ca974eb53e3529f4c4bd5ffbaa20bf335cb75965cea65fe473aed9647c162"; @@ -993,7 +994,9 @@ mod tests { let allowed_attestation_type = MeasurementPolicy::from_file("test-assets/measurements_2.json".into()).await.unwrap(); - allowed_attestation_type.check_measurement(&mock_dcap_measurements(), None).unwrap(); + allowed_attestation_type + .check_measurement(&mock_dcap_measurements(), Some(&self_hosted_platform_metadata())) + .unwrap(); // Will not match another attestation type assert!(matches!( @@ -1013,6 +1016,8 @@ mod tests { ram_bytes: 0, num_disks: 0, acpi: None, + dm_verity_boot: false, + smbios_handoff: None, }; policy.check_measurement(&measurements, Some(&gcp_metadata)).unwrap(); @@ -1034,6 +1039,8 @@ mod tests { ram_bytes: 0, num_disks: 0, acpi: None, + dm_verity_boot: false, + smbios_handoff: None, }; policy.check_measurement(&measurements, Some(&gcp_metadata)).unwrap(); @@ -1062,6 +1069,7 @@ mod tests { gpt_disk_guid_hash: decode_hash( "180bac1af9c35cc15e909623c005289539b4da2840d9c9b658fd4968ea4f03e0159402d03da1afc9035e0db30804e282", ), + pe_sections: None, }; let policy = MeasurementPolicy { accepted_measurements: vec![MeasurementRecord { @@ -1075,6 +1083,8 @@ mod tests { ram_bytes: 4 * 1024 * 1024 * 1024, num_disks: 1, acpi: Some(AcpiHashes { loader: [0x11; 48], rsdp: [0x22; 48], tables: [0x33; 48] }), + dm_verity_boot: false, + smbios_handoff: None, }; let firmware = gcp_firmware_fixture(); let expected_measurements = @@ -1091,6 +1101,87 @@ mod tests { policy.check_measurement(&measurements, Some(&platform_metadata)).unwrap(); } + #[test] + fn test_bare_metal_image_hash_policy_checks_image_registers() { + let image_hashes = DcapImageHashes { + uki_authenticode: [0x11; 48], + kernel_authenticode: [0x22; 48], + cmdline_hash: [0x33; 48], + initrd_hash: [0x44; 48], + gpt_disk_guid_hash: [0x55; 48], + pe_sections: Some([0x66; 48]), + }; + let platform_metadata = PlatformMetadata { + attestation_type: ImageAttestationType::SelfHostedTdx, + ram_bytes: 4 * 1024 * 1024 * 1024, + num_disks: 0, + acpi: Some(AcpiHashes { loader: [0x77; 48], rsdp: [0x88; 48], tables: [0x99; 48] }), + dm_verity_boot: false, + smbios_handoff: None, + }; + let firmware = any_trusted_firmware(); + let expected = + expected_dcap_registers(&image_hashes, &platform_metadata, Some(&firmware)).unwrap(); + let policy = MeasurementPolicy { + accepted_measurements: vec![MeasurementRecord { + measurement_id: "bare-metal-image-hash-policy".to_string(), + attestation_type: AttestationType::DcapTdx, + measurements: ExpectedMeasurements::Image(image_hashes), + }], + }; + let measurements = MultiMeasurements::Dcap(DcapMeasurements::new( + expected.mrtd.unwrap(), + expected.rtmr0.unwrap(), + expected.rtmr1, + expected.rtmr2, + [0xcc; 48], + )); + + policy.check_measurement(&measurements, Some(&platform_metadata)).unwrap(); + + assert!(matches!( + policy.check_measurement(&measurements, None), + Err(AttestationError::MeasurementsNotAccepted) + )); + + let ExpectedMeasurements::Image(image_hashes) = + &policy.accepted_measurements[0].measurements + else { + unreachable!(); + }; + let gcp_policy = MeasurementPolicy { + accepted_measurements: vec![MeasurementRecord { + measurement_id: "gcp-image-hash-policy".to_string(), + attestation_type: AttestationType::GcpTdx, + measurements: ExpectedMeasurements::Image(image_hashes.clone()), + }], + }; + assert!(matches!( + gcp_policy.check_measurement(&measurements, Some(&platform_metadata)), + Err(AttestationError::MeasurementsNotAccepted) + )); + + let mut unknown_firmware = measurements.clone(); + let MultiMeasurements::Dcap(dcap) = &mut unknown_firmware else { + unreachable!(); + }; + dcap.mrtd = [0xaa; 48]; + assert!(matches!( + policy.check_measurement(&unknown_firmware, Some(&platform_metadata)), + Err(AttestationError::MeasurementsNotAccepted) + )); + + let mut wrong_measurements = measurements.clone(); + let MultiMeasurements::Dcap(dcap) = &mut wrong_measurements else { + unreachable!(); + }; + dcap.rtmr2[0] ^= 1; + assert!(matches!( + policy.check_measurement(&wrong_measurements, Some(&platform_metadata)), + Err(AttestationError::MeasurementsNotAccepted) + )); + } + #[tokio::test] async fn test_buildernet_measurements() { // Refresh this fixture explicitly with: @@ -1188,28 +1279,32 @@ mod tests { async fn test_parse_image_hash_policy() { let json = r#"[ { - "measurement_id": "gcp-image-hash-example", - "attestation_type": "gcp-tdx", + "measurement_id": "bare-metal-image-hash-example", + "attestation_type": "dcap-tdx", "dcap_image_hashes": { "uki_authenticode": "fcaceb6d87694746ba2d93a87ef4209f2a7629b7f400097b93241e80b9ec3e1e80f9a4cd8028e6a83f297ea5de8d9abc", "kernel_authenticode": "b6c5133268aa8b440509f3d53ee855a5cd3aeb6441eb109a9f27f14c43bce3e2383856df4af876501ceeb4c9a3b15f0c", "cmdline_hash": "e03b89abf354a38976537b7a9138fd312e4cbf73b61eebc44086491701b1d167b9f6cb97a922325866c93e0834723d87", "initrd_hash": "a5b3d4742045e7d08aa19953c35098e784826b01a84f60568fa69f1a848dafd96ec98b8df616d6142779c9b97318166b", - "gpt_disk_guid_hash": "180bac1af9c35cc15e909623c005289539b4da2840d9c9b658fd4968ea4f03e0159402d03da1afc9035e0db30804e282" + "gpt_disk_guid_hash": "180bac1af9c35cc15e909623c005289539b4da2840d9c9b658fd4968ea4f03e0159402d03da1afc9035e0db30804e282", + "pe_sections": "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" } } ]"#; let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); assert_eq!(policy.accepted_measurements.len(), 1); - assert!(matches!( - policy.accepted_measurements[0].measurements, - ExpectedMeasurements::Image(_) - )); + assert_eq!(policy.accepted_measurements[0].attestation_type, AttestationType::DcapTdx); + let ExpectedMeasurements::Image(image_hashes) = + &policy.accepted_measurements[0].measurements + else { + panic!("expected portable DCAP image hashes"); + }; + assert_eq!(image_hashes.pe_sections, Some([0x11; 48])); } /// The object emitted by `attest measure portable` is accepted directly - /// and converted into Azure and GCP-compatible policy records. + /// and converted into Azure and DCAP-compatible policy records. #[test] fn test_parse_attest_measure_portable_output() { let json = r#"{ @@ -1233,6 +1328,7 @@ mod tests { let azure = &policy.accepted_measurements[0]; assert!(azure.measurement_id.is_empty()); + assert_eq!(azure.attestation_type, AttestationType::AzureTdx); let ExpectedMeasurements::Azure(registers) = &azure.measurements else { panic!("expected Azure measurements"); }; @@ -1245,6 +1341,7 @@ mod tests { let dcap = &policy.accepted_measurements[1]; assert!(dcap.measurement_id.is_empty()); + assert_eq!(dcap.attestation_type, AttestationType::DcapTdx); let ExpectedMeasurements::Image(image_hashes) = &dcap.measurements else { panic!("expected portable DCAP image hashes"); }; @@ -1274,6 +1371,7 @@ mod tests { let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); assert_eq!(policy.accepted_measurements.len(), 1); + assert_eq!(policy.accepted_measurements[0].attestation_type, AttestationType::DcapTdx); assert!(matches!( policy.accepted_measurements[0].measurements, ExpectedMeasurements::Image(_) @@ -1459,18 +1557,19 @@ mod tests { ]"#; let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); + let platform_metadata = self_hosted_platform_metadata(); // First value should match let measurements1 = test_dcap_measurements([0u8; 48], [0u8; 48]); - assert!(policy.check_measurement(&measurements1, None).is_ok()); + assert!(policy.check_measurement(&measurements1, Some(&platform_metadata)).is_ok()); // Second value should also match let measurements2 = test_dcap_measurements([0x11u8; 48], [0u8; 48]); - assert!(policy.check_measurement(&measurements2, None).is_ok()); + assert!(policy.check_measurement(&measurements2, Some(&platform_metadata)).is_ok()); // Different value should not match let measurements3 = test_dcap_measurements([0x22u8; 48], [0u8; 48]); - assert!(policy.check_measurement(&measurements3, None).is_err()); + assert!(policy.check_measurement(&measurements3, Some(&platform_metadata)).is_err()); } #[tokio::test] @@ -1544,18 +1643,19 @@ mod tests { ]"#; let policy = MeasurementPolicy::from_json_bytes(json.as_bytes().to_vec()).unwrap(); + let platform_metadata = self_hosted_platform_metadata(); // Both match (single + first of any) let measurements1 = test_dcap_measurements([0u8; 48], [0x11u8; 48]); - assert!(policy.check_measurement(&measurements1, None).is_ok()); + assert!(policy.check_measurement(&measurements1, Some(&platform_metadata)).is_ok()); // Both match (single + second of any) let measurements2 = test_dcap_measurements([0u8; 48], [0x22u8; 48]); - assert!(policy.check_measurement(&measurements2, None).is_ok()); + assert!(policy.check_measurement(&measurements2, Some(&platform_metadata)).is_ok()); // Single matches but any doesn't let measurements3 = test_dcap_measurements([0u8; 48], [0x33u8; 48]); - assert!(policy.check_measurement(&measurements3, None).is_err()); + assert!(policy.check_measurement(&measurements3, Some(&platform_metadata)).is_err()); } #[tokio::test] diff --git a/crates/attestation/src/trusted_firmware.rs b/crates/attestation/src/trusted_firmware.rs new file mode 100644 index 0000000..0ec6b2e --- /dev/null +++ b/crates/attestation/src/trusted_firmware.rs @@ -0,0 +1,44 @@ +//! Trusted self-hosted TDX firmware used to reconstruct MRTD and RTMR0. + +use std::collections::HashMap; + +use attest_measure::dcap::DcapFirmware; +use once_cell::sync::Lazy; + +// Read the JSON created by build.rs from included trusted firmware blobs +static TRUSTED_FIRMWARE_BY_MRTD: Lazy> = Lazy::new(|| { + let firmware: Vec = + serde_json::from_str(include_str!(concat!(env!("OUT_DIR"), "/trusted-firmware.json"))) + .expect("build script generated invalid trusted firmware"); + firmware.into_iter().map(|firmware| (firmware.mrtd, firmware)).collect() +}); + +/// Look up a trusted self-hosted firmware image by the MRTD in a TDX quote. +pub(crate) fn firmware_for_mrtd(mrtd: [u8; 48]) -> Option { + TRUSTED_FIRMWARE_BY_MRTD.get(&mrtd).cloned() +} + +/// Get some arbitrary trusted firmware to use in tests +#[cfg(test)] +pub(crate) fn any_trusted_firmware() -> DcapFirmware { + TRUSTED_FIRMWARE_BY_MRTD.values().next().expect("at least one trusted firmware").clone() +} + +#[cfg(test)] +mod tests { + use super::{TRUSTED_FIRMWARE_BY_MRTD, firmware_for_mrtd}; + + #[test] + fn generated_firmware_is_indexed_by_mrtd() { + assert!(!TRUSTED_FIRMWARE_BY_MRTD.is_empty()); + for (mrtd, expected) in TRUSTED_FIRMWARE_BY_MRTD.iter() { + let firmware = firmware_for_mrtd(*mrtd).expect("firmware should be trusted"); + assert_eq!(firmware.mrtd, expected.mrtd); + } + } + + #[test] + fn unknown_mrtd_is_not_trusted() { + assert!(firmware_for_mrtd([0xff; 48]).is_none()); + } +}