v0.4.10: A Trait for Hardware Keys, and Why P-256

Most of v0.4.10 is one Rust trait and a software reference. There is no PKCS#11 binding in this release. There is no YubiKey-aware CLI flag. There is no signed announcement in an admin tenant somewhere saying "your Oversight messages are now hardware-backed." What this release does is shape the seam that all of those things bolt onto, and it ships a working implementation of the curve they will use, so that the eventual PKCS#11 work is the only piece that has to deal with the hardware.

That sequencing was deliberate. PKCS#11 is heavy in three different ways at once: the binding itself (vendor differences, slot management, PIN UX), the test harness (softhsm or a token plugged in), and the curve plumbing (P-256 has different wire shapes than X25519). I wanted to land the curve plumbing while it was the only moving piece. Then PivKeyProvider becomes another KeyProvider implementation, plugged into a trait that already has a passing software reference, with no remaining doubt about the protocol-level constructions.

The trait

The recipient side of an Oversight open is small. Given the wrapped DEK envelope from the sealed file, the recipient does an ECDH against the sender's ephemeral public key, runs HKDF over the shared secret, and AEAD-decrypts the wrapped DEK. From there the outer ciphertext is symmetric. The only step that touches the recipient's long-lived private key is the ECDH.

That is the seam:

pub trait KeyProvider {
    fn algorithm(&self) -> KeyAlgorithm;
    fn public_key(&self) -> &[u8];
    fn ecdh(&self, peer_pub: &[u8]) -> Result<Zeroizing<Vec<u8>>, CryptoError>;
    fn label(&self) -> Option<&str> { None }
}

Hold on the size of this. The trait is four methods, three of them small. ecdh is the only operation a hardware token has to perform. Everything else, the HKDF, the AEAD, the canonical manifest verification, the policy enforcement, the content-hash check, all of it stays on the host. The token does one curve operation per open. That is the right division of labor: a PIV token's job is to hold a private scalar and never let it leave, and an Oversight host's job is to do the canonical-bytes work that the protocol depends on. Mixing those would be misuse.

FileKeyProvider ships as the default. It wraps the existing ClassicIdentity (X25519, the same one Oversight has been using since v0.3) and is byte-identical to the existing unwrap_dek path on file-backed keys, asserted by a test that runs both side by side and compares output. That is the migration guarantee for callers: opt into the trait whenever you want, and your decrypts do not change.

The suite: OSGT-HW-P256-v1

The default Oversight suite uses X25519, which is the right call for a software pipeline. X25519 is fast, the wire format is a 32-byte raw pubkey, and the implementations in x25519-dalek have been independently audited.

PIV slots are not X25519. PIV, the standard most enterprise hardware tokens implement (NIST SP 800-73), supports P-256 and P-384, and nothing else for ECDH. YubiKey 5.7+ added Curve25519 in firmware but only via the OpenPGP applet, not through PIV's PKCS#11 path. Nitrokey and OnlyKey are PIV-shaped and have similar constraints. If we want a single hardware-keys story that works on the broadest set of PIV deployments, the suite has to be on a curve the PIV applet supports.

So the new suite is OSGT-HW-P256-v1: NIST P-256 ECDH, SEC1 uncompressed (65 byte) public keys on the wire, the same XChaCha20-Poly1305 AEAD as classic, and HKDF info string "oversight-hw-p256-v1-dek-wrap". Cryptographic strength is unchanged. P-256 is FIPS 140-3 compliant, has been deployed in TLS 1.2 and 1.3 for over a decade, and has survived considerably more scrutiny than any post-quantum primitive will for a while yet. P-256 was not chosen because it is better; it was chosen because PIV runs on it. Crypto-agility was the feature that kept the option open.

The software reference

SoftwareP256KeyProvider sits next to FileKeyProvider as another implementation of the same trait. Internally it holds a P-256 scalar in process memory the way the file-backed provider holds an X25519 scalar. It exists for two reasons: to give the test suite a way to round-trip the new suite without a token plugged in, and to give recipients without a hardware token a fallback that produces interoperable OSGT-HW-P256-v1 envelopes. A token holder and a software-only holder of the same recipient identifier will both decrypt the same file, because the only thing PivKeyProvider changes is where the ECDH runs.

That symmetry matters. It means a regulated-industry deployment can roll out the suite in software, get the operational story right (revocation, key rotation, audit trail), and then issue hardware tokens to recipients later without rewriting the seal path or the manifest schema. The hardware is an upgrade to the runtime, not a fork of the protocol.

End to end on main

Two follow-up commits past the v0.4.10 tag wire the suite through oversight-container so that OSGT-HW-P256-v1 sealed files can actually be created and opened today. seal_hw_p256 is the seal-side counterpart: it consumes a SEC1 P-256 recipient public key, cross-checks the manifest's recipient.p256_pub field, generates an ephemeral P-256 keypair, runs ECDH, and packs a container with suite_id = 3. open_sealed_with_provider dispatches on the container's suite_id and delegates ECDH to a &dyn KeyProvider; today it accepts the classic and HW P-256 suites, and rejects cross-suite pairings explicitly rather than silently producing garbage shared secrets.

The manifest schema gained an optional p256_pub field on Recipient. It is gated by serde(default, skip_serializing_if = "Option::is_none") so existing sealed-file manifests deserialize unchanged. A classic recipient never serializes a p256_pub; a hardware recipient leaves x25519_pub empty and populates p256_pub instead. The container's existing rule that the unsigned suite_id header must match the signed manifest.suite takes care of any cross-pollination attacks for free.

What is not in v0.4.10

PivKeyProvider is the obvious miss. Plugging the existing trait into cryptoki is the next bounded session, and it brings PIN UX and softhsm-based CI with it. Until that lands, hardware-backed Oversight is in the same position the browser hybrid decrypt was in before a sample file existed: the protocol is real, the testing path is software-only.

CLI plumbing for hardware recipients is also absent. No oversight seal --recipient-hw, no oversight open --recipient-hw piv:9d. Those land once the underlying PivKeyProvider is in place and I can be sure the CLI is asking the token to do something it actually supports on the user's device.

Python parity for the new suite is open. The Python reference implementation is the protocol's source of truth, and right now it knows about classic and hybrid but not about OSGT-HW-P256-v1. The next conformance addition will bring the Python side up to the Rust side so the cross-language tests catch any drift.

What I want from this release

I want a regulated-industry reviewer to read the hardware-keys guide and the spec, run the same crate test suite I run, and say two things: the seam is in the right place, and the curve choice is honest. The PKCS#11 binding is mechanical work. Getting the protocol shape wrong is not.

Public API is purely additive. Every existing v0.4.9 caller continues to compile and behave the same. The oversight-crypto crate now passes 21 of 21 unit tests; oversight-container passes 17 of 17 with the seal-side additions; the workspace builds clean. The release is small in lines of code and large in the doors it opens.