├── .gitignore ├── az-cvm-vtpm ├── LICENSE ├── az-snp-vtpm │ ├── LICENSE │ ├── test │ │ ├── certs.pem │ │ └── hcl-report-snp.bin │ ├── example │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── src │ │ ├── imds.rs │ │ ├── amd_kds.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── certs.rs │ │ └── report.rs │ ├── Cargo.toml │ ├── Makefile │ ├── tests │ │ └── integration_tests.rs │ ├── README.md │ └── arm │ │ └── cvm.bicep ├── az-tdx-vtpm │ ├── LICENSE │ ├── test │ │ ├── certs.pem │ │ └── hcl-report-tdx.bin │ ├── src │ │ ├── main.rs │ │ ├── report.rs │ │ ├── imds.rs │ │ └── lib.rs │ ├── Cargo.toml │ ├── README.md │ └── tests │ │ └── integration_tests.rs ├── .gitignore ├── test │ ├── quote.bin │ ├── hcl-report-snp.bin │ ├── hcl-report-tdx.bin │ ├── akpub.pem │ ├── var-data.bin │ └── certs.pem ├── src │ ├── lib.rs │ ├── tdx │ │ └── mod.rs │ ├── vtpm │ │ ├── verify.rs │ │ └── mod.rs │ └── hcl │ │ └── mod.rs ├── README.md └── Cargo.toml ├── Readme.md ├── .github ├── dependabot.yml └── workflows │ ├── security.yml │ ├── e2e.yaml │ └── rust.yml ├── LICENSE └── docs └── end-to-end-test.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /az-cvm-vtpm/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /az-cvm-vtpm/az-tdx-vtpm/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/test/certs.pem: -------------------------------------------------------------------------------- 1 | ../../test/certs.pem -------------------------------------------------------------------------------- /az-cvm-vtpm/az-tdx-vtpm/test/certs.pem: -------------------------------------------------------------------------------- 1 | ../../test/certs.pem -------------------------------------------------------------------------------- /az-cvm-vtpm/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | arm/*.json 3 | Cargo.lock 4 | *.swp 5 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/test/hcl-report-snp.bin: -------------------------------------------------------------------------------- 1 | ../../test/hcl-report-snp.bin -------------------------------------------------------------------------------- /az-cvm-vtpm/az-tdx-vtpm/test/hcl-report-tdx.bin: -------------------------------------------------------------------------------- 1 | ../../test/hcl-report-tdx.bin -------------------------------------------------------------------------------- /az-cvm-vtpm/test/quote.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinvolk/azure-cvm-tooling/HEAD/az-cvm-vtpm/test/quote.bin -------------------------------------------------------------------------------- /az-cvm-vtpm/test/hcl-report-snp.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinvolk/azure-cvm-tooling/HEAD/az-cvm-vtpm/test/hcl-report-snp.bin -------------------------------------------------------------------------------- /az-cvm-vtpm/test/hcl-report-tdx.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinvolk/azure-cvm-tooling/HEAD/az-cvm-vtpm/test/hcl-report-tdx.bin -------------------------------------------------------------------------------- /az-cvm-vtpm/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | pub mod hcl; 5 | pub mod tdx; 6 | #[cfg(feature = "tpm")] 7 | pub mod vtpm; 8 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Azure CVM Tooling 2 | 3 | Assorted tools and libraries to use with [Azure CVMs](https://azure.microsoft.com/en-us/solutions/confidential-compute/). 4 | 5 | ## az-cvm-vtpm 6 | 7 | Attestation for Azure Confidential Virtual Machines 8 | -------------------------------------------------------------------------------- /az-cvm-vtpm/README.md: -------------------------------------------------------------------------------- 1 | # az-cvm-vtpm 2 | 3 | Attestation for Azure Confidential Virtual Machines 4 | 5 | ## az-snp-vtpm 6 | 7 | Attestation Library for Azure AMD SEV-SNP Confidential Virtual Machines. 8 | 9 | ## az-tdx-vtpm 10 | 11 | Attestation Library for Azure Intel TDX Confidential Virtual Machines. 12 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "snp-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | az-snp-vtpm.path = "../" 10 | openssl.workspace = true 11 | -------------------------------------------------------------------------------- /az-cvm-vtpm/test/akpub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh/zPnAAAQVXPyGWeKFj0 3 | UmbmtufZK7yeoeLZn0GbA0VVyjh+BPybG/ZrsgXFF7aQsOyaW2OLaKeeFzXqy6v3 4 | kCZRONtxLOXWlTSK2ytRrXvzJnjF86gqD4z9VkJ5GyWhPNI4P67+eJKu8iaHmSrP 5 | WKAVJbJ9+YaZwP48E3Q0wQ1rZjRT8VVJNrjCAT0gRivoEqN5GZMrwIeCjddvs13/ 6 | A4pBc6+Na7ojQ8ljmF6I/dV9dvJWi/GsQXNgjjSjw2SgYdyuZts7syyuKx42idCJ 7 | qxJb6Zmmjb6VWfoOo/cr5ZvjSeQFaBEVuAgP47fYLlhVjIQddKM/IDxW6fovr8OO 8 | YwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/az-cvm-vtpm" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | push: 7 | paths: 8 | - '**/Cargo.toml' 9 | - '**/Cargo.lock' 10 | 11 | jobs: 12 | az-cvm-vtpm: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | # https://github.com/actions/checkout/issues/1430 19 | - name: Move az-cvm-vtpm/* to root 20 | run: mv az-cvm-vtpm/* . 21 | 22 | - uses: rustsec/audit-check@v1.4.1 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-tdx-vtpm/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | use az_tdx_vtpm::{hcl, imds, tdx, vtpm}; 5 | use std::error::Error; 6 | 7 | fn main() -> Result<(), Box> { 8 | let bytes = vtpm::get_report()?; 9 | let hcl_report = hcl::HclReport::new(bytes)?; 10 | let var_data_hash = hcl_report.var_data_sha256(); 11 | let ak_pub = hcl_report.ak_pub()?; 12 | 13 | let td_report: tdx::TdReport = hcl_report.try_into()?; 14 | assert!(var_data_hash == td_report.report_mac.reportdata[..32]); 15 | println!("vTPM AK_pub: {ak_pub:?}"); 16 | let td_quote_bytes = imds::get_td_quote(&td_report)?; 17 | std::fs::write("td_quote.bin", td_quote_bytes)?; 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yaml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | build-and-run-example: 9 | runs-on: [ "self-hosted", "azure-cvm", "ubuntu-2204" ] 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | profile: minimal 16 | toolchain: stable 17 | 18 | - name: Install dependencies 19 | run: | 20 | sudo apt-get update 21 | sudo apt-get install -y build-essential libssl-dev pkg-config libtss2-dev 22 | 23 | - name: Build example project 24 | working-directory: ./az-snp-vtpm 25 | run: cargo build --release -p example 26 | 27 | - name: Run example project 28 | working-directory: ./az-snp-vtpm 29 | run: sudo ./target/release/example 30 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-tdx-vtpm/src/report.rs: -------------------------------------------------------------------------------- 1 | use crate::hcl::{self, HclReport}; 2 | use crate::tdx::TdReport; 3 | use crate::vtpm; 4 | use bincode::deserialize; 5 | use thiserror::Error; 6 | 7 | #[derive(Error, Debug)] 8 | pub enum ReportError { 9 | #[error("deserialization error")] 10 | Parse(#[from] Box), 11 | #[error("vTPM error")] 12 | Vtpm(#[from] vtpm::ReportError), 13 | #[error("HCL error")] 14 | Hcl(#[from] hcl::HclError), 15 | } 16 | 17 | /// Parse raw bytes into TdReport 18 | pub fn parse(bytes: &[u8]) -> Result { 19 | deserialize::(bytes).map_err(|e| e.into()) 20 | } 21 | 22 | /// Fetch TdReport from vTPM and parse it 23 | pub fn get_report() -> Result { 24 | let bytes = vtpm::get_report()?; 25 | let hcl_report = HclReport::new(bytes)?; 26 | let td_report = hcl_report.try_into()?; 27 | Ok(td_report) 28 | } 29 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/src/imds.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | use crate::HttpError; 5 | use serde::Deserialize; 6 | 7 | const IMDS_CERT_URL: &str = "http://169.254.169.254/metadata/THIM/amd/certification"; 8 | 9 | /// PEM encoded VCEK certificate and AMD certificate chain. 10 | #[derive(Deserialize)] 11 | pub struct Certificates { 12 | #[serde(rename = "vcekCert")] 13 | pub vcek: String, 14 | #[serde(rename = "certificateChain")] 15 | pub amd_chain: String, 16 | } 17 | 18 | /// Get the VCEK certificate and the certificate chain from the Azure IMDS. 19 | /// **Note:** this can only be called from a Confidential VM. 20 | pub fn get_certs() -> Result { 21 | let res: Certificates = ureq::get(IMDS_CERT_URL) 22 | .set("Metadata", "true") 23 | .call() 24 | .map_err(Box::new)? 25 | .into_json()?; 26 | Ok(res) 27 | } 28 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-tdx-vtpm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "az-tdx-vtpm" 3 | version = "0.7.4" 4 | edition = "2021" 5 | repository = "https://github.com/kinvolk/azure-cvm-tooling/" 6 | license = "MIT" 7 | keywords = ["azure", "tpm", "tdx"] 8 | categories = ["cryptography", "virtualization"] 9 | description = "vTPM based TDX attestation for Azure Confidential VMs" 10 | 11 | [lib] 12 | path = "src/lib.rs" 13 | 14 | [[bin]] 15 | name = "tdx-vtpm" 16 | path = "src/main.rs" 17 | 18 | [dependencies] 19 | az-cvm-vtpm = { path = "..", version = "0.7.4" } 20 | base64-url = "3.0.0" 21 | bincode.workspace = true 22 | serde.workspace = true 23 | serde_json.workspace = true 24 | thiserror.workspace = true 25 | ureq.workspace = true 26 | zerocopy.workspace = true 27 | 28 | [dev-dependencies] 29 | openssl.workspace = true 30 | hex.workspace = true 31 | 32 | [features] 33 | default = ["attester", "verifier"] 34 | attester = [] 35 | verifier = ["az-cvm-vtpm/verifier"] 36 | integration_test =[] 37 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "az-snp-vtpm" 3 | version = "0.7.4" 4 | edition = "2021" 5 | repository = "https://github.com/kinvolk/azure-cvm-tooling/" 6 | license = "MIT" 7 | keywords = ["azure", "tpm", "sev-snp"] 8 | categories = ["cryptography", "virtualization"] 9 | description = "vTPM based SEV-SNP attestation for Azure Confidential VMs" 10 | 11 | [lib] 12 | path = "src/lib.rs" 13 | 14 | [[bin]] 15 | name = "snp-vtpm" 16 | path = "src/main.rs" 17 | required-features = ["attester", "verifier"] 18 | 19 | [dependencies] 20 | az-cvm-vtpm = { path = "..", version = "0.7.4" } 21 | bincode.workspace = true 22 | clap.workspace = true 23 | openssl = { workspace = true, optional = true } 24 | serde.workspace = true 25 | sev.workspace = true 26 | thiserror.workspace = true 27 | ureq.workspace = true 28 | 29 | [dev-dependencies] 30 | serde_json.workspace = true 31 | hex.workspace = true 32 | 33 | [features] 34 | default = ["attester", "verifier"] 35 | attester = [] 36 | verifier = ["az-cvm-vtpm/openssl", "openssl", "ureq/tls"] 37 | integration_test = [] 38 | -------------------------------------------------------------------------------- /az-cvm-vtpm/test/var-data.bin: -------------------------------------------------------------------------------- 1 | {"keys":[{"kid":"HCLAkPub","key_ops":["sign"],"kty":"RSA","e":"AQAB","n":"tegZ0wABCCWLhBK-LqnHoHW16fe3IN9MHs9bvdN9BpMnva_OqvqoFw4lp0lrZoMTi51C1jzjkRIpo2ICUhEUZdwIbXSUb1gb8UnSb73IDUYldyWSIF9uG--o9N1iBPAkfHzA3Fy3IA0yJ1fI8AYG-k3JJ4I-4itXEcjlAxtm3UcCNaf2moil_sWJXkw9K9o8_uflPmssBpNZT6i5kH09JiYamTA1eCVoUhc2zCwnP_NWScm7ViZA-vj9YUzAa7ek0iBRwFo5EPyiMiOroxJ79Fl27ftWq8EUOs8qo241G6fj7k8VbbivhRb0uImr2LXvyJOAQwX0FpAdXlvWG8jwmQ"},{"kid":"HCLEkPub","key_ops":["encrypt"],"kty":"RSA","e":"AQAB","n":"p9jNOQAAfu_s_5nU93l_LVB2xwhTp4N3cWuAfG_gm30xkBu_QWDkxRv3ihaIyuDIO57JP50Oub9rvYNHVuTl-EmPnCYdLsl0VPUTGG0d1_wsqlxszPiDCwxb7SEzLMZTm2k3DvBaeu5AO9ONjvXRGCmygMueUwZcbPw8i1XlO-ZvDnV3b6_C_3HjcjLyUhuED0znW897BwMOIW2isj5cpCpzJbpNbs-IUyN0RPClua4qkXcjn1E6XxiXbxNqzqjlzMn6qpj65FjznUl8KGRYNawm2Yefs9hBYt40UDk0_ydNO6c2N1SLXFAlOIHOz8oLMpl_bLf_iRAxeTnSWDabhQ"}],"vm-configuration":{"console-enabled":true,"secure-boot":true,"tpm-enabled":true,"vmUniqueId":"3404EF27-32A2-4A07-A4C7-1A1171624C5D"},"user-data":"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2022 Microsoft Corporation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-tdx-vtpm/README.md: -------------------------------------------------------------------------------- 1 | # az-tdx-vtpm 2 | 3 | [![Rust](https://github.com/kinvolk/azure-cvm-tooling/actions/workflows/rust.yml/badge.svg)](https://github.com/kinvolk/azure-cvm-tooling/actions/workflows/rust.yml) 4 | [![Crate](https://img.shields.io/crates/v/az-tdx-vtpm.svg)](https://crates.io/crates/az-tdx-vtpm) 5 | [![Docs](https://docs.rs/rand/badge.svg)](https://docs.rs/az-tdx-vtpm) 6 | 7 | This library enables guest attestation and verification for [TDX CVMs on Azure](https://learn.microsoft.com/en-us/azure/confidential-computing/tdx-confidential-vm-overview). 8 | 9 | ## Build & Install 10 | 11 | ```bash 12 | cargo b --release -p az-tdx-vtpm 13 | scp ../target/release/tdx-vtpm azureuser@$CONFIDENTIAL_VM: 14 | ``` 15 | 16 | ## Run Binary 17 | 18 | On the TDX CVM, retrieve a TD Quote and write it to disk: 19 | 20 | ```bash 21 | sudo ./tdx-vtpm 22 | ``` 23 | 24 | ## Integration Tests 25 | 26 | The integration test suite can run on a TDX CVM. It needs to be executed as root and the tests have to run sequentially. 27 | 28 | ```bash 29 | sudo -E env "PATH=$PATH" cargo t --features integration_test -- --test-threads 1 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: az-cvm-vtpm 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Install deps 23 | run: sudo apt-get update && sudo apt-get install -y libtss2-dev 24 | 25 | - uses: actions-rust-lang/setup-rust-toolchain@v1 26 | with: 27 | toolchain: stable 28 | components: rustfmt, clippy 29 | override: true 30 | 31 | - name: Build 32 | run: cargo build --all 33 | 34 | - name: Check verifier-only 35 | run: cargo check --no-default-features --features=verifier 36 | 37 | - name: Check attester-only 38 | run: cargo check --no-default-features --features=attester 39 | 40 | - name: Run tests 41 | run: cargo test --all 42 | 43 | - name: Compile integration tests 44 | run: cargo test --all --features integration_test --no-run 45 | 46 | - name: Format 47 | run: cargo fmt --all -- --check 48 | 49 | - name: Lint 50 | run: cargo clippy --all-targets --all-features --all -- -D warnings 51 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/Makefile: -------------------------------------------------------------------------------- 1 | CVM_RESOURCE_GROUP ?= azure-cvm-tooling-ci 2 | LOCATION ?= eastus 3 | IMAGE_ID ?= - 4 | SSH_PUB_KEY_PATH ?= ~/.ssh/id_rsa.pub 5 | ADMIN_PUBLIC_KEY = $(shell cat $(SSH_PUB_KEY_PATH)) 6 | ifeq ($(SUFFIX),) 7 | SUFFIX := $(shell bash -c 'echo $$RANDOM | md5sum | head -c 6') 8 | endif 9 | VM_NAME := cvm-$(SUFFIX) 10 | ASSIGN_PUBLIC_IP ?= false 11 | 12 | .PHONY: deploy 13 | deploy: 14 | az deployment group create \ 15 | --template-file ./arm/cvm.bicep \ 16 | --resource-group=$(CVM_RESOURCE_GROUP) \ 17 | --name $(VM_NAME) \ 18 | --parameters virtualMachineName=$(VM_NAME) \ 19 | --parameters location=$(LOCATION) \ 20 | $(if $(IMAGE_ID:-=),--parameters imageId=$(IMAGE_ID)) \ 21 | --parameters adminPublicKey='$(ADMIN_PUBLIC_KEY)' \ 22 | --parameters assignPublicIP=$(ASSIGN_PUBLIC_IP) && \ 23 | echo -n "$(VM_NAME): " && \ 24 | az network nic show \ 25 | --resource-group $(CVM_RESOURCE_GROUP) \ 26 | --name $(VM_NAME)-nic \ 27 | --query 'ipConfigurations[0].privateIpAddress' \ 28 | --output tsv 29 | 30 | .PHONY: delete 31 | delete: 32 | az vm delete \ 33 | --resource-group $(CVM_RESOURCE_GROUP) \ 34 | --name cvm-$(SUFFIX) \ 35 | --yes && \ 36 | az network public-ip delete \ 37 | --resource-group $(CVM_RESOURCE_GROUP) \ 38 | --name $(VM_NAME)-ip && \ 39 | az network vnet delete \ 40 | --resource-group azure-cvm-tooling-ci \ 41 | --name $(VM_NAME)-vnet 42 | -------------------------------------------------------------------------------- /az-cvm-vtpm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "az-cvm-vtpm" 3 | version = "0.7.4" 4 | edition = "2021" 5 | repository = "https://github.com/kinvolk/azure-cvm-tooling/" 6 | license = "MIT" 7 | keywords = ["azure", "tpm", "sev-snp", "tdx"] 8 | categories = ["cryptography", "virtualization"] 9 | description = "Package with shared code for Azure Confidential VMs" 10 | 11 | [workspace] 12 | members = [ 13 | "az-snp-vtpm", 14 | "az-tdx-vtpm", 15 | "az-snp-vtpm/example", 16 | ] 17 | 18 | [lib] 19 | path = "src/lib.rs" 20 | 21 | [dependencies] 22 | bincode.workspace = true 23 | jsonwebkey = { version = "0.3.5", features = ["pkcs-convert"] } 24 | memoffset = "0.9.0" 25 | openssl = { workspace = true, optional = true } 26 | serde.workspace = true 27 | serde_json.workspace = true 28 | serde-big-array = "0.5.1" 29 | sev.workspace = true 30 | sha2 = "0.10.8" 31 | thiserror.workspace = true 32 | tss-esapi = { version = "7.5.1", optional = true } 33 | zerocopy.workspace = true 34 | 35 | [features] 36 | tpm = ["tss-esapi"] 37 | default = ["attester", "verifier"] 38 | attester = ["tpm"] 39 | verifier = ["openssl", "sev/openssl", "tpm"] 40 | 41 | [workspace.dependencies] 42 | bincode = "1.3.1" 43 | clap = { version = "4", features = ["derive"] } 44 | openssl = "0.10" 45 | serde = { version = "1.0.189", features = ["derive"] } 46 | serde_json = "1.0.107" 47 | thiserror = "2.0.3" 48 | sev = "6.2.1" 49 | ureq = { version = "2.6.2", default-features = false, features = ["json"] } 50 | zerocopy = { version = "0.8.26", features = ["derive"] } 51 | hex = "0.4" 52 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-tdx-vtpm/src/imds.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | use az_cvm_vtpm::tdx::TdReport; 5 | use serde::Deserialize; 6 | use thiserror::Error; 7 | use zerocopy::IntoBytes; 8 | 9 | const IMDS_QUOTE_URL: &str = "http://169.254.169.254/acc/tdquote"; 10 | 11 | #[derive(Error, Debug)] 12 | pub enum ImdsError { 13 | #[error("http error")] 14 | HttpError(#[from] Box), 15 | #[error("base64 error")] 16 | Base64Error(#[from] base64_url::base64::DecodeError), 17 | #[error("io error")] 18 | IoError(#[from] std::io::Error), 19 | } 20 | 21 | struct ReportBody { 22 | report: String, 23 | } 24 | 25 | impl ReportBody { 26 | fn new(report_bytes: &[u8]) -> Self { 27 | let report = base64_url::encode(report_bytes); 28 | Self { report } 29 | } 30 | } 31 | 32 | #[derive(Clone, Debug, Deserialize)] 33 | struct QuoteResponse { 34 | quote: String, 35 | } 36 | 37 | /// Retrieves a TDX quote from the Azure Instance Metadata Service (IMDS) using a provided TD 38 | /// report. 39 | pub fn get_td_quote(td_report: &TdReport) -> Result, ImdsError> { 40 | let bytes = td_report.as_bytes(); 41 | let report_body = ReportBody::new(bytes); 42 | let response: QuoteResponse = ureq::post(IMDS_QUOTE_URL) 43 | .send_json(ureq::json!({ 44 | "report": report_body.report, 45 | })) 46 | .map_err(Box::new)? 47 | .into_json()?; 48 | let quote = base64_url::decode(&response.quote)?; 49 | Ok(quote) 50 | } 51 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-tdx-vtpm/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "integration_test")] 2 | mod tests { 3 | use az_tdx_vtpm::{hcl, tdx, vtpm}; 4 | use serde::Deserialize; 5 | 6 | #[test] 7 | fn get_report_with_varying_report_data_len() { 8 | let mut report_data = "test".as_bytes(); 9 | vtpm::get_report_with_report_data(report_data).unwrap(); 10 | report_data = "test_test".as_bytes(); 11 | vtpm::get_report_with_report_data(report_data).unwrap(); 12 | } 13 | 14 | #[derive(Deserialize, Debug)] 15 | struct VarDataUserData { 16 | #[serde(rename = "user-data")] 17 | user_data: String, 18 | } 19 | 20 | #[test] 21 | fn get_report_with_report_data() { 22 | let mut report_data: [u8; 64] = [0; 64]; 23 | report_data[42] = 42; 24 | let bytes = vtpm::get_report_with_report_data(&report_data).unwrap(); 25 | let hcl_report = hcl::HclReport::new(bytes).unwrap(); 26 | let var_data = hcl_report.var_data(); 27 | let VarDataUserData { user_data } = serde_json::from_slice(var_data).unwrap(); 28 | assert_eq!(user_data.to_lowercase(), hex::encode(report_data)); 29 | 30 | let var_data_hash = hcl_report.var_data_sha256(); 31 | let td_report: tdx::TdReport = hcl_report.try_into().unwrap(); 32 | assert_eq!(var_data_hash, td_report.report_mac.reportdata[..32]); 33 | } 34 | 35 | #[test] 36 | fn get_report() { 37 | let bytes = vtpm::get_report().unwrap(); 38 | let hcl_report = hcl::HclReport::new(bytes).unwrap(); 39 | 40 | let var_data_hash = hcl_report.var_data_sha256(); 41 | let td_report: tdx::TdReport = hcl_report.try_into().unwrap(); 42 | assert_eq!(var_data_hash, td_report.report_mac.reportdata[..32]); 43 | } 44 | 45 | #[test] 46 | fn ak_pub() { 47 | let _ = vtpm::get_ak_pub().unwrap(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "integration_test")] 2 | mod tests { 3 | use az_snp_vtpm::{hcl, report, vtpm}; 4 | use serde::Deserialize; 5 | 6 | #[test] 7 | fn get_report_with_varying_report_data_len() { 8 | let mut report_data = "test".as_bytes(); 9 | vtpm::get_report_with_report_data(report_data).unwrap(); 10 | report_data = "test_test".as_bytes(); 11 | vtpm::get_report_with_report_data(report_data).unwrap(); 12 | } 13 | 14 | #[derive(Deserialize, Debug)] 15 | struct VarDataUserData { 16 | #[serde(rename = "user-data")] 17 | user_data: String, 18 | } 19 | 20 | #[test] 21 | fn get_report_with_report_data() { 22 | let mut report_data: [u8; 64] = [0; 64]; 23 | report_data[42] = 42; 24 | let bytes = vtpm::get_report_with_report_data(&report_data).unwrap(); 25 | let hcl_report = hcl::HclReport::new(bytes).unwrap(); 26 | let var_data = hcl_report.var_data(); 27 | let VarDataUserData { user_data } = serde_json::from_slice(var_data).unwrap(); 28 | assert_eq!(user_data.to_lowercase(), hex::encode(report_data)); 29 | 30 | let var_data_hash = hcl_report.var_data_sha256(); 31 | let snp_report: report::AttestationReport = hcl_report.try_into().unwrap(); 32 | assert_eq!(var_data_hash, snp_report.report_data[..32]); 33 | } 34 | 35 | #[test] 36 | fn get_report() { 37 | let bytes = vtpm::get_report().unwrap(); 38 | let hcl_report = hcl::HclReport::new(bytes).unwrap(); 39 | 40 | let var_data_hash = hcl_report.var_data_sha256(); 41 | let snp_report: report::AttestationReport = hcl_report.try_into().unwrap(); 42 | assert_eq!(var_data_hash, snp_report.report_data[..32]); 43 | } 44 | 45 | #[test] 46 | fn ak_pub() { 47 | let _ = vtpm::get_ak_pub().unwrap(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /az-cvm-vtpm/src/tdx/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | // Types are based on "Architecture Specification: Intel Trust Domain Extensions 5 | // Module 1.0", Feb 2023, Section 22.6 6 | 7 | use serde::{Deserialize, Serialize}; 8 | use serde_big_array::BigArray; 9 | use zerocopy::{Immutable, IntoBytes}; 10 | 11 | #[repr(C)] 12 | #[derive(Immutable, IntoBytes, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] 13 | pub struct ReportType { 14 | pub r#type: u8, 15 | pub subtype: u8, 16 | pub version: u8, 17 | pub _reserved: u8, 18 | } 19 | 20 | #[repr(C)] 21 | #[derive(Immutable, IntoBytes, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] 22 | pub struct ReportMac { 23 | pub reporttype: ReportType, 24 | pub _reserved_1: [u8; 12], 25 | pub cpusvn: [u8; 16], 26 | #[serde(with = "BigArray")] 27 | pub tee_tcb_info_hash: [u8; 48], 28 | #[serde(with = "BigArray")] 29 | pub tee_info_hash: [u8; 48], 30 | #[serde(with = "BigArray")] 31 | pub reportdata: [u8; 64], 32 | pub _reserved_2: [u8; 32], 33 | pub mac: [u8; 32], 34 | } 35 | 36 | #[repr(C)] 37 | #[derive(Immutable, IntoBytes, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] 38 | pub struct Rtmr { 39 | #[serde(with = "BigArray")] 40 | pub register_data: [u8; 48], 41 | } 42 | 43 | #[repr(C)] 44 | #[derive(Immutable, IntoBytes, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] 45 | pub struct TdInfo { 46 | pub attributes: [u8; 8], 47 | pub xfam: [u8; 8], 48 | #[serde(with = "BigArray")] 49 | pub mrtd: [u8; 48], 50 | #[serde(with = "BigArray")] 51 | pub mrconfigid: [u8; 48], 52 | #[serde(with = "BigArray")] 53 | pub mrowner: [u8; 48], 54 | #[serde(with = "BigArray")] 55 | pub mrownerconfig: [u8; 48], 56 | pub rtrm: [Rtmr; 4], 57 | #[serde(with = "BigArray")] 58 | pub _reserved: [u8; 112], 59 | } 60 | 61 | #[repr(C)] 62 | #[derive(Immutable, IntoBytes, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] 63 | pub struct TdReport { 64 | pub report_mac: ReportMac, 65 | #[serde(with = "BigArray")] 66 | pub tee_tcb_info: [u8; 239], 67 | pub _reserved: [u8; 17], 68 | pub tdinfo: TdInfo, 69 | } 70 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/src/amd_kds.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | use crate::certs::{AmdChain, Vcek}; 5 | use crate::HttpError; 6 | use openssl::x509::X509; 7 | use sev::firmware::guest::AttestationReport; 8 | use thiserror::Error; 9 | 10 | const KDS_CERT_SITE: &str = "https://kdsintf.amd.com"; 11 | const KDS_VCEK: &str = "/vcek/v1"; 12 | const SEV_PROD_NAME: &str = "Milan"; 13 | const KDS_CERT_CHAIN: &str = "cert_chain"; 14 | 15 | fn get(url: &str) -> Result, HttpError> { 16 | let mut body = ureq::get(url).call().map_err(Box::new)?.into_reader(); 17 | let mut buffer = Vec::new(); 18 | body.read_to_end(&mut buffer)?; 19 | Ok(buffer) 20 | } 21 | 22 | #[derive(Error, Debug)] 23 | pub enum AmdKdsError { 24 | #[error("openssl error")] 25 | OpenSsl(#[from] openssl::error::ErrorStack), 26 | #[error("Http error")] 27 | Http(#[from] HttpError), 28 | } 29 | 30 | /// Retrieve the AMD chain of trust (ASK & ARK) from AMD's KDS 31 | pub fn get_cert_chain() -> Result { 32 | let url = format!("{KDS_CERT_SITE}{KDS_VCEK}/{SEV_PROD_NAME}/{KDS_CERT_CHAIN}"); 33 | let bytes = get(&url)?; 34 | 35 | let certs = X509::stack_from_pem(&bytes)?; 36 | let ask = certs[0].clone(); 37 | let ark = certs[1].clone(); 38 | 39 | let chain = AmdChain { ask, ark }; 40 | 41 | Ok(chain) 42 | } 43 | 44 | fn hexify(bytes: &[u8]) -> String { 45 | let mut hex_string = String::new(); 46 | for byte in bytes { 47 | hex_string.push_str(&format!("{byte:02x}")); 48 | } 49 | hex_string 50 | } 51 | 52 | /// Retrieve a VCEK cert from AMD's KDS, based on an AttestationReport's platform information 53 | pub fn get_vcek(report: &AttestationReport) -> Result { 54 | let hw_id = hexify(&*report.chip_id); 55 | let url = format!( 56 | "{KDS_CERT_SITE}{KDS_VCEK}/{SEV_PROD_NAME}/{hw_id}?blSPL={:02}&teeSPL={:02}&snpSPL={:02}&ucodeSPL={:02}", 57 | report.reported_tcb.bootloader, 58 | report.reported_tcb.tee, 59 | report.reported_tcb.snp, 60 | report.reported_tcb.microcode 61 | ); 62 | 63 | let bytes = get(&url)?; 64 | let cert = X509::from_der(&bytes)?; 65 | let vcek = Vcek(cert); 66 | Ok(vcek) 67 | } 68 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | //! This library enables guest attestation flows for [SEV-SNP CVMs on Azure](https://learn.microsoft.com/en-us/azure/confidential-computing/confidential-vm-overview). Please refer to the documentation in [this repository](https://github.com/Azure/confidential-computing-cvm-guest-attestation) for details on the attestation procedure. 5 | //! 6 | //! # SNP Report Validation 7 | //! 8 | //! The following code will retrieve an SNP report from the vTPM device, parse it, and validate it against the AMD certificate chain. Finally it will verify that a hash of a raw HCL report's Variable Data is equal to the `report_data` field in an embedded [Attestation Report](sev::firmware::guest::AttestationReport) structure. 9 | //! 10 | //! # 11 | //! ```no_run 12 | //! use az_snp_vtpm::{amd_kds, hcl, vtpm}; 13 | //! use az_snp_vtpm::report::{AttestationReport, Validateable}; 14 | //! use std::error::Error; 15 | //! 16 | //! fn main() -> Result<(), Box> { 17 | //! let bytes = vtpm::get_report()?; 18 | //! let hcl_report = hcl::HclReport::new(bytes)?; 19 | //! let var_data_hash = hcl_report.var_data_sha256(); 20 | //! let snp_report: AttestationReport = hcl_report.try_into()?; 21 | //! 22 | //! let vcek = amd_kds::get_vcek(&snp_report)?; 23 | //! let cert_chain = amd_kds::get_cert_chain()?; 24 | //! 25 | //! cert_chain.validate()?; 26 | //! vcek.validate(&cert_chain)?; 27 | //! snp_report.validate(&vcek)?; 28 | //! 29 | //! if var_data_hash != snp_report.report_data[..32] { 30 | //! return Err("var_data_hash mismatch".into()); 31 | //! } 32 | //! 33 | //! Ok(()) 34 | //! } 35 | //! ``` 36 | 37 | pub use az_cvm_vtpm::{hcl, vtpm}; 38 | use thiserror::Error; 39 | 40 | #[derive(Error, Debug)] 41 | pub enum HttpError { 42 | #[error("HTTP error")] 43 | Http(#[from] Box), 44 | #[error("failed to read HTTP response")] 45 | Io(#[from] std::io::Error), 46 | } 47 | 48 | /// Determines if the current VM is an SEV-SNP CVM. 49 | /// Returns `Ok(true)` if the VM is an SEV-SNP CVM, `Ok(false)` if it is not, 50 | /// and `Err` if an error occurs. 51 | pub fn is_snp_cvm() -> Result { 52 | let bytes = vtpm::get_report()?; 53 | let Ok(hcl_report) = hcl::HclReport::new(bytes) else { 54 | return Ok(false); 55 | }; 56 | let is_snp = hcl_report.report_type() == hcl::ReportType::Snp; 57 | Ok(is_snp) 58 | } 59 | 60 | #[cfg(feature = "verifier")] 61 | pub mod amd_kds; 62 | #[cfg(feature = "verifier")] 63 | pub mod certs; 64 | #[cfg(feature = "attester")] 65 | pub mod imds; 66 | pub mod report; 67 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/example/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | use az_snp_vtpm::certs::Vcek; 5 | use az_snp_vtpm::hcl::HclReport; 6 | use az_snp_vtpm::report::{AttestationReport, Validateable}; 7 | use az_snp_vtpm::{amd_kds, imds, vtpm}; 8 | use openssl::pkey::PKey; 9 | use std::error::Error; 10 | 11 | struct Evidence { 12 | report: Vec, 13 | quote: vtpm::Quote, 14 | certs: imds::Certificates, 15 | } 16 | 17 | struct Attester; 18 | 19 | impl Attester { 20 | fn gather_evidence(nonce: &[u8]) -> Result> { 21 | let report = vtpm::get_report()?; 22 | let quote = vtpm::get_quote(nonce)?; 23 | let certs = imds::get_certs()?; 24 | 25 | Ok(Evidence { 26 | report, 27 | quote, 28 | certs, 29 | }) 30 | } 31 | } 32 | 33 | struct Verifier; 34 | 35 | impl Verifier { 36 | fn verify(nonce: &[u8], evidence: &Evidence) -> Result<(), Box> { 37 | let Evidence { quote, report, .. } = evidence; 38 | 39 | let hcl_report = HclReport::new(report.clone())?; 40 | let var_data_hash = hcl_report.var_data_sha256(); 41 | let ak_pub = hcl_report.ak_pub()?; 42 | let snp_report: AttestationReport = hcl_report.try_into()?; 43 | 44 | let cert_chain = amd_kds::get_cert_chain()?; 45 | let vcek = Vcek::from_pem(&evidence.certs.vcek)?; 46 | 47 | cert_chain.validate()?; 48 | vcek.validate(&cert_chain)?; 49 | snp_report.validate(&vcek)?; 50 | 51 | if var_data_hash != snp_report.report_data[..32] { 52 | return Err("var_data_hash mismatch".into()); 53 | } 54 | let der = ak_pub.key.try_to_der()?; 55 | let pub_key = PKey::public_key_from_der(&der)?; 56 | quote.verify(&pub_key, nonce)?; 57 | 58 | Ok(()) 59 | } 60 | } 61 | 62 | #[derive(Default)] 63 | struct RelyingParty { 64 | nonce: Vec, 65 | } 66 | 67 | impl RelyingParty { 68 | pub fn request_secret(&mut self) -> Vec { 69 | // placeholder for a real nonce, it is usually randomly generated ephemeral value. 70 | let nonce = "challenge".as_bytes().to_vec(); 71 | self.nonce.clone_from(&nonce); 72 | nonce 73 | } 74 | 75 | pub fn release_secret(&self, evidence: &Evidence) -> Result<&'static str, Box> { 76 | Verifier::verify(&self.nonce, evidence)?; 77 | Ok("secret") 78 | } 79 | } 80 | 81 | fn main() -> Result<(), Box> { 82 | let mut rp = RelyingParty::default(); 83 | let nonce = rp.request_secret(); 84 | 85 | let evidence = Attester::gather_evidence(&nonce)?; 86 | let secret = rp.release_secret(&evidence)?; 87 | 88 | println!("Secret: {secret}"); 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | use az_cvm_vtpm::hcl::HclReport; 5 | use az_cvm_vtpm::vtpm; 6 | use az_snp_vtpm::{amd_kds, certs, imds, report}; 7 | use clap::Parser; 8 | use report::{AttestationReport, Validateable}; 9 | use std::error::Error; 10 | use std::fs::File; 11 | use std::io::Read; 12 | use std::path::PathBuf; 13 | 14 | #[derive(Parser)] 15 | #[command(author, version, about, long_about = None)] 16 | struct Args { 17 | #[command(subcommand)] 18 | action: Action, 19 | } 20 | 21 | #[derive(clap::Subcommand)] 22 | enum Action { 23 | Report { 24 | /// Raw unmodified report bytes 25 | #[arg(short, long)] 26 | file: Option, 27 | 28 | /// Print the report to stdout 29 | #[arg(short, long)] 30 | print: bool, 31 | 32 | /// Retrieve certificates from IMDS endpoint 33 | #[arg(short, long)] 34 | imds: bool, 35 | }, 36 | Quote { 37 | /// A nonce to use for the quote 38 | #[arg(short, long)] 39 | nonce: String, 40 | }, 41 | } 42 | 43 | fn main() -> Result<(), Box> { 44 | let args = Args::parse(); 45 | 46 | match args.action { 47 | Action::Report { file, imds, print } => { 48 | let bytes = match file { 49 | Some(file_name) => read_file(&file_name)?, 50 | None => vtpm::get_report()?, 51 | }; 52 | let hcl_report = HclReport::new(bytes)?; 53 | let snp_report: AttestationReport = hcl_report.try_into()?; 54 | 55 | let (vcek, cert_chain) = if imds { 56 | let pem_certs = imds::get_certs()?; 57 | let vcek = certs::Vcek::from_pem(&pem_certs.vcek)?; 58 | let cert_chain = certs::build_cert_chain(&pem_certs.amd_chain)?; 59 | (vcek, cert_chain) 60 | } else { 61 | let vcek = amd_kds::get_vcek(&snp_report)?; 62 | let cert_chain = amd_kds::get_cert_chain()?; 63 | (vcek, cert_chain) 64 | }; 65 | 66 | cert_chain.validate()?; 67 | vcek.validate(&cert_chain)?; 68 | snp_report.validate(&vcek)?; 69 | 70 | if print { 71 | println!("{snp_report}"); 72 | } 73 | } 74 | Action::Quote { nonce } => { 75 | println!("quote byte size: {}", nonce.len()); 76 | let quote = vtpm::get_quote(nonce.as_bytes())?; 77 | println!("{:02X?}", quote.message()); 78 | } 79 | } 80 | 81 | Ok(()) 82 | } 83 | 84 | fn read_file(path: &PathBuf) -> Result, std::io::Error> { 85 | let mut file = File::open(path)?; 86 | let mut bytes = Vec::new(); 87 | file.read_to_end(&mut bytes)?; 88 | Ok(bytes) 89 | } 90 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/src/certs.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | pub use openssl::x509::X509; 5 | use thiserror::Error; 6 | 7 | pub struct AmdChain { 8 | pub ask: X509, 9 | pub ark: X509, 10 | } 11 | 12 | #[derive(Error, Debug)] 13 | pub enum ValidateError { 14 | #[error("openssl error")] 15 | OpenSsl(#[from] openssl::error::ErrorStack), 16 | #[error("ARK is not self-signed")] 17 | ArkNotSelfSigned, 18 | #[error("ASK is not signed by ARK")] 19 | AskNotSignedByArk, 20 | #[error("VCEK is not signed by ASK")] 21 | VcekNotSignedByAsk, 22 | } 23 | 24 | impl AmdChain { 25 | pub fn validate(&self) -> Result<(), ValidateError> { 26 | let ark_pubkey = self.ark.public_key()?; 27 | 28 | let ark_signed = self.ark.verify(&ark_pubkey)?; 29 | if !ark_signed { 30 | return Err(ValidateError::ArkNotSelfSigned); 31 | } 32 | 33 | let ask_signed = self.ask.verify(&ark_pubkey)?; 34 | if !ask_signed { 35 | return Err(ValidateError::AskNotSignedByArk); 36 | } 37 | 38 | Ok(()) 39 | } 40 | } 41 | 42 | pub struct Vcek(pub X509); 43 | 44 | impl Vcek { 45 | pub fn from_pem(pem: &str) -> Result { 46 | let cert = X509::from_pem(pem.as_bytes())?; 47 | Ok(Self(cert)) 48 | } 49 | 50 | pub fn validate(&self, amd_chain: &AmdChain) -> Result<(), ValidateError> { 51 | let ask_pubkey = amd_chain.ask.public_key()?; 52 | let vcek_signed = self.0.verify(&ask_pubkey)?; 53 | if !vcek_signed { 54 | return Err(ValidateError::VcekNotSignedByAsk); 55 | } 56 | 57 | Ok(()) 58 | } 59 | } 60 | 61 | #[derive(Error, Debug)] 62 | pub enum ParseError { 63 | #[error("openssl error")] 64 | OpenSsl(#[from] openssl::error::ErrorStack), 65 | #[error("wrong amount of certificates (expected {0:?}, found {1:?})")] 66 | WrongAmount(usize, usize), 67 | } 68 | 69 | /// build ASK + ARK certificate chain from a multi-pem string 70 | pub fn build_cert_chain(pem: &str) -> Result { 71 | let certs = X509::stack_from_pem(pem.as_bytes())?; 72 | 73 | if certs.len() != 2 { 74 | return Err(ParseError::WrongAmount(2, certs.len())); 75 | } 76 | 77 | let ask = certs[0].clone(); 78 | let ark = certs[1].clone(); 79 | 80 | let chain = AmdChain { ask, ark }; 81 | 82 | Ok(chain) 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use super::*; 88 | 89 | #[test] 90 | fn test_validate_certificates() { 91 | let bytes = include_bytes!("../../test/certs.pem"); 92 | let certs = X509::stack_from_pem(bytes).unwrap(); 93 | let (vcek, ask, ark) = (certs[0].clone(), certs[1].clone(), certs[2].clone()); 94 | let vcek = Vcek(vcek); 95 | let cert_chain = AmdChain { ask, ark }; 96 | cert_chain.validate().unwrap(); 97 | vcek.validate(&cert_chain).unwrap(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-tdx-vtpm/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | //! This library enables guest attestation flows for [TDX CVMs on Azure](https://learn.microsoft.com/en-us/azure/confidential-computing/tdx-confidential-vm-overview). 5 | //! 6 | //! A TD report can be retrieved in parsed form using `report::get_report()` function, or as 7 | //! raw bytes including the hcl envelope using `vtpm::get_report()`. The library provides a 8 | //! function to retrieve the TD quote from the Azure Instance Metadata Service (IMDS) using 9 | //! `imds::get_td_quote()`, produce returning a quote signed by a TDX Quoting Enclave. 10 | //! 11 | //! Variable Data is part of the HCL envelope and holds the public part of the vTPM Attestation 12 | //! Key (AK). A hash of the Variable Data block is included in the TD report as `reportdata`. 13 | //! TPM quotes retrieved with `vtpm::get_quote()` should be signed by this AK. A verification 14 | //! function would need to check this to ensure the TD report is linked to this unique TDX CVM. 15 | //! 16 | //! # 17 | //! ```no_run 18 | //! use az_tdx_vtpm::{hcl, imds, report, tdx, vtpm}; 19 | //! use openssl::pkey::{PKey, Public}; 20 | //! use std::error::Error; 21 | //! 22 | //! fn main() -> Result<(), Box> { 23 | //! let td_report = report::get_report()?; 24 | //! let td_quote_bytes = imds::get_td_quote(&td_report)?; 25 | //! std::fs::write("td_quote.bin", td_quote_bytes)?; 26 | //! 27 | //! let bytes = vtpm::get_report()?; 28 | //! let hcl_report = hcl::HclReport::new(bytes)?; 29 | //! let var_data_hash = hcl_report.var_data_sha256(); 30 | //! let ak_pub = hcl_report.ak_pub()?; 31 | //! 32 | //! let td_report: tdx::TdReport = hcl_report.try_into()?; 33 | //! assert!(var_data_hash == td_report.report_mac.reportdata[..32]); 34 | //! let nonce = "a nonce".as_bytes(); 35 | //! 36 | //! let tpm_quote = vtpm::get_quote(nonce)?; 37 | //! let der = ak_pub.key.try_to_der()?; 38 | //! let pub_key = PKey::public_key_from_der(&der)?; 39 | //! tpm_quote.verify(&pub_key, nonce)?; 40 | //! 41 | //! Ok(()) 42 | //! } 43 | //! ``` 44 | 45 | pub mod imds; 46 | pub mod report; 47 | pub use az_cvm_vtpm::{hcl, tdx, vtpm}; 48 | 49 | /// Determines if the current VM is a TDX CVM. 50 | /// Returns `Ok(true)` if the VM is a TDX CVM, `Ok(false)` if it is not, 51 | /// and `Err` if an error occurs. 52 | pub fn is_tdx_cvm() -> Result { 53 | let bytes = vtpm::get_report()?; 54 | let Ok(hcl_report) = hcl::HclReport::new(bytes) else { 55 | return Ok(false); 56 | }; 57 | let is_tdx = hcl_report.report_type() == hcl::ReportType::Tdx; 58 | Ok(is_tdx) 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use super::*; 64 | use hcl::HclReport; 65 | use tdx::TdReport; 66 | 67 | #[test] 68 | fn test_report_data_hash() { 69 | let bytes: &[u8] = include_bytes!("../../test/hcl-report-tdx.bin"); 70 | let hcl_report = HclReport::new(bytes.to_vec()).unwrap(); 71 | let var_data_hash = hcl_report.var_data_sha256(); 72 | let td_report: TdReport = hcl_report.try_into().unwrap(); 73 | assert!(var_data_hash == td_report.report_mac.reportdata[..32]); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/README.md: -------------------------------------------------------------------------------- 1 | [![Rust](https://github.com/kinvolk/azure-cvm-tooling/actions/workflows/rust.yml/badge.svg)](https://github.com/kinvolk/azure-cvm-tooling/actions/workflows/rust.yml) 2 | [![Crate](https://img.shields.io/crates/v/az-snp-vtpm.svg)](https://crates.io/crates/az-snp-vtpm) 3 | [![Docs](https://docs.rs/rand/badge.svg)](https://docs.rs/az-snp-vtpm) 4 | 5 | # az-snp-vtpm 6 | 7 | This library enables guest attestation flows for [SEV-SNP CVMs on Azure](https://learn.microsoft.com/en-us/azure/confidential-computing/confidential-vm-overview). Please refer to the documentation in [this repository](https://github.com/Azure/confidential-computing-cvm-guest-attestation) for details on the attestation procedure. 8 | 9 | ## Create a CVM 10 | 11 | Default image is Ubuntu 22.04 cvm 12 | 13 | ```bash 14 | export IMAGE_ID=/subscriptions/.../resourceGroups/.../providers/Microsoft.Compute/galleries/.../images/.../versions/1.0.0 15 | make deploy 16 | ``` 17 | 18 | ## Build & Install 19 | 20 | ```bash 21 | cargo b --release -p az-snp-vtpm 22 | scp ../target/release/snp-vtpm azureuser@$CONFIDENTIAL_VM: 23 | ``` 24 | 25 | ## Run Binary 26 | 27 | Retrieve SEV-SNP report, validate and print it: 28 | 29 | ```bash 30 | sudo ./snp-vtpm -p 31 | ``` 32 | 33 | ## Example Project 34 | 35 | There is a project in the `./example` folder depicting how the crate can be leveraged in a Remote Attestation flow. **Note:** the code is merely illustrative and doesn't feature exhaustive validation, which would be required in a production scenario. 36 | 37 | ```bash 38 | cargo b -p snp-example 39 | ``` 40 | 41 | ## SEV-SNP Report & vTPM 42 | 43 | The vTPM is linked to the SEV-SNP report via the vTPM Attestation Key (AK). The public AK is part of a Runtime Data struct, which is hashed and submitted as Report Data when generating the SNP report. To provide freshness guarantees in an attestation exchange we can request a vTPM quote with a nonce. The resulting message is signed by the AK. 44 | 45 | ``` 46 | ┌────────────────────────┐ 47 | │ HCL Data │ 48 | │ │ 49 | │ ┌──────────────────────┴─┐ ─┐ 50 | │ │ Runtime Data │ │ 51 | │ │ │ │ 52 | ┌──────────────────────┐ │ │ ┌────────────────────┐ │ ├─┐ 53 | ┌─┤ vTPM AK ├──┼─┼─┤ vTPM Public AK │ │ │ │ 54 | │ └──────────────────────┘ │ │ └────────────────────┘ │ │ │ 55 | │ ┌──────────────┐ │ └──────────────────────┬─┘ ─┘ │ 56 | │ │ vTPM Quote │ │ ┌────────────────────┐ │ │ 57 | │ │ │ │ │ HCL Report │ │ │ 58 | signs ┌─ ┌─┴────────────┐ │ │ │ │ │ sha256 59 | │ │ │ Message │ │ │ │ ┌────────────────┐ │ │ │ 60 | │ │ │ │ │ │ │ │ SEV-SNP Report │ │ │ │ 61 | │ │ │ ┌──────────┐ │ │ │ │ │ │ │ │ │ 62 | │ │ │ │ PCR0 │ │ │ │ │ │ ┌──────────────┴─┴─┴─┐ │ 63 | │ │ │ └──────────┘ │ │ │ │ │ │ Report Data │ ◄───┘ 64 | │ │ │ ... │ │ │ │ │ └──────────────┬─┬─┬─┘ 65 | │ │ │ ┌──────────┐ │ │ │ │ └────────────────┘ │ │ 66 | └─► │ │ │ PCRn │ │ │ │ └────────────────────┘ │ 67 | │ │ └──────────┘ │ │ └────────────────────────┘ 68 | │ │ ┌──────────┐ │ │ 69 | │ │ │ Nonce │ │ │ 70 | │ │ └──────────┘ │ │ 71 | └─ └─┬────────────┘ │ 72 | └──────────────┘ 73 | ``` 74 | 75 | ## Integration Tests 76 | 77 | The integration test suite can run on an SNP CVM. It needs to be executed as root and the tests have to run sequentially. 78 | 79 | ```bash 80 | sudo -E env "PATH=$PATH" cargo t --features integration_test -- --test-threads 1 81 | ``` 82 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/src/report.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | #[cfg(feature = "verifier")] 5 | use super::certs::Vcek; 6 | #[cfg(feature = "verifier")] 7 | use az_cvm_vtpm::hcl::SNP_REPORT_SIZE; 8 | use az_cvm_vtpm::hcl::{self, HclReport}; 9 | use az_cvm_vtpm::vtpm; 10 | #[cfg(feature = "verifier")] 11 | use openssl::{ecdsa::EcdsaSig, sha::Sha384}; 12 | pub use sev::firmware::guest::AttestationReport; 13 | use thiserror::Error; 14 | 15 | #[derive(Error, Debug)] 16 | pub enum ValidateError { 17 | #[cfg(feature = "verifier")] 18 | #[error("openssl error")] 19 | OpenSsl(#[from] openssl::error::ErrorStack), 20 | #[error("TCB data is not valid")] 21 | Tcb, 22 | #[error("Measurement signature is not valid")] 23 | MeasurementSignature, 24 | #[error("IO error")] 25 | Io(#[from] std::io::Error), 26 | #[error("bincode error")] 27 | Bincode(#[from] Box), 28 | } 29 | 30 | #[cfg(feature = "verifier")] 31 | pub trait Validateable { 32 | fn validate(&self, vcek: &Vcek) -> Result<(), ValidateError>; 33 | } 34 | 35 | #[cfg(feature = "verifier")] 36 | impl Validateable for AttestationReport { 37 | fn validate(&self, vcek: &Vcek) -> Result<(), ValidateError> { 38 | if !is_tcb_data_valid(self) { 39 | return Err(ValidateError::Tcb); 40 | } 41 | 42 | let report_sig: EcdsaSig = (&self.signature).try_into()?; 43 | let vcek_pubkey = vcek.0.public_key()?.ec_key()?; 44 | 45 | let mut hasher = Sha384::new(); 46 | let base_message = get_report_base(self)?; 47 | hasher.update(&base_message); 48 | let base_message_digest = hasher.finish(); 49 | 50 | if !report_sig.verify(&base_message_digest, &vcek_pubkey)? { 51 | return Err(ValidateError::MeasurementSignature); 52 | } 53 | Ok(()) 54 | } 55 | } 56 | 57 | #[derive(Error, Debug)] 58 | pub enum ReportError { 59 | #[error("deserialization error")] 60 | Parse(#[from] Box), 61 | #[error("vTPM error")] 62 | Vtpm(#[from] vtpm::ReportError), 63 | #[error("HCL error")] 64 | Hcl(#[from] hcl::HclError), 65 | } 66 | 67 | pub fn parse(bytes: &[u8]) -> Result { 68 | // Use the sev's from_bytes method(since sev-6) which handles dynamic parsing 69 | // of different SNP report versions (v2, v3-PreTurin, v3-Turin) 70 | let snp_report = AttestationReport::from_bytes(bytes) 71 | .map_err(|e| ReportError::Parse(Box::new(bincode::ErrorKind::Io(e))))?; 72 | Ok(snp_report) 73 | } 74 | 75 | #[cfg(feature = "verifier")] 76 | fn is_tcb_data_valid(report: &AttestationReport) -> bool { 77 | report.reported_tcb == report.committed_tcb 78 | } 79 | 80 | #[cfg(feature = "verifier")] 81 | fn get_report_base(report: &AttestationReport) -> Result, Box> { 82 | // Use sev's write_bytes method (since SEV-6) for serializing SNP reports to ensure full compatibility 83 | // Original bincode::serialize + size_of calculation is inaccurate on SEV 6.x 84 | let mut raw_bytes = Vec::with_capacity(SNP_REPORT_SIZE); 85 | report 86 | .write_bytes(&mut raw_bytes) 87 | .map_err(|e| Box::new(bincode::ErrorKind::Io(e)))?; 88 | let report_bytes_without_sig = &raw_bytes[0..0x2a0]; 89 | Ok(report_bytes_without_sig.to_vec()) 90 | } 91 | 92 | /// Fetch TdReport from vTPM and parse it 93 | pub fn get_report() -> Result { 94 | let bytes = vtpm::get_report()?; 95 | let hcl_report = HclReport::new(bytes)?; 96 | let snp_report = hcl_report.try_into()?; 97 | Ok(snp_report) 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use super::*; 103 | use hcl::HclReport; 104 | 105 | #[test] 106 | fn test_report_data_hash() { 107 | let bytes: &[u8] = include_bytes!("../../test/hcl-report-snp.bin"); 108 | let hcl_report = HclReport::new(bytes.to_vec()).unwrap(); 109 | let var_data_hash = hcl_report.var_data_sha256(); 110 | let snp_report: AttestationReport = hcl_report.try_into().unwrap(); 111 | assert!(var_data_hash == snp_report.report_data[..32]); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /az-cvm-vtpm/az-snp-vtpm/arm/cvm.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param subnetId string = '' 3 | param virtualMachineName string 4 | param imageId string = '' 5 | param osDiskType string = 'Premium_LRS' 6 | param osDiskDeleteOption string = 'Delete' 7 | param virtualMachineSize string = 'Standard_DC2as_v5' 8 | param nicDeleteOption string = 'Delete' 9 | param adminUsername string = 'azureuser' 10 | param assignPublicIP bool = false 11 | @secure() 12 | param adminPublicKey string 13 | param securityType string = 'ConfidentialVM' 14 | param secureBoot bool = true 15 | param vTPM bool = true 16 | 17 | var networkInterfaceName = '${virtualMachineName}-nic' 18 | var publicIPName = '${virtualMachineName}-ip' 19 | var virtualNetworkName = '${virtualMachineName}-vnet' 20 | var subnetName = '${virtualMachineName}-subnet' 21 | var subnetAddressPrefix = '10.1.0.0/24' 22 | var addressPrefix = '10.1.0.0/16' 23 | 24 | resource publicIP_resource 'Microsoft.Network/publicIPAddresses@2022-07-01' = if (assignPublicIP == true) { 25 | name: publicIPName 26 | location: location 27 | properties: { 28 | publicIPAllocationMethod: 'Dynamic' 29 | } 30 | } 31 | 32 | resource virtualNetwork_resource 'Microsoft.Network/virtualNetworks@2021-05-01' = if (subnetId == '') { 33 | name: virtualNetworkName 34 | location: location 35 | properties: { 36 | addressSpace: { 37 | addressPrefixes: [ 38 | addressPrefix 39 | ] 40 | } 41 | } 42 | } 43 | 44 | resource subnet_resource 'Microsoft.Network/virtualNetworks/subnets@2021-05-01' = if (subnetId == '') { 45 | parent: virtualNetwork_resource 46 | name: subnetName 47 | properties: { 48 | addressPrefix: subnetAddressPrefix 49 | } 50 | } 51 | 52 | resource networkInterfaceName_resource 'Microsoft.Network/networkInterfaces@2021-08-01' = { 53 | name: networkInterfaceName 54 | location: location 55 | properties: { 56 | ipConfigurations: [ 57 | { 58 | name: 'ipconfig' 59 | properties: { 60 | subnet: { 61 | #disable-next-line use-resource-id-functions 62 | id: (subnetId == '') ? subnet_resource.id : subnetId 63 | } 64 | privateIPAllocationMethod: 'Dynamic' 65 | publicIPAddress: assignPublicIP ? { 66 | id: publicIP_resource.id 67 | } : null 68 | } 69 | } 70 | ] 71 | } 72 | } 73 | 74 | resource virtualMachineName_resource 'Microsoft.Compute/virtualMachines@2022-03-01' = { 75 | name: virtualMachineName 76 | location: location 77 | properties: { 78 | hardwareProfile: { 79 | vmSize: virtualMachineSize 80 | } 81 | storageProfile: { 82 | osDisk: { 83 | createOption: 'fromImage' 84 | managedDisk: { 85 | storageAccountType: osDiskType 86 | securityProfile: { 87 | securityEncryptionType: 'VMGuestStateOnly' 88 | } 89 | } 90 | deleteOption: osDiskDeleteOption 91 | } 92 | imageReference: imageId != '' ? { 93 | #disable-next-line use-resource-id-functions 94 | id: imageId 95 | } : { 96 | publisher: 'canonical' 97 | offer: '0001-com-ubuntu-confidential-vm-jammy' 98 | sku: '22_04-lts-cvm' 99 | version: 'latest' 100 | } 101 | } 102 | networkProfile: { 103 | networkInterfaces: [ 104 | { 105 | id: networkInterfaceName_resource.id 106 | properties: { 107 | deleteOption: nicDeleteOption 108 | } 109 | } 110 | ] 111 | } 112 | osProfile: { 113 | computerName: virtualMachineName 114 | adminUsername: adminUsername 115 | linuxConfiguration: { 116 | disablePasswordAuthentication: true 117 | ssh: { 118 | publicKeys: [ 119 | { 120 | path: '/home/${adminUsername}/.ssh/authorized_keys' 121 | keyData: adminPublicKey 122 | } 123 | ] 124 | } 125 | } 126 | } 127 | securityProfile: { 128 | securityType: securityType 129 | uefiSettings: { 130 | secureBootEnabled: secureBoot 131 | vTpmEnabled: vTPM 132 | } 133 | } 134 | } 135 | } 136 | 137 | resource virtualMachineName_GuestAttestation 'Microsoft.Compute/virtualMachines/extensions@2018-10-01' = { 138 | parent: virtualMachineName_resource 139 | name: 'GuestAttestation' 140 | location: location 141 | properties: { 142 | publisher: 'Microsoft.Azure.Security.LinuxAttestation' 143 | type: 'GuestAttestation' 144 | typeHandlerVersion: '1.0' 145 | autoUpgradeMinorVersion: true 146 | settings: { 147 | AttestationConfig: { 148 | MaaSettings: { 149 | maaEndpoint: '' 150 | maaTenantName: 'GuestAttestation' 151 | } 152 | AscSettings: { 153 | ascReportingEndpoint: '' 154 | ascReportingFrequency: '' 155 | } 156 | useCustomToken: 'false' 157 | disableAlerts: 'false' 158 | } 159 | } 160 | } 161 | } 162 | 163 | output adminUsername string = adminUsername 164 | -------------------------------------------------------------------------------- /az-cvm-vtpm/src/vtpm/verify.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | use super::{Quote, QuoteError}; 5 | use openssl::pkey::{PKey, Public}; 6 | use openssl::{hash::MessageDigest, sha::Sha256, sign::Verifier}; 7 | use thiserror::Error; 8 | use tss_esapi::structures::{Attest, AttestInfo}; 9 | use tss_esapi::traits::UnMarshall; 10 | 11 | #[non_exhaustive] 12 | #[derive(Error, Debug)] 13 | pub enum VerifyError { 14 | #[error("tss error")] 15 | Tss(#[from] tss_esapi::Error), 16 | #[error("openssl error")] 17 | OpenSsl(#[from] openssl::error::ErrorStack), 18 | #[error("quote is not signed by key")] 19 | SignatureMismatch, 20 | #[error("nonce mismatch")] 21 | NonceMismatch, 22 | #[error("quote error")] 23 | Quote(#[from] QuoteError), 24 | #[error("pcr mismatch")] 25 | PcrMismatch, 26 | } 27 | 28 | impl Quote { 29 | /// Verify a Quote's signature and nonce 30 | /// 31 | /// # Arguments 32 | /// 33 | /// * `pub_key` - A public key to verify the Quote's signature 34 | /// 35 | /// * `nonce` - A byte slice to verify the Quote's nonce 36 | pub fn verify(&self, pub_key: &PKey, nonce: &[u8]) -> Result<(), VerifyError> { 37 | self.verify_signature(pub_key)?; 38 | 39 | let quote_nonce = &self.nonce()?; 40 | if nonce != quote_nonce { 41 | return Err(VerifyError::NonceMismatch); 42 | } 43 | 44 | self.verify_pcrs()?; 45 | 46 | Ok(()) 47 | } 48 | 49 | /// Verify a Quote's signature 50 | /// 51 | /// # Arguments 52 | /// 53 | /// * `pub_key` - A public key to verify the Quote's signature 54 | pub fn verify_signature(&self, pub_key: &PKey) -> Result<(), VerifyError> { 55 | let mut verifier = Verifier::new(MessageDigest::sha256(), pub_key)?; 56 | verifier.update(&self.message)?; 57 | let is_verified = verifier.verify(&self.signature)?; 58 | if !is_verified { 59 | return Err(VerifyError::SignatureMismatch); 60 | } 61 | Ok(()) 62 | } 63 | 64 | /// Verify that the TPM Quote's PCR digest matches the digest of the bundled PCR values 65 | /// 66 | pub fn verify_pcrs(&self) -> Result<(), VerifyError> { 67 | let attest = Attest::unmarshall(&self.message)?; 68 | let AttestInfo::Quote { info } = attest.attested() else { 69 | return Err(VerifyError::Quote(QuoteError::NotAQuote)); 70 | }; 71 | 72 | let pcr_digest = info.pcr_digest(); 73 | 74 | // Read hashes of all the PCRs. 75 | let mut hasher = Sha256::new(); 76 | for pcr in self.pcrs.iter() { 77 | hasher.update(pcr); 78 | } 79 | 80 | let digest = hasher.finish(); 81 | if digest[..] != pcr_digest[..] { 82 | return Err(VerifyError::PcrMismatch); 83 | } 84 | 85 | Ok(()) 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::*; 92 | 93 | // // Use this code to generate the scriptures for the test on an AMD CVM. 94 | // 95 | // use az_snp_vtpm::vtpm; 96 | // use bincode; 97 | // use rsa; 98 | // use rsa::pkcs8::EncodePublicKey; 99 | // use std::error::Error; 100 | // use std::fs; 101 | // 102 | // fn main() -> Result<(), Box> { 103 | // // Extract the AK public key. 104 | // let foo = vtpm::get_ak_pub()?.to_public_key_pem(rsa::pkcs8::LineEnding::LF)?; 105 | // fs::write("/tmp/akpub.pem", foo)?; 106 | // 107 | // // Save the PCRs into binary file. 108 | // let nonce = "challenge".as_bytes().to_vec(); 109 | // let quote = vtpm::get_quote(&nonce)?; 110 | // let quote_encoded: Vec = bincode::serialize("e).unwrap(); 111 | // fs::write("/tmp/quote.bin", quote_encoded)?; 112 | // 113 | // Ok(()) 114 | // } 115 | 116 | #[cfg(feature = "verifier")] 117 | #[test] 118 | fn test_quote_validation() { 119 | // Can be retrieved by `get_ak_pub()` or via tpm2-tools: 120 | // sudo tpm2_readpublic -c 0x81000003 -f pem -o akpub.pem 121 | let pem = include_bytes!("../../test/akpub.pem"); 122 | let pkey = PKey::public_key_from_pem(pem).unwrap(); 123 | 124 | // Can be retrieved by `get_quote()` or via tpm2-tools: 125 | // For message and signature: 126 | // sudo tpm2_quote -c 0x81000003 -l sha256:5,8 -q challenge -m quote_msg -s quote_sig 127 | // 128 | // For PCR values: 129 | // sudo tpm2_pcrread sha256:0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 130 | let quote_bytes = include_bytes!("../../test/quote.bin"); 131 | let quote: Quote = bincode::deserialize(quote_bytes).unwrap(); 132 | 133 | // proper nonce in message 134 | let nonce = "challenge".as_bytes().to_vec(); 135 | let result = quote.verify(&pkey, &nonce); 136 | assert!(result.is_ok(), "Quote verification should not fail"); 137 | 138 | // wrong signature 139 | let mut wrong_quote = quote.clone(); 140 | wrong_quote.signature.reverse(); 141 | let result = wrong_quote.verify(&pkey, &nonce); 142 | let error = result.unwrap_err(); 143 | assert!( 144 | matches!(error, VerifyError::SignatureMismatch), 145 | "Expected signature mismatch" 146 | ); 147 | 148 | // improper nonce 149 | let nonce = vec![1, 2, 3, 4]; 150 | let result = quote.verify(&pkey, &nonce); 151 | let error = result.unwrap_err(); 152 | assert!( 153 | matches!(error, VerifyError::NonceMismatch), 154 | "Expected nonce verification error" 155 | ); 156 | } 157 | 158 | #[test] 159 | fn test_pcr_values() { 160 | let quote_bytes = include_bytes!("../../test/quote.bin"); 161 | let quote: Quote = bincode::deserialize(quote_bytes).unwrap(); 162 | let result = quote.verify_pcrs(); 163 | assert!(result.is_ok(), "PCR verification should not fail"); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /docs/end-to-end-test.md: -------------------------------------------------------------------------------- 1 | # CoCo End to End Testing 2 | 3 | In this document you will build and deploy Key Broker Service (KBS) & Attestation Agent. The KBS will host a secret which will be released only when Attestation Agent sends a valid attestation package. In the end we verify if the secret deployed on KBS matches the secret downloaded after successful attestation. For the demo purposes we will deploy everything on the same Confidential Virtual Machine (CVM). 4 | 5 | The following instructions have been tested on a Ubuntu 22 Azure CVM. It currently requires forks of the following CoCo upstream repositories, the KBS fork will pull the proper dependencies. 6 | 7 | * cc/kbs 8 | * cc/attestation-service 9 | * cc/attestation-agent 10 | * cc/kbs-types 11 | 12 | ## Deploy CVM on Azure 13 | 14 | The configuration to deploy CVM on Azure are in the directory `vtpm-snp`. Make changes as you prefer in the following environment variables: 15 | 16 | ```bash 17 | cd vtpm-snp 18 | 19 | export CVM_RESOURCE_GROUP="cvm-vtpm-e2e" 20 | export VM_NAME="cvm" 21 | export SSH_PUB_KEY_PATH=$HOME/.ssh/id_rsa.pub 22 | export ASSIGN_PUBLIC_IP=true 23 | export VNET_NAME="cvmtest" 24 | export SUBNET_NAME="cvmtest" 25 | ``` 26 | 27 | If you are testing a custom CVM image then export the following environment variable or skip this step: 28 | 29 | ```bash 30 | export IMAGE_ID=/subscriptions/.../resourceGroups/.../providers/Microsoft.Compute/galleries/.../images/.../versions/0.0.1 31 | ``` 32 | 33 | Create a VNET, if you want to reuse an existing VNET then just export the ID in the following environment variable: 34 | 35 | ```bash 36 | export VNET_ID=$(az network vnet create \ 37 | -g "${CVM_RESOURCE_GROUP}" \ 38 | -n "${VNET_NAME}" \ 39 | --subnet-name "${SUBNET_NAME}" \ 40 | --query=newVNet.id \ 41 | -o tsv) 42 | ``` 43 | 44 | Deploy the CVM: 45 | 46 | ```bash 47 | make deploy 48 | ``` 49 | 50 | Fetch the public IP of the VM and SSH into the machine: 51 | 52 | ```bash 53 | export CVM_IP=$(az vm show \ 54 | -d -g "${CVM_RESOURCE_GROUP}" \ 55 | -n "${VM_NAME}" \ 56 | --query=publicIps \ 57 | -o tsv) 58 | 59 | ssh -i "${SSH_PUB_KEY_PATH%.pub}" azureuser@"${CVM_IP}" 60 | ``` 61 | 62 | ## Install Dependencies 63 | 64 | > **NOTE:** Attestation Agent (and also, unfortunately at the moment, Attestation Service) code is linked to `tss-esapi` (TPM2 library) and OpenSSL, so we need to install the development packages. 65 | 66 | ```bash 67 | sudo apt update 68 | sudo apt install -y \ 69 | gcc \ 70 | make \ 71 | automake \ 72 | golang \ 73 | protobuf-compiler \ 74 | libtss2-dev \ 75 | libssl-dev \ 76 | tpm2-tools \ 77 | tss2 \ 78 | build-essential \ 79 | pkg-config \ 80 | jq \ 81 | build-essential 82 | 83 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 84 | . "$HOME/.cargo/env" 85 | 86 | cd $(mktemp -d) 87 | curl -LO https://github.com/fullstorydev/grpcurl/releases/download/v1.8.7/grpcurl_1.8.7_linux_x86_64.tar.gz 88 | tar -xvzf grpcurl_1.8.7_linux_x86_64.tar.gz 89 | sudo mv grpcurl /usr/local/bin 90 | ``` 91 | 92 | ## Key Broker Service (KBS) 93 | 94 | ### Download KBS fork 95 | 96 | ```bash 97 | cd /var/tmp 98 | mkdir kbs 99 | curl -fL "https://github.com/mkulke/kbs/tarball/mkulke%2Fadd-az-snp-vtpm-support" \ 100 | | tar -xz --strip-components 1 -C kbs 101 | ``` 102 | 103 | ### Build KBS 104 | 105 | ```bash 106 | export X86_64_UNKNOWN_LINUX_GNU_OPENSSL_NO_VENDOR=1 107 | cd kbs 108 | cargo b --release --no-default-features \ 109 | --features native-as-az-snp-vtpm-verifier \ 110 | --bin kbs 111 | cargo b --release --no-default-features \ 112 | --features native-as-az-snp-vtpm-verifier,attestation_agent/cc_kbc,attestation_agent/az-snp-vtpm-attester \ 113 | --bin client 114 | ``` 115 | 116 | ### Create a secret that will be released by KBS 117 | 118 | Create a folder to hold a file-backed secret: 119 | 120 | ```bash 121 | sudo mkdir -p /opt/confidential-containers/kbs/repository/my_repo/resource_type 122 | echo -n "a secret" | sudo tee /opt/confidential-containers/kbs/repository/my_repo/resource_type/123abc 123 | ``` 124 | 125 | ### Run KBS 126 | 127 | We run the KBS on the same node as the client and skip proper security. An actual setup would have to setup certificates, enable HTTPS and of course run the KBS on a remote machine. Create Key pair: 128 | 129 | ```bash 130 | openssl genpkey -algorithm ed25519 > kbs.key 131 | openssl pkey -in kbs.key -pubout -out kbs.pem 132 | sudo ./target/release/kbs --socket 127.0.0.1:8080 --auth-public-key kbs.pem --insecure-http 133 | ``` 134 | 135 | ## Attestation Agent (AA) 136 | 137 | > **NOTE**: In an actual scenario AA would be running on the CVM. And the workload (or the kata-agent) would talk to AA to get secret released from KBS. But here for demo purposes we are running it on the same host. 138 | 139 | ### Download the AA fork code 140 | 141 | ```bash 142 | cd /var/tmp 143 | mkdir attestation-agent 144 | curl -fL "https://github.com/mkulke/attestation-agent/tarball/mkulke%2Fadd-az-snp-vtpm-attester" | 145 | tar -xz --strip-components 1 -C attestation-agent 146 | ``` 147 | 148 | ### Build the AA 149 | 150 | ```bash 151 | cd attestation-agent/ 152 | export PKG_CONFIG_SYSROOT_DIR=/ 153 | make LIBC=gnu features=rust-crypto,grpc,cc_kbc_az_snp_vtpm 154 | ``` 155 | 156 | ### Run Attestation Agent 157 | 158 | ```bash 159 | sudo ./app/target/x86_64-unknown-linux-gnu/release/attestation-agent --keyprovider_sock 127.0.0.1:60000 --getresource_sock 127.0.0.1:60001 160 | ``` 161 | 162 | ## Simulate Key / Secret Release 163 | 164 | Download the proto file to talk to AA: 165 | 166 | ```bash 167 | curl -LO https://raw.githubusercontent.com/confidential-containers/attestation-agent/main/protos/getresource.proto 168 | ``` 169 | 170 | Get the secret from KBS but by talking to the AA: 171 | 172 | ```bash 173 | grpcurl -proto getresource.proto -plaintext -d @ 127.0.0.1:60001 getresource.GetResourceService.GetResource <123abc_downloaded 174 | { 175 | "ResourcePath": "/my_repo/resource_type/123abc", 176 | "KbcName":"cc_kbc", 177 | "KbsUri": "http://127.0.0.1:8080" 178 | } 179 | EOM 180 | ``` 181 | 182 | Verify the secret: 183 | 184 | ```bash 185 | sudo diff /opt/confidential-containers/kbs/repository/my_repo/resource_type/123abc 123abc_downloaded 186 | ``` 187 | -------------------------------------------------------------------------------- /az-cvm-vtpm/test/certs.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFTDCCAvugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA 3 | oRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATB7MRQwEgYD 4 | VQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDASBgNVBAcMC1NhbnRhIENs 5 | YXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5jZWQgTWljcm8gRGV2aWNl 6 | czESMBAGA1UEAwwJU0VWLU1pbGFuMB4XDTIzMDEwMTA0NDgxOVoXDTMwMDEwMTA0 7 | NDgxOVowejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYD 8 | VQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2Vk 9 | IE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WQ0VLMHYwEAYHKoZIzj0CAQYF 10 | K4EEACIDYgAEM5E7dP0QnFCdfyhQKW+c9h98J2m/H4jcgg47n0OpJvg+ZMxhT8uG 11 | jSWkTDtb8SwOyc1557h98TRIcwoQS7NW055mLDVqfYbXpJESTCY31rZVc9eRFHXT 12 | 4tLWj59ch77wo4IBFjCCARIwEAYJKwYBBAGceAEBBAMCAQAwFwYJKwYBBAGceAEC 13 | BAoWCE1pbGFuLUIwMBEGCisGAQQBnHgBAwEEAwIBAzARBgorBgEEAZx4AQMCBAMC 14 | AQAwEQYKKwYBBAGceAEDBAQDAgEAMBEGCisGAQQBnHgBAwUEAwIBADARBgorBgEE 15 | AZx4AQMGBAMCAQAwEQYKKwYBBAGceAEDBwQDAgEAMBEGCisGAQQBnHgBAwMEAwIB 16 | CDARBgorBgEEAZx4AQMIBAMCAXMwTQYJKwYBBAGceAEEBEDailaVta6v247S7cbC 17 | m+SXtxFXBLe4k7Rr3Td6MWle1dfqjr9kA2Zeh95HV0LrJZNaJ1huupOjLw75szWV 18 | y+jSMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0B 19 | AQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBA4ICAQCGMytjNdJdN5LlVPr88k5n 20 | cNYhWFuKJbxeDoDUv1k5tuBkRgukURhWvO0inTwjjJaMTGZtuBeHeXR78oFSDaRe 21 | E06ZNqFFK+lYJ++41cuqIRqWnxhaUcKIivKZ4LsqE/5Wg1m7WHVb7jeFxvqblfp1 22 | hCwd2rNqq5PP918RBFmDBOTw6jICbnZxr4ouuZi9tWGg7B0arJIn5TSXfDTsAmk4 23 | fNOgfLyWsIaJ91NTZ0jbcSQTk4CJ97XJYUgcG307HpWW56BieXFMPdMMKyqxbgBV 24 | rK00tu3e3BelhLFhlo8UHlrbntthUU/GP6AV40MhSxrXFB5JtwyIaejKkwKMA8p2 25 | hw44o1ikg6cJoft2BoRgwmkGQLnqUjg6bEx/62xiVATTy5/ksAFzeKooqoL67ceU 26 | HKdhjtY4aRTjiOsuRbkKoF6Ee+ifRq0SqTisbKGrwdUWYTYZM5DIbSfXwhuvygrt 27 | dt7SRS5eqVqz+cpeEBCvHzIqyWntinxUI3JK61aE2QuBXiZWUMILXfEjx+GJnHrh 28 | ZXobeV2aCwILt4BF7iHcAvMy+jaPpmI4DLU6u9hHyvc2eRlCKGwgLYCGRhmfSSoA 29 | YLYygA80Xc7w86m3EmVQKQ/dVcCz45+w04T7D/SjnXSsBko2ZELEAKiaBnvrhn7K 30 | hqNvy1IUx71AyVT3hpGJ/w== 31 | -----END CERTIFICATE----- 32 | -----BEGIN CERTIFICATE----- 33 | MIIGiTCCBDigAwIBAgIDAQABMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC 34 | BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS 35 | BgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg 36 | Q2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp 37 | Y2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTgyNDIwWhcNNDUxMDIy 38 | MTgyNDIwWjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS 39 | BgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j 40 | ZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJU0VWLU1pbGFuMIICIjANBgkqhkiG 41 | 9w0BAQEFAAOCAg8AMIICCgKCAgEAnU2drrNTfbhNQIllf+W2y+ROCbSzId1aKZft 42 | 2T9zjZQOzjGccl17i1mIKWl7NTcB0VYXt3JxZSzOZjsjLNVAEN2MGj9TiedL+Qew 43 | KZX0JmQEuYjm+WKksLtxgdLp9E7EZNwNDqV1r0qRP5tB8OWkyQbIdLeu4aCz7j/S 44 | l1FkBytev9sbFGzt7cwnjzi9m7noqsk+uRVBp3+In35QPdcj8YflEmnHBNvuUDJh 45 | LCJMW8KOjP6++Phbs3iCitJcANEtW4qTNFoKW3CHlbcSCjTM8KsNbUx3A8ek5EVL 46 | jZWH1pt9E3TfpR6XyfQKnY6kl5aEIPwdW3eFYaqCFPrIo9pQT6WuDSP4JCYJbZne 47 | KKIbZjzXkJt3NQG32EukYImBb9SCkm9+fS5LZFg9ojzubMX3+NkBoSXI7OPvnHMx 48 | jup9mw5se6QUV7GqpCA2TNypolmuQ+cAaxV7JqHE8dl9pWf+Y3arb+9iiFCwFt4l 49 | AlJw5D0CTRTC1Y5YWFDBCrA/vGnmTnqG8C+jjUAS7cjjR8q4OPhyDmJRPnaC/ZG5 50 | uP0K0z6GoO/3uen9wqshCuHegLTpOeHEJRKrQFr4PVIwVOB0+ebO5FgoyOw43nyF 51 | D5UKBDxEB4BKo/0uAiKHLRvvgLbORbU8KARIs1EoqEjmF8UtrmQWV2hUjwzqwvHF 52 | ei8rPxMCAwEAAaOBozCBoDAdBgNVHQ4EFgQUO8ZuGCrD/T1iZEib47dHLLT8v/gw 53 | HwYDVR0jBBgwFoAUhawa0UP3yKxV1MUdQUir1XhK1FMwEgYDVR0TAQH/BAgwBgEB 54 | /wIBADAOBgNVHQ8BAf8EBAMCAQQwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cHM6Ly9r 55 | ZHNpbnRmLmFtZC5jb20vdmNlay92MS9NaWxhbi9jcmwwRgYJKoZIhvcNAQEKMDmg 56 | DzANBglghkgBZQMEAgIFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgIFAKID 57 | AgEwowMCAQEDggIBAIgeUQScAf3lDYqgWU1VtlDbmIN8S2dC5kmQzsZ/HtAjQnLE 58 | PI1jh3gJbLxL6gf3K8jxctzOWnkYcbdfMOOr28KT35IaAR20rekKRFptTHhe+DFr 59 | 3AFzZLDD7cWK29/GpPitPJDKCvI7A4Ug06rk7J0zBe1fz/qe4i2/F12rvfwCGYhc 60 | RxPy7QF3q8fR6GCJdB1UQ5SlwCjFxD4uezURztIlIAjMkt7DFvKRh+2zK+5plVGG 61 | FsjDJtMz2ud9y0pvOE4j3dH5IW9jGxaSGStqNrabnnpF236ETr1/a43b8FFKL5QN 62 | mt8Vr9xnXRpznqCRvqjr+kVrb6dlfuTlliXeQTMlBoRWFJORL8AcBJxGZ4K2mXft 63 | l1jU5TLeh5KXL9NW7a/qAOIUs2FiOhqrtzAhJRg9Ij8QkQ9Pk+cKGzw6El3T3kFr 64 | Eg6zkxmvMuabZOsdKfRkWfhH2ZKcTlDfmH1H0zq0Q2bG3uvaVdiCtFY1LlWyB38J 65 | S2fNsR/Py6t5brEJCFNvzaDky6KeC4ion/cVgUai7zzS3bGQWzKDKU35SqNU2WkP 66 | I8xCZ00WtIiKKFnXWUQxvlKmmgZBIYPe01zD0N8atFxmWiSnfJl690B9rJpNR/fI 67 | ajxCW3Seiws6r1Zm+tCuVbMiNtpS9ThjNX4uve5thyfE2DgoxRFvY1CsoF5M 68 | -----END CERTIFICATE----- 69 | -----BEGIN CERTIFICATE----- 70 | MIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC 71 | BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS 72 | BgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg 73 | Q2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp 74 | Y2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy 75 | MTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS 76 | BgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j 77 | ZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG 78 | 9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg 79 | W41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta 80 | 1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2 81 | SzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0 82 | 60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05 83 | gmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg 84 | bKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs 85 | +gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi 86 | Qi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ 87 | eTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18 88 | fHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j 89 | WhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI 90 | rFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG 91 | KWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG 92 | SIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI 93 | AWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel 94 | ETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw 95 | STjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK 96 | dHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq 97 | zT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp 98 | KGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e 99 | pmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq 100 | HnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh 101 | 3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn 102 | JZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH 103 | CViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4 104 | AFZEAwoKCQ== 105 | -----END CERTIFICATE----- 106 | -------------------------------------------------------------------------------- /az-cvm-vtpm/src/hcl/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | use crate::tdx::TdReport; 5 | use jsonwebkey::JsonWebKey; 6 | use memoffset::offset_of; 7 | use serde::{Deserialize, Serialize}; 8 | use serde_big_array::BigArray; 9 | use sev::firmware::guest::AttestationReport as SnpReport; 10 | use sha2::{Digest, Sha256}; 11 | use std::convert::TryFrom; 12 | use std::mem::size_of; 13 | use std::ops::Range; 14 | use thiserror::Error; 15 | 16 | const HCL_AKPUB_KEY_ID: &str = "HCLAkPub"; 17 | const TD_REPORT_SIZE: usize = size_of::(); 18 | // SNP AttestationReport binary format size as defined in AMD SNP Firmware ABI specification. 19 | // This corresponds to ATT_REP_FW_LEN defined in sev-6.x crate's snp.rs (which is not public). 20 | // All SNP report versions (v2, v3-PreTurin, v3-Turin) use this fixed size. 21 | pub const SNP_REPORT_SIZE: usize = 1184; 22 | const fn max(a: usize, b: usize) -> usize { 23 | if a > b { 24 | return a; 25 | } 26 | b 27 | } 28 | const MAX_REPORT_SIZE: usize = max(SNP_REPORT_SIZE, TD_REPORT_SIZE); 29 | const SNP_REPORT_TYPE: u32 = 2; 30 | const TDX_REPORT_TYPE: u32 = 4; 31 | const HW_REPORT_OFFSET: usize = offset_of!(AttestationReport, hw_report); 32 | const fn report_range(report_size: usize) -> Range { 33 | HW_REPORT_OFFSET..(HW_REPORT_OFFSET + report_size) 34 | } 35 | const TD_REPORT_RANGE: Range = report_range(TD_REPORT_SIZE); 36 | const SNP_REPORT_RANGE: Range = report_range(SNP_REPORT_SIZE); 37 | 38 | #[derive(Error, Debug)] 39 | pub enum HclError { 40 | #[error("invalid report type")] 41 | InvalidReportType, 42 | #[error("AkPub not found")] 43 | AkPubNotFound, 44 | #[error("binary parse error")] 45 | BinaryParseError(#[from] bincode::Error), 46 | #[error("JSON parse error")] 47 | JsonParseError(#[from] serde_json::Error), 48 | } 49 | 50 | #[derive(Deserialize, Debug)] 51 | struct VarDataKeys { 52 | keys: Vec, 53 | } 54 | 55 | #[repr(u32)] 56 | #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] 57 | enum IgvmHashType { 58 | Invalid = 0, 59 | Sha256, 60 | Sha384, 61 | Sha512, 62 | } 63 | 64 | #[repr(C)] 65 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 66 | struct IgvmRequestData { 67 | data_size: u32, 68 | version: u32, 69 | report_type: u32, 70 | report_data_hash_type: IgvmHashType, 71 | variable_data_size: u32, 72 | variable_data: [u8; 0], 73 | } 74 | 75 | #[repr(C)] 76 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 77 | struct AttestationHeader { 78 | signature: u32, 79 | version: u32, 80 | report_size: u32, 81 | request_type: u32, 82 | status: u32, 83 | reserved: [u32; 3], 84 | } 85 | 86 | #[repr(C)] 87 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 88 | struct AttestationReport { 89 | header: AttestationHeader, 90 | #[serde(with = "BigArray")] 91 | hw_report: [u8; MAX_REPORT_SIZE], 92 | hcl_data: IgvmRequestData, 93 | } 94 | 95 | pub struct HclReport { 96 | bytes: Vec, 97 | attestation_report: AttestationReport, 98 | report_type: ReportType, 99 | } 100 | 101 | #[derive(Copy, Clone, Debug, PartialEq)] 102 | pub enum ReportType { 103 | Tdx, 104 | Snp, 105 | } 106 | 107 | #[allow(clippy::large_enum_variant)] 108 | pub enum HwReport { 109 | Tdx(TdReport), 110 | Snp(SnpReport), 111 | } 112 | 113 | impl HclReport { 114 | /// Parse a HCL report from a byte slice. 115 | pub fn new(bytes: Vec) -> Result { 116 | let attestation_report: AttestationReport = bincode::deserialize(&bytes)?; 117 | let report_type = match attestation_report.hcl_data.report_type { 118 | TDX_REPORT_TYPE => ReportType::Tdx, 119 | SNP_REPORT_TYPE => ReportType::Snp, 120 | _ => return Err(HclError::InvalidReportType), 121 | }; 122 | 123 | let report = Self { 124 | bytes, 125 | attestation_report, 126 | report_type, 127 | }; 128 | Ok(report) 129 | } 130 | 131 | /// Get the type of the nested hardware report 132 | pub fn report_type(&self) -> ReportType { 133 | self.report_type 134 | } 135 | 136 | fn report_slice(&self) -> &[u8] { 137 | match self.report_type { 138 | ReportType::Tdx => self.bytes[TD_REPORT_RANGE].as_ref(), 139 | ReportType::Snp => self.bytes[SNP_REPORT_RANGE].as_ref(), 140 | } 141 | } 142 | 143 | /// Get the SHA256 hash of the VarData section 144 | pub fn var_data_sha256(&self) -> [u8; 32] { 145 | if self.attestation_report.hcl_data.report_data_hash_type != IgvmHashType::Sha256 { 146 | unimplemented!( 147 | "Only SHA256 is supported, got {:?}", 148 | self.attestation_report.hcl_data.report_data_hash_type 149 | ); 150 | } 151 | let mut hasher = Sha256::new(); 152 | hasher.update(self.var_data()); 153 | hasher.finalize().into() 154 | } 155 | 156 | /// Get the slice of the VarData section 157 | pub fn var_data(&self) -> &[u8] { 158 | let var_data_offset = 159 | offset_of!(AttestationReport, hcl_data) + offset_of!(IgvmRequestData, variable_data); 160 | let hcl_data = &self.attestation_report.hcl_data; 161 | let var_data_end = var_data_offset + hcl_data.variable_data_size as usize; 162 | &self.bytes[var_data_offset..var_data_end] 163 | } 164 | 165 | /// Get the vTPM's AKpub from the VarData section 166 | pub fn ak_pub(&self) -> Result { 167 | let VarDataKeys { keys } = serde_json::from_slice(self.var_data())?; 168 | let ak_pub = keys 169 | .into_iter() 170 | .find(|key| { 171 | let Some(ref key_id) = key.key_id else { 172 | return false; 173 | }; 174 | key_id == HCL_AKPUB_KEY_ID 175 | }) 176 | .ok_or(HclError::AkPubNotFound)?; 177 | Ok(ak_pub) 178 | } 179 | } 180 | 181 | impl TryFrom<&HclReport> for TdReport { 182 | type Error = HclError; 183 | 184 | fn try_from(hcl_report: &HclReport) -> Result { 185 | if hcl_report.report_type != ReportType::Tdx { 186 | return Err(HclError::InvalidReportType); 187 | } 188 | let bytes = hcl_report.report_slice(); 189 | let td_report = bincode::deserialize::(bytes)?; 190 | Ok(td_report) 191 | } 192 | } 193 | 194 | impl TryFrom for TdReport { 195 | type Error = HclError; 196 | 197 | fn try_from(hcl_report: HclReport) -> Result { 198 | (&hcl_report).try_into() 199 | } 200 | } 201 | 202 | impl TryFrom<&HclReport> for SnpReport { 203 | type Error = HclError; 204 | 205 | fn try_from(hcl_report: &HclReport) -> Result { 206 | if hcl_report.report_type != ReportType::Snp { 207 | return Err(HclError::InvalidReportType); 208 | } 209 | let bytes = hcl_report.report_slice(); 210 | // Use the sev-6.x crate's from_bytes method which handles dynamic parsing 211 | // of different SNP report versions (v2, v3-PreTurin, v3-Turin) 212 | let snp_report = SnpReport::from_bytes(bytes) 213 | .map_err(|e| HclError::BinaryParseError(Box::new(bincode::ErrorKind::Io(e))))?; 214 | Ok(snp_report) 215 | } 216 | } 217 | 218 | impl TryFrom for SnpReport { 219 | type Error = HclError; 220 | 221 | fn try_from(hcl_report: HclReport) -> Result { 222 | (&hcl_report).try_into() 223 | } 224 | } 225 | 226 | #[cfg(test)] 227 | mod tests { 228 | use super::*; 229 | 230 | #[test] 231 | fn parse_hcl_report() { 232 | let bytes: &[u8] = include_bytes!("../../test/hcl-report-snp.bin"); 233 | let hcl_report = HclReport::new(bytes.to_vec()).unwrap(); 234 | let _ = hcl_report.ak_pub().unwrap(); 235 | 236 | let bytes: &[u8] = include_bytes!("../../test/hcl-report-tdx.bin"); 237 | let hcl_report = HclReport::new(bytes.to_vec()).unwrap(); 238 | let _ = hcl_report.ak_pub().unwrap(); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /az-cvm-vtpm/src/vtpm/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | use core::time::Duration; 5 | use serde::{Deserialize, Serialize}; 6 | use std::thread; 7 | use thiserror::Error; 8 | use tss_esapi::abstraction::{nv, pcr, public::DecodedKey}; 9 | use tss_esapi::attributes::NvIndexAttributesBuilder; 10 | use tss_esapi::handles::{NvIndexHandle, NvIndexTpmHandle, PcrHandle, TpmHandle}; 11 | use tss_esapi::interface_types::algorithm::HashingAlgorithm; 12 | use tss_esapi::interface_types::resource_handles::{NvAuth, Provision}; 13 | use tss_esapi::interface_types::session_handles::AuthSession; 14 | use tss_esapi::structures::pcr_selection_list::PcrSelectionListBuilder; 15 | use tss_esapi::structures::pcr_slot::PcrSlot; 16 | use tss_esapi::structures::{ 17 | Attest, AttestInfo, Data, DigestValues, MaxNvBuffer, NvPublicBuilder, Signature, 18 | SignatureScheme, 19 | }; 20 | use tss_esapi::tcti_ldr::{DeviceConfig, TctiNameConf}; 21 | use tss_esapi::traits::{Marshall, UnMarshall}; 22 | use tss_esapi::Context; 23 | 24 | #[cfg(feature = "verifier")] 25 | mod verify; 26 | 27 | #[cfg(feature = "verifier")] 28 | pub use verify::VerifyError; 29 | 30 | const VTPM_HCL_REPORT_NV_INDEX: u32 = 0x01400001; 31 | const INDEX_REPORT_DATA: u32 = 0x01400002; 32 | const VTPM_AK_HANDLE: u32 = 0x81000003; 33 | const VTPM_QUOTE_PCR_SLOTS: [PcrSlot; 24] = [ 34 | PcrSlot::Slot0, 35 | PcrSlot::Slot1, 36 | PcrSlot::Slot2, 37 | PcrSlot::Slot3, 38 | PcrSlot::Slot4, 39 | PcrSlot::Slot5, 40 | PcrSlot::Slot6, 41 | PcrSlot::Slot7, 42 | PcrSlot::Slot8, 43 | PcrSlot::Slot9, 44 | PcrSlot::Slot10, 45 | PcrSlot::Slot11, 46 | PcrSlot::Slot12, 47 | PcrSlot::Slot13, 48 | PcrSlot::Slot14, 49 | PcrSlot::Slot15, 50 | PcrSlot::Slot16, 51 | PcrSlot::Slot17, 52 | PcrSlot::Slot18, 53 | PcrSlot::Slot19, 54 | PcrSlot::Slot20, 55 | PcrSlot::Slot21, 56 | PcrSlot::Slot22, 57 | PcrSlot::Slot23, 58 | ]; 59 | 60 | fn to_pcr_handle(pcr: u8) -> Result { 61 | match pcr { 62 | 0 => Ok(PcrHandle::Pcr0), 63 | 1 => Ok(PcrHandle::Pcr1), 64 | 2 => Ok(PcrHandle::Pcr2), 65 | 3 => Ok(PcrHandle::Pcr3), 66 | 4 => Ok(PcrHandle::Pcr4), 67 | 5 => Ok(PcrHandle::Pcr5), 68 | 6 => Ok(PcrHandle::Pcr6), 69 | 7 => Ok(PcrHandle::Pcr7), 70 | 8 => Ok(PcrHandle::Pcr8), 71 | 9 => Ok(PcrHandle::Pcr9), 72 | 10 => Ok(PcrHandle::Pcr10), 73 | 11 => Ok(PcrHandle::Pcr11), 74 | 12 => Ok(PcrHandle::Pcr12), 75 | 13 => Ok(PcrHandle::Pcr13), 76 | 14 => Ok(PcrHandle::Pcr14), 77 | 15 => Ok(PcrHandle::Pcr15), 78 | 16 => Ok(PcrHandle::Pcr16), 79 | 17 => Ok(PcrHandle::Pcr17), 80 | 18 => Ok(PcrHandle::Pcr18), 81 | 19 => Ok(PcrHandle::Pcr19), 82 | 20 => Ok(PcrHandle::Pcr20), 83 | 21 => Ok(PcrHandle::Pcr21), 84 | 22 => Ok(PcrHandle::Pcr22), 85 | 23 => Ok(PcrHandle::Pcr23), 86 | _ => Err(ExtendError::InvalidPcr), 87 | } 88 | } 89 | 90 | #[derive(Error, Debug)] 91 | pub enum ReportError { 92 | #[error("tpm error")] 93 | Tpm(#[from] tss_esapi::Error), 94 | #[error("Failed to write value to nvindex")] 95 | NvWriteFailed, 96 | } 97 | 98 | /// Get a HCL report from an nvindex 99 | pub fn get_report() -> Result, ReportError> { 100 | let nv_index = NvIndexTpmHandle::new(VTPM_HCL_REPORT_NV_INDEX)?; 101 | let mut context = get_session_context()?; 102 | 103 | let report = nv::read_full(&mut context, NvAuth::Owner, nv_index)?; 104 | Ok(report) 105 | } 106 | 107 | /// Retrieve a fresh HCL report from a nvindex. The specified report_data will be reflected 108 | /// in the HCL report in its user_data field and mixed into a hash in the TEE report's report_data. 109 | /// The Function contains a 3 seconds delay to avoid retrieving a stale report. 110 | pub fn get_report_with_report_data(report_data: &[u8]) -> Result, ReportError> { 111 | let mut context = get_session_context()?; 112 | 113 | let nv_index_report_data = NvIndexTpmHandle::new(INDEX_REPORT_DATA)?; 114 | write_nv_index(&mut context, nv_index_report_data, report_data)?; 115 | 116 | thread::sleep(Duration::new(3, 0)); 117 | 118 | let nv_index = NvIndexTpmHandle::new(VTPM_HCL_REPORT_NV_INDEX)?; 119 | let report = nv::read_full(&mut context, NvAuth::Owner, nv_index)?; 120 | Ok(report) 121 | } 122 | 123 | fn get_session_context() -> Result { 124 | let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default()); 125 | let mut context = Context::new(conf)?; 126 | let auth_session = AuthSession::Password; 127 | context.set_sessions((Some(auth_session), None, None)); 128 | Ok(context) 129 | } 130 | 131 | enum NvSearchResult { 132 | Found, 133 | NotFound, 134 | SizeMismatch, 135 | } 136 | 137 | fn find_index( 138 | context: &mut Context, 139 | nv_index: NvIndexTpmHandle, 140 | len: usize, 141 | ) -> Result { 142 | let list = nv::list(context)?; 143 | let result = list 144 | .iter() 145 | .find(|(public, _)| public.nv_index() == nv_index); 146 | let Some((public, _)) = result else { 147 | return Ok(NvSearchResult::NotFound); 148 | }; 149 | if public.data_size() != len { 150 | return Ok(NvSearchResult::SizeMismatch); 151 | } 152 | 153 | Ok(NvSearchResult::Found) 154 | } 155 | 156 | fn create_index( 157 | context: &mut Context, 158 | handle: NvIndexTpmHandle, 159 | len: usize, 160 | ) -> Result { 161 | let attributes = NvIndexAttributesBuilder::new() 162 | .with_owner_write(true) 163 | .with_owner_read(true) 164 | .build()?; 165 | 166 | let owner = NvPublicBuilder::new() 167 | .with_nv_index(handle) 168 | .with_index_name_algorithm(HashingAlgorithm::Sha256) 169 | .with_index_attributes(attributes) 170 | .with_data_area_size(len) 171 | .build()?; 172 | 173 | let index = context.nv_define_space(Provision::Owner, None, owner)?; 174 | Ok(index) 175 | } 176 | 177 | fn resolve_handle( 178 | context: &mut Context, 179 | handle: NvIndexTpmHandle, 180 | ) -> Result { 181 | let key_handle = context.execute_without_session(|c| c.tr_from_tpm_public(handle.into()))?; 182 | Ok(key_handle.into()) 183 | } 184 | 185 | fn delete_index(context: &mut Context, handle: NvIndexTpmHandle) -> Result<(), ReportError> { 186 | let index = resolve_handle(context, handle)?; 187 | context.nv_undefine_space(Provision::Owner, index)?; 188 | Ok(()) 189 | } 190 | 191 | fn write_nv_index( 192 | context: &mut Context, 193 | handle: NvIndexTpmHandle, 194 | data: &[u8], 195 | ) -> Result<(), ReportError> { 196 | let buffer = MaxNvBuffer::try_from(data)?; 197 | let result = find_index(context, handle, data.len())?; 198 | let index = match result { 199 | NvSearchResult::NotFound => create_index(context, handle, data.len())?, 200 | NvSearchResult::SizeMismatch => { 201 | delete_index(context, handle)?; 202 | create_index(context, handle, data.len())? 203 | } 204 | NvSearchResult::Found => resolve_handle(context, handle)?, 205 | }; 206 | context.nv_write(NvAuth::Owner, index, buffer, 0)?; 207 | Ok(()) 208 | } 209 | 210 | #[derive(Error, Debug)] 211 | pub enum ExtendError { 212 | #[error("tpm error")] 213 | Tpm(#[from] tss_esapi::Error), 214 | #[error("invalid pcr number (expected 0-23)")] 215 | InvalidPcr, 216 | } 217 | 218 | /// Extend a PCR register with a sha256 digest 219 | pub fn extend_pcr(pcr: u8, digest: &[u8; 32]) -> Result<(), ExtendError> { 220 | let pcr_handle = to_pcr_handle(pcr)?; 221 | 222 | let mut vals = DigestValues::new(); 223 | let sha256_digest = digest.to_vec().try_into()?; 224 | vals.set(HashingAlgorithm::Sha256, sha256_digest); 225 | 226 | let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default()); 227 | let mut context = Context::new(conf)?; 228 | 229 | let auth_session = AuthSession::Password; 230 | context.set_sessions((Some(auth_session), None, None)); 231 | context.pcr_extend(pcr_handle, vals)?; 232 | 233 | Ok(()) 234 | } 235 | 236 | #[derive(Error, Debug)] 237 | pub enum AKPubError { 238 | #[error("tpm error")] 239 | Tpm(#[from] tss_esapi::Error), 240 | #[error("asn1 der error")] 241 | WrongKeyType, 242 | } 243 | 244 | #[derive(Serialize, Deserialize, Debug)] 245 | pub struct PublicKey { 246 | n: Vec, 247 | e: Vec, 248 | } 249 | 250 | impl PublicKey { 251 | /// Get the modulus of the public key as big-endian unsigned bytes 252 | pub fn modulus(&self) -> &[u8] { 253 | &self.n 254 | } 255 | 256 | /// Get the public exponent of the public key as big-endian unsigned bytes 257 | pub fn exponent(&self) -> &[u8] { 258 | &self.e 259 | } 260 | } 261 | 262 | /// Get the AK pub of the vTPM 263 | pub fn get_ak_pub() -> Result { 264 | let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default()); 265 | let mut context = Context::new(conf)?; 266 | let tpm_handle: TpmHandle = VTPM_AK_HANDLE.try_into()?; 267 | let key_handle = context.tr_from_tpm_public(tpm_handle)?; 268 | let (pk, _, _) = context.read_public(key_handle.into())?; 269 | 270 | let decoded_key: DecodedKey = pk.try_into()?; 271 | let DecodedKey::RsaPublicKey(rsa_pk) = decoded_key else { 272 | return Err(AKPubError::WrongKeyType); 273 | }; 274 | 275 | let bytes_n = rsa_pk.modulus.as_unsigned_bytes_be(); 276 | let bytes_e = rsa_pk.public_exponent.as_unsigned_bytes_be(); 277 | let pkey = PublicKey { 278 | n: bytes_n.into(), 279 | e: bytes_e.into(), 280 | }; 281 | Ok(pkey) 282 | } 283 | 284 | #[non_exhaustive] 285 | #[derive(Error, Debug)] 286 | pub enum QuoteError { 287 | #[error("tpm error")] 288 | Tpm(#[from] tss_esapi::Error), 289 | #[error("data too large")] 290 | DataTooLarge, 291 | #[error("Not a quote, that should not occur")] 292 | NotAQuote, 293 | #[error("Wrong signature, that should not occur")] 294 | WrongSignature, 295 | #[error("PCR bank not found")] 296 | PcrBankNotFound, 297 | #[error("PCR reading error")] 298 | PcrRead, 299 | } 300 | 301 | #[derive(Serialize, Deserialize, Debug, Clone)] 302 | pub struct Quote { 303 | signature: Vec, 304 | message: Vec, 305 | pcrs: Vec<[u8; 32]>, 306 | } 307 | 308 | impl Quote { 309 | /// Retrieve sha256 PCR values from a Quote 310 | pub fn pcrs_sha256(&self) -> impl Iterator { 311 | self.pcrs.iter() 312 | } 313 | 314 | /// Extract nonce from a Quote 315 | pub fn nonce(&self) -> Result, QuoteError> { 316 | let attest = Attest::unmarshall(&self.message)?; 317 | let nonce = attest.extra_data().to_vec(); 318 | Ok(nonce) 319 | } 320 | 321 | /// Extract message from a Quote 322 | pub fn message(&self) -> Vec { 323 | self.message.clone() 324 | } 325 | } 326 | 327 | /// Get a signed vTPM Quote 328 | /// 329 | /// # Arguments 330 | /// 331 | /// * `data` - A byte slice to use as nonce 332 | pub fn get_quote(data: &[u8]) -> Result { 333 | if data.len() > Data::MAX_SIZE { 334 | return Err(QuoteError::DataTooLarge); 335 | } 336 | let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default()); 337 | let mut context = Context::new(conf)?; 338 | let tpm_handle: TpmHandle = VTPM_AK_HANDLE.try_into()?; 339 | let key_handle = context.tr_from_tpm_public(tpm_handle)?; 340 | 341 | let quote_data: Data = data.try_into()?; 342 | let scheme = SignatureScheme::Null; 343 | let hash_algo = HashingAlgorithm::Sha256; 344 | let selection_list = PcrSelectionListBuilder::new() 345 | .with_selection(hash_algo, &VTPM_QUOTE_PCR_SLOTS) 346 | .build()?; 347 | 348 | let auth_session = AuthSession::Password; 349 | context.set_sessions((Some(auth_session), None, None)); 350 | 351 | let (attest, signature) = context.quote( 352 | key_handle.into(), 353 | quote_data, 354 | scheme, 355 | selection_list.clone(), 356 | )?; 357 | 358 | let AttestInfo::Quote { .. } = attest.attested() else { 359 | return Err(QuoteError::NotAQuote); 360 | }; 361 | let Signature::RsaSsa(rsa_sig) = signature else { 362 | return Err(QuoteError::WrongSignature); 363 | }; 364 | 365 | let signature = rsa_sig.signature().to_vec(); 366 | let message = attest.marshall()?; 367 | 368 | context.clear_sessions(); 369 | let pcr_data = pcr::read_all(&mut context, selection_list)?; 370 | 371 | let pcr_bank = pcr_data 372 | .pcr_bank(hash_algo) 373 | .ok_or(QuoteError::PcrBankNotFound)?; 374 | 375 | let pcrs: Result, _> = pcr_bank 376 | .into_iter() 377 | .map(|(_, digest)| digest.clone().try_into().map_err(|_| QuoteError::PcrRead)) 378 | .collect(); 379 | let pcrs = pcrs?; 380 | 381 | Ok(Quote { 382 | signature, 383 | message, 384 | pcrs, 385 | }) 386 | } 387 | --------------------------------------------------------------------------------