Private betaRequest early access →

Sign in

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:

Text
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:

Shell
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.