From 0fe7e2c07c69f65763e9f3e776293febf0eb34b1 Mon Sep 17 00:00:00 2001 From: 0xEthamin Date: Wed, 17 Jun 2026 18:03:58 +0200 Subject: [PATCH] chore(tropic01-driver): prepare the crate for a beta crates.io release Make the driver ready for a first beta publication as version 0.0.1. The focus is documentation examples, with a feature gate, the publish metadata, and the README brought to a publishable state around them. Four runnable doc examples are added, all marked no_run because they cannot reach real hardware. A crate-level example opens a secure channel and runs one command, and there are per-item examples on open_session, on the random_into trait method, and on the bootloader update flow. Each builds a stub SpiDevice and SeWait behind hidden setup lines, so the visible code stays focused on the real API. The examples carry placeholder keys with a comment that real key material comes from a TRNG and from provisioning, and they point at read_verified_chip_stpub for a genuine-chip trust decision rather than the unverified read_chip_stpub. The read_chip_stpub documentation itself is hardened to say plainly that it does not attest authenticity. The X.509 chain verification moves behind an attestation feature that stays on by default. It pulls the ecdsa, p384, and p521 curve crates, which are release candidates today, so a consumer that only needs STPUB extraction can drop them with default-features = false and keep a release-candidate-free build. The feature gates verify_cert_chain, parse_verified_stpub, RootAnchor, read_verified_chip_stpub, the ChainError type and its SeError variant, the curve verify helpers, the chain-only DER navigators, the chain tests, and the fuzz seam. parse_stpub stays always available. A build without the feature no longer compiles the curve crates into the tree. The publish metadata is completed. The workspace version drops to 0.0.1 and declares a 1.88 minimum supported Rust, which the let-chain and is_multiple_of uses require. The invalid documentation field is removed so crates.io points at docs.rs by default, and a docs.rs metadata block renders the attestation API. The README gains a beta banner, a quick-start that mirrors the crate example, a note on the release-candidate curve crates, and an updated roadmap. The keyword list drops to the five crates.io allows. cargo publish --dry-run packages and verifies the crate standalone. 407 host tests pass with the feature on and 385 with it off, the four doc examples compile, clippy is clean on host all targets, all features, no default features, the fuzz seam, and thumbv8m, cargo doc is warning free, hermetic coverage holds at 93 percent, and the six fuzz targets stay crash free. The build is confirmed on Rust 1.88. --- Cargo.lock | 2 +- Cargo.toml | 5 +- README.md | 4 +- crates/tropic01-driver/Cargo.toml | 24 ++++-- crates/tropic01-driver/README.md | 72 ++++++++++++++++-- crates/tropic01-driver/fuzz/Cargo.lock | 2 +- crates/tropic01-driver/fuzz/Cargo.toml | 3 +- crates/tropic01-driver/src/cert.rs | 66 ++++++++++++++++- crates/tropic01-driver/src/crypto.rs | 8 ++ .../tropic01-driver/src/device/bootloader.rs | 32 ++++++++ .../tropic01-driver/src/device/nosession.rs | 58 ++++++++++++++- crates/tropic01-driver/src/error.rs | 4 + crates/tropic01-driver/src/lib.rs | 73 ++++++++++++++++++- crates/tropic01-driver/src/port.rs | 45 ++++++++++++ crates/tropic01-driver/tests/model_itest.rs | 6 ++ 15 files changed, 376 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 516d0e6..067e0f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,7 +526,7 @@ dependencies = [ [[package]] name = "tropic01-driver" -version = "0.1.0" +version = "0.0.1" dependencies = [ "aes-gcm", "ecdsa", diff --git a/Cargo.toml b/Cargo.toml index c51be40..0d566b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,8 @@ members = ["crates/tropic01-driver"] [workspace.package] edition = "2024" -version = "0.1.0" +version = "0.0.1" +rust-version = "1.88" authors = ["PatinaKey "] repository = "https://github.com/PatinaKey/firmware" license = "GPL-3.0-or-later" @@ -28,7 +29,7 @@ hmac = { version = "0.13", default-features = false } # release candidates. the last STABLE curve releases (p384 0.13, ecdsa 0.16) speak the older # digest 0.10 generation and would pull a SECOND copy of sha2/digest into the # binary. Pinning the rc keeps a single crypto generation, which is the smaller -# audit and flash surface. +# audit and flash surface. # TODO: drop the -rc pins once p384/p521 0.14 and ecdsa 0.17 ship stable. ecdsa = { version = "0.17.0-rc.19", default-features = false } p384 = { version = "0.14.0-rc.11", default-features = false, features = ["ecdsa"] } diff --git a/README.md b/README.md index f7e1d0e..b1e00fb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CI](https://github.com/PatinaKey/firmware/actions/workflows/ci.yml/badge.svg)](https://github.com/PatinaKey/firmware/actions/workflows/ci.yml) [![License: GPL-3.0-or-later](https://img.shields.io/badge/License-GPL--3.0--or--later-blue.svg)](LICENSE) -[![Rust 1.95+](https://img.shields.io/badge/Rust-1.95%2B%20edition%202024-orange.svg)](https://doc.rust-lang.org/edition-guide/) +[![Rust 1.88+](https://img.shields.io/badge/Rust-1.88%2B%20edition%202024-orange.svg)](https://doc.rust-lang.org/edition-guide/) [![no_std](https://img.shields.io/badge/no__std-bare--metal-green.svg)](https://docs.rust-embedded.org/book/intro/no-std.html) Open-source Rust firmware for **PatinaKey**, a USB hardware security key implementing FIDO2, OpenPGP card, and PKCS#11. @@ -51,7 +51,7 @@ The project is under active development. Only the secure-element driver (`crates ## Building -Developed using rustc 1.95. No guarantee is provided that the code will work with an earlier version. +Minimum supported Rust version (MSRV): 1.88 (edition 2024), verified to build. Developed on a newer stable. No guarantee is provided below 1.88. ```sh # Host check and tests diff --git a/crates/tropic01-driver/Cargo.toml b/crates/tropic01-driver/Cargo.toml index ef93f27..d8d37fb 100644 --- a/crates/tropic01-driver/Cargo.toml +++ b/crates/tropic01-driver/Cargo.toml @@ -2,13 +2,13 @@ name = "tropic01-driver" edition.workspace = true version.workspace = true +rust-version.workspace = true authors.workspace = true repository.workspace = true license.workspace = true description = "Unofficial no_std, panic-free Rust driver for the TROPIC01 secure element." readme = "README.md" -documentation = "Not published to docs.rs yet." -keywords = ["tropic01", "secure-element", "no-std", "no_std", "embedded", "crypto"] +keywords = ["tropic01", "secure-element", "no-std", "embedded", "crypto"] categories = ["embedded", "no-std", "cryptography", "hardware-support"] # Dev/validation tooling kept out of the published package. The crate is the # library. The fuzz harness has its own detached workspace and the scripts need @@ -16,6 +16,13 @@ categories = ["embedded", "no-std", "cryptography", "hardware-support"] exclude = ["/fuzz", "/scripts"] [features] +# X.509 attestation chain verification (verify_cert_chain, parse_verified_stpub, +# RootAnchor, read_verified_chip_stpub). ON by default. It pulls the ECDSA curve +# crates (ecdsa / p384 / p521). Disable via default-features = false to drop +# those deps when only STPUB extraction (parse_stpub / read_chip_stpub) is +# needed. The handshake still binds STPUB, so a session stays authenticated. +default = ["attestation"] +attestation = ["dep:ecdsa", "dep:p384", "dep:p521"] # Exposes the `fuzz` module (attacker-facing parsers) to libFuzzer harnesses. # Not for production. Enabled only by the fuzz crate. _fuzz = [] @@ -33,9 +40,14 @@ x25519-dalek = { workspace = true } aes-gcm = { workspace = true } sha2 = { workspace = true } hmac = { workspace = true } -ecdsa = { workspace = true } -p384 = { workspace = true } -p521 = { workspace = true } +ecdsa = { workspace = true, optional = true } +p384 = { workspace = true, optional = true } +p521 = { workspace = true, optional = true } + +# Render the attestation API on docs.rs. Explicit (not all-features) so the +# dev-only _fuzz / model-itest modules stay hidden from the published docs. +[package.metadata.docs.rs] +features = ["attestation"] [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/crates/tropic01-driver/README.md b/crates/tropic01-driver/README.md index b62faab..373c3c3 100644 --- a/crates/tropic01-driver/README.md +++ b/crates/tropic01-driver/README.md @@ -1,10 +1,69 @@ # tropic01-driver - TROPIC01 secure-element driver (no_std) +[![crates.io](https://img.shields.io/crates/v/tropic01-driver.svg)](https://crates.io/crates/tropic01-driver) +[![docs.rs](https://docs.rs/tropic01-driver/badge.svg)](https://docs.rs/tropic01-driver) +[![MSRV 1.88+](https://img.shields.io/badge/MSRV-1.88-blue.svg)](https://www.rust-lang.org) +[![License: GPL-3.0-or-later](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) + + +> **BETA - unofficial, NOT silicon-validated.** This is an early `0.0.x` +> release. The driver has been validated host-side against the official +> `tropic01_model` emulator, but it has NOT run on real TROPIC01 silicon. Do not +> use it for production trust decisions yet. + A `no_std`, heap-free, `unsafe-free` Rust driver for the **TROPIC01** secure element (Tropic Square, part `TR01-C2P-T301`), spoken over SPI through an authenticated, encrypted **Noise KK1** session. -⚠️ Disclaimer: This is an unofficial, community-driven project. It is not affiliated with, endorsed by, or officially supported by Tropic Square. For the official SDK, please refer to Tropic Square's libtropic. +## Quick start + +The integrator supplies the SPI bus (`embedded_hal::spi::SpiDevice`) and a +ready/timeout provider (the crate's `SeWait` trait). All key material is +caller-provided via `SessionConfig`. Open a channel, run a command, close it: + +```rust,no_run +use tropic01_driver::{SeCommands, SessionConfig, StartupId, Tropic01}; +use zeroize::Zeroizing; + +fn run(spi: impl embedded_hal::spi::SpiDevice, wait: impl tropic01_driver::SeWait) + -> Result<(), tropic01_driver::SeError> +{ + let mut dev = Tropic01::new(spi, wait); + dev.reboot(StartupId::Reboot)?; // load the Application firmware + + // Placeholder keys: real ephemerals come from a TRNG, the pairing keys from + // provisioning, and `stpub` from the chip certificate. For a genuine-chip + // trust decision, get `stpub` via `read_verified_chip_stpub` (not the + // unverified `read_chip_stpub`) against an out-of-band-pinned `RootAnchor`. + let ehpriv = Zeroizing::new([0u8; 32]); + let shipriv = Zeroizing::new([0u8; 32]); + let shipub = [0u8; 32]; + let stpub = [0u8; 32]; + let cfg = SessionConfig + { + ehpriv: &ehpriv, + shipriv: &shipriv, + shipub: &shipub, + stpub: &stpub, + pkey_index: 0, + }; + // open_session reports its error as a tuple (handle, error). + let mut session = dev.open_session(cfg).map_err(|(_dev, e)| e)?; + + let mut random = [0u8; 32]; + session.random_into(&mut random)?; + let _dev = session.close_session(); + Ok(()) +} +``` + +The `attestation` feature (ON by default) enables X.509 chain verification +(`verify_cert_chain` / `read_verified_chip_stpub`) and pulls the ECDSA curve +crates (`ecdsa` / `p384` / `p521`), which are PRE-RELEASE (`-rc`) today (pinned to +keep one `digest` generation in the tree). Build with `default-features = false` +to drop those dependencies when only STPUB extraction is needed. + +**Disclaimer:** This is an unofficial, community-driven project. It is not affiliated with, endorsed by, or officially supported by Tropic Square. For the official SDK, please refer to Tropic Square's libtropic. Written as a clean-room rewrite with the official C SDK [`libtropic`](https://github.com/tropicsquare/libtropic) used as a differential @@ -106,11 +165,12 @@ This runs in the normal hermetic test suite. The TROPIC01 command surface is fully wired: every L2 request and L3 command, attestation, and the firmware-update bootloader are implemented. -Non-command work toward a publishable crate: validate against silicon (the -`tropic01_model` emulator is already wired, see -[Validation](#validation-against-real-libtropic)), examples on docs.rs, -and an optional `embedded-hal`-based port so external users can plug -their own HAL (currently the ports are the crate's own `SpiDevice` / `SeWait` traits). +Work toward a production `1.0`: validate against real silicon (the +`tropic01_model` emulator is wired, see +[Validation](#validation-against-real-libtropic), but no silicon run yet). Move the +`ecdsa` / `p384` / `p521` curve crates off their release candidates once stable +versions on the same `digest` generation ship. A type-state `open_session` that +consumes a chain-verified STPUB and an `embedded-hal-async` path. ## Design principles diff --git a/crates/tropic01-driver/fuzz/Cargo.lock b/crates/tropic01-driver/fuzz/Cargo.lock index 996fa06..88c5f81 100644 --- a/crates/tropic01-driver/fuzz/Cargo.lock +++ b/crates/tropic01-driver/fuzz/Cargo.lock @@ -594,7 +594,7 @@ dependencies = [ [[package]] name = "tropic01-driver" -version = "0.1.0" +version = "0.0.1" dependencies = [ "aes-gcm", "ecdsa", diff --git a/crates/tropic01-driver/fuzz/Cargo.toml b/crates/tropic01-driver/fuzz/Cargo.toml index e13c42e..8075ce2 100644 --- a/crates/tropic01-driver/fuzz/Cargo.toml +++ b/crates/tropic01-driver/fuzz/Cargo.toml @@ -16,7 +16,8 @@ libfuzzer-sys = "0.4" [dependencies.tropic01-driver] path = ".." -features = ["_fuzz"] +# attestation is needed so the verify_cert_chain fuzz target still builds. +features = ["_fuzz", "attestation"] [[bin]] name = "parse_l2_response" diff --git a/crates/tropic01-driver/src/cert.rs b/crates/tropic01-driver/src/cert.rs index 483cf28..bc66e4b 100644 --- a/crates/tropic01-driver/src/cert.rs +++ b/crates/tropic01-driver/src/cert.rs @@ -24,8 +24,10 @@ //! root. It covers the cryptographic path only. Validity dates and revocation are //! left to the integrator. +#[cfg(feature = "attestation")] use crate::crypto; use crate::error::CertError; +#[cfg(feature = "attestation")] use crate::error::ChainError; use crate::error::SeError; use crate::parse::take; @@ -59,28 +61,38 @@ const TAG_OID: u8 = 0x06; const TAG_BIT_STRING: u8 = 0x03; /// ecdsa-with-SHA384 OID content bytes (1.2.840.10045.4.3.3). +#[cfg(feature = "attestation")] const OID_ECDSA_SHA384: [u8; 8] = [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03]; /// ecdsa-with-SHA512 OID content bytes (1.2.840.10045.4.3.4). +#[cfg(feature = "attestation")] const OID_ECDSA_SHA512: [u8; 8] = [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x04]; /// id-ecPublicKey OID content bytes (1.2.840.10045.2.1). +#[cfg(feature = "attestation")] const OID_EC_PUBLIC_KEY: [u8; 7] = [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01]; /// secp384r1 (P-384) named-curve OID content bytes (1.3.132.0.34). +#[cfg(feature = "attestation")] const OID_SECP384R1: [u8; 5] = [0x2b, 0x81, 0x04, 0x00, 0x22]; /// secp521r1 (P-521) named-curve OID content bytes (1.3.132.0.35). +#[cfg(feature = "attestation")] const OID_SECP521R1: [u8; 5] = [0x2b, 0x81, 0x04, 0x00, 0x23]; /// SEC1 uncompressed point length for P-384: 0x04 || X(48) || Y(48). +#[cfg(feature = "attestation")] const P384_POINT_LEN: usize = 97; /// SEC1 uncompressed point length for P-521: 0x04 || X(66) || Y(66). +#[cfg(feature = "attestation")] const P521_POINT_LEN: usize = 133; /// Number of certificates the chain expects in the store. +#[cfg(feature = "attestation")] const CHAIN_CERT_COUNT: usize = 4; /// Extracts STPUB from a raw `Get_Info` X.509 certificate store. /// -/// WARNING: this does NOT verify the certificate chain. For any trust decision -/// use `parse_verified_stpub`. +/// WARNING: this does NOT attest TROPIC01 authenticity. It does NOT verify the +/// certificate chain up to the Tropic root, so a counterfeit chip can serve any +/// STPUB here. For the genuine-chip guarantee, use `parse_verified_stpub` with a +/// `RootAnchor` pinned out-of-band. /// /// Parses the 10-byte store header (version 0x01, num_certs 0x04, four big-endian /// u16 per-cert lengths), takes the DEVICE certificate body, then walks its DER @@ -261,6 +273,7 @@ fn parse_der_len(input: &[u8]) -> Result<(&[u8], usize), CertError> /// bytes or indefinite) becomes `ChainError::Unsupported`, every other fault /// becomes `ChainError::Malformed`. This keeps the chain path's error taxonomy /// aligned with the STPUB path while reusing the one length parser. +#[cfg(feature = "attestation")] fn der_len(input: &[u8]) -> Result<(&[u8], usize), ChainError> { parse_der_len(input).map_err(|e| match e @@ -306,12 +319,14 @@ fn crop_x25519_key(content: &[u8]) -> Result<[u8; 32], CertError> /// The wrapped bytes are the SEC1 uncompressed point (0x04 || X || Y, 133 bytes). /// The TEST root here differs from PROD: the integrator compiles in the correct /// production root point. +#[cfg(feature = "attestation")] #[derive(Clone, Copy)] pub struct RootAnchor { point: [u8; P521_POINT_LEN], } +#[cfg(feature = "attestation")] impl RootAnchor { /// Builds an anchor from a P-521 SEC1 uncompressed point. @@ -348,6 +363,7 @@ impl RootAnchor } /// A certificate's signatureAlgorithm: the ECDSA curve+digest it was signed with. +#[cfg(feature = "attestation")] #[derive(Clone, Copy, PartialEq, Eq)] enum SigAlg { @@ -358,6 +374,7 @@ enum SigAlg } /// A key's elliptic curve, taken from its SubjectPublicKeyInfo named-curve OID. +#[cfg(feature = "attestation")] #[derive(Clone, Copy, PartialEq, Eq)] enum Curve { @@ -371,6 +388,7 @@ enum Curve /// /// All three are sub-slices of the cert body, never copies. `tbs` is the exact /// signed byte range (the first inner SEQUENCE INCLUDING its tag+length header). +#[cfg(feature = "attestation")] struct CertParts<'a> { /// The tbsCertificate bytes that were signed (SEQUENCE header included). @@ -408,6 +426,7 @@ struct CertParts<'a> /// `SeError::Chain` when a signature link fails to verify under the expected /// key, or when the store header, a certificate, or a key is malformed, /// unsupported, or carries an unexpected signature algorithm. +#[cfg(feature = "attestation")] pub fn verify_cert_chain(cert_store: &[u8], anchor: &RootAnchor) -> Result<(), SeError> { let mut bodies: [&[u8]; CHAIN_CERT_COUNT] = [&[]; CHAIN_CERT_COUNT]; @@ -435,6 +454,7 @@ pub fn verify_cert_chain(cert_store: &[u8], anchor: &RootAnchor) -> Result<(), S /// /// `SeError::Chain` when the chain does not verify under `anchor`, or /// `SeError::Cert` when the DEVICE certificate does not parse. +#[cfg(feature = "attestation")] pub fn parse_verified_stpub ( cert_store: &[u8], @@ -451,6 +471,7 @@ pub fn parse_verified_stpub /// Reads the subject's tbsCertificate, signatureAlgorithm, and signatureValue, /// reads the issuer's SubjectPublicKeyInfo point, then dispatches the verify by /// the subject's algorithm. The issuer key's curve must match the algorithm. +#[cfg(feature = "attestation")] fn verify_link(subject: &[u8], issuer: &[u8]) -> Result<(), ChainError> { let parts = parse_cert_parts(subject)?; @@ -462,6 +483,7 @@ fn verify_link(subject: &[u8], issuer: &[u8]) -> Result<(), ChainError> /// /// SECURITY: the load-bearing trust step. The anchor is the caller's pinned key, /// not store bytes. The subject's signatureAlgorithm must be ecdsa-with-SHA512. +#[cfg(feature = "attestation")] fn verify_under_anchor(subject: &[u8], anchor: &RootAnchor) -> Result<(), ChainError> { let parts = parse_cert_parts(subject)?; @@ -479,6 +501,7 @@ fn verify_under_anchor(subject: &[u8], anchor: &RootAnchor) -> Result<(), ChainE /// be the matching one (SHA-384 with P-384, SHA-512 with P-521). Any other pairing /// is rejected as `BadPublicKey` before any crypto runs. This names the /// curve<->digest constraint instead of comparing two signatureAlgorithm values. +#[cfg(feature = "attestation")] fn verify_with_curve ( sig_alg: SigAlg, @@ -511,6 +534,7 @@ fn verify_with_curve /// VERSION == 0x01 and NUM_CERTS == 4. Each cert body is `LEN[i]` bytes, taken in /// order, `take` rejects any length that overruns the store. Trailing padding is /// ignored. +#[cfg(feature = "attestation")] fn split_cert_bodies<'a> ( store: &'a [u8], @@ -558,6 +582,7 @@ fn split_cert_bodies<'a> /// SEQUENCE, its first OID selects the algorithm. The signatureValue is the /// trailing BIT STRING. Its content is one 0x00 unused-bits byte then the /// ECDSA-Sig-Value DER. +#[cfg(feature = "attestation")] fn parse_cert_parts(cert: &[u8]) -> Result, ChainError> { // Outer cert SEQUENCE must span the whole body. @@ -597,6 +622,7 @@ fn parse_cert_parts(cert: &[u8]) -> Result, ChainError> /// /// The first object is the algorithm OID. It must equal ecdsa-with-SHA384 or /// ecdsa-with-SHA512. Trailing params (if any) are ignored. +#[cfg(feature = "attestation")] fn parse_sig_alg(alg_seq: &[u8]) -> Result { let (oid, _rest) = der_oid(alg_seq)?; @@ -618,6 +644,7 @@ fn parse_sig_alg(alg_seq: &[u8]) -> Result /// /// The BIT STRING content is one 0x00 unused-bits byte then SEQUENCE { r, s }. /// Returns `(sig_der, trailing)` where `sig_der` is the bytes after the 0x00. +#[cfg(feature = "attestation")] fn parse_signature_value(input: &[u8]) -> Result<(&[u8], &[u8]), ChainError> { let (after_tag, tag) = take_u8(input).map_err(|_| ChainError::Malformed)?; @@ -643,6 +670,7 @@ fn parse_signature_value(input: &[u8]) -> Result<(&[u8], &[u8]), ChainError> /// returned point is the 0x04 || X || Y SEC1 form (the BIT STRING content minus /// its leading 0x00 unused-bits byte). The curve is derived from the curve OID /// (secp384r1 -> P-384, secp521r1 -> P-521). +#[cfg(feature = "attestation")] fn parse_spki_point(cert: &[u8]) -> Result<(Curve, &[u8]), ChainError> { let (inner, _trailing) = der_sequence_content(cert)?; @@ -667,6 +695,7 @@ fn parse_spki_point(cert: &[u8]) -> Result<(Curve, &[u8]), ChainError> /// Returns `Ok(Some(..))` when `elem` is `SEQUENCE { SEQUENCE { OID /// id-ecPublicKey, OID curve }, BIT STRING }`, `Ok(None)` when it is not an SPKI /// (so scanning continues), and `Err` only on a structurally broken SPKI. +#[cfg(feature = "attestation")] fn try_spki(elem: &[u8]) -> Result, ChainError> { let (after_tag, tag) = take_u8(elem).map_err(|_| ChainError::Malformed)?; @@ -719,6 +748,7 @@ fn try_spki(elem: &[u8]) -> Result, ChainError> } /// Maps a named-curve OID to the matching curve. +#[cfg(feature = "attestation")] fn curve_from_oid(curve_oid: &[u8]) -> Result { if curve_oid == OID_SECP384R1 @@ -740,6 +770,7 @@ fn curve_from_oid(curve_oid: &[u8]) -> Result /// The BIT STRING content is one 0x00 unused-bits byte then 0x04 || X || Y. The /// returned slice is the 0x04 || X || Y part and must be exactly `expected_len` /// bytes with a leading 0x04, else the key is rejected. +#[cfg(feature = "attestation")] fn parse_ec_point(input: &[u8], expected_len: usize) -> Result<&[u8], ChainError> { let (after_tag, tag) = take_u8(input).map_err(|_| ChainError::BadPublicKey)?; @@ -772,6 +803,7 @@ fn parse_ec_point(input: &[u8], expected_len: usize) -> Result<&[u8], ChainError /// /// The leading tag must be 0x30. The content is bounded to the declared length, /// `trailing` is whatever follows the SEQUENCE in `input`. +#[cfg(feature = "attestation")] fn der_sequence_content(input: &[u8]) -> Result<(&[u8], &[u8]), ChainError> { let (after_tag, tag) = take_u8(input).map_err(|_| ChainError::Malformed)?; @@ -788,6 +820,7 @@ fn der_sequence_content(input: &[u8]) -> Result<(&[u8], &[u8]), ChainError> /// Returns `(element, rest)` where `element` is the contiguous tag || length || /// content byte range and `rest` is what follows. This is how the exact signed /// tbsCertificate byte range is captured. +#[cfg(feature = "attestation")] fn der_element_with_header(input: &[u8]) -> Result<(&[u8], &[u8]), ChainError> { let (after_tag, _tag) = take_u8(input).map_err(|_| ChainError::Malformed)?; @@ -802,6 +835,7 @@ fn der_element_with_header(input: &[u8]) -> Result<(&[u8], &[u8]), ChainError> } /// Reads a DER OBJECT IDENTIFIER, returning its `(content, rest)`. +#[cfg(feature = "attestation")] fn der_oid(input: &[u8]) -> Result<(&[u8], &[u8]), ChainError> { let (after_tag, tag) = take_u8(input).map_err(|_| ChainError::Malformed)?; @@ -1187,6 +1221,7 @@ mod tests // and cross-checked with openssl (the full TEST chain verifies, the root is // self-signed, cert[0] STPUB == model s_t_pub). See golden_chain module. + #[cfg(feature = "attestation")] fn test_anchor() -> RootAnchor { RootAnchor::from_sec1_p521(&golden_chain::MODEL_TEST_ROOT_PUBKEY) @@ -1194,6 +1229,7 @@ mod tests } /// Embeds the un-padded store into a 3840-byte buffer like the chip serves. + #[cfg(feature = "attestation")] fn store_padded_3840() -> [u8; 3840] { let mut padded = [0u8; 3840]; @@ -1202,18 +1238,21 @@ mod tests padded } + #[cfg(feature = "attestation")] #[test] fn verify_cert_chain_accepts_the_model_chain() { assert_eq!(verify_cert_chain(&golden_chain::MODEL_CERT_STORE, &test_anchor()), Ok(())); } + #[cfg(feature = "attestation")] #[test] fn verify_cert_chain_accepts_block_padded_store() { assert_eq!(verify_cert_chain(&store_padded_3840(), &test_anchor()), Ok(())); } + #[cfg(feature = "attestation")] #[test] fn parse_verified_stpub_returns_model_stpub() { @@ -1223,6 +1262,7 @@ mod tests ); } + #[cfg(feature = "attestation")] #[test] fn parse_verified_stpub_matches_unverified_on_good_chain() { @@ -1236,12 +1276,14 @@ mod tests /// /// Computed from the pointer offset, so the test byte to flip is DERIVED from /// the parsed structure rather than a hard-coded magic number. + #[cfg(feature = "attestation")] fn abs_offset(outer: &[u8], inner: &[u8]) -> usize { (inner.as_ptr() as usize) - (outer.as_ptr() as usize) } /// Returns the absolute store index of a byte inside cert[i]'s sig_der. + #[cfg(feature = "attestation")] fn sig_byte_index(store: &[u8], cert_index: usize) -> usize { let mut bodies: [&[u8]; CHAIN_CERT_COUNT] = [&[]; CHAIN_CERT_COUNT]; @@ -1251,6 +1293,7 @@ mod tests abs_offset(store, parts.sig_der) + parts.sig_der.len() / 2 } + #[cfg(feature = "attestation")] #[test] fn flipping_a_signature_byte_fails_bad_signature() { @@ -1265,6 +1308,7 @@ mod tests ); } + #[cfg(feature = "attestation")] #[test] fn flipping_a_tbs_byte_fails() { @@ -1286,6 +1330,7 @@ mod tests ); } + #[cfg(feature = "attestation")] #[test] fn flipping_cert1_signature_fails_bad_signature() { @@ -1300,6 +1345,7 @@ mod tests ); } + #[cfg(feature = "attestation")] #[test] fn flipping_cert2_signature_fails_bad_signature() { @@ -1315,6 +1361,7 @@ mod tests ); } + #[cfg(feature = "attestation")] #[test] fn wrong_anchor_fails_bad_signature() { @@ -1335,6 +1382,7 @@ mod tests /// Derived from a fixed non-trivial scalar so it is a real on-curve point and /// is accepted by the eagerly-validating anchor constructor, yet differs from /// the model TEST root, so signatures fail to verify under it. + #[cfg(feature = "attestation")] fn other_valid_p521_point() -> [u8; P521_POINT_LEN] { use p521::ecdsa::SigningKey; @@ -1347,6 +1395,7 @@ mod tests out } + #[cfg(feature = "attestation")] #[test] fn anchor_without_uncompressed_tag_rejected() { @@ -1358,6 +1407,7 @@ mod tests )); } + #[cfg(feature = "attestation")] #[test] fn wrong_num_certs_in_header_rejected() { @@ -1369,6 +1419,7 @@ mod tests ); } + #[cfg(feature = "attestation")] #[test] fn bad_version_in_header_rejected() { @@ -1380,6 +1431,7 @@ mod tests ); } + #[cfg(feature = "attestation")] #[test] fn empty_store_rejected() { @@ -1387,6 +1439,7 @@ mod tests assert!(matches!(r, Err(SeError::Chain(_)))); } + #[cfg(feature = "attestation")] #[test] fn verify_cert_chain_never_panics_on_any_truncation() { @@ -1400,6 +1453,7 @@ mod tests } } + #[cfg(feature = "attestation")] #[test] fn verify_cert_chain_never_panics_on_single_byte_flips_across_store() { @@ -1415,6 +1469,7 @@ mod tests } } + #[cfg(feature = "attestation")] #[test] fn parse_spki_point_reads_the_xxxx_ca_p384_key() { @@ -1428,6 +1483,7 @@ mod tests assert_eq!(point[0], 0x04); } + #[cfg(feature = "attestation")] #[test] fn parse_spki_point_reads_the_root_p521_key() { @@ -1441,6 +1497,7 @@ mod tests assert_eq!(point, &golden_chain::MODEL_TEST_ROOT_PUBKEY[..]); } + #[cfg(feature = "attestation")] #[test] fn each_leaf_sig_alg_is_dispatched_from_its_own_oid() { @@ -1454,6 +1511,7 @@ mod tests assert!(matches!(parse_cert_parts(bodies[3]).unwrap().sig_alg, SigAlg::EcdsaSha512)); } + #[cfg(feature = "attestation")] #[test] fn algorithm_confusion_in_leaf_is_rejected_bad_public_key() { @@ -1488,6 +1546,7 @@ mod tests ); } + #[cfg(feature = "attestation")] #[test] fn chain_der_length_long_form_over_two_bytes_unsupported() { @@ -1514,6 +1573,7 @@ mod tests ); } + #[cfg(feature = "attestation")] #[test] fn parse_spki_point_without_ec_public_key_is_bad_public_key() { @@ -1532,7 +1592,7 @@ mod tests } } -#[cfg(test)] +#[cfg(all(test, feature = "attestation"))] mod golden_chain { // HERMETIC GOLDEN for chain verification diff --git a/crates/tropic01-driver/src/crypto.rs b/crates/tropic01-driver/src/crypto.rs index eff884c..19644ea 100644 --- a/crates/tropic01-driver/src/crypto.rs +++ b/crates/tropic01-driver/src/crypto.rs @@ -18,10 +18,15 @@ use x25519_dalek::PublicKey; use x25519_dalek::StaticSecret; use zeroize::Zeroizing; +#[cfg(feature = "attestation")] use p384::ecdsa::signature::Verifier; +#[cfg(feature = "attestation")] use p384::ecdsa::Signature as P384Signature; +#[cfg(feature = "attestation")] use p384::ecdsa::VerifyingKey as P384VerifyingKey; +#[cfg(feature = "attestation")] use p521::ecdsa::Signature as P521Signature; +#[cfg(feature = "attestation")] use p521::ecdsa::VerifyingKey as P521VerifyingKey; /// AES-GCM authentication tag length, in bytes. @@ -154,6 +159,7 @@ pub(crate) fn aes256gcm_open /// verification failure maps to `CryptoError`. /// /// This operates on PUBLIC certificate data, so constant time is not required. +#[cfg(feature = "attestation")] pub(crate) fn ecdsa_p384_sha384_verify ( pubkey_sec1: &[u8], @@ -177,6 +183,7 @@ pub(crate) fn ecdsa_p384_sha384_verify /// verification failure maps to `CryptoError`. /// /// This operates on PUBLIC certificate data, so constant time is not required. +#[cfg(feature = "attestation")] pub(crate) fn ecdsa_p521_sha512_verify ( pubkey_sec1: &[u8], @@ -198,6 +205,7 @@ pub(crate) fn ecdsa_p521_sha512_verify /// a malformed pinned anchor fails at construction rather than at first verify. /// /// This operates on PUBLIC key data, so constant time is not required. +#[cfg(feature = "attestation")] pub(crate) fn p521_validate_point(point: &[u8]) -> Result<(), CryptoError> { P521VerifyingKey::from_sec1_bytes(point).map_err(|_| CryptoError)?; diff --git a/crates/tropic01-driver/src/device/bootloader.rs b/crates/tropic01-driver/src/device/bootloader.rs index 309385c..ed9ef98 100644 --- a/crates/tropic01-driver/src/device/bootloader.rs +++ b/crates/tropic01-driver/src/device/bootloader.rs @@ -199,6 +199,38 @@ where /// the returned `Bootloader` handle gates the 0xB0/0xB1 update primitives. /// Mirrors the maintenance-reboot step of libtropic `lt_do_mutable_fw_update`. /// + /// # Example + /// + /// The full update flow: enter the bootloader, drive both bank pairs, then + /// reboot back to the Application firmware. (For a single call that does all + /// of this, see `NoSession::update_firmware`.) + /// + /// ```no_run + /// # use embedded_hal::spi::{ErrorType, Operation, SpiDevice}; + /// # use core::convert::Infallible; + /// # struct Spi; + /// # impl ErrorType for Spi { type Error = Infallible; } + /// # impl SpiDevice for Spi { + /// # fn transaction(&mut self, _ops: &mut [Operation<'_, u8>]) -> Result<(), Infallible> { Ok(()) } + /// # } + /// # struct Wait; + /// # impl tropic01_driver::SeWait for Wait { + /// # type Error = Infallible; + /// # fn wait_ready(&mut self, _ms: u32) -> Result<(), Infallible> { Ok(()) } + /// # fn delay_ms(&mut self, _ms: u32) -> Result<(), Infallible> { Ok(()) } + /// # } + /// use tropic01_driver::Tropic01; + /// + /// fn update(cpu_image: &[u8], spect_image: &[u8]) -> Result<(), tropic01_driver::SeError> + /// { + /// let dev = Tropic01::new(Spi, Wait); + /// let mut bl = dev.enter_bootloader().map_err(|(_dev, e)| e)?; + /// bl.update_firmware(cpu_image, spect_image)?; + /// let _dev = bl.exit_to_application().map_err(|(_bl, e)| e)?; + /// Ok(()) + /// } + /// ``` + /// /// # Errors /// /// On failure returns the `NoSession` handle plus the error (a bus fault or diff --git a/crates/tropic01-driver/src/device/nosession.rs b/crates/tropic01-driver/src/device/nosession.rs index 758a318..f22d9b8 100644 --- a/crates/tropic01-driver/src/device/nosession.rs +++ b/crates/tropic01-driver/src/device/nosession.rs @@ -156,10 +156,15 @@ where /// static handle does not grow a 3840-byte stack frame. Requires Application /// FW mode. /// - /// SECURITY: this extracts STPUB only. It does NOT verify the certificate - /// chain up to the Tropic root (mirrors libtropic `lt_get_st_pub`). The - /// handshake auth tag already binds STPUB. To also attest the chip identity, - /// use `read_verified_chip_stpub`. See `cert::parse_stpub`. + /// SECURITY: this DOES NOT ATTEST that the chip is a genuine TROPIC01. It + /// extracts STPUB only and does NOT verify the certificate chain up to the + /// Tropic root (mirrors libtropic `lt_get_st_pub`). A counterfeit or swapped + /// chip can serve any STPUB here, and this call will return it. The handshake + /// auth tag binds STPUB into the session, so a wrong STPUB cannot silently + /// open a channel, but that is integrity, NOT authenticity. For the + /// true-TROPIC01 guarantee, use `read_verified_chip_stpub` with a + /// `RootAnchor` pinned OUT-OF-BAND (compiled in, never read from the chip). + /// See `cert::parse_stpub`. /// /// # Errors /// @@ -208,6 +213,7 @@ where /// chain does not verify under `anchor`, or `SeError::Cert(_)` when a /// certificate does not parse. Otherwise `SeError` on a bus fault or a /// malformed reply. + #[cfg(feature = "attestation")] pub fn read_verified_chip_stpub ( &mut self, @@ -424,6 +430,50 @@ where /// Consumes the handle. On success returns an `ActiveSession` handle ready /// for L3 commands. /// + /// # Example + /// + /// ```no_run + /// # use embedded_hal::spi::{ErrorType, Operation, SpiDevice}; + /// # use core::convert::Infallible; + /// # struct Spi; + /// # impl ErrorType for Spi { type Error = Infallible; } + /// # impl SpiDevice for Spi { + /// # fn transaction(&mut self, _ops: &mut [Operation<'_, u8>]) -> Result<(), Infallible> { Ok(()) } + /// # } + /// # struct Wait; + /// # impl tropic01_driver::SeWait for Wait { + /// # type Error = Infallible; + /// # fn wait_ready(&mut self, _ms: u32) -> Result<(), Infallible> { Ok(()) } + /// # fn delay_ms(&mut self, _ms: u32) -> Result<(), Infallible> { Ok(()) } + /// # } + /// use tropic01_driver::{SessionConfig, StartupId, Tropic01}; + /// use zeroize::Zeroizing; + /// + /// fn open() -> Result<(), tropic01_driver::SeError> + /// { + /// let mut dev = Tropic01::new(Spi, Wait); + /// // The secure channel lives in Application FW: reboot into it first. + /// dev.reboot(StartupId::Reboot)?; + /// // Placeholder keys: real ones come from a TRNG / provisioning, and + /// // `stpub` from the chip certificate (verified via read_verified_chip_stpub). + /// let ehpriv = Zeroizing::new([0u8; 32]); + /// let shipriv = Zeroizing::new([0u8; 32]); + /// let shipub = [0u8; 32]; + /// let stpub = [0u8; 32]; + /// let cfg = SessionConfig + /// { + /// ehpriv: &ehpriv, + /// shipriv: &shipriv, + /// shipub: &shipub, + /// stpub: &stpub, + /// pkey_index: 0, + /// }; + /// // The error path returns the handle too. Keep only the SeError here. + /// let _session = dev.open_session(cfg).map_err(|(_dev, e)| e)?; + /// Ok(()) + /// } + /// ``` + /// /// # Errors /// /// On failure returns the `NoSession` handle plus the error (a handshake, diff --git a/crates/tropic01-driver/src/error.rs b/crates/tropic01-driver/src/error.rs index 7ef9bf7..e24341d 100644 --- a/crates/tropic01-driver/src/error.rs +++ b/crates/tropic01-driver/src/error.rs @@ -102,6 +102,7 @@ pub enum CertError /// from the chip), so every variant is a fail-closed rejection. The load-bearing /// trust step is verifying the product CA under the caller-PINNED root key, never /// under a key taken from the store. +#[cfg(feature = "attestation")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ChainError { @@ -170,6 +171,7 @@ pub enum SeError /// Parsing the X.509 certificate store (STPUB extraction) failed. Cert(CertError), /// Verifying the X.509 certificate chain up to the pinned root failed. + #[cfg(feature = "attestation")] Chain(ChainError), /// The session was torn down. Re-handshake before any further L3 command. SessionLost, @@ -272,6 +274,7 @@ impl From for SeError } } +#[cfg(feature = "attestation")] impl From for SeError { fn from(e: ChainError) -> Self @@ -341,6 +344,7 @@ mod tests assert_eq!(e, SeError::Cert(CertError::KeyNotFound)); } + #[cfg(feature = "attestation")] #[test] fn chain_folds_into_se_error() { diff --git a/crates/tropic01-driver/src/lib.rs b/crates/tropic01-driver/src/lib.rs index 4362502..98f83ee 100644 --- a/crates/tropic01-driver/src/lib.rs +++ b/crates/tropic01-driver/src/lib.rs @@ -14,10 +14,74 @@ //! //! # Crate features //! -//! There is no user-facing feature. Both Cargo features are development-only and -//! a consumer leaves them off. `_fuzz` exposes the attacker-facing parsers to +//! The user-facing feature is `attestation` (ON by default): it enables X.509 +//! chain verification and pulls the ECDSA curve crates (`ecdsa`, `p384`, +//! `p521`). Those are PRE-RELEASE (`-rc`) crates today, pinned to keep one +//! `digest` generation in the tree, so a default build drags in release-candidate +//! crypto. Disable the feature via `default-features = false` to drop them when +//! only STPUB extraction is needed. The other two features are development-only +//! and a consumer leaves them off. `_fuzz` exposes the attacker-facing parsers to //! the libFuzzer harnesses. `model-itest` compiles the live integration tests //! that run against the official TROPIC01 emulator. +//! +//! # Example +//! +//! Open a secure channel and run one L3 command. The chip wiring (the SPI bus +//! and the ready/timeout provider) is supplied by the integrator, here it is +//! stubbed. All keys come from the caller via [`SessionConfig`]. +//! +//! ```no_run +//! # use embedded_hal::spi::{ErrorType, Operation, SpiDevice}; +//! # use core::convert::Infallible; +//! # struct Spi; +//! # impl ErrorType for Spi { type Error = Infallible; } +//! # impl SpiDevice for Spi { +//! # fn transaction(&mut self, _ops: &mut [Operation<'_, u8>]) -> Result<(), Infallible> { Ok(()) } +//! # } +//! # struct Wait; +//! # impl tropic01_driver::SeWait for Wait { +//! # type Error = Infallible; +//! # fn wait_ready(&mut self, _ms: u32) -> Result<(), Infallible> { Ok(()) } +//! # fn delay_ms(&mut self, _ms: u32) -> Result<(), Infallible> { Ok(()) } +//! # } +//! use tropic01_driver::{SeCommands, SessionConfig, StartupId, Tropic01}; +//! use zeroize::Zeroizing; +//! +//! fn run() -> Result<(), tropic01_driver::SeError> +//! { +//! let mut dev = Tropic01::new(Spi, Wait); +//! // Load the Application firmware: the secure channel lives there. +//! dev.reboot(StartupId::Reboot)?; +//! +//! // All key material is caller-provided. The driver hardcodes no secrets. +//! // These zeros are PLACEHOLDERS: real ephemerals come from a TRNG, the +//! // pairing keys from provisioning, and `stpub` from the chip certificate. +//! // For a genuine-chip trust decision, obtain `stpub` via +//! // `read_verified_chip_stpub` against a `RootAnchor` pinned out-of-band, +//! // not the unverified `read_chip_stpub`. +//! let ehpriv = Zeroizing::new([0u8; 32]); +//! let shipriv = Zeroizing::new([0u8; 32]); +//! let shipub = [0u8; 32]; +//! let stpub = [0u8; 32]; +//! let cfg = SessionConfig +//! { +//! ehpriv: &ehpriv, +//! shipriv: &shipriv, +//! shipub: &shipub, +//! stpub: &stpub, +//! pkey_index: 0, +//! }; +//! // open_session consumes the handle and reports the error as a tuple, so +//! // recover the SeError with map_err before using `?`. +//! let mut session = dev.open_session(cfg).map_err(|(_dev, e)| e)?; +//! +//! // Run one encrypted L3 command, then tear the channel down. +//! let mut random = [0u8; 32]; +//! session.random_into(&mut random)?; +//! let _dev = session.close_session(); +//! Ok(()) +//! } +//! ``` #![cfg_attr(not(test), no_std)] @@ -43,8 +107,11 @@ mod test_support; // Curated public surface. Nothing else is exported. pub use crate::cert::parse_stpub; +#[cfg(feature = "attestation")] pub use crate::cert::parse_verified_stpub; +#[cfg(feature = "attestation")] pub use crate::cert::verify_cert_chain; +#[cfg(feature = "attestation")] pub use crate::cert::RootAnchor; pub use crate::device::ActiveSession; pub use crate::device::Bootloader; @@ -56,6 +123,7 @@ pub use crate::device::SessionConfig; pub use crate::device::StartupId; pub use crate::device::Tropic01; pub use crate::error::CertError; +#[cfg(feature = "attestation")] pub use crate::error::ChainError; pub use crate::error::FwImageError; pub use crate::error::HandshakeError; @@ -147,6 +215,7 @@ pub mod fuzz /// pinned anchor. Must never panic. The anchor's exact value is irrelevant: /// fuzzing targets the bounded DER parsing in front of the crypto, which /// fails closed on essentially every mutated input. + #[cfg(feature = "attestation")] pub fn verify_cert_chain(data: &[u8]) { // A fixed, REAL P-521 SEC1 point (0x04 || X(66) || Y(66)). The anchor now diff --git a/crates/tropic01-driver/src/port.rs b/crates/tropic01-driver/src/port.rs index 0a54129..f6f1a47 100644 --- a/crates/tropic01-driver/src/port.rs +++ b/crates/tropic01-driver/src/port.rs @@ -560,6 +560,51 @@ pub trait SeCommands /// Returns the number of bytes written, which equals `out.len()`. An empty /// `out` returns `Ok(0)` with no chip traffic. /// + /// # Example + /// + /// ```no_run + /// # use embedded_hal::spi::{ErrorType, Operation, SpiDevice}; + /// # use core::convert::Infallible; + /// # struct Spi; + /// # impl ErrorType for Spi { type Error = Infallible; } + /// # impl SpiDevice for Spi { + /// # fn transaction(&mut self, _ops: &mut [Operation<'_, u8>]) -> Result<(), Infallible> { Ok(()) } + /// # } + /// # struct Wait; + /// # impl tropic01_driver::SeWait for Wait { + /// # type Error = Infallible; + /// # fn wait_ready(&mut self, _ms: u32) -> Result<(), Infallible> { Ok(()) } + /// # fn delay_ms(&mut self, _ms: u32) -> Result<(), Infallible> { Ok(()) } + /// # } + /// # use tropic01_driver::{SessionConfig, StartupId, Tropic01}; + /// # use zeroize::Zeroizing; + /// use tropic01_driver::SeCommands; + /// + /// # fn demo() -> Result<(), tropic01_driver::SeError> + /// # { + /// # let mut dev = Tropic01::new(Spi, Wait); + /// # dev.reboot(StartupId::Reboot)?; + /// # let ehpriv = Zeroizing::new([0u8; 32]); + /// # let shipriv = Zeroizing::new([0u8; 32]); + /// # let shipub = [0u8; 32]; + /// # let stpub = [0u8; 32]; + /// # let cfg = SessionConfig + /// # { + /// # ehpriv: &ehpriv, + /// # shipriv: &shipriv, + /// # shipub: &shipub, + /// # stpub: &stpub, + /// # pkey_index: 0, + /// # }; + /// # let mut session = dev.open_session(cfg).map_err(|(_d, e)| e)?; + /// // `session` is a Tropic01<_, _, ActiveSession>, which implements SeCommands. + /// let mut buf = [0u8; 16]; + /// let n = session.random_into(&mut buf)?; + /// assert_eq!(n, buf.len()); + /// # Ok(()) + /// # } + /// ``` + /// /// # Errors /// /// `SeError::InvalidArgument` when `out.len() > 255` (chunking is a caller diff --git a/crates/tropic01-driver/tests/model_itest.rs b/crates/tropic01-driver/tests/model_itest.rs index 3a7a735..d9f4c3a 100644 --- a/crates/tropic01-driver/tests/model_itest.rs +++ b/crates/tropic01-driver/tests/model_itest.rs @@ -78,6 +78,7 @@ const EHPRIV: [u8; 32] = hex32("0102030405060708090a0b0c0d0e0f101112131415161718 // This is the model's TEST root, captured from model_cfg.yml and cross-checked // with openssl. PROD differs: the integrator compiles in the real root. The // chain verifier anchors trust HERE, never in the store's self-signed root. +#[cfg(feature = "attestation")] const MODEL_TEST_ROOT_PUBKEY: [u8; 133] = hex_arr::<133>( "040135c7a24d16b374b207ade8fe50f503ad34e0e596c83fc98adb4c4388ca0ad9b24e\ 77e984b8978253a8e0d6fd68eaa8d9c9a9a6c8835a138cccff51130da109868000cdf7f\ @@ -712,6 +713,7 @@ fn read_chip_stpub_returns_pinned_stpub() assert_eq!(stpub, STPUB, "read_chip_stpub must match the model's pinned key"); } +#[cfg(feature = "attestation")] #[test] fn verify_cert_chain_accepts_the_model_chain() { @@ -728,6 +730,7 @@ fn verify_cert_chain_accepts_the_model_chain() tropic01_driver::verify_cert_chain(&store, &anchor).expect("model chain verifies under pinned root"); } +#[cfg(feature = "attestation")] #[test] fn verify_cert_chain_rejects_a_wrong_anchor() { @@ -745,6 +748,7 @@ fn verify_cert_chain_rejects_a_wrong_anchor() ); } +#[cfg(feature = "attestation")] #[test] fn read_verified_chip_stpub_returns_pinned_stpub() { @@ -869,6 +873,7 @@ fn sleep_is_reachable() /// Derived from a fixed non-trivial scalar so it is a real on-curve point that /// the eagerly-validating anchor constructor accepts, yet differs from the model /// TEST root, so the chain fails to verify under it. +#[cfg(feature = "attestation")] fn other_valid_p521_point() -> [u8; 133] { use p521::ecdsa::SigningKey; @@ -898,6 +903,7 @@ const fn hex32(s: &str) -> [u8; 32] /// Decodes a hex string (whitespace allowed) to `N` bytes at compile time. /// /// Skips ASCII whitespace, so the literal may be wrapped across lines. +#[cfg(feature = "attestation")] const fn hex_arr(s: &str) -> [u8; N] { let b = s.as_bytes();