Skip to content

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.

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.com
Version: 1
Chain ID: 8141
Nonce: k3Jd92xPq1aB
Issued At: 2026-08-15T12:00:00Z

The 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 account block (codeId, optional index, and backupPkHash for 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 with counterfactual 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-1271 isValidSignature probe. 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.

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.

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 + sessionId
park signIn request ───────► store
open frost://signin?req=<url> ──────────────────────────────────► fetch request
request ◄───────────────────────── build message
review + sign
reply (envelope) ◄────────────────── (ML-DSA, frost-siwf)
verify envelope ◄─── read ───
open session

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 sessionId tied to the requesting browser session, put it in the message (Request ID), and reject a reply whose signed sessionId is 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 honour Expiration 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.

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-65 signature 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.