Skip to main content
lattice
Developer Resources

Build Security That Can Evolve

Templates, checklists and educational examples for teams preparing a cryptographic migration. Everything here is copyable; every code example is educational unless marked otherwise.

01

Quickstart Guide

  1. Step 1

    Read two explainers

    Start with “Why Digital Ownership Depends on Cryptography” and “Why Quantum Readiness Is More Than an Algorithm Swap”. Together they take about fifteen minutes and set the vocabulary.

  2. Step 2

    Run the readiness assessment

    Answer the six sections honestly. “Not sure” is a valid answer and becomes an open question. Export the JSON and keep it with your architecture documents.

  3. Step 3

    Build your cryptographic inventory

    Use the template below. List every algorithm, library, version, the data it protects and an owner. This is the artefact every later decision depends on.

  4. Step 4

    Introduce a provider interface

    Route signing and verification through one interface that carries an algorithm identifier. Prove it works by enabling a second algorithm behind a flag in staging.

  5. Step 5

    Experiment in the playground, then on Devnet

    Compare sizes and timings with ephemeral keys. If you prototype on-chain behaviour, use Solana Devnet with disposable keys and label the work as a prototype.

  6. Step 6

    Write a one-page plan

    Even “monitor these three roadmaps and review quarterly” is a plan. Assign an owner and a review date.

02

Threat Model Template

Copy, fill in, and keep next to your architecture documentation.

THREAT_MODEL.md
# Threat model: <system name>
Last reviewed: <date>   Owner: <name>

## 1. Assets
- On-chain assets controlled by user keys
- Service signing keys (list each: purpose, algorithm, storage)
- Long-lived encrypted data (what, where, confidentiality lifetime in years)
- Session and API credentials

## 2. Trust boundaries
- User device ↔ application frontend
- Frontend ↔ backend services
- Backend ↔ key management (KMS / HSM / signing service)
- Application ↔ third-party wallet or signing providers
- Application ↔ blockchain RPC

## 3. Cryptographic dependencies (link to inventory)
| Use | Algorithm | Library + version | Quantum-vulnerable? | Lifetime of protected data |
|-----|-----------|-------------------|---------------------|----------------------------|

## 4. Threats considered
- Key theft from application servers
- Key misuse by insiders
- Harvest-now-decrypt-later of recorded traffic or backups
- Forgery of signatures if the scheme is broken (classical or quantum)
- Supply-chain compromise of a cryptographic dependency
- Loss of keys without recovery

## 5. Mitigations and gaps
| Threat | Current mitigation | Gap | Owner | Target date |
|--------|--------------------|-----|-------|-------------|

## 6. Migration triggers
- Which standards or ecosystem events would change priorities?
- Who monitors them and how often?

## 7. Out of scope
State explicitly what this model does not cover.

03

Cryptographic Inventory Template

The artefact every migration decision depends on.

CRYPTO_INVENTORY.md
# Cryptographic inventory: <system name>
Last reviewed: <date>   Owner: <name>

| # | Component | Use (sign / verify / KEM / encrypt / hash / TLS) | Algorithm + parameters | Library + version | Where keys live | Data protected | Required lifetime | Rotatable? | Algorithm id stored with data? | Owner |
|---|-----------|--------------------------------------------------|------------------------|-------------------|-----------------|----------------|-------------------|------------|-------------------------------|-------|
| 1 | Wallet adapter | sign | Ed25519 | @solana/web3.js 1.x | user wallet | transaction authorization | n/a | no (identity) | implicit | |
| 2 | Session auth | verify | Ed25519 | @noble/curves 2.4.0 | user wallet | sign-in challenge | minutes | n/a | yes | |
| 3 | Backup encryption | encrypt | AES-256-GCM, key wrapped by KMS | ... | KMS | user data | 10 years | yes | partially | |

Notes
- Include third-party providers and what they sign or hold on your behalf.
- "Required lifetime" drives harvest-now-decrypt-later priority.
- Review after every dependency upgrade and at least quarterly.

04

Key Management Checklist

  • Every class of key has a documented purpose, algorithm and owner.
  • Sensitive keys are generated in hardware, a KMS/HSM, MPC or on the user's device, never on general-purpose servers.
  • Keys are stored in hardware or platform secure storage; nothing in configuration files or environment variables.
  • Signing is isolated behind a narrow service boundary with its own authentication and audit log.
  • Key rotation is automated, rehearsed in staging on a schedule, and supports an overlap window.
  • Recovery is documented and has been tested end to end.
  • Key access and signing operations are logged and alerted on anomalies.
  • Every stored key carries an algorithm identifier and a key id.
  • Backups and sync channels that encrypt key material use hybrid post-quantum key establishment where available.
  • No tooling, form or support process ever asks users for seed phrases or private keys.

05

Migration Planning Checklist

  • Inventory complete

    Every algorithm, library and version listed with owners and protected data.

  • Exposure ranked

    Long-lived confidentiality first; hard-to-change identities planned earliest.

  • Provider interface in place

    No direct library calls outside the cryptography module.

  • Algorithm identifiers stored

    Keys, signatures and ciphertexts are tagged; untagged data is treated as legacy.

  • Second algorithm proven in staging

    Enabled behind a flag; tests cover both paths.

  • Hybrid key exchange enabled

    TLS and internal channels use hybrid ML-KEM where the platform supports it.

  • Dual-support period defined

    Verification accepts old and new; adoption is measured.

  • Ecosystem dependencies tracked

    Wallet, RPC, HSM and chain roadmaps monitored with named owners.

  • Retirement criteria written

    What adoption level and date allow removing the legacy path.

  • Plan owned and dated

    One page, one owner, one review cadence.

Background: Planning a Cryptographic Migration.

06

Cryptographic Agility Guide

Six habits that make an algorithm change a configuration change.

  • One interface, many algorithms

    All signing, verification and key establishment flows through a provider interface. The interface returns metadata (algorithm, mode, library) with every result.

  • Tag everything

    Keys, signatures, ciphertexts and sessions carry an algorithm identifier and a version. Code never infers the algorithm from byte length.

  • No hard-coded sizes

    Serialisation reads sizes from algorithm metadata. Tests that assert 64-byte signatures are tests of the past.

  • Rotation is routine

    If rotating a key requires a meeting, migrating an algorithm will require a crisis. Automate rotation and run it.

  • Fail closed on unknowns

    An unrecognised algorithm identifier is rejected, not defaulted. Simulated providers are never substituted for real ones.

  • Measure before choosing

    Know your byte budgets and latency budgets. On Solana, the 1,232-byte transaction limit decides more than raw speed does.

07

Solana Integration Notes

Devnet only. Never production funds.

Lattice never sends transactions or asks for a wallet connection. Run your own on-chain experiments on Solana Devnet with disposable keys.
Devnet only
All experimentation must target Solana Devnet with disposable keys funded by the Devnet faucet. Never reuse mainnet keys and never test with real funds.
Base-layer authorization is Ed25519
Every transaction needs Ed25519 signatures from its signers, including the fee payer. Nothing an application does changes this.
Application-level verification is possible but constrained
A program can verify a signature passed in instruction data and act on a PDA-controlled account. It must fit within the transaction size limit and compute budget, which rules out standard post-quantum signatures in a single transaction today.
No native post-quantum verifier
Solana ships native Ed25519 and secp256k1 verification programs. There is no post-quantum equivalent at the time of review.
Label prototypes clearly
Any on-chain experiment must state that it is a prototype, does not make accounts quantum-resistant, and is unreviewed.

08

Example Architecture Diagrams

Layered architecture with a cryptography adapterThe UI calls the API layer, which calls a domain service layer. The service layer uses a database layer and a cryptography adapter. The adapter resolves each algorithm id to a named @noble provider and rejects unknown algorithms instead of falling back.UI layerserver + client componentsAPI layerroute handlers · zod · same-originDomain / service layerassessment · research · playgroundDatabase layerdrizzle · libSQL / SQLiteCrypto adapterSignatureProvider · KemProvider13 algorithms · @noble · named on every result
Lattice's own layering. The UI never imports a cryptographic library directly.
Dual-support migration flowPhase one signs and verifies with the legacy algorithm. Phase two still signs with legacy while verification accepts both. Phase three signs with the new algorithm while verification accepts both. Phase four retires the legacy path.1Legacy onlysign: legacyverify: legacy2Accept bothsign: legacyverify: legacy + new3Sign newsign: newverify: legacy + new4Retiresign: newverify: newAdoption metrics decide when each phase ends
A generic dual-support sequence. On-chain identities depend on protocol and wallet support before phase 3.

09

Example API Contracts

Lattice's own typed routes. Every input is validated with Zod and every response uses one envelope.

  • GET/api/assessments

    List this browser's assessments.

    response: { "ok": true, "data": [ { "id": "<uuid>", "projectName": "…", "status": "draft", "answeredCount": 12, … } ] }
  • POST/api/assessments

    Create a draft assessment.

    request: { "projectName": "Example dApp" }
    response: { "ok": true, "data": { "id": "<uuid>", "status": "draft", … } }
  • PUT/api/assessments/{id}/answers

    Upsert a batch of answers. Each is scored deterministically on save.

    request: { "answers": [ { "questionId": "key_storage", "values": ["hsm"] } ], "step": 3 }
    response: { "ok": true, "data": [ { "questionId": "key_storage", "score": 4, "priority": "informational", "unknown": false } ] }
  • POST/api/assessments/{id}/complete

    Check required questions, generate the report and mark the assessment completed.

    response: { "ok": true, "data": { "assessment": {…}, "answers": […], "report": { "categoryScores": […], "findings": […] } } }
  • GET/api/assessments/{id}/export?format=json|md

    Download the report as JSON or Markdown (Content-Disposition: attachment).

    response: { "schema": "lattice.assessment.v1", "platform": { "disclaimer": "…" }, "assessment": {…}, "answers": […] }
  • POST/api/assessments/{id}/share

    Create a read-only link to a completed report. DELETE turns the link off.

    response: { "ok": true, "data": { "shared": true, "token": "…", "url": "https://getlattice.online/report/…" } }
  • GET/api/research?q=&category=&difficulty=

    Search and filter research articles.

    response: { "ok": true, "data": [ { "slug": "…", "title": "…", "readingTime": 8, … } ] }
  • POST/api/playground/signature

    Run a stateless signature demonstration on the server with an ephemeral key.

    request: { "algorithm": "ml-dsa-65", "message": "hello" }
    response: { "ok": true, "data": { "mode": "prototype", "publicKeyBytes": 1952, "signatureBytes": 3309, "steps": […] } }
  • POST/api/playground/benchmark

    Bounded timing run in the server environment; slow algorithms run fewer iterations.

    request: { "algorithm": "ml-kem-768", "iterations": 5 }
    response: { "ok": true, "data": { "mode": "prototype", "environment": {…}, "samples": […], "mean": {…} } }
  • ANYError shape

    All errors share one envelope and never include stack traces.

    response: { "ok": false, "error": { "code": "validation_error", "message": "…", "details": [ { "path": "answers.0.values", "message": "…" } ] } }

10

Code Examples

Copyable TypeScript. Each example is educational and says so in its first line.

Creating a cryptographic provider interface

Put every signing and verification call behind one interface that carries algorithm metadata. This is the single most valuable agility step.

provider-interface.ts
// EDUCATIONAL EXAMPLE — illustrates structure, not a production implementation.
// Review any real implementation independently before use.

export type ProviderMode = "prototype" | "simulated" | "unavailable";

export interface ProviderMetadata {
  id: string;            // e.g. "noble-ml-dsa-65"
  algorithm: string;     // e.g. "ml-dsa-65"
  mode: ProviderMode;    // never let a simulation masquerade as real
  library?: { name: string; version: string };
}

export interface SignatureProvider {
  readonly metadata: ProviderMetadata;
  generateKeyPair(): Promise<{ publicKey: Uint8Array; secretKey: Uint8Array }>;
  sign(message: Uint8Array, secretKey: Uint8Array): Promise<Uint8Array>;
  verify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): Promise<boolean>;
}

// A registry resolves algorithm ids to providers. There is deliberately no
// fallback from a real provider to a simulated one.
const providers = new Map<string, SignatureProvider>();

export function registerProvider(provider: SignatureProvider) {
  providers.set(provider.metadata.algorithm, provider);
}

export function getProvider(algorithm: string): SignatureProvider {
  const provider = providers.get(algorithm);
  if (!provider) throw new Error(`No provider registered for ${algorithm}`);
  return provider;
}

Signing a challenge

A server issues a random, short-lived challenge; the client signs it with an ephemeral key through the provider. Never sign arbitrary server-supplied bytes with a wallet key without a clear domain prefix.

sign-challenge.ts
// EDUCATIONAL EXAMPLE — uses ephemeral demo keys, never wallet keys.
import { getProvider } from "./providers";

const DOMAIN_PREFIX = new TextEncoder().encode("lattice-demo-challenge:v1:");

export async function signChallenge(algorithm: string, challenge: Uint8Array) {
  const provider = getProvider(algorithm);
  const { publicKey, secretKey } = await provider.generateKeyPair(); // demo key only

  // Domain separation: prefix the challenge so the signature cannot be replayed
  // as something else (for example a transaction) in another context.
  const message = new Uint8Array(DOMAIN_PREFIX.length + challenge.length);
  message.set(DOMAIN_PREFIX, 0);
  message.set(challenge, DOMAIN_PREFIX.length);

  const signature = await provider.sign(message, secretKey);
  return { algorithm, publicKey, signature, mode: provider.metadata.mode };
}

Verifying a signature

Verification must use the algorithm recorded with the signature, reconstruct the exact signed bytes, and treat any exception as a failed verification.

verify-signature.ts
// EDUCATIONAL EXAMPLE — verification path with explicit algorithm metadata.
import { getProvider } from "./providers";

interface SignedChallenge {
  algorithm: string;      // taken from the envelope, never guessed
  publicKey: Uint8Array;
  signature: Uint8Array;
}

const DOMAIN_PREFIX = new TextEncoder().encode("lattice-demo-challenge:v1:");

export async function verifyChallenge(input: SignedChallenge, challenge: Uint8Array) {
  const provider = getProvider(input.algorithm);
  const message = new Uint8Array(DOMAIN_PREFIX.length + challenge.length);
  message.set(DOMAIN_PREFIX, 0);
  message.set(challenge, DOMAIN_PREFIX.length);

  try {
    return await provider.verify(input.signature, message, input.publicKey);
  } catch {
    // Malformed keys or signatures are a verification failure, not a crash.
    return false;
  }
}

Recording algorithm metadata

Store an algorithm identifier and version alongside every key, signature and ciphertext. Untagged artefacts are the hardest part of a migration.

algorithm-metadata.ts
// EDUCATIONAL EXAMPLE — a tagged envelope for stored signatures.
export interface SignatureEnvelope {
  v: 1;                       // envelope format version
  alg: string;                // "ed25519" | "ml-dsa-65" | ...
  lib: string;                // "@noble/post-quantum@0.7.1" — provenance for audits
  kid: string;                // key identifier, so rotation is traceable
  createdAt: string;          // ISO-8601
  sig: string;                // base64url signature bytes
}

export function wrapSignature(alg: string, lib: string, kid: string, sig: Uint8Array): SignatureEnvelope {
  return { v: 1, alg, lib, kid, createdAt: new Date().toISOString(), sig: toBase64Url(sig) };
}

// Reject envelopes with unknown algorithms instead of guessing a default.
export function assertKnownAlgorithm(env: SignatureEnvelope, allowed: ReadonlySet<string>) {
  if (!allowed.has(env.alg)) throw new Error(`Unsupported algorithm: ${env.alg}`);
}

function toBase64Url(bytes: Uint8Array): string {
  return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

Designing key rotation

Rotation with an overlap window: new signatures use the current key, verification accepts current and previous keys until the window closes. The same mechanism later carries an algorithm change.

key-rotation.ts
// EDUCATIONAL EXAMPLE — rotation policy with an overlap window.
interface KeyRecord {
  kid: string;
  alg: string;
  publicKey: Uint8Array;
  notBefore: string;   // ISO-8601
  notAfter: string;    // ISO-8601, end of the verification overlap
  status: "current" | "previous" | "retired";
}

export function selectSigningKey(keys: KeyRecord[]): KeyRecord {
  const current = keys.find((k) => k.status === "current");
  if (!current) throw new Error("No current signing key");
  return current;
}

export function keysAcceptedForVerification(keys: KeyRecord[], at = new Date()): KeyRecord[] {
  return keys.filter(
    (k) => k.status !== "retired" && new Date(k.notBefore) <= at && at <= new Date(k.notAfter),
  );
}

// Rotation steps (also rehearsed in staging on a schedule):
// 1. Generate the next key (possibly with a new algorithm) → status "current".
// 2. Demote the old key → status "previous" with notAfter = now + overlap.
// 3. After the overlap, mark the old key "retired" and remove its code path.

Separating wallet authorization from application sessions

A wallet signature should prove control once, at sign-in. The application then issues its own short-lived session, which can change algorithm independently of the chain.

wallet-session.ts
// EDUCATIONAL EXAMPLE — conceptual sign-in flow; not a complete auth system.

// 1. Server issues a one-time challenge bound to the origin and a nonce.
export function issueChallenge(origin: string) {
  const nonce = randomId();
  const issuedAt = new Date().toISOString();
  // Human-readable so the wallet can display what is being signed.
  const text = `${origin} wants you to sign in.\nNonce: ${nonce}\nIssued at: ${issuedAt}`;
  store(nonce, { origin, issuedAt, used: false });
  return { nonce, text };
}

// 2. The wallet signs the text. The server verifies it ONCE and marks the nonce used.
export async function exchangeForSession(nonce: string, publicKey: Uint8Array, signature: Uint8Array) {
  const challenge = take(nonce);            // single use
  if (!challenge || challenge.used) throw new Error("Invalid or reused challenge");
  const ok = await verifyWithRegisteredProvider("ed25519", signature, encode(challengeText(challenge)), publicKey);
  if (!ok) throw new Error("Signature invalid");

  // 3. Issue an application session that does not depend on the wallet key again.
  //    Rotating the session scheme later does not touch the chain or the wallet.
  return createSession({ subject: toAddress(publicKey), expiresInMinutes: 30 });
}

// Helper stubs omitted for brevity: randomId, store, take, encode, challengeText,
// verifyWithRegisteredProvider, createSession, toAddress.

11

Frequently Asked Questions