Sign in with Frost
Sign in with Frost (SIWF) lets a web service prove that a visitor controls a Frost account, by having their wallet sign a human-readable message. It is the Sign-In with Ethereum (EIP-4361) pattern, generalised to the chain-agnostic CAIP-122 data model and adapted to Frost’s post-quantum accounts.
It is not a transaction: no funds move, no gas is paid, and nothing is written on chain. The message is signed off-chain and verified off-chain. What differs from Ethereum’s SIWE is entirely in the signature and the account model.
The message
Section titled “The message”A SIWF message is an EIP-4361-shaped plaintext block. The only changes from SIWE
are the first line, the chain id, and two fields promoted to required
(Nonce and Issued At):
https://example.com wants you to sign in with your Frost account:0x1234…AbCd
Sign in to Example.
URI: https://example.comVersion: 1Chain ID: 8141Nonce: k3Jd92xPq1aBIssued At: 2026-08-15T12:00:00ZThe address must be ERC-55
checksummed. Field order follows EIP-4361. Optional Expiration Time,
Not Before, Request ID, and Resources follow, in that order.
Three things SIWE doesn’t have to handle
Section titled “Three things SIWE doesn’t have to handle”Frost accounts are post-quantum smart accounts, not secp256k1 EOAs, so a SIWF verifier does three things a SIWE verifier doesn’t.
The signature carries the public key. ML-DSA — like every NIST
post-quantum signature — has no public-key recovery, so there is no ecrecover
that turns a signature into an address. The wallet’s reply therefore includes
the full public key (1,952 bytes for ML-DSA-65) alongside the signature, and the
verifier binds that key to the account itself (below).
Binding the key to the address is an account lookup, not a hash. A Frost
address is not hash(pubkey); it is the counterfactual
CREATE2 address of the account’s init code, and a
rotatable account can change its active key
without changing its address. So the verifier dispatches on the account:
- Not yet deployed — the envelope must carry an
accountblock (codeId, optionalindex, andbackupPkHashfor a rotatable or hash-commit account). The verifier recomputes the CREATE2 address from the public key and that block and requires it to equal the claimed address; a counterfactual account without one fails withcounterfactual account without account block. - Deployed — read the account’s current key from chain state (the embedded
key for a fixed-key account, the key-pointer for a rotatable account, the
keccak256(pk)commitment in slot 0 for a hash-commit v3 account) and require it to match the supplied key. Code the verifier does not recognise falls back to an ERC-1271isValidSignatureprobe. Live chain state always wins, so a rotation immediately changes which key is accepted.
A signing context firewalls sign-in from transactions. SIWF signatures are
made under the FIPS 204 context string frost-siwf. Frame-transaction witnesses
are verified by the 0x14/0x15 precompiles under
the empty context, so a sign-in signature can never be replayed as a
transaction, and a transaction witness can never be replayed as a sign-in — by
construction, not by convention.
Verifying a sign-in
Section titled “Verifying a sign-in”The viem-8141 SDK does the whole check —
message parsing, the signature verification under the frost-siwf context, and
the account binding — behind one call. Give it the wallet’s reply, what your
server expects, and a viem public client to read chain state:
import { createPublicClient, http } from 'viem';import { frost, verifySiwfMessage } from 'viem-8141';
const client = createPublicClient({ chain: frost, transport: http('https://rpc.frostfi.net'),});
const result = await verifySiwfMessage({ envelope, // { message, type, signature, publicKey, account? } // account is required until the account is deployed expected: { domain: 'example.com', // must equal the origin that served the request nonce, // the single-use nonce you issued sessionId, // the session this browser started (see below) chainId: 8141, }, reader: client,});
if (result.valid) { // result.address is authenticated — open a session bound to it.} else { // result.reason explains why it failed.}verifySiwfMessage checks, in order: the message parses; domain, nonce,
sessionId, and chain id match; the timestamps are in range; the public key and
signature lengths match the declared type; the signature verifies under the
frost-siwf context; and the key binds to the address. Your server still owns
issuing the nonce, consuming it once, and creating the session.
The flow
Section titled “The flow”A wallet drives SIWF over whatever channel it uses for signing requests — for
the reference FrostWallet app, a
frost://signin?req=<relay-url> deep link. The wallet builds the message
itself from the origin it fetched the request from and its own account
address; the requesting site supplies only parameters (statement, nonce,
issued-at, resources). That is the anti-phishing property: a page cannot make
the wallet sign a message naming a different site.
service relay (service origin) wallet─────── ────────────────────── ──────issue nonce + sessionIdpark signIn request ───────► storeopen frost://signin?req=<url> ──────────────────────────────────► fetch request request ◄───────────────────────── build message review + sign reply (envelope) ◄────────────────── (ML-DSA, frost-siwf)verify envelope ◄─── read ───open sessionSecurity checklist
Section titled “Security checklist”SIWF’s message binding stops a page from naming another site, but not every attack. A correct integration also does the following.
- Bind the session to the browser that started it. Issue a single-use
sessionIdtied to the requesting browser session, put it in the message (Request ID), and reject a reply whose signedsessionIdis not the one this browser started. Without this, an attacker can start a real sign-in, relay the deep link to a victim, and receive the victim’s authenticated session — a confused-deputy takeover. Pair it with a short match code shown on both the site and the wallet’s consent screen for the user to compare. - Nonce is single-use. Consume it the first time it verifies.
- Check the time bounds. Reject a future
Issued At(allow a small skew) and honourExpiration Time/Not Before. - Re-verify on session refresh. Because a key rotation changes which key the account accepts, sessions are revoked only as fast as you re-check the binding. Re-run verification on a bounded cadence so a rotated-away key (for example, after a lost-device recovery) loses its session promptly.
- Bind the session to the address only, never to a resource that can change.
Standards
Section titled “Standards”SIWF is a Frost profile of the same standards SIWE builds on, so it stays interoperable with the wider ecosystem’s tooling and mental model:
- EIP-4361 — the Sign-In with Ethereum message format SIWF mirrors.
- CAIP-122 — the chain-agnostic
“Sign in With X” data model; a Frost namespace declares the
frost:ml-dsa-65signature type. - ERC-1271 — the smart-account signature-validation interface, the same abstraction that lets SIWE and SIWF verify non-EOA accounts.
See Account contracts for the code ids and key layouts the binding step reads, and viem-8141 for the signing and verification API.