Verifiable signatures
Sometimes "the user clicked approve" is not enough: a release, a payment instruction, a contract or an agent's high-risk action needs a signature that a third party can verify without trusting Perihelium's word for it. The signing ceremony produces exactly that: a WebAuthn assertion made by the person's passkey over a challenge derived from what they were shown, returned to you unaltered with everything needed to verify it offline.
The ceremony
Two calls, both from your application's backend, with the person's browser in between.
Open. Your backend opens a signing request with a purpose (a short code your application
defines), a SHA-256 digest of the thing being signed and the display_text the person will
read. The response carries the WebAuthn options for the browser: the challenge, the person's
registered passkeys as allowCredentials, and userVerification: required.
Complete. The browser performs the passkey ceremony and your backend posts the result. The
response returns the assertion unaltered — authenticatorData, clientDataJSON, signature,
credentialId, userHandle — the credential's public key as both COSE and JWK, and the
signer's sub, tid, the time, and the amr of the session.
A signing request is single-use, bound to the session that opened it, expires in minutes, and requires a recent authentication, exactly like a step-up. A signature cannot be made by an agent, an API key or a delegated session; only the person's own passkey.
The challenge derivation is published
A verifier must be able to recompute the challenge from the inputs, so the derivation is part of the product contract:
challenge = SHA-256( "zynth-sign-v1" ‖ len(purpose) ‖ purpose
‖ len(digest) ‖ digest
‖ len(text) ‖ display_text
‖ len(nonce) ‖ server_nonce )
Every field is length-prefixed; display_text is bound so the text the person read and the text
that was signed cannot differ; the server nonce is single-use, so a signature cannot be replayed.
The domain-separation prefix guarantees a signing assertion can never satisfy a login challenge:
both are WebAuthn get ceremonies, and the prefix is what tells them apart.
The reference verifier
A small, dependency-free verifier is published with the documentation. It checks a signature from
clientDataJSON, authenticatorData, signature, the public key and the original inputs alone
— no call to the platform, no network. The acceptance test of the whole feature is that it works
with nothing else:
verify-signature --public-key key.jwk --client-data clientDataJSON --authenticator-data authenticatorData \
--signature signature --purpose release-approval --digest <sha256> --text "Approve release 2026.10" --nonce <nonce>
A verifier written in your own stack needs about forty lines: recompute the challenge, confirm it
appears in clientDataJSON, confirm the RP ID hash and the user-verified flag in
authenticatorData, and verify the signature over authenticatorData ‖ SHA-256(clientDataJSON)
with the public key.
Key history, so old signatures stay verifiable
A passkey a person removes is retired, not erased: its public key is kept with created_at
and removed_at, so a signature made last year still ties to the key that made it. A person's
account erasure still deletes everything, key history included — a signature that must outlive an
erasure is yours to keep, which is why the ceremony returns the public key with the assertion.
Public-key export for a service
A service credential with the right scope can export the public keys of the organization's members' passkeys, with their validity windows, so a verifying system can pre-load the keys it will need. The export shows only what the organization may know: keys registered through other organizations a person belongs to are not revealed.
Several allowed origins
A relying party with more than one origin (a web app and an admin app, a production and a staging host) registers an allow-list of origins under one relying-party ID; passkeys registered on any of them sign for all of them.
Where it is used
- External action approvals — an approval that must be provable to an auditor is made as a signature over the action's digest (Approvals for actions Perihelium does not execute).
- Payment instructions — strong customer authentication with a signature over the transaction's amount and payee (Payment security).
- Agent actions in the highest band — the person's signature over what the agent asked for.