Security
Security Model
A valid signature is not enough.
Trana enforces a live device second-factor approval at execution time — inside the program, at the moment the instruction runs. This page documents exactly what that guarantee covers, how each attack vector is neutralized, and what Trana does not protect against.
What Trana Trusts
Trana is designed to trust nothing except two things the user directly controls:
The Solana secp256r1 precompile (SIMD-0075). Added to the Solana validator in February 2025, this is a native instruction that verifies P-256 (secp256r1) signatures. It is part of the validator software itself — not a Trana smart contract. Its correctness is guaranteed by Solana Labs and the validator client teams.
The user's TwoFactorRegistry PDA. This account stores the user's registered P-256 public key. It is owned by the guard program and can only be modified by the wallet owner. It contains a nonce that increments on every successful enforcement.
Everything else — the SDK, the dApp frontend, the RPC node, Trana as an organization — is explicitly untrusted. Enforcement doesn't depend on any of them behaving correctly.
Attack Matrix
Every attack below was considered during design. Each one fails.
Raw transaction without proof
An attacker who compromises a wallet key sends a raw transaction calling the protected instruction directly, without the secp256r1 and record_proof instructions.
Result: MissingProof error. The guard program calls verify(), which reads the Instructions sysvar to find the secp256r1 instruction at ix[N-2]. If it is absent, enforcement fails immediately inside the program. This check happens onchain — it cannot be bypassed at the frontend or RPC layer.
Replay of an old proof
An attacker captures a valid passkey signature from a previous transaction and re-submits it.
Result: PayloadMismatch error. The intent hash includes the registry nonce, a u64 that the guard program increments after every successful enforcement. A replayed proof carries the old nonce value, which no longer matches the current registry state.
Parameter tampering after approval
A user approves "withdraw 0.1 SOL" but an attacker modifies the amount to 100 SOL before the transaction lands.
Result: PayloadMismatch error. The intent hash includes params_hash = SHA-256(raw_instruction_params). The guard recomputes this hash from the live transaction data — not from the stored proof. Any change to the instruction parameters produces a different hash, which fails the comparison.
Account substitution
An attacker substitutes a different recipient account after the passkey has signed.
Result: PayloadMismatch error. The intent hash includes accounts_hash = SHA-256(concat of all account pubkeys). The guard reads accounts from the live transaction at execution time. Swapping any account changes the hash.
Cross-program abuse
A malicious program tries to call guard::cpi::enforce() and pass a proof that was originally approved for a different program.
Result: PayloadMismatch error. The intent hash includes targetProgramId. The guard verifies that the protected instruction at the index identified by the Instructions sysvar belongs to the expected program. A proof approved for Program A cannot authorize Program B.
Cross-cluster replay
An attacker captures a devnet proof and replays it on mainnet.
Result: PayloadMismatch error. The intent hash includes the cluster string ("devnet" or "mainnet-beta"). Devnet and mainnet produce different hashes from identical inputs.
Expired proof
An attacker captures a proof and waits until network congestion passes to replay it at a favorable moment.
Result: ProofExpired error. The intent hash includes expiry_unix, a Unix timestamp set at proof creation time (default: 120 seconds ahead). The guard reads Clock::get() and rejects any proof where now >= expiry_unix.
Wrong signing device
An attacker registers their own P-256 key into the user's registry by compromising the wallet key.
Result: This is the primary residual risk and is covered in the limitations section below. During normal operation, WrongSigner error if a different device's key attempts to sign.
The Proof Pipeline
When enforce() is called:
- Load Instructions sysvar. Read the current instruction index (
current_idx). - Find secp256r1 instruction. Expect it at
ix[current_idx - 2]. If absent:MissingProof. - Find record_proof instruction. Expect it at
ix[current_idx - 1]. Parse the WebAuthn payload. - Recompute accounts_hash. SHA-256 of all account pubkeys in the live instruction at
current_idx. - Recompute params_hash. SHA-256 of instruction data bytes
[8..](skipping the 8-byte Anchor discriminator). - Reconstruct intent hash. Build the canonical binary payload from all intent fields and hash it.
- Compare with WebAuthn clientDataHash. The passkey signs
SHA-256(authenticatorData || clientDataJSON), whereclientDataJSONcontains the challenge (= intent hash). Verify they match. - Verify secp256r1 signature. Read the public key and signature offsets from the precompile instruction. Confirm the pubkey matches the registry. Confirm the signature is valid over the WebAuthn message.
- Check expiry. Reject if
Clock::get().unix_timestamp >= proof.expiry_unix. - Evaluate policy. Check policy conditions (Threshold, Velocity, RapidDrain, Always, Admin). If condition not met: noop. If met and proof valid: enforce.
- Increment nonce. Write
registry.nonce += 1to prevent replay. - Emit ProofVerified event. Policy, target program, nonce — permanent onchain audit trail.
Audit Trail
Every successful enforcement emits a ProofVerified event onchain:
ProofVerified {
owner: Pubkey, // wallet that owns the registry
policy: String, // e.g. "trana.threshold"
target_program: Pubkey, // program being protected
nonce: u64, // registry nonce after this enforcement
}
This event is indexed by the Solana event log and is permanently visible on any block explorer. Protocols can subscribe to these events for alerting and compliance logging.
Limitations
Trana is honest about what it doesn't protect against.
Phishing / deceptive UX. If an attacker builds a convincing fake frontend that shows "deposit 0.1 SOL" while actually passing "withdraw 100 SOL" as the intent, the user may approve the wrong thing. The confirmation modal in the Trana SDK addresses this by decoding the actual parameter bytes — but the SDK is untrusted. A custom integration could show anything. Users should verify the decoded amount shown in the approval prompt.
Complete wallet + device compromise. If an attacker has both the wallet private key and access to the registered FIDO2 device (e.g., an unlocked laptop), they can authorize any action. This is the worst-case scenario and is equivalent to a physical breach.
Registry hijacking via wallet compromise. A wallet key alone can call register_two_fa and replace the registered passkey. The 72-hour time-locked recovery mechanism (v1.1) adds a delay window for this scenario — existing passkeys can cancel a recovery initiated by a compromised wallet key.
Consensus-level attacks. A validator majority could theoretically reorder or censor transactions. This is not a Trana-specific risk — it applies to all Solana programs.
Trust Model Summary
| Component | Trusted? | Reason |
|---|---|---|
| Solana secp256r1 precompile | Yes | Part of validator software |
| TwoFactorRegistry PDA | Yes | Onchain state, user-controlled |
| Trana SDK | No | Client-side, can be forked or modified |
| dApp frontend | No | Can be compromised (XSS, supply chain) |
| RPC node | No | Can censor, delay, return wrong state |
| Trana organization | No | No admin key after upgrade authority burned |
| User's FIDO2 device | Yes | WebAuthn: private key never leaves hardware |