diff --git a/sw/device/silicon_creator/rom_ext/e2e/attestation/BUILD b/sw/device/silicon_creator/rom_ext/e2e/attestation/BUILD new file mode 100644 index 00000000000000..129829e664f785 --- /dev/null +++ b/sw/device/silicon_creator/rom_ext/e2e/attestation/BUILD @@ -0,0 +1,36 @@ +# Copyright lowRISC contributors (OpenTitan project). +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +load( + "//rules/opentitan:defs.bzl", + "CLEAR_KEY_SET", + "DEFAULT_TEST_FAILURE_MSG", + "DEFAULT_TEST_SUCCESS_MSG", + "EARLGREY_TEST_ENVS", + "fpga_params", + "opentitan_test", +) + +package(default_visibility = ["//visibility:public"]) + +opentitan_test( + name = "print_certs_test", + srcs = ["print_certs.c"], + exec_env = { + "//hw/top_earlgrey:fpga_hyper310_rom_ext": None, + }, + fpga = fpga_params( + test_cmd = """ + --bootstrap={firmware} + """, + test_harness = "//sw/host/tests/attestation:attestation_test", + ), + deps = [ + "//sw/device/lib/base:status", + "//sw/device/lib/runtime:log", + "//sw/device/lib/runtime:print", + "//sw/device/lib/testing/test_framework:ottf_main", + "//sw/device/silicon_creator/lib/drivers:flash_ctrl", + ], +) diff --git a/sw/device/silicon_creator/rom_ext/e2e/attestation/print_certs.c b/sw/device/silicon_creator/rom_ext/e2e/attestation/print_certs.c new file mode 100644 index 00000000000000..6b99b117bca8e5 --- /dev/null +++ b/sw/device/silicon_creator/rom_ext/e2e/attestation/print_certs.c @@ -0,0 +1,82 @@ +// Copyright lowRISC contributors (OpenTitan project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include "sw/device/lib/base/status.h" +#include "sw/device/lib/runtime/log.h" +#include "sw/device/lib/runtime/print.h" +#include "sw/device/lib/testing/test_framework/ottf_main.h" +#include "sw/device/silicon_creator/lib/drivers/flash_ctrl.h" + +OTTF_DEFINE_TEST_CONFIG(); + +const char base64[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +void base64_encode(char *dest, const uint8_t *data, int32_t len) { + *dest = '\0'; + for (int32_t i = 0; len > 0; i += 3, len -= 3) { + // clang-format off + uint32_t val = (uint32_t)(data[i] << 16 | + (len > 1 ? data[i + 1] << 8 : 0) | + (len > 2 ? data[i + 2] : 0)); + // clang-format on + *dest++ = base64[(val >> 18) & 0x3f]; + *dest++ = base64[(val >> 12) & 0x3f]; + *dest++ = len > 1 ? base64[(val >> 6) & 0x3f] : '='; + *dest++ = len > 2 ? base64[(val >> 0) & 0x3f] : '='; + *dest = '\0'; + } +} + +status_t print_cert(char *dest, const flash_ctrl_info_page_t *info_page) { + uint8_t data[2048]; + TRY(flash_ctrl_info_read(info_page, 0, sizeof(data) / sizeof(uint32_t), + data)); + + int16_t len = (int16_t)(data[2] << 8 | data[3]); + if (len == -1) { + *dest = '\0'; + return OK_STATUS(); + } + len += 4; + base64_encode(dest, data, len); + return OK_STATUS(); +} + +status_t print_owner_block(char *dest, + const flash_ctrl_info_page_t *info_page) { + uint8_t data[2048]; + TRY(flash_ctrl_info_read(info_page, 0, sizeof(data) / sizeof(uint32_t), + data)); + base64_encode(dest, data, sizeof(data)); + return OK_STATUS(); +} + +status_t print_certs(void) { + char buf[3072]; + TRY(print_cert(buf, &kFlashCtrlInfoPageUdsCertificate)); + LOG_INFO("UDS: %s", buf); + + TRY(print_cert(buf, &kFlashCtrlInfoPageCdi0Certificate)); + LOG_INFO("CDI_0: %s", buf); + + TRY(print_cert(buf, &kFlashCtrlInfoPageCdi1Certificate)); + LOG_INFO("CDI_1: %s", buf); + + TRY(print_owner_block(buf, &kFlashCtrlInfoPageOwnerSlot0)); + LOG_INFO("OWNER_PAGE_0: %s", buf); + + TRY(print_owner_block(buf, &kFlashCtrlInfoPageOwnerSlot1)); + LOG_INFO("OWNER_PAGE_1: %s", buf); + + return OK_STATUS(); +} + +bool test_main(void) { + status_t sts = print_certs(); + if (status_err(sts)) { + LOG_ERROR("print_certs: %r", sts); + } + return status_ok(sts); +} diff --git a/sw/host/opentitanlib/src/image/image.rs b/sw/host/opentitanlib/src/image/image.rs index a2f4eee72ef1ed..e04c8a59f62da4 100644 --- a/sw/host/opentitanlib/src/image/image.rs +++ b/sw/host/opentitanlib/src/image/image.rs @@ -512,6 +512,22 @@ impl Image { } } +impl SubImage<'_> { + /// Operates on the signed region of the image. + pub fn map_signed_region(&self, f: F) -> Result + where + F: FnOnce(&[u8]) -> R, + { + Ok(f(&self.data[offset_of!(Manifest, usage_constraints) + ..self.manifest.signed_region_end as usize])) + } + + /// Compute the SHA256 digest for the signed portion of the `Image`. + pub fn compute_digest(&self) -> Result { + self.map_signed_region(|v| sha256::sha256(v)) + } +} + impl ImageAssembler { /// Creates an `ImageAssembler` with a given `size` and mirroring parameters. pub fn with_params(size: usize, mirrored: bool) -> Self { diff --git a/sw/host/opentitanlib/src/ownership/owner.rs b/sw/host/opentitanlib/src/ownership/owner.rs index 8b4d0a6aea7d21..ba0db1fa063d71 100644 --- a/sw/host/opentitanlib/src/ownership/owner.rs +++ b/sw/host/opentitanlib/src/ownership/owner.rs @@ -99,9 +99,9 @@ impl Default for OwnerBlock { } impl OwnerBlock { - const SIZE: usize = 2048; - const DATA_SIZE: usize = 1536; - const SIGNATURE_OFFSET: usize = 1952; + pub const SIZE: usize = 2048; + pub const DATA_SIZE: usize = 1536; + pub const SIGNATURE_OFFSET: usize = 1952; // The not present value must be reflected in the TlvTag::NotPresent value. const NOT_PRESENT: u8 = 0x5a; diff --git a/sw/host/tests/attestation/BUILD b/sw/host/tests/attestation/BUILD new file mode 100644 index 00000000000000..6f0b611ad1e706 --- /dev/null +++ b/sw/host/tests/attestation/BUILD @@ -0,0 +1,25 @@ +# Copyright lowRISC contributors (OpenTitan project). +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library") +load("//rules:ujson.bzl", "ujson_rust") + +package(default_visibility = ["//visibility:public"]) + +rust_binary( + name = "attestation_test", + srcs = ["attestation_test.rs"], + deps = [ + "//sw/host/opentitanlib", + "//sw/host/ot_certs", + "@crate_index//:anyhow", + "@crate_index//:base64ct", + "@crate_index//:clap", + "@crate_index//:humantime", + "@crate_index//:log", + "@crate_index//:num-bigint-dig", + "@crate_index//:regex", + "@crate_index//:serde_json", + ], +) diff --git a/sw/host/tests/attestation/attestation_test.rs b/sw/host/tests/attestation/attestation_test.rs new file mode 100644 index 00000000000000..b10d5bfcac3dc0 --- /dev/null +++ b/sw/host/tests/attestation/attestation_test.rs @@ -0,0 +1,132 @@ +// Copyright lowRISC contributors (OpenTitan project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#![allow(clippy::bool_assert_comparison)] +use anyhow::{anyhow, Result}; +use base64ct::{Base64, Decoder}; +use clap::Parser; +use num_bigint_dig::BigUint; +use regex::Regex; +use std::time::Duration; + +use opentitanlib::app::TransportWrapper; +use opentitanlib::crypto::sha256; +use opentitanlib::image::image::Image; +use opentitanlib::ownership::OwnerBlock; +use opentitanlib::test_utils::init::InitializeTest; +use opentitanlib::uart::console::UartConsole; +use opentitanlib::util::file::FromReader; + +use ot_certs::template::{CertificateExtension, Value}; +use ot_certs::x509; + +#[derive(Debug, Parser)] +struct Opts { + #[command(flatten)] + init: InitializeTest, + + /// Console receive timeout. + #[arg(long, value_parser = humantime::parse_duration, default_value = "10s")] + timeout: Duration, +} + +// A helper trait for extracting data out of the `Value` type. +trait GetValue { + fn get_value(&self) -> &T; +} + +impl GetValue for Value { + fn get_value(&self) -> &T { + match self { + Value::Variable(_) => panic!("Not expecting a Variable: {self:?}"), + Value::Literal(x) => x, + } + } +} + +impl GetValue for Option> { + fn get_value(&self) -> &T { + match self { + None => panic!("Not expecting Option::None"), + Some(Value::Variable(_)) => panic!("Not expecting a Variable: {self:?}"), + Some(Value::Literal(x)) => x, + } + } +} + +fn get_base64_blob(haystack: &str, rx: &str) -> Result> { + let rx = Regex::new(rx)?; + let encoded = rx + .captures(haystack) + .ok_or(anyhow!("Encoded certificate not found"))?; + let mut bin = Vec::new(); + if !encoded[1].is_empty() { + let mut decoder = Decoder::::new(encoded[1].as_bytes())?; + decoder.decode_to_end(&mut bin)?; + } + Ok(bin) +} + +fn attestation_test(opts: &Opts, transport: &TransportWrapper) -> Result<()> { + let uart = transport.uart("console")?; + let capture = UartConsole::wait_for( + &*uart, + r"(?msR)Running.*PASS!$|FAIL!$|BFV:([0-9A-Fa-f]{8})$", + opts.timeout, + )?; + + let _uds_bin = get_base64_blob(&capture[0], r"(?msR)UDS: (.*?)$")?; + let cdi0_bin = get_base64_blob(&capture[0], r"(?msR)CDI_0: (.*?)$")?; + let cdi1_bin = get_base64_blob(&capture[0], r"(?msR)CDI_1: (.*?)$")?; + let owner_page_0 = get_base64_blob(&capture[0], r"(?msR)OWNER_PAGE_0: (.*?)$")?; + let owner_page_1 = get_base64_blob(&capture[0], r"(?msR)OWNER_PAGE_1: (.*?)$")?; + + // TODO: check UDS certificate. + let cdi0 = x509::parse_certificate(&cdi0_bin)?; + let cdi1 = x509::parse_certificate(&cdi1_bin)?; + + // TODO: verify signature chain from CDI_1 to CDI_0 to UDS. + + let image = Image::read_from_file(opts.init.bootstrap.bootstrap.as_deref().unwrap())?; + + // TODO: determine the correct endianness for expressing these measurements. + let measurements = image + .subimages()? + .iter() + .map(|s| s.compute_digest().unwrap().to_le_bytes()) + .collect::>(); + let owner_measurements = [ + // The owner page digests should not include the signature or seal fields. + sha256::sha256(&owner_page_0[0..OwnerBlock::SIGNATURE_OFFSET]).to_le_bytes(), + sha256::sha256(&owner_page_1[0..OwnerBlock::SIGNATURE_OFFSET]).to_le_bytes(), + ]; + + let CertificateExtension::DiceTcbInfo(dice) = &cdi0.private_extensions[0]; + log::info!("Checking CDI_0 (ROM_EXT) DICE Extension: {dice:#?}"); + assert_eq!(dice.model.get_value(), "ROM_EXT"); + assert_eq!(dice.vendor.get_value(), "OpenTitan"); + assert_eq!(dice.layer.get_value(), &BigUint::from(1u8)); + let fw_ids = dice.fw_ids.as_ref().expect("list of fw_ids"); + assert_eq!(fw_ids.len(), 1); + assert_eq!(fw_ids[0].digest.get_value(), &measurements[0]); + + let CertificateExtension::DiceTcbInfo(dice) = &cdi1.private_extensions[0]; + log::info!("Checking CDI_1 (Owner) DICE Extension: {dice:#?}"); + assert_eq!(dice.model.get_value(), "Owner"); + assert_eq!(dice.vendor.get_value(), "OpenTitan"); + assert_eq!(dice.layer.get_value(), &BigUint::from(2u8)); + let fw_ids = dice.fw_ids.as_ref().expect("list of fw_ids"); + assert_eq!(fw_ids.len(), 2); + assert_eq!(fw_ids[0].digest.get_value(), &measurements[1]); + assert_eq!(fw_ids[1].digest.get_value(), &owner_measurements[0]); + Ok(()) +} + +fn main() -> Result<()> { + let opts = Opts::parse(); + opts.init.init_logging(); + let transport = opts.init.init_target()?; + attestation_test(&opts, &transport)?; + Ok(()) +}