SecuritySigning Guide

Signing Guide

The complete verify-and-sign procedure: rebuild every operation from values you trust, compare byte for byte, and only then let your key touch it.

This is the reference for implementing your signing service: the exact checks to run, the constants to pin, and the bytes to sign. It is written so that a correct implementation refuses to sign anything we could not legitimately have produced, including anything a fully compromised Rasto might try.

Everything in the tables below is pinned by documentation, not by our responses. A compromised platform writes the response. It cannot write this page. Hard-code these values, and change them only when this page changes.

What you are defending against

An operation's signing.digest is what your Primary Signing Key signs, and that signature is the on-chain authorization: the MPC network honors it directly. So your signing service must guarantee one thing. The digest commits to exactly the transaction you intend, and nothing else.

The procedure below makes that guarantee by reconstruction. You rebuild the entire signed structure from your own trusted inputs and require your bytes to equal ours. Anything that differs anywhere, an extra call, a swapped recipient, a smuggled field, changes the bytes, and therefore the digest, and therefore fails.

This is exhaustive by construction, which is why the envelope carries no byte offsets, no markers, and no counts for you to check individually.

The signing contract, version 1

Fixed values for signing.version: 1. These change only through a versioned, announced migration.

ConstantMainnet (live)Testnet (sandbox)
Signer contractv1.signerv1.signer-prod.testnet
Methodsignsign
Attached deposit1 yoctoNEAR1 yoctoNEAR
Attached gas30 Tgas (30000000000000)same
Domain, ed25519 (Solana)11
Domain, secp256k1 (EVM)00
Maximum authorization lifetime900 seconds900 seconds

Signature algorithm throughout: ed25519, raw 64-byte signatures, base64-encoded in transit.

The 900-second figure is the contract ceiling your verifier enforces, not the window you get. Operations we issue expire in 30 seconds. An authorization claiming more than 900 seconds is not ours.

The signing object

Every operation, and the one-time key activation, carries the same envelope.

FieldMeaningYour obligation
versionSigning-contract versionMust equal a version you support (1)
algorithmAlways ed25519Reject anything else
delegateThe exact bytes to be relayed, base64Rebuild and compare. Never decode and inspect
digestWhat you sign, hex, 32 bytesMust equal the canonical hash of your rebuild
nonce, nonce_index, max_block_heightScheduling: when the authorization may landOurs to choose. They bound replay and timing, and cannot change what is authorized

The procedure

Verify the destination transaction against your own request

For a Solana send, decode authorization.solana.message_base64 with @solana/web3.js and require all of:

  • The fee payer equals authorization.solana.fee_payer, which is ours, because your customer holds no SOL.
  • The token mint is the asset you requested, at authorization.solana.decimals.
  • Exactly one TransferChecked, from the wallet's associated token account to the recipient's, for exactly the amount you requested, optionally preceded by one idempotent create-account instruction for the recipient. We open first-time accounts and the rent is ours.
  • Nothing else. Any additional instruction fails verification.
  • The message bytes equal commitment.destination_payload exactly.

Do not check the important fields. Require the whole decoded message to be one of the two legitimate shapes. An instruction you did not think to look for must fail by construction, not by enumeration.

Rebuild the delegate

Using near-kit's serializeDelegateActionV2, reconstruct the delegate from values you already hold:

  • Your identity: verify authorization.identity.signing_identity equals the identity you registered, which is the hex of your Recovery Key's public bytes.
  • Your Primary Signing Key: the one you registered, never a key named in the response.
  • The wallet's signing_reference: published at wallet creation and immutable. Compare it to what you stored then.
  • The verified payload from step 1.
  • The contract constants from the table above.

The number and order of sign calls is part of what you are authorizing. A Solana send is exactly one. Rebuilding the wrong count or order yields a different digest, which is precisely how a smuggled extra signature request fails without you enumerating it.

Compare the canonical digest

Compute the canonical NEP-461 v2 hash of your rebuild and require it to equal signing.digest:

digest = SHA-256( u32_le(1073742435) || 0x00 || borsh(delegate) )

1073742435 is the NEP-611 domain discriminant (2^30 plus 611), and 0x00 is the payload version byte. near-kit computes this for you.

This is not SHA-256 of the bare delegate bytes. Hashing those directly mismatches every time.

As a final check, require anti_replay.expiry_unix to be within 900 seconds of your own clock. That needs no chain access, which is why the limit is part of the contract.

Sign and return

Sign the 32-byte digest raw with your Primary Signing Key. No prefix, no extra hashing, pure ed25519. Return it base64-encoded.

curl -s $BASE/api/wallets/wal_.../operations/wop_.../authorize \
  -H "Api-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"signature": "<base64 raw 64-byte ed25519>"}'

We verify it against your registered key at the door (422 invalid_signature otherwise), and the wire format re-verifies it again before relay.

The 30-second clock starts when the operation is created. The authorization pins live chain state, so verify-and-sign must run at machine speed: decode, rebuild, one KMS call, well under a second. Put any human approval before creating the operation, never inside the window. An expired operation costs nothing, so create a new one.

One time: activating your Primary Signing Key

Registration is arithmetic. Activation is the single on-chain step that turns sending on, and it is authorized by your Recovery Key, the only time the cold key signs anything routine.

Register, and receive the envelope

POST /dashboard/partner/wallet_keys with both public halves returns the registration and an activation object carrying the same signing envelope a send does. If it comes back null, the relay could not prepare it just then: retry with POST /dashboard/partner/wallet_keys/activation.

{
  "object": "wallet_key_activation",
  "signing_identity": "9f1c...",
  "recovery_key": "ed25519:...",
  "installing_primary_signing_key": "ed25519:...",
  "nonce_lanes": 8,
  "replacing": null,
  "signing": {
    "version": 1,
    "algorithm": "ed25519",
    "delegate": "<base64>",
    "digest": "<hex>",
    "nonce": 265547925000001,
    "nonce_index": 0,
    "max_block_height": 265555262
  }
}

Verify what is being installed

Rebuild the activation delegate with near-kit from your identity, the constants above, and, this is the point, your own Primary Signing Key as the key being installed. Confirm installing_primary_signing_key is yours, and confirm the digest matches your rebuild. You are verifying that the key gaining authority is yours and that nothing else rides along.

Sign with the Recovery Key, once

Sign the digest raw with the Recovery Key and return it, with the delegate echoed back:

curl -s $BASE/dashboard/partner/wallet_keys/activation/authorize \
  -H "Content-Type: application/json" \
  -d '{"delegate": "<the base64 you verified>", "signature": "<base64>"}'

A delegate that is not the activation we prepared, or a signature that is not the Recovery Key's over it, returns 422 activation_rejected. On success, sending is enabled for every wallet you will ever create and the ceremony never repeats. Put the cold key away.

Until activation completes, wallet creation and deposits work normally, and creating a send returns 400 activation_required.

Reference implementation

The verification above is implementable from public libraries alone: near-kit for the delegate, @solana/web3.js for the message. Our own test suite runs the partner's procedure, in the partner's libraries, against 40 adversarial payloads (swapped recipients, smuggled instructions, extra sign calls, unicode-escaped keys, wrong-count rebuilds) and requires every one to refuse before any key is touched. Ask us for the harness if you want to run it against your implementation.

Key handling expectations

  • Primary Signing Key: generate and keep it in a KMS or HSM that signs raw ed25519. AWS KMS ECC_NIST_EDWARDS25519 with MessageType: RAW works. The pre-hashed variant does not: it is HashEdDSA, and the signing network rejects it. Rotate whenever you like. The Recovery Key installs the replacement, and addresses never move.
  • Recovery Key: generate it offline, back it up like a root of trust, and touch it only for activation and rotation. It is your identity, it cannot be rotated, and losing it means no new signing key can ever be installed.