The finding
Observed in the subduction spike, phase 2a (spikes/subduction/, commit 42b6193; discussion on #8), and carried through the walking skeleton (#8, commit 3177cbd):
subduction_crypto::Signer is infallible:
// subduction_crypto/src/signer.rs (rev 2401102)
pub trait Signer<Async: FutureForm> {
fn sign(&self, message: &[u8]) -> Async::Future<'_, Signature>;
fn verifying_key(&self) -> VerifyingKey;
}
A platform-backed signer — WebCrypto crypto.subtle, an OS keystore, an HSM — fails at runtime in ordinary ways: the key handle was evicted, the keystore is temporarily unavailable, the platform call errors. With this trait shape the failure has nowhere to go but a panic. In a wasm component a panic is a trap that poisons the whole instance — and in the engine-composite topology the instance also holds the keyhive state, the sedimentree store, and the automerge docs, so a transient signing hiccup would kill everything.
Where it bites in our code: the WebcryptoSigner impls in spikes/subduction/guest/src/lib.rs and spikes/skeleton/guest/src/lib.rs carry
.expect("webcrypto signing failed (Signer trait is infallible)")
with a comment marking it as this recorded finding.
The contrast is upstream's own sibling: keyhive's AsyncSigner::try_sign_bytes_async returns Result<Signature, SigningError>, its docs name WebCrypto and KMS as intended backends, and keyhive_wasm's own WebCrypto signer uses that fallibility. The two projects share authorship and the platform-held-key posture; subduction_crypto just predates or missed it.
Where the trait is exercised
The subduction handshake (Signed::seal in handshake::initiate/respond) and anything else signing through subduction_crypto::Signer. (The subduction_keyhive bridge signs its protocol messages through keyhive's fallible signer, so it is not affected.)
Suggested fix shape
Make the trait fallible, mirroring keyhive:
pub trait Signer<Async: FutureForm> {
type Error: core::error::Error;
fn sign(&self, message: &[u8]) -> Async::Future<'_, Result<Signature, Self::Error>>;
fn verifying_key(&self) -> VerifyingKey;
}
MemorySigner sets Error = Infallible. Call sites (handshake) already return rich error enums with transport variants, so threading a Signing(S::Error) variant through is mechanical. Breaking, but the workspace is pre-1.0 and marked unstable.
Draft upstream issue (for inkandswitch/subduction — do not file without explicit go)
Title: [api]: Signer::sign is infallible — platform-backed signers (WebCrypto, keystores) have no failure path
subduction_crypto::Signer::sign returns Signature directly. Signers whose key lives behind a platform handle — browser WebCrypto (crypto.subtle.sign over a non-extractable CryptoKey), OS keystores, HSMs — can fail at runtime (handle evicted, store unavailable, transient platform error), and with the current shape the only expressible outcome is a panic. In a wasm embedding that panic is a component trap, which destroys the whole instance over what may be a transient condition.
Keyhive's AsyncSigner models this correctly (Result<Signature, SigningError>, with WebCrypto/KMS named as intended backends, and keyhive_wasm's own WebCrypto signer relying on it), so the ecosystems diverge on the same design question: a Signer impl that holds keys the keyhive way cannot satisfy subduction's trait honestly.
Suggested: make the trait fallible (type Error: core::error::Error; + Result<Signature, Self::Error>), with MemorySigner::Error = Infallible; the handshake error enums already have room for a Signing variant. Happy to note our embedding as a concrete consumer: we back the signer with a non-extractable WebCrypto Ed25519 handle inside a wasm32-wasip2 component (https://github.com/polymorph-components/polymorph-apps/tree/main/spikes/subduction), currently via .expect(...).
Status
The finding
Observed in the subduction spike, phase 2a (spikes/subduction/, commit 42b6193; discussion on #8), and carried through the walking skeleton (#8, commit 3177cbd):
subduction_crypto::Signeris infallible:A platform-backed signer — WebCrypto
crypto.subtle, an OS keystore, an HSM — fails at runtime in ordinary ways: the key handle was evicted, the keystore is temporarily unavailable, the platform call errors. With this trait shape the failure has nowhere to go but a panic. In a wasm component a panic is a trap that poisons the whole instance — and in the engine-composite topology the instance also holds the keyhive state, the sedimentree store, and the automerge docs, so a transient signing hiccup would kill everything.Where it bites in our code: the
WebcryptoSignerimpls inspikes/subduction/guest/src/lib.rsandspikes/skeleton/guest/src/lib.rscarrywith a comment marking it as this recorded finding.
The contrast is upstream's own sibling: keyhive's
AsyncSigner::try_sign_bytes_asyncreturnsResult<Signature, SigningError>, its docs name WebCrypto and KMS as intended backends, and keyhive_wasm's own WebCrypto signer uses that fallibility. The two projects share authorship and the platform-held-key posture;subduction_cryptojust predates or missed it.Where the trait is exercised
The subduction handshake (
Signed::sealinhandshake::initiate/respond) and anything else signing throughsubduction_crypto::Signer. (Thesubduction_keyhivebridge signs its protocol messages through keyhive's fallible signer, so it is not affected.)Suggested fix shape
Make the trait fallible, mirroring keyhive:
MemorySignersetsError = Infallible. Call sites (handshake) already return rich error enums with transport variants, so threading aSigning(S::Error)variant through is mechanical. Breaking, but the workspace is pre-1.0 and marked unstable.Draft upstream issue (for inkandswitch/subduction — do not file without explicit go)
Status
expectcalls in the spike signers with real error propagation