The v0.4.10 post
ended with a short, honest list of three things that release did not
ship: the PivKeyProvider binding to real hardware tokens,
the --recipient-hw CLI flag, and Python parity for the new
suite. v0.4.11 closes one of those hard, and it adds a third surface
neither list mentioned. The OSGT-HW-P256-v1 suite now runs
end to end in every reference implementation Oversight has: the Rust
core, the Python core, and the public browser inspector. Every layer of
the protocol ships every suite.
What is still not here is the actual hardware. PivKeyProvider
and the CLI flag remain the next bounded session. But the shape of the
work has changed. In v0.4.10 the suite was real in one implementation
and a software-only test path. After v0.4.11, three independent
implementations agree on the same wire bytes for suite_id = 3,
and the conformance tests between them are the thing that will catch any
drift when the PKCS#11 binding lands. The hard part of the curve plumbing
is done in three places at once instead of one.
Python parity, byte for byte
The Python reference implementation is the protocol's source of truth, so
a suite that only exists in Rust is a suite the conformance harness cannot
see. v0.4.11 brings Python up. New wrap_dek_for_recipient_p256
and unwrap_dek_p256 in oversight_core.crypto
mirror the Rust reference exactly, and "exactly" is a testable claim here:
same HKDF info string, same AEAD AAD, same 65-byte SEC1 uncompressed
ephemeral public key on the wire, same wrapped-envelope JSON shape
including the explicit "suite" field.
HKDF info: "oversight-hw-p256-v1-dek-wrap"
AEAD AAD: "oversight-hw-p256-dek"
ephemeral: SEC1 uncompressed, 65 bytes
container: suite_id = 3 -> OSGT-HW-P256-v1
oversight_core.container now recognizes suite_id = 3
and maps it to OSGT-HW-P256-v1 in SUITE_ID_TO_NAME,
so inspecting a hardware-suite file in Python reports the right name
instead of an unknown-suite error. On the unwrap side,
unwrap_dek_p256 accepts the recipient key in three forms:
an EllipticCurvePrivateKey, PKCS#8-encoded bytes, or a raw
integer scalar. That is not convenience typing. It is the on-ramp for the
eventual PIV / PKCS#11 binding, which will hand the function a scalar
recovered from a token operation without forcing the caller through a
particular serialization.
The new tests/test_hw_p256.py is ten tests, and the negative
paths matter more than the happy path. Round trips run across all three
private-key input forms. The on-wire envelope shape is checked against
SPEC.md § 5.2. Then the rejection set: wrong recipient, wrong
ephemeral key length, missing fields, and the one I care about most, AAD
binding. A classic envelope's bytes must not silently decrypt through the
hardware path. The AAD tags the envelope to its suite, so an attacker who
swaps a classic wrapped DEK into a hardware container gets an AEAD failure,
not a plaintext. That is the class of bug a parity pass is supposed to
surface before a real deployment does.
The Rust container closes the loop
The v0.4.10 post described seal_hw_p256 and
open_sealed_with_provider as follow-up commits on
main past the tag. v0.4.11 is the release where they ride
into a tagged version. seal_hw_p256 mirrors seal
but consumes a P-256 SEC1 uncompressed recipient public key and writes a
container with suite_id = 3. open_sealed_with_provider
is the polymorphic open path: it dispatches on the container's
suite_id and delegates the recipient-side ECDH to a
KeyProvider.
Today that dispatcher supports the classic suite through
FileKeyProvider and the hardware suite through
SoftwareP256KeyProvider, with a future
PivKeyProvider dropping into the same slot. A hybrid-aware
provider extension lands later. Cross-suite mismatches are refused
explicitly: hand an X25519 provider to a hardware-P-256 container and you
get an error, not a garbage shared secret that happens to decrypt. That
refusal is the same posture as the Python AAD-binding test, enforced at
the dispatch boundary instead of the AEAD boundary.
The manifest schema gains an optional p256_pub field on
Recipient, gated by
serde(default, skip_serializing_if = "Option::is_none"). A
classic recipient never serializes it; a hardware recipient leaves
x25519_pub empty and fills p256_pub instead.
Existing JSON manifests parse unchanged, which is the migration guarantee
that matters: sealed files written before this release stay readable, and
the container's existing rule that the unsigned suite_id
header must match the signed manifest.suite keeps the two
from being mixed. Five new round-trip and negative tests land with it;
oversight-container is 17 of 17 and the workspace builds
clean.
The browser inspector
The third surface is the public browser inspector.
It now decrypts the OSGT-HW-P256-v1 sample fixture end to end,
backed by a pinned, vendored P-256 ECDH from @noble/curves.
A recipient can open a hardware-suite file in a browser tab without
installing the CLI, and the answer is bit-identical to the Rust core and
the Python core on the same input.
Pinning a vendored curve implementation in the viewer is deliberate. The browser is the surface where I have the least control over the runtime, and the least ability to debug a recipient who got a different answer. A single, auditable implementation, shipped with the page, removes "whichever WebCrypto the browser exposed" as a variable. It is heavier on the wire, and it is the right tradeoff for a page whose entire job is to give the same cryptographic answer as the desktop.
What is still not in v0.4.11
PivKeyProvider remains the obvious miss: the
PKCS#11 binding to actual YubiKey, Nitrokey, and OnlyKey tokens through
PIV. The trait it plugs into has shipped, has a passing software
reference, and now has passing parity tests on both sides of the
Python / Rust conformance line. The binding itself brings PIN UX and a
softhsm-backed CI harness with it, and that is the next bounded session.
CLI plumbing for hardware recipients is still absent. No
oversight seal --recipient-hw, no
oversight open --recipient-hw piv:9d. Those land with the
provider, so the CLI is asking the token to do something I have confirmed
it supports on the user's device, rather than guessing.
Everything in this release is reachable through library and test surfaces today. The token binding turns the software path into a hardware path without changing the seal, the manifest, or the wire format.
What I want from this release
The cross-language conformance test is the ground truth this project is
built on. Before v0.4.11, the hardware suite existed in one
implementation and was invisible to it. After v0.4.11, three
implementations agree on suite_id = 3 down to the HKDF info
string and the AEAD AAD, and the negative tests on both sides enforce
that the suites cannot be quietly crossed. When
PivKeyProvider lands, the only thing it changes is where one
ECDH runs. Everything else, the manifest, the container, the policy, the
transparency-log entry, is already decided in code that three runtimes
agree on.
The release is on GitHub, the protocol detail is in the hardware-keys guide and SPEC.md, and you can open a hardware-suite sample in the browser inspector without installing anything.