Open-source Rust firmware for PatinaKey, a USB hardware security key implementing FIDO2, OpenPGP card, and PKCS#11.
| Component | Part | Role |
|---|---|---|
| MCU | STM32U545CEU6Q (Cortex-M33 + TrustZone) | USB FS, application logic, on-chip crypto block |
| Secure Element | TROPIC01 TR01-C2P-T301 | Key storage, ECC signing, TRNG, PIN enforcement |
The MCU and the TROPIC01 communicate over SPI. All long-term private keys live inside the TROPIC01 and are non-exportable. Every command exchange is protected by a Noise KK1 encrypted session negotiated at startup.
The project is under active development. Only the secure-element driver (crates/tropic01-driver) exists so far. The USB stack and the FIDO2/OpenPGP/PKCS#11 layers are not yet started. See crates/tropic01-driver/README.md for the driver's detailed status and command-coverage roadmap.
Secure-element driver - working
- Noise KK1 handshake: authenticated key agreement with the TROPIC01
- AES-256-GCM command/response codec with advance-after-verify nonces
- Fail-closed command gate: a crypto, structural, or parse fault on any command tears the session down and zeroizes the keys
- The full
SeCommandstrait is assembled over twenty-two commands:random(TRNG), monotonic-counter read / init / update, R-memory read / write / erase, ECC key generation / public-key read / import / erase, ECDSA / EdDSA signing, MAC-and-Destroy (PIN primitive), host-pairing-key write / read / invalidate, and R/I-config object write / read / erase (access privileges, irreversible OTP), plus apingdiagnostic - ECC public-key read returns the chip's attested curve, so an upper layer cannot pick the wrong signing algorithm
- The MAC-and-Destroy output is returned in a zeroize-on-drop secret type
- Range-checked slot types: an out-of-range key/counter/memory/PIN/pairing index cannot be constructed
reboot(Startup_Req) to enter Application FW before a sessionsleep(Sleep_Req) for low power, andchip_modedecoding CHIP_STATUS to Application / Startup / Alarmabort_sessionnotifies the chip to drop the secure session, wiping the host session keys and the L3 plaintext before the round-tripget_log_into(Get_Log_Req) reads the raw RISC-V FW debug log, disabled on production partsGet_Info(L2, no session): reads the raw X.509 certificate store, CHIP_ID, and RISCV/SPECT firmware versions- STPUB extraction:
parse_stpub/read_chip_stpubwalk the X.509 cert store's DER (depth-bounded, panic-free) to pull the chip static X25519 key for the handshake - X.509 chain verification:
verify_cert_chain/parse_verified_stpubauthenticate the chip identity by verifying the cert chain (ECDSA P-384/SHA-384 then P-521/SHA-512) up to a caller-pinned Tropic root, never trusting the store's own root. Cryptographic path only - validity dates and revocation are left to the integrator - Configuration objects: R-Config write / read / erase and I-Config write / read, gating per-command access by pairing key (CFG_UAP). The I-Config write is a documented irreversible OTP bit-burn
- Firmware-update bootloader (0xB0 / 0xB1): the
Bootloadertype-state, the boundedFwImageChunksblob decoder, and theupdate_firmwareorchestrator. The host is a pure transport (the chip verifies the EdDSA signature). Golden-byte tested only - the emulator models none of the bootloader, so a real-silicon power-fault test is a hard gate before production - Validated end-to-end against the official
tropic01_modelemulator: real handshake + real AES-GCM, every command's success path plus protocol-reachable failures (see the driver's validation table). The bootloader is the exception: golden-byte tested, not model-backed - 407 host tests, six libFuzzer targets on parser entry points, 39 live-model tests
- Clean
thumbv8m.main-none-eabihfbuild (no_std proven on the target)
Not yet implemented
- Validation against real silicon (the
tropic01_modelemulator is already wired up. See the validation table). The firmware-update bootloader especially: it is golden-byte tested only and needs a hardware power-fault test before any production use - MCU firmware: USB stack, FIDO2/CTAP2, OpenPGP card, PKCS#11, TrustZone partition
Minimum supported Rust version (MSRV): 1.88 (edition 2024), verified to build. Developed on a newer stable. No guarantee is provided below 1.88.
# Host check and tests
cargo check --workspace --locked
cargo test --workspace --locked
# Firmware target (no_std proof - bare-metal has no test harness)
rustup target add thumbv8m.main-none-eabihf
cargo check -p tropic01-driver --locked --target thumbv8m.main-none-eabihfThe pipeline runs on every push and pull request. Run the same checks locally with:
scripts/ci-local.sh # full run
scripts/ci-local.sh --quick # skip coverage and fuzzGates (all blocking unless noted):
| Gate | Tool | Notes |
|---|---|---|
| Check | cargo check |
host and thumbv8m.main-none-eabihf |
| Lint | cargo clippy |
zero warnings, JSON report for SonarQube |
| Test | cargo test |
host via mock ports |
| Model integration | tropic01_model (ts-tvl) |
live end-to-end against the official model (libtropic pinned). Run under coverage |
| Coverage | cargo-llvm-cov |
hermetic + live-model tests, line floor 90%, lcov for SonarQube |
| Advisories | cargo audit |
blocks on any RustSec finding, SARIF export |
| Dependency policy | cargo deny |
license allow-list, no unknown sources, no yanked crates |
| Unused deps | cargo udeps |
nightly |
| Outdated | cargo outdated |
informational, never blocking |
| Fuzz | cargo fuzz |
60s per target on PR, 15 min on weekly schedule |
| Quality scan | SonarQube | consumes the three reports above |
See .github/workflows/ci.yml for the full pipeline and sonar-project.properties for the SonarQube configuration.
Note: rustfmt is intentionally absent. The project uses a strict Allman brace style that rustfmt cannot reproduce. Formatting is reviewed, not auto-applied.
Live model integration. A suite drives tropic01-driver end-to-end against the official TROPIC01 model (ts-tvl): the real Noise KK1 handshake and AES-GCM codec run against an independent implementation of the chip. The GitHub coverage job clones libtropic (pinned to a tag + commit), installs the model, and runs these under coverage, so the library paths they exercise count toward the line floor (the test-harness files are excluded from the report). Locally:
crates/tropic01-driver/scripts/model-itest.sh # just the live tests
LIBTROPIC=/path/to/libtropic scripts/ci-local.sh # coverage then includes themThe model needs Python and a one-time install (scripts/tropic01_model/install_linux.sh in the libtropic checkout). Without LIBTROPIC, the local coverage stage stays hermetic and the live tests are skipped.
- No heap - all buffers are statically allocated. No
VecorBoxanywhere in the firmware crates - No unsafe - enforced by
#![forbid(unsafe_code)]at the workspace level - Zeroize on drop - every secret (session keys, ephemeral scalars) implements
ZeroizeOnDrop - Typed errors - no
unwraporpanic!outside tests. Every failure path returns a typedResult - Minimal supply chain - prefer rewriting a small piece over pulling a non-essential crate. Every dependency is an audit liability on a security product
This project is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later).
Any modifications or products built on this code must remain open-source under the same terms. This ensures that improvements to a security tool flow back to the community.
To integrate PatinaKey into a proprietary closed-source product, contact us to discuss a commercial license.
Contact: contact@patinakey.fr