diff --git a/services/attest/BUILD.bazel b/services/attest/BUILD.bazel new file mode 100644 index 00000000..ce1d6212 --- /dev/null +++ b/services/attest/BUILD.bazel @@ -0,0 +1,23 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +package(default_visibility = ["//visibility:public"]) + +# All production crates (embedded-safe, no test stubs). +filegroup( + name = "attest_embedded_all", + srcs = [ + "//services/attest/api:attest_api", + "//services/attest/producer:attest_producer", + ], +) + +# Host-side tests; run without embedded target config. +test_suite( + name = "attest_host_tests", + tests = [ + "//services/attest/api:attest_api_test", + "//services/attest/producer:attest_producer_integration_test", + "//services/attest/producer:attest_producer_unit_test", + ], +) diff --git a/services/attest/README.md b/services/attest/README.md new file mode 100644 index 00000000..232ae7f8 --- /dev/null +++ b/services/attest/README.md @@ -0,0 +1,28 @@ + + +# attest service + +OCP-EAT attestation token producer for OpenPRoT. + +## Crates + +| Crate | Path | Purpose | +|---|---|---| +| `openprot-attest-api` | `api/` | Platform-independent traits, types, and error definitions. Callers depend only on this crate. | +| `openprot-attest-producer` | `producer/` | Concrete `AttestProducer` implementations (`HwAttestProducer`, `SwAttestProducer`, `SoftwareAttestProducer`). | + +## Dependency structure + +``` +application / verifier service + └── openprot-attest-api (traits + types only) + └── openprot-attest-producer (production implementations) +``` + +Platform code selects an implementation at construction time: + +- **`HwAttestProducer`** — backed by the Caliptra mailbox driver; all signing inside the hardware boundary. +- **`SwAttestProducer`** — backed by a caller-supplied P-384 key (`SwSigner`); no Caliptra required. +- **`SoftwareAttestProducer`** — software stub for tests (feature = `test-support`). + +See each crate's README for API details and usage examples. diff --git a/services/attest/api/BUILD.bazel b/services/attest/api/BUILD.bazel new file mode 100644 index 00000000..776bdbf5 --- /dev/null +++ b/services/attest/api/BUILD.bazel @@ -0,0 +1,24 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "attest_api", + srcs = glob(["src/**/*.rs"]), + crate_name = "openprot_attest_api", + edition = "2021", + visibility = ["//visibility:public"], + deps = [ + "@rust_crates//:heapless", + "@rust_crates//:p384", + "@rust_crates//:sha2", + "@rust_crates//:thiserror", + "@rust_crates//:zeroize", + ], +) + +rust_test( + name = "attest_api_test", + crate = ":attest_api", +) diff --git a/services/attest/api/Cargo.toml b/services/attest/api/Cargo.toml new file mode 100644 index 00000000..c1413536 --- /dev/null +++ b/services/attest/api/Cargo.toml @@ -0,0 +1,16 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "openprot-attest-api" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +description = "Platform-independent API for the OpenPRoT attestation producer service" + +[dependencies] +heapless = { version = "0.9", default-features = false } +thiserror = "2" +p384 = { version = "0.13", default-features = false, features = ["ecdsa", "arithmetic"] } +sha2 = { version = "0.10", default-features = false } +zeroize = { version = "1.8", default-features = false, features = ["derive"] } diff --git a/services/attest/api/README.md b/services/attest/api/README.md new file mode 100644 index 00000000..0ba7c817 --- /dev/null +++ b/services/attest/api/README.md @@ -0,0 +1,140 @@ + + +# openprot-attest-api + +Platform-independent trait and type definitions for the OpenPRoT attestation +producer service. + +Callers and other OpenPRoT services depend **only** on this crate. It has no +dependency on the producer implementation, the verifier module, or `spdm-lib`. + +## Purpose + +This crate defines the stable interface boundary for attestation token +generation. By depending on `openprot-attest-api` rather than +`openprot-attest-producer`, services can be tested with any `AttestProducer` +implementation — including the `SoftwareAttestProducer` stub — without pulling +in hardware dependencies. + +## Source files + +| File | Contents | +|---|---| +| `src/lib.rs` | Public re-exports. `#![no_std]` `#![forbid(unsafe_code)]`. | +| `src/traits.rs` | `AttestProducer` trait. | +| `src/signing_abstraction.rs` | `HwSigner` trait; `SwSigner` struct with P-384 key validation and ECDSA signing. | +| `src/types.rs` | `Measurement`, `DigestAlgorithm`, `MeasurementAuthority`, `AttestConfig`, `OemId`, `MeasurementProvider` trait, `SwSignerConfig` struct. | +| `src/consts.rs` | Fixed-capacity constants (`MAX_CERT_SIZE`, `MAX_CHAIN_LEN`, etc.). | +| `src/error.rs` | `AttestError` — shared error type for both service crates. | + +## Key traits + +### `AttestProducer` + +The primary interface implemented by `HwAttestProducer`, `SwAttestProducer`, +and the `SoftwareAttestProducer` stub in the producer crate. + +```rust +pub trait AttestProducer { + fn generate_token( + &self, + nonce: &[u8], + out: &mut Vec, + ) -> Result<(), AttestError>; + + /// `buf` is cleared before being populated with the chain (leaf → root). + fn cert_chain( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError>; +} +``` + +### `HwSigner` + +Abstracts signing and certificate operations backed by the Caliptra hardware +boundary. + +```rust +pub trait HwSigner { + fn sign(&self, payload: &[u8]) -> Result<[u8; 96], AttestError>; + fn cert_chain_der( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError>; + fn measurements( + &self, + out: &mut Vec, + ) -> Result<(), AttestError>; +} +``` + +The private Alias Key never leaves Caliptra. Production code implements this +trait via the Caliptra mailbox driver (`caliptra-sw`). + +### `SwSigner` + +Holds a caller-supplied P-384 private key and DER certificate chain for +software signing (no Caliptra hardware required). Constructed via +`SwSigner::new(SwSignerConfig { ... })`, which validates: + +- Scalar is a valid P-384 private key (`1 ≤ d < n`), via `p384::ecdsa::SigningKey::from_bytes`. +- Cert chain contains at least one certificate. +- Every certificate begins with `0x30` (DER SEQUENCE tag). + +Returns `Err(AttestError::InvalidKey)` on any violation. The key is stored as +a `SigningKey` which zeroizes on drop. `SwSignerConfig` also zeroizes the raw +scalar on drop. + +`sign()` produces a real ECDSA P-384 signature (SHA-384 prehash, RFC 6979 +deterministic nonce) over the COSE `Sig_Structure`. + +### `MeasurementProvider` + +Plug in platform-specific firmware measurement sources (UEFI, BMC, etc.) +beyond the Caliptra-internal measurements. + +```rust +pub trait MeasurementProvider { + fn measurements( + &self, + out: &mut Vec, + ) -> Result<(), AttestError>; +} +``` + +## Key types + +| Type | Description | +|---|---| +| `Measurement` | Single firmware measurement: component name, version, digest algorithm, digest bytes, measurement authority. | +| `DigestAlgorithm` | `Sha384` or `Sha512`. | +| `MeasurementAuthority` | `Caliptra` (hardware-measured) or `Platform` (software-registered). | +| `AttestConfig` | Producer configuration: `oemid`, `hw_model`. | +| `OemId` | OEM identifier (IANA Private Enterprise Number or UUID form). | +| `SwSignerConfig` | Input to `SwSigner::new`: 48-byte P-384 scalar and DER cert chain. Zeroizes scalar on drop. | + +## Error variants + +| Variant | Meaning | +|---|---| +| `Mailbox` | Caliptra mailbox communication failure. | +| `Der` | DER parse error (malformed certificate structure). | +| `ChainValidation` | DICE chain structural or compliance failure. | +| `InvalidNonce` | Nonce length outside the 8–64 byte range. | +| `Cbor` | CBOR encoding error. | +| `BufferFull` | Fixed-size buffer capacity exceeded. | +| `Cose` | COSE signing error. | +| `Provider` | Measurement provider error. | +| `InvalidKey` | Invalid P-384 key material supplied to `SwSigner::new`. | + +## Cargo + +```toml +[dependencies] +openprot-attest-api = { path = "services/attest/api" } +``` + +The crate is `no_std` and depends on `heapless` for fixed-capacity +collections, `thiserror` for `AttestError`, `p384` + `sha2` for ECDSA +signing, and `zeroize` for key material cleanup. diff --git a/services/attest/api/src/consts.rs b/services/attest/api/src/consts.rs new file mode 100644 index 00000000..03784ce8 --- /dev/null +++ b/services/attest/api/src/consts.rs @@ -0,0 +1,18 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// Caliptra chain: VendorCA → IDevID → LDevID → AliasFMC → AliasRT (leaf) +pub const MAX_CHAIN_LEN: usize = 5; +pub const MAX_CERT_SIZE: usize = 2048; + +pub const MAX_MEASUREMENTS: usize = 16; +pub const MAX_PROVIDERS: usize = 8; +pub const MAX_COMPONENT_LEN: usize = 64; +pub const MAX_VERSION_LEN: usize = 32; +pub const MAX_DIGEST_LEN: usize = 64; // SHA-512 + +pub const MAX_OEMID_LEN: usize = 16; +pub const MAX_HW_MODEL_LEN: usize = 64; + +// Upper bound for a fully-populated COSE_Sign1 token +pub const MAX_TOKEN_SIZE: usize = 8192; diff --git a/services/attest/api/src/error.rs b/services/attest/api/src/error.rs new file mode 100644 index 00000000..c920c559 --- /dev/null +++ b/services/attest/api/src/error.rs @@ -0,0 +1,24 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#[derive(Debug, thiserror::Error)] +pub enum AttestError { + #[error("Mailbox error: {0}")] + Mailbox(&'static str), + #[error("DER parse error: {0}")] + Der(&'static str), + #[error("DICE chain validation error: {0}")] + ChainValidation(&'static str), + #[error("Invalid nonce: {0}")] + InvalidNonce(&'static str), + #[error("CBOR encoding error")] + Cbor, + #[error("Fixed-size buffer capacity exceeded")] + BufferFull, + #[error("COSE signing error")] + Cose, + #[error("Measurement provider error: {0}")] + Provider(&'static str), + #[error("Invalid key material: {0}")] + InvalidKey(&'static str), +} diff --git a/services/attest/api/src/lib.rs b/services/attest/api/src/lib.rs new file mode 100644 index 00000000..8600137d --- /dev/null +++ b/services/attest/api/src/lib.rs @@ -0,0 +1,42 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Platform-independent API for the OpenPRoT attestation producer service. +//! +//! # Usage +//! +//! Applications depend only on this crate. Platform code provides a concrete +//! [`AttestProducer`] implementation (hardware-backed via a platform signer, or the +//! `test-support`-gated software stub in the `openprot-attest-producer` crate). +//! +//! ```text +//! ┌───────────────────────────────────────────────┐ +//! │ application / verifier service │ +//! │ depends on: openprot-attest-api │ +//! │ calls: AttestProducer::generate_token│ +//! └──────────────────┬────────────────────────────┘ +//! │ trait object / generic bound +//! ┌──────────────────▼────────────────────────────┐ +//! │ openprot-attest-producer │ +//! │ HwAttestProducer (production, HW key) │ +//! │ SwAttestProducer (production, SW key) │ +//! │ SoftwareAttestProducer (test-support) │ +//! └───────────────────────────────────────────────┘ +//! ``` + +#![no_std] +#![forbid(unsafe_code)] + +pub mod consts; +mod error; +mod signing_abstraction; +mod traits; +mod types; + +pub use error::AttestError; +pub use signing_abstraction::{HwSigner, SwSigner}; +pub use traits::AttestProducer; +pub use types::{ + AttestConfig, DigestAlgorithm, Measurement, MeasurementAuthority, MeasurementProvider, OemId, + SwSignerConfig, +}; diff --git a/services/attest/api/src/signing_abstraction.rs b/services/attest/api/src/signing_abstraction.rs new file mode 100644 index 00000000..274a4331 --- /dev/null +++ b/services/attest/api/src/signing_abstraction.rs @@ -0,0 +1,184 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use heapless::Vec; +use p384::ecdsa::{signature::DigestSigner, Signature, SigningKey}; +use sha2::{Digest, Sha384}; + +use crate::consts::{MAX_CERT_SIZE, MAX_CHAIN_LEN, MAX_MEASUREMENTS}; +use crate::error::AttestError; +use crate::types::{Measurement, SwSignerConfig}; + +/// Hardware-backed signing operations implemented by a Caliptra mailbox driver. +/// +/// The private Alias Key never leaves the Caliptra hardware boundary. +/// Testing: implement with a software key (`SoftwareAttestProducer` in the +/// producer crate behind `test-support`). +pub trait HwSigner { + /// Sign `payload` with the platform alias key. Returns raw (r‖s) bytes. + fn sign(&self, payload: &[u8]) -> Result<[u8; 96], AttestError>; + /// Return the full DER-encoded certificate chain, leaf → root. + fn cert_chain_der( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError>; + /// Return Caliptra-internal firmware measurements (ROM, FMC, runtime, etc.). + fn measurements(&self, out: &mut Vec) + -> Result<(), AttestError>; +} + +/// Validates and holds a caller-supplied P-384 private key and cert chain. +/// +/// Constructed via [`SwSigner::new`], which checks that: +/// - The scalar is a valid P-384 private key (`1 ≤ d < n`, via `SigningKey::from_bytes`). +/// - The cert chain contains at least one certificate. +/// - Every certificate begins with `0x30` (DER SEQUENCE tag). +/// +/// The private key is held as a [`SigningKey`], which zeroizes on drop. +pub struct SwSigner { + signing_key: SigningKey, + chain: Vec, MAX_CHAIN_LEN>, +} + +impl SwSigner { + /// Construct a `SwSigner` from caller-supplied key material. + /// + /// Returns `Err(AttestError::InvalidKey)` if the scalar or cert chain + /// fails validation. + pub fn new(config: SwSignerConfig) -> Result { + let signing_key = SigningKey::from_bytes(config.private_key_scalar.as_ref().into()) + .map_err(|_| AttestError::InvalidKey("invalid P-384 scalar"))?; + + if config.cert_chain.is_empty() { + return Err(AttestError::InvalidKey("cert chain must not be empty")); + } + for cert in &config.cert_chain { + if cert.first() != Some(&0x30) { + return Err(AttestError::InvalidKey("cert is not a DER SEQUENCE")); + } + } + + // Clone cert_chain out before config drops (and zeroizes the scalar). + let chain = config.cert_chain.clone(); + Ok(Self { signing_key, chain }) + } +} + +impl HwSigner for SwSigner { + /// Signs `payload` with ECDSA P-384 (SHA-384 prehash). Returns raw r‖s (96 bytes). + fn sign(&self, payload: &[u8]) -> Result<[u8; 96], AttestError> { + let digest = Sha384::new_with_prefix(payload); + let sig: Signature = self.signing_key.sign_digest(digest); + let bytes = sig.to_bytes(); + let mut out = [0u8; 96]; + out.copy_from_slice(&bytes); + Ok(out) + } + + fn cert_chain_der( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + for cert in &self.chain { + buf.push(cert.clone()) + .map_err(|_| AttestError::BufferFull)?; + } + Ok(()) + } + + fn measurements( + &self, + _out: &mut Vec, + ) -> Result<(), AttestError> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::SwSignerConfig; + + fn valid_scalar() -> [u8; 48] { + // A fixed non-zero scalar well below the P-384 group order. + let mut s = [0u8; 48]; + s[47] = 1; + s + } + + fn valid_chain() -> Vec, MAX_CHAIN_LEN> { + let mut chain: Vec, MAX_CHAIN_LEN> = Vec::new(); + let mut cert: Vec = Vec::new(); + cert.push(0x30).unwrap(); + chain.push(cert).unwrap(); + chain + } + + #[test] + fn rejects_zero_scalar() { + let config = SwSignerConfig { + private_key_scalar: [0u8; 48], + cert_chain: valid_chain(), + }; + assert!(matches!( + SwSigner::new(config), + Err(AttestError::InvalidKey(_)) + )); + } + + #[test] + fn rejects_scalar_equal_to_group_order() { + // P-384 group order n — must be rejected (d must be < n). + let order: [u8; 48] = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0x63, 0x4D, 0x81, + 0xF4, 0x37, 0x2D, 0xDF, 0x58, 0x1A, 0x0D, 0xB2, 0x48, 0xB0, 0xA7, 0x7A, 0xEC, 0xEC, + 0x19, 0x6A, 0xCC, 0xC5, 0x29, 0x73, + ]; + let config = SwSignerConfig { + private_key_scalar: order, + cert_chain: valid_chain(), + }; + assert!(matches!( + SwSigner::new(config), + Err(AttestError::InvalidKey(_)) + )); + } + + #[test] + fn rejects_empty_cert_chain() { + let config = SwSignerConfig { + private_key_scalar: valid_scalar(), + cert_chain: Vec::new(), + }; + assert!(matches!( + SwSigner::new(config), + Err(AttestError::InvalidKey(_)) + )); + } + + #[test] + fn rejects_cert_without_der_sequence_tag() { + let mut chain: Vec, MAX_CHAIN_LEN> = Vec::new(); + let mut bad_cert: Vec = Vec::new(); + bad_cert.push(0x04).unwrap(); // OCTET STRING tag, not SEQUENCE + chain.push(bad_cert).unwrap(); + let config = SwSignerConfig { + private_key_scalar: valid_scalar(), + cert_chain: chain, + }; + assert!(matches!( + SwSigner::new(config), + Err(AttestError::InvalidKey(_)) + )); + } + + #[test] + fn accepts_valid_config() { + let config = SwSignerConfig { + private_key_scalar: valid_scalar(), + cert_chain: valid_chain(), + }; + assert!(SwSigner::new(config).is_ok()); + } +} diff --git a/services/attest/api/src/traits.rs b/services/attest/api/src/traits.rs new file mode 100644 index 00000000..019810f4 --- /dev/null +++ b/services/attest/api/src/traits.rs @@ -0,0 +1,28 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use heapless::Vec; + +use crate::consts::{MAX_CERT_SIZE, MAX_CHAIN_LEN, MAX_TOKEN_SIZE}; +use crate::AttestError; + +/// Platform-independent attestation producer interface. +pub trait AttestProducer { + /// Generate a signed OCP-EAT COSE_Sign1 token bound to `nonce`. + /// + /// The encoded token is appended to `out`. + fn generate_token( + &self, + nonce: &[u8], + out: &mut Vec, + ) -> Result<(), AttestError>; + + /// Return the current DICE certificate chain, ordered leaf → root. + /// + /// On success `buf` is **cleared** and then populated with the chain. + /// Any contents in `buf` before the call are discarded. + fn cert_chain( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError>; +} diff --git a/services/attest/api/src/types.rs b/services/attest/api/src/types.rs new file mode 100644 index 00000000..85e72d23 --- /dev/null +++ b/services/attest/api/src/types.rs @@ -0,0 +1,72 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use heapless::{String, Vec}; +use zeroize::Zeroize; + +use crate::consts::{ + MAX_CERT_SIZE, MAX_CHAIN_LEN, MAX_COMPONENT_LEN, MAX_DIGEST_LEN, MAX_HW_MODEL_LEN, + MAX_MEASUREMENTS, MAX_OEMID_LEN, MAX_VERSION_LEN, +}; +use crate::error::AttestError; + +/// OEM identifier (IANA Private Enterprise Number or UUID form). +#[derive(Clone, Debug)] +pub struct OemId(pub Vec); + +#[derive(Clone, Copy, Debug)] +pub enum DigestAlgorithm { + Sha384, + Sha512, +} + +#[derive(Clone, Copy, Debug)] +pub enum MeasurementAuthority { + Caliptra, + Platform, +} + +/// A single firmware measurement record to include in the EAT token. +#[derive(Clone, Debug)] +pub struct Measurement { + pub component: String, + pub version: String, + pub digest_alg: DigestAlgorithm, + pub digest: Vec, + pub authority: MeasurementAuthority, +} + +/// Caller-supplied material for the software signer path. +/// +/// `private_key_scalar` is the 48-byte big-endian P-384 private scalar `d`. +/// It must satisfy `1 ≤ d < n` (P-384 group order) — validated at construction +/// time by [`crate::SwSigner::new`]. +/// +/// `cert_chain` holds the DER-encoded certificate chain, leaf → root. Each +/// certificate must start with `0x30` (DER SEQUENCE). At least one certificate +/// is required. +pub struct SwSignerConfig { + pub private_key_scalar: [u8; 48], + pub cert_chain: Vec, MAX_CHAIN_LEN>, +} + +impl Drop for SwSignerConfig { + fn drop(&mut self) { + self.private_key_scalar.zeroize(); + } +} + +/// Producer configuration, set once at platform initialisation. +pub struct AttestConfig { + pub oemid: OemId, + pub hw_model: String, +} + +/// Platform-specific measurement source. +/// +/// Implement for each firmware component (UEFI, BMC, etc.) the platform +/// wants to measure beyond Caliptra-internal measurements. +pub trait MeasurementProvider { + fn measurements(&self, out: &mut Vec) + -> Result<(), AttestError>; +} diff --git a/services/attest/producer/BUILD.bazel b/services/attest/producer/BUILD.bazel new file mode 100644 index 00000000..820366a5 --- /dev/null +++ b/services/attest/producer/BUILD.bazel @@ -0,0 +1,38 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "attest_producer", + srcs = glob(["src/**/*.rs"]), + crate_name = "openprot_attest_producer", + edition = "2021", + visibility = ["//visibility:public"], + deps = [ + "//services/attest/api:attest_api", + "@rust_crates//:heapless", + "@rust_crates//:minicbor", + ], +) + +rust_test( + name = "attest_producer_unit_test", + crate = ":attest_producer", + features = ["test-support"], + deps = [ + "//services/attest/api:attest_api", + ], +) + +rust_test( + name = "attest_producer_integration_test", + srcs = ["tests/producer_integration.rs"], + edition = "2021", + features = ["test-support"], + deps = [ + ":attest_producer", + "//services/attest/api:attest_api", + "@rust_crates//:ciborium", + ], +) diff --git a/services/attest/producer/README.md b/services/attest/producer/README.md new file mode 100644 index 00000000..e5cc459a --- /dev/null +++ b/services/attest/producer/README.md @@ -0,0 +1,140 @@ + + +# openprot-attest-producer + +Concrete OCP-EAT attestation token producer for OpenPRoT. + +Implements the `AttestProducer` trait from `openprot-attest-api` with two +production backends and a test stub. + +## Source files + +| File | Contents | +|---|---| +| `src/lib.rs` | Public re-exports; feature gates. | +| `src/signer.rs` | `HwAttestProducer`, `SwAttestProducer`, and `SoftwareAttestProducer` (feature = `test-support`). | +| `src/builder.rs` | Assembles the CBOR claim map and constructs the `COSE_Sign1` envelope. Used by all three producers. | +| `src/der.rs` | Generic DER/X.509 primitives: `sequence_body`, `take_sequence`, `find_tag`, `has_extension_oid`, `is_x509_v3`, etc. Shared by `cert_ueid` and `dice_identity`. | +| `src/cert_ueid.rs` | Extracts the TCG UEID (OID 2.23.133.5.4.4) from a Caliptra DER certificate chain and verifies consistency across all certs that carry it. | +| `src/dice_identity.rs` | Retrieves the DER-encoded DICE certificate chain from the signer and validates it for Caliptra compliance: chain length ≥ 3, X.509 v3, and tcg-dice-MultiTcbInfo (OID 2.23.133.5.4.5) on all non-root certs. Used by `HwAttestProducer` only. | +| `src/measurements.rs` | Appends platform-registered `MeasurementProvider` outputs into the measurement buffer. | + +## Implementations + +### `HwAttestProducer` (production, hardware key) + +Backed by an `HwSigner` implementation from the Caliptra mailbox driver. +All ES384 signing operations occur inside the Caliptra hardware boundary; the +private Alias Key is never exposed to host software. Enforces full DICE chain +validation (length ≥ 3, X.509 v3, tcg-dice-MultiTcbInfo on all non-root certs). + +```rust +let mut producer = HwAttestProducer::new(&caliptra_driver, config); +producer.add_provider(&uefi_measurements)?; + +let mut out: Vec = Vec::new(); +producer.generate_token(&nonce, &mut out)?; +``` + +### `SwAttestProducer` (production, software key) + +Backed by a [`SwSigner`](../api/src/signing_abstraction.rs) holding a caller-supplied +P-384 private key and DER certificate chain. No Caliptra hardware required. +DICE chain validation is skipped (SW-generated certs carry no DICE extensions). + +The leaf certificate must carry the TCG UEID extension (OID 2.23.133.5.4.4). +If any other cert in the chain also carries the extension, its value must match +the leaf — a mismatch returns `Err(AttestError::ChainValidation)`. + +```rust +use openprot_attest_api::{SwSigner, SwSignerConfig}; +use heapless::Vec; + +let mut chain: Vec, MAX_CHAIN_LEN> = Vec::new(); +// ... push DER-encoded certs ... + +let sw_config = SwSignerConfig { + private_key_scalar: my_48_byte_scalar, + cert_chain: chain, +}; +let signer = SwSigner::new(sw_config)?; // validates scalar and certs + +let config = AttestConfig { oemid, hw_model }; +let producer = SwAttestProducer::new(signer, config); + +let mut out: Vec = Vec::new(); +producer.generate_token(&nonce, &mut out)?; +``` + +`SwSigner::new` returns `Err(AttestError::InvalidKey)` if the scalar is not a +valid P-384 private key, or any certificate fails the DER SEQUENCE check. + +### `SoftwareAttestProducer` (feature = `test-support`) + +Software-only stub for unit and integration tests. Uses a deterministic +all-zero ES384 signature and placeholder DER certificates. No Caliptra +hardware or driver is required. + +```rust +#[cfg(feature = "test-support")] +let producer = SoftwareAttestProducer::new(config); +let mut out: Vec = Vec::new(); +producer.generate_token(&nonce, &mut out)?; +``` + +Enable the feature in `Cargo.toml`: + +```toml +[dev-dependencies] +openprot-attest-producer = { path = "services/attest/producer", features = ["test-support"] } +``` + +## Token structure + +`builder.rs` produces a `COSE_Sign1`-wrapped CWT conforming to the OCP-EAT +profile. The algorithm identifier (`-35` = ES384) is in the protected header. +The `x5chain` certificate chain is in the **unprotected** header (key 33, not +covered by the signature, per the OCP-EAT profile). + +Claims are written in CBOR deterministic encoding order (RFC 8949 §4.2.1). + +| Claim | Key | Required | Description | +|---|---|---|---| +| `eat_nonce` | 10 | MUST | Caller-supplied freshness nonce (8–64 bytes). | +| `ueid` | 256 | OPTIONAL | Device UEID from the TCG UEID extension in the leaf cert. | +| `oemid` | 258 | OPTIONAL | OEM identifier (IANA PEN form). | +| `hwmodel` | 259 | OPTIONAL | Hardware model string. | +| `dbgstat` | 263 | MUST | Debug status (hardcoded `3` = disabled). | +| `eat_profile` | 265 | MUST | OCP EAT profile OID `1.3.6.1.4.1.42623.1.3`. | +| `measurements` | 273 | MUST | Per-component firmware measurement records. | + +The CWT payload is wrapped as `tag(55799, tag(61, map))` per the OCP profile +requirement for self-described CBOR. + +## Dependencies + +| Crate | Purpose | +|---|---| +| `openprot-attest-api` | Trait and type definitions (`AttestProducer`, `HwSigner`, `AttestError`, etc.) | +| `minicbor` | `no_std` CBOR encoding of token claims and `COSE_Sign1` envelope | + +## Cargo build + +```bash +# Production build +cargo build -p openprot-attest-producer + +# With software stub +cargo build -p openprot-attest-producer --features test-support + +# Tests (software stub required) +cargo test -p openprot-attest-producer --features test-support +``` + +## Bazel targets + +``` +//services/attest/producer:attest_producer # production library +//services/attest/producer:attest_producer_unit_test # unit tests +//services/attest/producer:attest_producer_integration_test # integration tests +``` diff --git a/services/attest/producer/src/builder.rs b/services/attest/producer/src/builder.rs new file mode 100644 index 00000000..eae90010 --- /dev/null +++ b/services/attest/producer/src/builder.rs @@ -0,0 +1,491 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! OCP-EAT COSE_Sign1 token assembly. +//! +//! Token structure (CBOR diagnostic notation): +//! +//! ```text +//! 18( ; COSE_Sign1 +//! [ +//! << { 1: -35 } >>, ; protected header (alg only) +//! { 33: [cert-chain...] }, ; unprotected header (x5chain per OCP profile) +//! << 55799(61({ ...CWT claims... })) >>, ; payload (self-described EAT CWT) +//! h'...' ; ES384 signature +//! ] +//! ) +//! ``` +//! +//! Claim key numbers follow RFC 9711 and the OCP-EAT profile. +//! Claim order follows CBOR deterministic encoding (RFC 8949 §4.2.1): +//! keys sorted by bytewise lexicographic order of their CBOR encodings. + +use heapless::Vec; +use minicbor::encode::write::EndOfSlice; +use minicbor::Encoder; + +use openprot_attest_api::consts::{MAX_CERT_SIZE, MAX_CHAIN_LEN, MAX_TOKEN_SIZE}; +use openprot_attest_api::{AttestConfig, AttestError, DigestAlgorithm, HwSigner, Measurement}; + +use crate::cert_ueid::UEID_LEN; + +// Registered EAT claim keys (OCP-EAT profile / RFC 9711 / RFC 8392). +// Written in CBOR deterministic order (RFC 8949 §4.2.1): sorted by +// bytewise lexicographic order of each key's CBOR encoding. +// 1-byte key (0x0a): nonce +// 3-byte keys (0x1901xx): ueid, oemid, hwmodel, dbgstat, eat_profile, measurements +const CLAIM_NONCE: i64 = 10; +const CLAIM_UEID: i64 = 256; +const CLAIM_OEMID: i64 = 258; +const CLAIM_HWMODEL: i64 = 259; +const CLAIM_DBGSTAT: i64 = 263; +const CLAIM_EAT_PROFILE: i64 = 265; +const CLAIM_MEASUREMENTS: i64 = 273; + +const ALG_ES384: i64 = -35; +const HDR_X5CHAIN: i64 = 33; + +// OID 1.3.6.1.4.1.42623.1.3 encoded as raw OID content bytes (~oid per OCP profile CDDL). +const OCP_PROFILE_OID: [u8; 10] = [0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0xCC, 0x7F, 0x01, 0x03]; + +// Protected header contains only {1: -35} ≈ 5 bytes. +const PHDR_SCRATCH: usize = 16; +// Payload scratch: fixed-claim overhead (~128 B) + per-measurement bound. +// Per measurement: component (MAX_COMPONENT_LEN) + version (MAX_VERSION_LEN) +// + digest (MAX_DIGEST_LEN) + ~12 B CBOR framing = ~192 B each. +const PAYLOAD_SCRATCH: usize = 256 + openprot_attest_api::consts::MAX_MEASUREMENTS * 192; +// Sig_Structure scratch: payload + Signature1 context (~80 B overhead). +const SIG_SCRATCH: usize = PAYLOAD_SCRATCH + 80; + +/// Writer over a fixed `[u8]` slice; tracks how many bytes have been written. +type BufWriter<'a> = minicbor::encode::write::Cursor<&'a mut [u8]>; + +fn cbor_err(e: minicbor::encode::Error) -> AttestError { + if e.is_write() { + AttestError::BufferFull + } else { + AttestError::Cbor + } +} + +/// Nonce length bounds per RFC 9711 §4.3.4.3 and OCP-EAT profile. +const MIN_NONCE_LEN: usize = 8; +const MAX_NONCE_LEN: usize = 64; + +/// Build and sign a complete OCP-EAT token into `out`. +pub(crate) fn build( + config: &AttestConfig, + signer: &dyn HwSigner, + ueid: &[u8; UEID_LEN], + measurements: &[Measurement], + nonce: &[u8], + out: &mut Vec, +) -> Result<(), AttestError> { + if nonce.len() < MIN_NONCE_LEN || nonce.len() > MAX_NONCE_LEN { + return Err(AttestError::InvalidNonce( + "nonce must be 8–64 bytes (RFC 9711 §4.3.4.3, OCP-EAT profile)", + )); + } + + // Fetch cert chain into a stack buffer. + let mut chain: Vec, MAX_CHAIN_LEN> = Vec::new(); + signer.cert_chain_der(&mut chain)?; + + // ── Encode protected header ──────────────────────────────────────────── + // Per OCP-EAT profile: protected header contains only the algorithm ID. + // x5chain goes in the unprotected header (not covered by the signature). + // Must be encoded before signing so it can be included in Sig_Structure. + let mut phdr_scratch = [0u8; PHDR_SCRATCH]; + let phdr_len = (|| -> Result> { + let mut w = BufWriter::new(&mut phdr_scratch[..]); + let mut e = Encoder::new(&mut w); + e.map(1)?; + e.i64(1)?; + e.i64(ALG_ES384)?; + Ok(w.position()) + })() + .map_err(cbor_err)?; + let phdr_bytes = &phdr_scratch[..phdr_len]; + + // ── Encode CWT claims map ────────────────────────────────────────────── + // Claims written in CBOR deterministic order (RFC 8949 §4.2.1): + // 10=nonce, 256=ueid, 258=oemid, 259=hwmodel, 263=dbgstat, + // 265=eat_profile, 273=measurements → 7 claims total. + let mut payload_scratch = [0u8; PAYLOAD_SCRATCH]; + let payload_len = (|| -> Result> { + let mut w = BufWriter::new(&mut payload_scratch[..]); + let mut e = Encoder::new(&mut w); + e.tag(minicbor::data::Tag::new(55799))?; + e.tag(minicbor::data::Tag::new(61))?; + e.map(7)?; + + e.i64(CLAIM_NONCE)?; + e.bytes(nonce)?; + e.i64(CLAIM_UEID)?; + e.bytes(ueid)?; + e.i64(CLAIM_OEMID)?; + e.bytes(&config.oemid.0)?; + e.i64(CLAIM_HWMODEL)?; + e.str(&config.hw_model)?; + e.i64(CLAIM_DBGSTAT)?; + e.i64(3)?; + e.i64(CLAIM_EAT_PROFILE)?; + e.bytes(&OCP_PROFILE_OID)?; + + e.i64(CLAIM_MEASUREMENTS)?; + e.array(measurements.len() as u64)?; + for m in measurements { + let alg: i64 = match m.digest_alg { + DigestAlgorithm::Sha384 => -43, + DigestAlgorithm::Sha512 => -44, + }; + e.array(3)?; + e.str(&m.component)?; + e.i64(alg)?; + e.bytes(&m.digest)?; + } + + Ok(w.position()) + })() + .map_err(cbor_err)?; + let payload_bytes = &payload_scratch[..payload_len]; + + // ── Sign ────────────────────────────────────────────────────────────── + // RFC 9052 §4.4: Sig_Structure = ["Signature1", phdr_bstr, h'', payload_bstr] + let sig = { + let mut sig_scratch = [0u8; SIG_SCRATCH]; + let sig_input_len = (|| -> Result> { + let mut w = BufWriter::new(&mut sig_scratch[..]); + let mut e = Encoder::new(&mut w); + e.array(4)?; + e.str("Signature1")?; + e.bytes(phdr_bytes)?; + e.bytes(b"")?; + e.bytes(payload_bytes)?; + Ok(w.position()) + })() + .map_err(cbor_err)?; + signer.sign(&sig_scratch[..sig_input_len])? + }; + + // ── Assemble COSE_Sign1 directly into `out` ──────────────────────────── + // Encode into out's backing memory, then truncate to actual length. + // This avoids a separate cose_scratch buffer and the copy that follows. + out.resize_default(MAX_TOKEN_SIZE) + .map_err(|_| AttestError::BufferFull)?; + let cose_len = (|| -> Result> { + let mut w = BufWriter::new(&mut out[..]); + let mut e = Encoder::new(&mut w); + e.tag(minicbor::data::Tag::new(18))?; + e.array(4)?; + e.bytes(phdr_bytes)?; + e.map(1)?; + e.i64(HDR_X5CHAIN)?; + e.array(chain.len() as u64)?; + for cert in &chain { + e.bytes(cert)?; + } + e.bytes(payload_bytes)?; + e.bytes(&sig)?; + Ok(w.position()) + })() + .map_err(cbor_err)?; + out.truncate(cose_len); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use heapless::{String, Vec}; + use openprot_attest_api::consts::{ + MAX_CERT_SIZE, MAX_CHAIN_LEN, MAX_COMPONENT_LEN, MAX_DIGEST_LEN, MAX_MEASUREMENTS, + MAX_TOKEN_SIZE, MAX_VERSION_LEN, + }; + use openprot_attest_api::{AttestError, DigestAlgorithm, MeasurementAuthority, OemId}; + + use crate::signer::STUB_CERT; + + struct TestSigner; + + impl HwSigner for TestSigner { + fn sign(&self, _: &[u8]) -> Result<[u8; 96], AttestError> { + Ok([0u8; 96]) + } + fn cert_chain_der( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + let mut c0: Vec = Vec::new(); + c0.extend_from_slice(&STUB_CERT).unwrap(); + let mut c1: Vec = Vec::new(); + c1.extend_from_slice(&STUB_CERT).unwrap(); + buf.push(c0).map_err(|_| AttestError::BufferFull)?; + buf.push(c1).map_err(|_| AttestError::BufferFull)?; + Ok(()) + } + fn measurements( + &self, + _out: &mut Vec, + ) -> Result<(), AttestError> { + Ok(()) + } + } + + fn config() -> openprot_attest_api::AttestConfig { + let mut hw_model: String<64> = String::new(); + hw_model.push_str("TestModel").unwrap(); + let mut oemid_bytes: Vec = Vec::new(); + oemid_bytes + .extend_from_slice(&[0x00, 0x01, 0x47, 0xae]) + .unwrap(); + openprot_attest_api::AttestConfig { + oemid: OemId(oemid_bytes), + hw_model, + } + } + + fn meas() -> Vec { + let mut v = Vec::new(); + let mut component: String = String::new(); + component.push_str("Test ROM").unwrap(); + let mut version: String = String::new(); + version.push_str("1.0.0").unwrap(); + let mut digest: Vec = Vec::new(); + digest.extend_from_slice(&[0xAAu8; 48]).unwrap(); + v.push(Measurement { + component, + version, + digest_alg: DigestAlgorithm::Sha384, + digest, + authority: MeasurementAuthority::Caliptra, + }) + .unwrap(); + v + } + + const STUB_UEID: [u8; crate::cert_ueid::UEID_LEN] = [0x01u8; crate::cert_ueid::UEID_LEN]; + + fn build_token() -> Vec { + let mut out = Vec::new(); + build( + &config(), + &TestSigner, + &STUB_UEID, + &meas(), + b"testnonce", + &mut out, + ) + .unwrap(); + out + } + + fn decode_outer(token: &[u8]) -> (Vec, Vec) { + // Minimal CBOR decode: 18([phdr-bstr, {33:[...]}, payload-bstr, sig-bstr]) + // Return (phdr_bytes, payload_bytes). + let mut d = minicbor::Decoder::new(token); + d.tag().unwrap(); // tag(18) + d.array().unwrap(); // outer array len + let phdr = d.bytes().unwrap(); + let mut phdr_v: Vec = Vec::new(); + phdr_v.extend_from_slice(phdr).unwrap(); + d.skip().unwrap(); // unprotected header map (x5chain) + let payload = d.bytes().unwrap(); + let mut payload_v: Vec = Vec::new(); + payload_v.extend_from_slice(payload).unwrap(); + (phdr_v, payload_v) + } + + fn cwt_decoder(payload: &[u8]) -> minicbor::Decoder<'_> { + let mut d = minicbor::Decoder::new(payload); + d.tag().unwrap(); // tag(55799) self-described CBOR + d.tag().unwrap(); // tag(61) CWT + d + } + + fn find_claim_bytes(payload: &[u8], key: i64) -> Option<&[u8]> { + let mut d = cwt_decoder(payload); + let n = d.map().unwrap().unwrap_or(0); + for _ in 0..n { + let k = d.i64().unwrap(); + if k == key { + return Some(d.bytes().unwrap()); + } + d.skip().unwrap(); + } + None + } + + fn find_claim_str(payload: &[u8], key: i64) -> Option<&str> { + let mut d = cwt_decoder(payload); + let n = d.map().unwrap().unwrap_or(0); + for _ in 0..n { + let k = d.i64().unwrap(); + if k == key { + return Some(d.str().unwrap()); + } + d.skip().unwrap(); + } + None + } + + fn find_claim_i64(payload: &[u8], key: i64) -> Option { + let mut d = cwt_decoder(payload); + let n = d.map().unwrap().unwrap_or(0); + for _ in 0..n { + let k = d.i64().unwrap(); + if k == key { + return Some(d.i64().unwrap()); + } + d.skip().unwrap(); + } + None + } + + #[test] + fn output_is_four_element_cbor_array() { + let token = build_token(); + let mut d = minicbor::Decoder::new(&token); + assert_eq!(d.tag().unwrap(), minicbor::data::Tag::new(18)); // COSE_Sign1 + assert_eq!(d.array().unwrap(), Some(4)); + } + + #[test] + fn nonce_appears_in_payload() { + let mut out = Vec::::new(); + build( + &config(), + &TestSigner, + &STUB_UEID, + &meas(), + b"testnonce", + &mut out, + ) + .unwrap(); + let (_, payload) = decode_outer(&out); + assert_eq!( + find_claim_bytes(&payload, CLAIM_NONCE), + Some(b"testnonce" as &[u8]) + ); + } + + #[test] + fn hw_model_in_payload() { + let token = build_token(); + let (_, payload) = decode_outer(&token); + assert_eq!(find_claim_str(&payload, CLAIM_HWMODEL), Some("TestModel")); + } + + #[test] + fn sign_receives_cose_sig_structure_for_payload() { + // Verify that signer.sign() is called with the RFC 9052 §4.4 + // Sig_Structure: array(4) ["Signature1", phdr_bstr, h'', payload_bstr]. + use core::cell::RefCell; + let captured: RefCell> = RefCell::new(heapless::Vec::new()); + + struct CapturingSigner<'a>(&'a RefCell>); + impl HwSigner for CapturingSigner<'_> { + fn sign(&self, payload: &[u8]) -> Result<[u8; 96], AttestError> { + let mut buf = self.0.borrow_mut(); + buf.clear(); + buf.extend_from_slice(&payload[..payload.len().min(2048)]) + .unwrap(); + Ok([0u8; 96]) + } + fn cert_chain_der( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + let mut c0: Vec = Vec::new(); + c0.extend_from_slice(&STUB_CERT).unwrap(); + let mut c1: Vec = Vec::new(); + c1.extend_from_slice(&STUB_CERT).unwrap(); + buf.push(c0).map_err(|_| AttestError::BufferFull)?; + buf.push(c1).map_err(|_| AttestError::BufferFull) + } + fn measurements( + &self, + _out: &mut Vec, + ) -> Result<(), AttestError> { + Ok(()) + } + } + + let signer = CapturingSigner(&captured); + let mut out = Vec::::new(); + build( + &config(), + &signer, + &STUB_UEID, + &meas(), + b"testnonce", + &mut out, + ) + .unwrap(); + + let cap = captured.borrow(); + let mut d = minicbor::Decoder::new(&cap); + assert_eq!(d.array().unwrap(), Some(4)); // Sig_Structure is array(4) + assert_eq!(d.str().unwrap(), "Signature1"); // context string + assert!(!d.bytes().unwrap().is_empty()); // phdr_bstr (non-empty) + assert!(d.bytes().unwrap().is_empty()); // aad = h'' + // payload_bstr decodes to tag(55799, tag(61, CWT map)) + let payload_bstr = d.bytes().unwrap(); + let mut pd = minicbor::Decoder::new(payload_bstr); + assert_eq!(pd.tag().unwrap(), minicbor::data::Tag::new(55799)); + assert_eq!(pd.tag().unwrap(), minicbor::data::Tag::new(61)); + } + + #[test] + fn nonce_outside_rfc9711_length_range_is_rejected() { + let mut out = Vec::::new(); + // Too short: 5 bytes < MIN_NONCE_LEN (8) + let err = build( + &config(), + &TestSigner, + &STUB_UEID, + &meas(), + b"short", + &mut out, + ) + .unwrap_err(); + assert!(matches!(err, AttestError::InvalidNonce(_))); + // Too long: 65 bytes > MAX_NONCE_LEN (64) + let err = build( + &config(), + &TestSigner, + &STUB_UEID, + &meas(), + &[0xFFu8; 65], + &mut out, + ) + .unwrap_err(); + assert!(matches!(err, AttestError::InvalidNonce(_))); + } + + #[test] + fn dbgstat_ueid_oemid_and_sw_claims_match_config() { + let token = build_token(); + let (_, payload) = decode_outer(&token); + // dbgstat = 3 (disabled, hardcoded per OCP-EAT profile) + assert_eq!(find_claim_i64(&payload, CLAIM_DBGSTAT), Some(3)); + // ueid matches STUB_UEID passed to build() + assert_eq!( + find_claim_bytes(&payload, CLAIM_UEID), + Some(STUB_UEID.as_slice()) + ); + // oemid matches config + assert_eq!( + find_claim_bytes(&payload, CLAIM_OEMID), + Some(&[0x00u8, 0x01, 0x47, 0xae][..]) + ); + // eat_profile OID matches spec + assert_eq!( + find_claim_bytes(&payload, CLAIM_EAT_PROFILE), + Some(OCP_PROFILE_OID.as_slice()) + ); + // verify total fixed claims = 7 + let mut d = cwt_decoder(&payload); + assert_eq!(d.map().unwrap(), Some(7)); + } +} diff --git a/services/attest/producer/src/cert_ueid.rs b/services/attest/producer/src/cert_ueid.rs new file mode 100644 index 00000000..a616cb5e --- /dev/null +++ b/services/attest/producer/src/cert_ueid.rs @@ -0,0 +1,234 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! TCG UEID extraction from a Caliptra DICE certificate chain. +//! +//! The Caliptra DICE chain encodes the device UEID in an X.509 extension with +//! OID 2.23.133.5.4.4 (TCG UEID). The extension value is an OCTET STRING whose +//! contents are `SEQUENCE { OCTET STRING(ueid_bytes) }`. +//! +//! This module is intentionally narrow: it only handles cert structures +//! Caliptra emits. Every cert in the Caliptra chain is X.509 v3 with at least +//! one extension; a cert with no `[3]` extensions wrapper is treated as +//! malformed and returns `Err`. A cert that has extensions but lacks the TCG +//! UEID OID returns `Ok(None)` (e.g. intermediate or root CA certs). + +use heapless::Vec; +use openprot_attest_api::{consts::MAX_CERT_SIZE, consts::MAX_CHAIN_LEN, AttestError}; + +use crate::der::{ + find_tag, octet_string_body, sequence_body, skip_optional_boolean, take_sequence, +}; + +/// DER encoding of OID 2.23.133.5.4.4 (TCG UEID extension). +const OID_TCG_UEID: [u8; 8] = [0x06, 0x06, 0x67, 0x81, 0x05, 0x05, 0x04, 0x04]; + +/// Length of the UEID value in Caliptra certs (17 bytes: 1-byte type + 16-byte ID). +pub const UEID_LEN: usize = 17; + +/// Extract the UEID bytes from a single DER-encoded X.509 certificate. +/// +/// Returns `Ok(None)` if the cert has a `[3]` extensions wrapper but does not +/// carry the TCG UEID OID (e.g. a root CA or intermediate without UEID). +/// Returns `Err` if the DER structure is malformed (missing extensions wrapper, +/// bad length encoding, unexpected UEID field length). +fn extract(cert_der: &[u8]) -> Result, AttestError> { + let tbs = sequence_body(cert_der).ok_or(AttestError::Der("cert: bad outer SEQUENCE"))?; + let tbs_body = sequence_body(tbs).ok_or(AttestError::Der("cert: bad TBS SEQUENCE"))?; + + // All Caliptra-emitted certs are X.509 v3 with extensions. A missing [3] + // wrapper indicates a malformed cert, not a "no UEID" condition. + let extensions_wrapper = + find_tag(tbs_body, 0xa3).ok_or(AttestError::Der("cert: no extensions wrapper"))?; + + let ext_seq = sequence_body(extensions_wrapper) + .ok_or(AttestError::Der("cert: bad extensions SEQUENCE"))?; + + let mut remaining = ext_seq; + while !remaining.is_empty() { + let (ext_body, rest) = + take_sequence(remaining).ok_or(AttestError::Der("cert: bad extension entry"))?; + remaining = rest; + + if ext_body.starts_with(&OID_TCG_UEID) { + let after_oid = &ext_body[OID_TCG_UEID.len()..]; + let extn_value_outer = skip_optional_boolean(after_oid); + let contents = octet_string_body(extn_value_outer) + .ok_or(AttestError::Der("ueid: bad extnValue OCTET STRING"))?; + let inner_seq = + sequence_body(contents).ok_or(AttestError::Der("ueid: bad inner SEQUENCE"))?; + let ueid_bytes = octet_string_body(inner_seq) + .ok_or(AttestError::Der("ueid: bad inner OCTET STRING"))?; + if ueid_bytes.len() != UEID_LEN { + return Err(AttestError::Der("ueid: unexpected length")); + } + let mut out = [0u8; UEID_LEN]; + out.copy_from_slice(ueid_bytes); + return Ok(Some(out)); + } + } + Ok(None) +} + +/// Extract the UEID from the leaf cert and verify every other cert in the chain +/// that carries a TCG UEID extension has the same value. +/// +/// The leaf cert is at index 0 (AliasRT). Intermediate and root CA certs may +/// not carry the UEID extension; those return `Ok(None)` from `extract` and +/// are skipped without error. +pub fn extract_and_verify( + chain: &Vec, MAX_CHAIN_LEN>, +) -> Result<[u8; UEID_LEN], AttestError> { + if chain.is_empty() { + return Err(AttestError::ChainValidation("cert chain is empty")); + } + + let leaf_ueid = extract(&chain[0])?.ok_or(AttestError::ChainValidation( + "leaf cert missing TCG UEID extension", + ))?; + + for cert in chain.iter().skip(1) { + if let Some(ueid) = extract(cert)? { + if ueid != leaf_ueid { + return Err(AttestError::ChainValidation( + "UEID mismatch across certificate chain", + )); + } + } + } + + Ok(leaf_ueid) +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn make_cert_with_ueid(ueid: &[u8; UEID_LEN]) -> heapless::Vec { + let inner_os = der_tlv(0x04, ueid); + let inner_seq = der_tlv(0x30, &inner_os); + let extn_value = der_tlv(0x04, &inner_seq); + let mut ext_body: heapless::Vec = heapless::Vec::new(); + ext_body.extend_from_slice(&OID_TCG_UEID).unwrap(); + ext_body.extend_from_slice(&extn_value).unwrap(); + let ext_seq = der_tlv(0x30, &ext_body); + let exts_seq = der_tlv(0x30, &ext_seq); + let exts_wrapper = der_tlv(0xa3, &exts_seq); + + let mut tbs_body: heapless::Vec = heapless::Vec::new(); + tbs_body + .extend_from_slice(&[0xa0, 0x03, 0x02, 0x01, 0x02]) + .unwrap(); + tbs_body.extend_from_slice(&[0x02, 0x01, 0x01]).unwrap(); + for _ in 0..5 { + tbs_body.extend_from_slice(&[0x30, 0x00]).unwrap(); + } + tbs_body.extend_from_slice(&exts_wrapper).unwrap(); + let tbs = der_tlv(0x30, &tbs_body); + + let mut cert_body: heapless::Vec = heapless::Vec::new(); + cert_body.extend_from_slice(&tbs).unwrap(); + cert_body.extend_from_slice(&[0x30, 0x00]).unwrap(); + cert_body.extend_from_slice(&[0x03, 0x01, 0x00]).unwrap(); + let mut buf: heapless::Vec = heapless::Vec::new(); + buf.extend_from_slice(&der_tlv(0x30, &cert_body)).unwrap(); + buf + } + + fn der_tlv(tag: u8, value: &[u8]) -> heapless::Vec { + let mut out: heapless::Vec = heapless::Vec::new(); + out.push(tag).unwrap(); + let l = value.len(); + if l < 128 { + out.push(l as u8).unwrap(); + } else if l < 256 { + out.push(0x81).unwrap(); + out.push(l as u8).unwrap(); + } else { + out.push(0x82).unwrap(); + out.push((l >> 8) as u8).unwrap(); + out.push((l & 0xff) as u8).unwrap(); + } + out.extend_from_slice(value).unwrap(); + out + } + + #[test] + fn extracts_ueid_from_cert() { + let ueid = [0xABu8; UEID_LEN]; + let cert = make_cert_with_ueid(&ueid); + assert_eq!(extract(&cert).unwrap(), Some(ueid)); + } + + #[test] + fn returns_err_for_cert_with_no_extensions_wrapper() { + let tbs_body = [ + 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, + 0x30, 0x00, 0x30, 0x00, + ]; + let tbs = der_tlv(0x30, &tbs_body); + let mut cert_body: heapless::Vec = heapless::Vec::new(); + cert_body.extend_from_slice(&tbs).unwrap(); + cert_body.extend_from_slice(&[0x30, 0x00]).unwrap(); + cert_body.extend_from_slice(&[0x03, 0x01, 0x00]).unwrap(); + let cert = der_tlv(0x30, &cert_body); + assert!(extract(&cert).is_err()); + } + + #[test] + fn returns_none_for_cert_with_extensions_but_no_ueid() { + let other_oid: [u8; 5] = [0x06, 0x03, 0x55, 0x1d, 0x0e]; + let ext_val = der_tlv(0x04, &[0x04, 0x14]); + let mut ext_body: heapless::Vec = heapless::Vec::new(); + ext_body.extend_from_slice(&other_oid).unwrap(); + ext_body.extend_from_slice(&ext_val).unwrap(); + let exts_wrapper = der_tlv(0xa3, &der_tlv(0x30, &der_tlv(0x30, &ext_body))); + + let mut tbs_body: heapless::Vec = heapless::Vec::new(); + tbs_body + .extend_from_slice(&[0xa0, 0x03, 0x02, 0x01, 0x02]) + .unwrap(); + tbs_body.extend_from_slice(&[0x02, 0x01, 0x01]).unwrap(); + for _ in 0..5 { + tbs_body.extend_from_slice(&[0x30, 0x00]).unwrap(); + } + tbs_body.extend_from_slice(&exts_wrapper).unwrap(); + let tbs = der_tlv(0x30, &tbs_body); + + let mut cert_body: heapless::Vec = heapless::Vec::new(); + cert_body.extend_from_slice(&tbs).unwrap(); + cert_body.extend_from_slice(&[0x30, 0x00]).unwrap(); + cert_body.extend_from_slice(&[0x03, 0x01, 0x00]).unwrap(); + let cert = der_tlv(0x30, &cert_body); + assert_eq!(extract(&cert).unwrap(), None); + } + + #[test] + fn verify_passes_when_all_certs_agree() { + let ueid = [0x11u8; UEID_LEN]; + let cert = make_cert_with_ueid(&ueid); + let mut chain: Vec, MAX_CHAIN_LEN> = Vec::new(); + for _ in 0..2 { + let mut c: Vec = Vec::new(); + c.extend_from_slice(&cert).unwrap(); + chain.push(c).unwrap(); + } + assert_eq!(extract_and_verify(&chain).unwrap(), ueid); + } + + #[test] + fn verify_fails_on_mismatch() { + let cert_a = make_cert_with_ueid(&[0x11u8; UEID_LEN]); + let cert_b = make_cert_with_ueid(&[0x22u8; UEID_LEN]); + let mut chain: Vec, MAX_CHAIN_LEN> = Vec::new(); + let mut c0: Vec = Vec::new(); + c0.extend_from_slice(&cert_a).unwrap(); + let mut c1: Vec = Vec::new(); + c1.extend_from_slice(&cert_b).unwrap(); + chain.push(c0).unwrap(); + chain.push(c1).unwrap(); + assert!(extract_and_verify(&chain).is_err()); + } +} diff --git a/services/attest/producer/src/der.rs b/services/attest/producer/src/der.rs new file mode 100644 index 00000000..9020c27b --- /dev/null +++ b/services/attest/producer/src/der.rs @@ -0,0 +1,112 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Minimal DER/X.509 primitives shared by `cert_ueid` and `dice_identity`. + +use openprot_attest_api::AttestError; + +/// Parse a DER length field at `data[0..]`. Returns `(length, bytes_consumed)`. +pub(crate) fn parse_length(data: &[u8]) -> Option<(usize, usize)> { + let first = *data.first()?; + if first < 0x80 { + Some((first as usize, 1)) + } else { + let n = (first & 0x7f) as usize; + if n == 0 || n > core::mem::size_of::() || data.len() < 1 + n { + return None; + } + let mut len = 0usize; + for &b in &data[1..1 + n] { + len = len.checked_shl(8)?.checked_add(b as usize)?; + } + Some((len, 1 + n)) + } +} + +/// Return the body of the first SEQUENCE found at the start of `data`. +pub(crate) fn sequence_body(data: &[u8]) -> Option<&[u8]> { + if data.first()? != &0x30 { + return None; + } + let (len, consumed) = parse_length(&data[1..])?; + data.get(1 + consumed..1 + consumed + len) +} + +/// Return the body and remaining bytes after consuming the first SEQUENCE in `data`. +pub(crate) fn take_sequence(data: &[u8]) -> Option<(&[u8], &[u8])> { + if data.first()? != &0x30 { + return None; + } + let (len, consumed) = parse_length(&data[1..])?; + let body = data.get(1 + consumed..1 + consumed + len)?; + let rest = data.get(1 + consumed + len..)?; + Some((body, rest)) +} + +/// Return the body of the first OCTET STRING at the start of `data`. +pub(crate) fn octet_string_body(data: &[u8]) -> Option<&[u8]> { + if data.first()? != &0x04 { + return None; + } + let (len, consumed) = parse_length(&data[1..])?; + data.get(1 + consumed..1 + consumed + len) +} + +/// Scan `data` for the first TLV with the given tag, returning its value bytes. +pub(crate) fn find_tag(data: &[u8], tag: u8) -> Option<&[u8]> { + let mut pos = 0; + while pos < data.len() { + let t = data[pos]; + let (len, consumed) = parse_length(data.get(pos + 1..)?)?; + if t == tag { + return data.get(pos + 1 + consumed..pos + 1 + consumed + len); + } + pos = pos.checked_add(1 + consumed + len)?; + } + None +} + +/// If `data` starts with a DER BOOLEAN TLV, skip past it and return the rest. +pub(crate) fn skip_optional_boolean(data: &[u8]) -> &[u8] { + if data.first() == Some(&0x01) { + if let Some((len, consumed)) = parse_length(&data[1..]) { + let end = 1 + consumed + len; + if end <= data.len() { + return &data[end..]; + } + } + } + data +} + +/// Return `true` if `cert_der` is an X.509 v3 certificate (version field = 2). +pub(crate) fn is_x509_v3(cert_der: &[u8]) -> bool { + const V3: [u8; 5] = [0xA0, 0x03, 0x02, 0x01, 0x02]; + sequence_body(cert_der) + .and_then(sequence_body) + .and_then(|b| b.get(..5)) + .is_some_and(|v| v == V3) +} + +/// Return `true` if `cert_der` contains an X.509 extension whose OID bytes +/// (full DER TLV) match `oid`. Returns `false` if the certificate carries no +/// extensions at all (valid for root CA certs). +pub(crate) fn has_extension_oid(cert_der: &[u8], oid: &[u8]) -> Result { + let tbs = sequence_body(cert_der).ok_or(AttestError::Der("bad outer SEQUENCE"))?; + let tbs_body = sequence_body(tbs).ok_or(AttestError::Der("bad TBS SEQUENCE"))?; + let ext_wrapper = match find_tag(tbs_body, 0xa3) { + Some(e) => e, + None => return Ok(false), + }; + let ext_seq = sequence_body(ext_wrapper).ok_or(AttestError::Der("bad extensions SEQUENCE"))?; + let mut remaining = ext_seq; + while !remaining.is_empty() { + let (ext_body, rest) = + take_sequence(remaining).ok_or(AttestError::Der("bad extension entry"))?; + remaining = rest; + if ext_body.starts_with(oid) { + return Ok(true); + } + } + Ok(false) +} diff --git a/services/attest/producer/src/dice_identity.rs b/services/attest/producer/src/dice_identity.rs new file mode 100644 index 00000000..903db271 --- /dev/null +++ b/services/attest/producer/src/dice_identity.rs @@ -0,0 +1,277 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Caliptra DICE certificate chain retrieval and compliance validation. +//! +//! The chain is assembled by Caliptra during boot (stored leaf → root): +//! index 0: AliasRT (leaf) +//! index 1: AliasFMC +//! index 2: LDevID +//! index 3: IDevID (optional, fuse-provisioned) +//! index 4: Vendor CA (optional, standard root CA) + +use heapless::Vec; + +use openprot_attest_api::consts::{MAX_CERT_SIZE, MAX_CHAIN_LEN}; +use openprot_attest_api::{AttestError, HwSigner}; + +use crate::der::{has_extension_oid, is_x509_v3}; + +/// DER encoding of OID 2.23.133.5.4.5 (tcg-dice-MultiTcbInfo). +const OID_TCG_MULTI_TCBINFO: [u8; 8] = [0x06, 0x06, 0x67, 0x81, 0x05, 0x05, 0x04, 0x05]; + +/// Retrieve the full DICE certificate chain from the signer and validate it. +pub fn cert_chain( + signer: &dyn HwSigner, +) -> Result, MAX_CHAIN_LEN>, AttestError> { + let mut buf: Vec, MAX_CHAIN_LEN> = Vec::new(); + signer.cert_chain_der(&mut buf)?; + validate_chain(&buf)?; + Ok(buf) +} + +/// Verify that `chain` meets the minimum structural and Caliptra DICE requirements. +/// +/// Checks applied to every certificate: +/// - Chain length ≥ 3 (AliasRT + AliasFMC + LDevID minimum). +/// - First byte is `0x30` (DER SEQUENCE). +/// - X.509 v3 (TBS version field = `A0 03 02 01 02`). +/// +/// Additionally, every cert **except the last** (root CA) must carry the +/// `tcg-dice-MultiTcbInfo` extension (OID 2.23.133.5.4.5). This extension is +/// present in all Caliptra 1.x and 2.x DICE-generated certificates (AliasRT, +/// AliasFMC, LDevID, IDevID). The root Vendor CA cert is a standard X.509 +/// cert that does not carry DICE extensions and is therefore exempt. +pub fn validate_chain(chain: &[Vec]) -> Result<(), AttestError> { + if chain.len() < 3 { + return Err(AttestError::ChainValidation( + "DICE chain must have at least 3 certificates", + )); + } + for (i, cert) in chain.iter().enumerate() { + if cert.first() != Some(&0x30) { + return Err(AttestError::Der("cert: not a DER SEQUENCE")); + } + if !is_x509_v3(cert) { + return Err(AttestError::ChainValidation("cert: not X.509 v3")); + } + let is_root = i == chain.len() - 1; + if !is_root && !has_extension_oid(cert, &OID_TCG_MULTI_TCBINFO)? { + return Err(AttestError::ChainValidation( + "cert: missing tcg-dice-MultiTcbInfo extension", + )); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use heapless::Vec; + use openprot_attest_api::consts::{MAX_CERT_SIZE, MAX_CHAIN_LEN, MAX_MEASUREMENTS}; + use openprot_attest_api::AttestError; + + use crate::signer::STUB_CERT; + + // ── HwSigner stubs ──────────────────────────────────────────────────────── + + struct OneCert; + struct TwoCerts; + struct ThreeDiceCerts; + + impl HwSigner for OneCert { + fn sign(&self, _: &[u8]) -> Result<[u8; 96], AttestError> { + Ok([0u8; 96]) + } + fn cert_chain_der( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + let mut c: Vec = Vec::new(); + c.extend_from_slice(&STUB_CERT).unwrap(); + buf.push(c).map_err(|_| AttestError::BufferFull) + } + fn measurements( + &self, + _out: &mut Vec, + ) -> Result<(), AttestError> { + Ok(()) + } + } + + impl HwSigner for TwoCerts { + fn sign(&self, _: &[u8]) -> Result<[u8; 96], AttestError> { + Ok([0u8; 96]) + } + fn cert_chain_der( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + let mut c0: Vec = Vec::new(); + c0.extend_from_slice(&STUB_CERT).unwrap(); + let mut c1: Vec = Vec::new(); + c1.extend_from_slice(&[0x30, 0x01]).unwrap(); + buf.push(c0).map_err(|_| AttestError::BufferFull)?; + buf.push(c1).map_err(|_| AttestError::BufferFull) + } + fn measurements( + &self, + _out: &mut Vec, + ) -> Result<(), AttestError> { + Ok(()) + } + } + + impl HwSigner for ThreeDiceCerts { + fn sign(&self, _: &[u8]) -> Result<[u8; 96], AttestError> { + Ok([0u8; 96]) + } + fn cert_chain_der( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + // leaf (AliasRT) and intermediate (AliasFMC): carry MultiTcbInfo + // root (LDevID in this minimal stub): standard v3 cert, no MultiTcbInfo + buf.push(make_dice_cert(true, true)) + .map_err(|_| AttestError::BufferFull)?; + buf.push(make_dice_cert(true, true)) + .map_err(|_| AttestError::BufferFull)?; + buf.push(make_dice_cert(true, false)) + .map_err(|_| AttestError::BufferFull) + } + fn measurements( + &self, + _out: &mut Vec, + ) -> Result<(), AttestError> { + Ok(()) + } + } + + // ── DER test helpers ────────────────────────────────────────────────────── + + fn der_tlv(tag: u8, value: &[u8]) -> heapless::Vec { + let mut out: heapless::Vec = heapless::Vec::new(); + out.push(tag).unwrap(); + let l = value.len(); + if l < 128 { + out.push(l as u8).unwrap(); + } else if l < 256 { + out.push(0x81).unwrap(); + out.push(l as u8).unwrap(); + } else { + out.push(0x82).unwrap(); + out.push((l >> 8) as u8).unwrap(); + out.push((l & 0xff) as u8).unwrap(); + } + out.extend_from_slice(value).unwrap(); + out + } + + /// Build a minimal DER cert for testing. + /// + /// `is_v3`: include the X.509 v3 version field (`A0 03 02 01 02`). + /// `has_multitcbinfo`: embed a tcg-dice-MultiTcbInfo extension. + fn make_dice_cert(is_v3: bool, has_multitcbinfo: bool) -> heapless::Vec { + // Optional MultiTcbInfo extension + let mut ext_items: heapless::Vec = heapless::Vec::new(); + if has_multitcbinfo { + let oid = OID_TCG_MULTI_TCBINFO; + let extn_val = der_tlv(0x04, &der_tlv(0x30, &[])); // OCTET STRING { SEQUENCE{} } + let mut ext_body: heapless::Vec = heapless::Vec::new(); + ext_body.extend_from_slice(&oid).unwrap(); + ext_body.extend_from_slice(&extn_val).unwrap(); + ext_items + .extend_from_slice(&der_tlv(0x30, &ext_body)) + .unwrap(); + } + let ext_wrapper = der_tlv(0xa3, &der_tlv(0x30, &ext_items)); + + // TBSCertificate body + let mut tbs_body: heapless::Vec = heapless::Vec::new(); + if is_v3 { + tbs_body + .extend_from_slice(&[0xa0, 0x03, 0x02, 0x01, 0x02]) + .unwrap(); + } + tbs_body.extend_from_slice(&[0x02, 0x01, 0x01]).unwrap(); // serialNumber INTEGER 1 + for _ in 0..5 { + tbs_body.extend_from_slice(&[0x30, 0x00]).unwrap(); // placeholder fields + } + tbs_body.extend_from_slice(&ext_wrapper).unwrap(); + + // Outer cert SEQUENCE { TBS, algId, signature } + let mut cert_body: heapless::Vec = heapless::Vec::new(); + cert_body + .extend_from_slice(&der_tlv(0x30, &tbs_body)) + .unwrap(); + cert_body.extend_from_slice(&[0x30, 0x00]).unwrap(); // placeholder algId + cert_body.extend_from_slice(&[0x03, 0x01, 0x00]).unwrap(); // placeholder sig + + let mut out: heapless::Vec = heapless::Vec::new(); + out.extend_from_slice(&der_tlv(0x30, &cert_body)).unwrap(); + out + } + + // ── cert_chain() integration tests ─────────────────────────────────────── + + #[test] + fn rejects_chain_shorter_than_three() { + assert!(cert_chain(&OneCert).is_err()); + assert!(cert_chain(&TwoCerts).is_err()); + } + + #[test] + fn accepts_valid_three_cert_dice_chain() { + let chain = cert_chain(&ThreeDiceCerts).unwrap(); + assert_eq!(chain.len(), 3); + } + + // ── validate_chain() unit tests ─────────────────────────────────────────── + + fn make_chain(specs: &[(bool, bool)]) -> Vec, MAX_CHAIN_LEN> { + let mut chain: Vec, MAX_CHAIN_LEN> = Vec::new(); + for &(is_v3, has_mti) in specs { + chain.push(make_dice_cert(is_v3, has_mti)).unwrap(); + } + chain + } + + #[test] + fn validate_rejects_non_sequence_cert() { + let mut chain = make_chain(&[(true, true), (true, true), (true, false)]); + // Corrupt the first byte of the leaf cert. + chain[0][0] = 0x04; + assert!(validate_chain(&chain).is_err()); + } + + #[test] + fn validate_rejects_non_v3_cert() { + // Leaf is v1 (no version field). + let chain = make_chain(&[(false, true), (true, true), (true, false)]); + let err = validate_chain(&chain).unwrap_err(); + assert!(matches!(err, AttestError::ChainValidation(_))); + } + + #[test] + fn validate_rejects_dice_cert_missing_multitcbinfo() { + // Leaf is v3 but lacks the MultiTcbInfo extension. + let chain = make_chain(&[(true, false), (true, true), (true, false)]); + let err = validate_chain(&chain).unwrap_err(); + assert!(matches!(err, AttestError::ChainValidation(_))); + } + + #[test] + fn validate_root_cert_allowed_without_multitcbinfo() { + // Root (last cert) is not required to carry MultiTcbInfo. + let chain = make_chain(&[(true, true), (true, true), (true, false)]); + assert!(validate_chain(&chain).is_ok()); + } + + #[test] + fn validate_accepts_four_cert_chain_with_root_exempt() { + // 4-cert chain: leaf, two intermediates, root without MultiTcbInfo. + let chain = make_chain(&[(true, true), (true, true), (true, true), (true, false)]); + assert!(validate_chain(&chain).is_ok()); + } +} diff --git a/services/attest/producer/src/lib.rs b/services/attest/producer/src/lib.rs new file mode 100644 index 00000000..b1b8583e --- /dev/null +++ b/services/attest/producer/src/lib.rs @@ -0,0 +1,26 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Concrete attestation producer for OpenPRoT. +//! +//! Provides [`HwAttestProducer`] and [`SwAttestProducer`], which implement +//! [`openprot_attest_api::AttestProducer`] backed by a hardware signer (Caliptra +//! mailbox) or a caller-supplied software key respectively. Under the +//! `test-support` feature, [`SoftwareAttestProducer`] provides a fully +//! software-backed stub for unit and integration tests. + +#![no_std] +#![forbid(unsafe_code)] + +pub mod builder; +pub mod cert_ueid; +pub mod der; +pub mod dice_identity; +pub mod measurements; +mod signer; + +pub use signer::HwAttestProducer; +pub use signer::SwAttestProducer; + +#[cfg(feature = "test-support")] +pub use signer::SoftwareAttestProducer; diff --git a/services/attest/producer/src/measurements.rs b/services/attest/producer/src/measurements.rs new file mode 100644 index 00000000..d7c98b44 --- /dev/null +++ b/services/attest/producer/src/measurements.rs @@ -0,0 +1,233 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Firmware measurement collection. +//! +//! Aggregates Caliptra-internal measurements (ROM, FMC, RT digests) with any +//! platform-registered [`MeasurementProvider`] instances. + +use heapless::Vec; + +use openprot_attest_api::consts::MAX_MEASUREMENTS; +use openprot_attest_api::{AttestError, Measurement, MeasurementProvider}; + +/// Append measurements from all registered providers into `out`. +/// +/// Providers may only add entries. A provider that removes entries (e.g. by +/// calling `out.clear()`) would silently drop hardware measurements, so this +/// function detects and rejects that. +pub fn collect( + providers: &[&dyn MeasurementProvider], + out: &mut Vec, +) -> Result<(), AttestError> { + for p in providers { + let before = out.len(); + p.measurements(out)?; + if out.len() < before { + return Err(AttestError::Provider( + "provider must not remove measurements", + )); + } + } + Ok(()) +} + +/// Stub Caliptra measurements for use in tests (requires `test-support` feature). +#[cfg(feature = "test-support")] +pub fn test_measurements() -> Vec { + use heapless::String; + use openprot_attest_api::consts::{MAX_COMPONENT_LEN, MAX_DIGEST_LEN, MAX_VERSION_LEN}; + use openprot_attest_api::{DigestAlgorithm, MeasurementAuthority}; + + let mut v: Vec = Vec::new(); + + for (name, ver, fill) in &[ + ("Caliptra ROM", "1.0.0", 0xAAu8), + ("Caliptra FMC", "2.3.1", 0xBBu8), + ("Caliptra RT", "2.3.1", 0xCCu8), + ] { + let mut component: String = String::new(); + component.push_str(name).unwrap(); + let mut version: String = String::new(); + version.push_str(ver).unwrap(); + let mut digest: Vec = Vec::new(); + digest.extend_from_slice(&[*fill; 48]).unwrap(); + v.push(Measurement { + component, + version, + digest_alg: DigestAlgorithm::Sha384, + digest, + authority: MeasurementAuthority::Caliptra, + }) + .unwrap(); + } + v +} + +#[cfg(test)] +mod tests { + use super::*; + use heapless::{String, Vec}; + use openprot_attest_api::consts::{ + MAX_COMPONENT_LEN, MAX_DIGEST_LEN, MAX_MEASUREMENTS, MAX_VERSION_LEN, + }; + use openprot_attest_api::{DigestAlgorithm, MeasurementAuthority}; + + struct StubProvider { + name: &'static str, + fail: bool, + } + + impl MeasurementProvider for StubProvider { + fn measurements( + &self, + out: &mut Vec, + ) -> Result<(), AttestError> { + if self.fail { + return Err(AttestError::Provider("intentional failure")); + } + let mut component: String = String::new(); + component.push_str(self.name).unwrap(); + let mut version: String = String::new(); + version.push_str("0.1").unwrap(); + let mut digest: Vec = Vec::new(); + digest.extend_from_slice(&[0xBBu8; 48]).unwrap(); + out.push(Measurement { + component, + version, + digest_alg: DigestAlgorithm::Sha384, + digest, + authority: MeasurementAuthority::Platform, + }) + .map_err(|_| AttestError::BufferFull) + } + } + + fn rom() -> Measurement { + let mut component: String = String::new(); + component.push_str("ROM").unwrap(); + let mut version: String = String::new(); + version.push_str("1.0").unwrap(); + let mut digest: Vec = Vec::new(); + digest.extend_from_slice(&[0xAAu8; 48]).unwrap(); + Measurement { + component, + version, + digest_alg: DigestAlgorithm::Sha384, + digest, + authority: MeasurementAuthority::Caliptra, + } + } + + #[test] + fn no_providers_leaves_out_unchanged() { + let mut out = Vec::new(); + out.push(rom()).unwrap(); + collect(&[], &mut out).unwrap(); + assert_eq!(out.len(), 1); + assert_eq!(out[0].component.as_str(), "ROM"); + } + + #[test] + fn provider_measurements_are_appended() { + let p = StubProvider { + name: "UEFI", + fail: false, + }; + let mut out = Vec::new(); + out.push(rom()).unwrap(); + collect(&[&p as &dyn MeasurementProvider], &mut out).unwrap(); + assert_eq!(out.len(), 2); + assert_eq!(out[1].component.as_str(), "UEFI"); + } + + #[test] + fn multiple_providers_all_appended() { + let p1 = StubProvider { + name: "UEFI", + fail: false, + }; + let p2 = StubProvider { + name: "BMC", + fail: false, + }; + let mut out = Vec::new(); + collect( + &[ + &p1 as &dyn MeasurementProvider, + &p2 as &dyn MeasurementProvider, + ], + &mut out, + ) + .unwrap(); + assert_eq!(out.len(), 2); + } + + #[test] + fn failing_provider_propagates_error() { + let p = StubProvider { + name: "BMC", + fail: true, + }; + let mut out = Vec::new(); + let err = collect(&[&p as &dyn MeasurementProvider], &mut out).unwrap_err(); + assert!(matches!(err, AttestError::Provider(_))); + } + + #[test] + fn seventeenth_measurement_overflows_buffer() { + // Fill to capacity; a provider that tries to push one more must fail. + let mut out: Vec = Vec::new(); + for _ in 0..MAX_MEASUREMENTS { + out.push(rom()).unwrap(); + } + let p = StubProvider { + name: "Extra", + fail: false, + }; + let err = collect(&[&p as &dyn MeasurementProvider], &mut out).unwrap_err(); + assert!(matches!(err, AttestError::BufferFull)); + } + + #[test] + fn provider_clearing_out_cannot_produce_a_token() { + struct ClearingProvider; + impl MeasurementProvider for ClearingProvider { + fn measurements( + &self, + out: &mut Vec, + ) -> Result<(), AttestError> { + out.clear(); + Ok(()) + } + } + let mut out: Vec = Vec::new(); + out.push(rom()).unwrap(); + let err = collect(&[&ClearingProvider as &dyn MeasurementProvider], &mut out).unwrap_err(); + assert!(matches!(err, AttestError::Provider(_))); + } + + #[test] + fn later_provider_failure_preserves_earlier_entries() { + let good = StubProvider { + name: "UEFI", + fail: false, + }; + let bad = StubProvider { + name: "BMC", + fail: true, + }; + let mut out = Vec::new(); + let err = collect( + &[ + &good as &dyn MeasurementProvider, + &bad as &dyn MeasurementProvider, + ], + &mut out, + ) + .unwrap_err(); + assert!(matches!(err, AttestError::Provider(_))); + assert_eq!(out.len(), 1); + assert_eq!(out[0].component.as_str(), "UEFI"); + } +} diff --git a/services/attest/producer/src/signer.rs b/services/attest/producer/src/signer.rs new file mode 100644 index 00000000..498eb37b --- /dev/null +++ b/services/attest/producer/src/signer.rs @@ -0,0 +1,274 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Concrete [`AttestProducer`] implementations. +//! +//! `HwAttestProducer` — backed by a real `HwSigner` (mailbox driver). +//! `SoftwareAttestProducer` — software-only stub, available under `test-support`. + +use heapless::Vec; + +use openprot_attest_api::consts::{ + MAX_CERT_SIZE, MAX_CHAIN_LEN, MAX_MEASUREMENTS, MAX_PROVIDERS, MAX_TOKEN_SIZE, +}; +use openprot_attest_api::{ + AttestConfig, AttestError, AttestProducer, HwSigner, MeasurementProvider, SwSigner, +}; + +use crate::{builder, cert_ueid, dice_identity, measurements}; + +// ── Hardware-backed producer ────────────────────────────────────────────────── + +/// Attestation producer backed by a platform hardware signer. +pub struct HwAttestProducer<'a> { + signer: &'a dyn HwSigner, + config: AttestConfig, + providers: Vec<&'a dyn MeasurementProvider, MAX_PROVIDERS>, +} + +impl<'a> HwAttestProducer<'a> { + pub fn new(signer: &'a dyn HwSigner, config: AttestConfig) -> Self { + Self { + signer, + config, + providers: Vec::new(), + } + } + + pub fn add_provider( + &mut self, + provider: &'a dyn MeasurementProvider, + ) -> Result<(), AttestError> { + self.providers + .push(provider) + .map_err(|_| AttestError::BufferFull) + } +} + +impl AttestProducer for HwAttestProducer<'_> { + fn generate_token( + &self, + nonce: &[u8], + out: &mut Vec, + ) -> Result<(), AttestError> { + let mut chain: Vec, MAX_CHAIN_LEN> = Vec::new(); + self.signer.cert_chain_der(&mut chain)?; + dice_identity::validate_chain(&chain)?; + let ueid = cert_ueid::extract_and_verify(&chain)?; + + let mut meas: Vec = Vec::new(); + self.signer.measurements(&mut meas)?; + measurements::collect(&self.providers, &mut meas)?; + builder::build(&self.config, self.signer, &ueid, &meas, nonce, out) + } + + fn cert_chain( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + dice_identity::cert_chain(self.signer).map(|c| *buf = c) + } +} + +// ── Software-key producer ───────────────────────────────────────────────────── + +/// Attestation producer backed by a caller-supplied P-384 private scalar and +/// certificate chain. +/// +/// Unlike [`HwAttestProducer`], this path does not enforce DICE chain +/// requirements (length, MultiTcbInfo extension) because the certificates are +/// not generated by the Caliptra DICE engine. The UEID field in the token is +/// derived from the leaf certificate if it carries the TCG UEID extension; +/// otherwise a zero-filled placeholder is used. +pub struct SwAttestProducer<'a> { + signer: SwSigner, + config: AttestConfig, + providers: Vec<&'a dyn MeasurementProvider, MAX_PROVIDERS>, +} + +impl<'a> SwAttestProducer<'a> { + pub fn new(signer: SwSigner, config: AttestConfig) -> Self { + Self { + signer, + config, + providers: Vec::new(), + } + } + + pub fn add_provider( + &mut self, + provider: &'a dyn MeasurementProvider, + ) -> Result<(), AttestError> { + self.providers + .push(provider) + .map_err(|_| AttestError::BufferFull) + } +} + +impl AttestProducer for SwAttestProducer<'_> { + fn generate_token( + &self, + nonce: &[u8], + out: &mut Vec, + ) -> Result<(), AttestError> { + let mut chain: Vec, MAX_CHAIN_LEN> = Vec::new(); + self.signer.cert_chain_der(&mut chain)?; + + let ueid: [u8; cert_ueid::UEID_LEN] = cert_ueid::extract_and_verify(&chain)?; + + let mut meas: Vec = Vec::new(); + measurements::collect(&self.providers, &mut meas)?; + builder::build(&self.config, &self.signer, &ueid, &meas, nonce, out) + } + + fn cert_chain( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + buf.clear(); + self.signer.cert_chain_der(buf) + } +} + +// ── Software stub (test-support) ───────────────────────────────────────────── + +/// Software-backed attestation producer for use in tests. +/// +/// Produces structurally valid COSE_Sign1 tokens without any hardware. +#[cfg(feature = "test-support")] +pub struct SoftwareAttestProducer { + config: AttestConfig, +} + +#[cfg(feature = "test-support")] +impl SoftwareAttestProducer { + pub fn new(config: AttestConfig) -> Self { + Self { config } + } +} + +#[cfg(feature = "test-support")] +impl AttestProducer for SoftwareAttestProducer { + fn generate_token( + &self, + nonce: &[u8], + out: &mut Vec, + ) -> Result<(), AttestError> { + // Stub UEID: type EAT_RAND (0x01) followed by 16 deterministic bytes. + let stub_ueid = [ + 0x01, 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x11, 0x22, 0x33, 0x44, + 0x55, 0x66, 0x77, + ]; + let meas = measurements::test_measurements(); + builder::build(&self.config, &StubSigner, &stub_ueid, &meas, nonce, out) + } + + fn cert_chain( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + buf.clear(); + let mut leaf: Vec = Vec::new(); + leaf.extend_from_slice(&STUB_CERT) + .map_err(|_| AttestError::BufferFull)?; + let mut ca: Vec = Vec::new(); + ca.extend_from_slice(&STUB_CERT) + .map_err(|_| AttestError::BufferFull)?; + buf.push(leaf).map_err(|_| AttestError::BufferFull)?; + buf.push(ca).map_err(|_| AttestError::BufferFull) + } +} + +// ── Stub signer used internally by SoftwareAttestProducer ──────────────────── + +/// Placeholder DER: an empty SEQUENCE. Not a parseable certificate. +#[cfg(any(test, feature = "test-support"))] +pub(crate) const STUB_CERT: [u8; 2] = [0x30, 0x00]; + +#[cfg(feature = "test-support")] +struct StubSigner; + +#[cfg(feature = "test-support")] +impl HwSigner for StubSigner { + fn sign(&self, _payload: &[u8]) -> Result<[u8; 96], AttestError> { + Ok([0u8; 96]) + } + fn cert_chain_der( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + let mut leaf: Vec = Vec::new(); + leaf.extend_from_slice(&STUB_CERT).unwrap(); + let mut ca: Vec = Vec::new(); + ca.extend_from_slice(&STUB_CERT).unwrap(); + buf.push(leaf).map_err(|_| AttestError::BufferFull)?; + buf.push(ca).map_err(|_| AttestError::BufferFull) + } + fn measurements( + &self, + out: &mut Vec, + ) -> Result<(), AttestError> { + let stub = measurements::test_measurements(); + for m in stub { + out.push(m).map_err(|_| AttestError::BufferFull)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use heapless::String; + use openprot_attest_api::consts::{ + MAX_CERT_SIZE, MAX_CHAIN_LEN, MAX_MEASUREMENTS, MAX_TOKEN_SIZE, + }; + use openprot_attest_api::{AttestConfig, AttestError, OemId}; + + /// A signer that returns a two-cert chain of stub DER (below the 3-cert minimum). + struct ShortChainSigner; + + impl HwSigner for ShortChainSigner { + fn sign(&self, _: &[u8]) -> Result<[u8; 96], AttestError> { + Ok([0u8; 96]) + } + fn cert_chain_der( + &self, + buf: &mut Vec, MAX_CHAIN_LEN>, + ) -> Result<(), AttestError> { + for _ in 0..2 { + let mut c: Vec = Vec::new(); + c.extend_from_slice(&STUB_CERT).unwrap(); + buf.push(c).map_err(|_| AttestError::BufferFull)?; + } + Ok(()) + } + fn measurements( + &self, + _out: &mut Vec, + ) -> Result<(), AttestError> { + Ok(()) + } + } + + fn config() -> AttestConfig { + let mut hw_model: String<64> = String::new(); + hw_model.push_str("TestModel").unwrap(); + let mut oemid_bytes: Vec = Vec::new(); + oemid_bytes + .extend_from_slice(&[0x00, 0x01, 0x47, 0xae]) + .unwrap(); + AttestConfig { + oemid: OemId(oemid_bytes), + hw_model, + } + } + + #[test] + fn generate_token_rejects_chain_with_fewer_than_three_certs() { + let producer = HwAttestProducer::new(&ShortChainSigner, config()); + let mut out: Vec = Vec::new(); + let err = producer.generate_token(b"testnonce", &mut out).unwrap_err(); + assert!(matches!(err, AttestError::ChainValidation(_))); + } +} diff --git a/services/attest/producer/tests/producer_integration.rs b/services/attest/producer/tests/producer_integration.rs new file mode 100644 index 00000000..8c0b49b2 --- /dev/null +++ b/services/attest/producer/tests/producer_integration.rs @@ -0,0 +1,127 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +// Integration tests for openprot-attest-producer. +// Requires: cargo test --features test-support + +#![cfg(feature = "test-support")] + +use heapless::{String, Vec}; +use openprot_attest_api::consts::{MAX_CERT_SIZE, MAX_CHAIN_LEN, MAX_TOKEN_SIZE}; +use openprot_attest_api::{AttestConfig, AttestProducer, OemId}; +use openprot_attest_producer::SoftwareAttestProducer; + +fn config() -> AttestConfig { + let mut hw_model: String<64> = String::new(); + hw_model.push_str("TestPlatform").unwrap(); + let mut oemid_bytes: Vec = Vec::new(); + oemid_bytes + .extend_from_slice(&[0x00, 0x01, 0x47, 0xae]) + .unwrap(); + AttestConfig { + oemid: OemId(oemid_bytes), + hw_model, + } +} + +fn generate(producer: &SoftwareAttestProducer, nonce: &[u8]) -> Vec { + let mut out: Vec = Vec::new(); + producer.generate_token(nonce, &mut out).unwrap(); + out +} + +fn unwrap_cose_sign1(token: &[u8]) -> std::vec::Vec { + let outer: ciborium::value::Value = ciborium::de::from_reader(token).unwrap(); + // token = tag(18, [phdr-bstr, uphdr-map, payload-bstr, sig-bstr]) + let (_, inner) = outer.as_tag().unwrap(); + inner.as_array().unwrap().clone() +} + +fn decode_payload_map( + token: &[u8], +) -> std::vec::Vec<(ciborium::value::Value, ciborium::value::Value)> { + let arr = unwrap_cose_sign1(token); + let payload_bytes = arr[2].as_bytes().unwrap().clone(); + // payload bytes decode to tag(55799, tag(61, map{...})) + let outer: ciborium::value::Value = + ciborium::de::from_reader(payload_bytes.as_slice()).unwrap(); + let (_, cwt_tagged) = outer.as_tag().unwrap(); // strip tag(55799) + let (_, map_val) = cwt_tagged.as_tag().unwrap(); // strip tag(61) + map_val.as_map().unwrap().clone() +} + +fn find_claim( + map: &[(ciborium::value::Value, ciborium::value::Value)], + key: i64, +) -> Option { + map.iter() + .find(|(k, _)| k.as_integer().and_then(|i| i64::try_from(i).ok()) == Some(key)) + .map(|(_, v)| v.clone()) +} + +// ── Token structure ────────────────────────────────────────────────────────── + +#[test] +fn token_is_four_element_cbor_array() { + let producer = SoftwareAttestProducer::new(config()); + let token = generate(&producer, b"testnonce12345678"); + let arr = unwrap_cose_sign1(&token); + assert_eq!(arr.len(), 4); +} + +#[test] +fn protected_header_is_bytes() { + let producer = SoftwareAttestProducer::new(config()); + let token = generate(&producer, b"testnonce"); + let arr = unwrap_cose_sign1(&token); + assert!(arr[0].as_bytes().is_some()); +} + +#[test] +fn signature_is_96_zero_bytes() { + let producer = SoftwareAttestProducer::new(config()); + let token = generate(&producer, b"testnonce"); + let arr = unwrap_cose_sign1(&token); + let sig = arr[3].as_bytes().unwrap(); + assert_eq!(sig.len(), 96); + assert!(sig.iter().all(|&b| b == 0)); +} + +// ── Payload claims ─────────────────────────────────────────────────────────── + +#[test] +fn nonce_claim_matches_input() { + let producer = SoftwareAttestProducer::new(config()); + let nonce = b"unique_nonce_bytes"; + let token = generate(&producer, nonce); + let map = decode_payload_map(&token); + let v = find_claim(&map, 10).unwrap(); // eat_nonce + assert_eq!(v.as_bytes().unwrap(), nonce); +} + +#[test] +fn hw_model_claim_matches_config() { + let producer = SoftwareAttestProducer::new(config()); + let token = generate(&producer, b"testnonce"); + let map = decode_payload_map(&token); + let v = find_claim(&map, 259).unwrap(); // hwmodel + assert_eq!(v.as_text().unwrap(), "TestPlatform"); +} + +#[test] +fn measurements_claim_contains_three_caliptra_components() { + let producer = SoftwareAttestProducer::new(config()); + let token = generate(&producer, b"testnonce"); + let map = decode_payload_map(&token); + let v = find_claim(&map, 273).unwrap(); // measurements (OCP-EAT key 273) + assert_eq!(v.as_array().unwrap().len(), 3); +} + +// ── Certificate chain ──────────────────────────────────────────────────────── + +#[test] +fn cert_chain_returns_two_certs() { + let producer = SoftwareAttestProducer::new(config()); + let mut chain: Vec, MAX_CHAIN_LEN> = Vec::new(); + producer.cert_chain(&mut chain).unwrap(); + assert_eq!(chain.len(), 2); +} diff --git a/third_party/crates_io/Cargo.lock b/third_party/crates_io/Cargo.lock index bcad71ab..32a321d5 100644 --- a/third_party/crates_io/Cargo.lock +++ b/third_party/crates_io/Cargo.lock @@ -927,6 +927,55 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "minicbor" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e127f215f03feb25fa8c496f6f6e5720d33730e6eea7a4b0ffcdd3141c13e86" [[package]] name = "nom" version = "7.1.3" diff --git a/third_party/crates_io/Cargo.toml b/third_party/crates_io/Cargo.toml index d4c68bbc..68706379 100644 --- a/third_party/crates_io/Cargo.toml +++ b/third_party/crates_io/Cargo.toml @@ -82,6 +82,11 @@ aes-gcm = { version = "0.10", default-features = false, features = ["aes"] } cipher = { version = "0.4", default-features = false } # Host tool crates needed by pigweed pw_kernel/tooling +ciborium = { version = "0.2", default-features = false } + +# no_std CBOR encoder for the attestation producer (production dependency) +minicbor = { version = "0.21", default-features = false } + anyhow = "1.0.103" clap = { version = "4.5.40", features = ["derive", "env", "wrap_help"] } futures = "0.3" diff --git a/third_party/crates_io/deny.toml b/third_party/crates_io/deny.toml index 1b643784..1be86451 100644 --- a/third_party/crates_io/deny.toml +++ b/third_party/crates_io/deny.toml @@ -24,6 +24,7 @@ allow = [ "Unicode-3.0", "CC0-1.0", "Zlib", + "BlueOak-1.0.0", ]