Ed25519 keys, signing, and verification
All snippets below import from
@algorandfoundation/algokit-utils/crypto, the subpath that re-exports@algorandfoundation/algokit-crypto. This is the low-level cryptographic layer underAccountManager— reach for it when you are wiring up a custom signer, implementing protocol helpers, or verifying signatures produced byalgokit-utils.
Generate a random Ed25519 keypair
Create a fresh Ed25519 keypair along with a raw signer function bound to it.
import { ed25519Generator } from "@algorandfoundation/algokit-utils/crypto";
const { ed25519Pubkey, ed25519SecretKey, rawEd25519Signer } = ed25519Generator();
console.log(ed25519Pubkey.length); // 32
console.log(ed25519SecretKey.length); // 32What just happened: ed25519Generator() drew a cryptographically random 32-byte
secret key, derived its 32-byte public key, and returned both alongside a
RawEd25519Signer closure that already captures the secret key. Any call to
rawEd25519Signer(bytes) will produce an Ed25519 signature over those bytes. The
default export is currently an alias for nobleEd25519Generator (backed by
@noble/ed25519) — import nobleEd25519Generator directly if you want to pin the
backend explicitly.
Generate a deterministic Ed25519 keypair from a seed
Pass a 32-byte seed to produce the same keypair every time.
import { ed25519Generator } from "@algorandfoundation/algokit-utils/crypto";
const seed = new Uint8Array(32);
seed.set([1, 2, 3, 4, 5]);
const { ed25519Pubkey, ed25519SecretKey } = ed25519Generator(seed);What just happened: When you supply a seed, @noble/ed25519 uses it directly as
the Ed25519 secret key — no extra hashing — so the resulting keypair is fully
deterministic in the seed. This is useful for tests and for reproducing keys from a
known source. Do not reuse a seed across unrelated identities.
Sign bytes with the generated signer
Call the returned rawEd25519Signer to produce a signature over arbitrary bytes.
import { ed25519Generator } from "@algorandfoundation/algokit-utils/crypto";
const { rawEd25519Signer } = ed25519Generator();
const message = new TextEncoder().encode("Hello, Algorand!");
const signature = await rawEd25519Signer(message);
console.log(signature.length); // 64What just happened: rawEd25519Signer implements the RawEd25519Signer contract
— an async function taking raw bytes and returning a 64-byte Ed25519 signature. It
signs exactly the bytes you pass in, with no added domain separator. When signing
Algorand transactions, logic sigs, or program data, let AccountManager or
generateAddressWithSigners handle the Algorand prefix (TX, Program, ProgData,
MX) for you — the raw signer is deliberately agnostic to domain separation.
Build a custom RawEd25519Signer
Implement the RawEd25519Signer type to plug any external signing source into
AlgoKit.
import type { RawEd25519Signer } from "@algorandfoundation/algokit-utils/crypto";
const customSigner: RawEd25519Signer = async (bytesToSign) => {
// Forward the bytes to a hardware wallet, KMS, HSM, or browser extension
const response = await fetch("https://my-signer.example.com/sign", {
method: "POST",
body: bytesToSign,
});
return new Uint8Array(await response.arrayBuffer());
};What just happened: RawEd25519Signer is simply
(bytesToSign: Uint8Array) => Promise<Uint8Array>. Anything that satisfies that
shape is accepted throughout the AlgoKit stack — AccountManager.setSigner and
generateAddressWithSigners combine it with a public key (via Ed25519SigningKey)
to build full Algorand signers. The signer is expected to produce a raw 64-byte
Ed25519 signature over the bytes it receives; any domain separation must be applied
by the caller before invocation.
Construct an Ed25519SigningKey manually
Assemble an Ed25519SigningKey from an existing public key and signer.
import type {
Ed25519SigningKey,
RawEd25519Signer,
} from "@algorandfoundation/algokit-utils/crypto";
declare const pubkey: Uint8Array; // 32 bytes from your key store
declare const rawSigner: RawEd25519Signer; // e.g. a hardware wallet wrapper
const signingKey: Ed25519SigningKey = {
ed25519Pubkey: pubkey,
rawEd25519Signer: rawSigner,
};What just happened: Ed25519SigningKey is a minimal record — just the 32-byte
public key and a RawEd25519Signer. Every AlgoKit helper that accepts a "signing
key" takes this shape, which is why third-party key sources (hardware wallets, KMS,
browser extensions) can be integrated without any extra adapter layer: expose a
public key and a sign function, wrap them in an object literal, and pass it on.
Verify an Ed25519 signature
Check whether a 64-byte signature is valid for the given message and public key.
import {
ed25519Generator,
ed25519Verifier,
} from "@algorandfoundation/algokit-utils/crypto";
const { ed25519Pubkey, rawEd25519Signer } = ed25519Generator();
const message = new TextEncoder().encode("Hello, Algorand!");
const signature = await rawEd25519Signer(message);
const isValid = await ed25519Verifier(signature, message, ed25519Pubkey);
console.log(isValid); // trueWhat just happened: ed25519Verifier(signature, message, pubkey) returns a
Promise<boolean>. Internally it delegates to @noble/ed25519's verifyAsync and
returns true only when the signature is a valid Ed25519 signature for exactly the
message bytes you pass in under the given public key. Flipping a single bit in any
argument causes it to return false rather than throw.
Verify a signature produced by the Algorand signing path
When the bytes you want to check came from a higher-level Algorand signer, verify against the same domain-prefixed bytes the signer saw.
import {
ed25519Generator,
ed25519Verifier,
} from "@algorandfoundation/algokit-utils/crypto";
import {
bytesForSigning,
generateAddressWithSigners,
} from "@algorandfoundation/algokit-utils/transact";
const generated = ed25519Generator();
const { addr, mxBytesSigner } = generateAddressWithSigners(generated);
const message = new TextEncoder().encode("Hello, Algorand!");
const signature = await mxBytesSigner(message);
// The signer hashed `MX` || message — feed the verifier the same bytes
const signedBytes = bytesForSigning.mxBytes(message);
const isValid = await ed25519Verifier(signature, signedBytes, addr.publicKey);
// The "raw" message would not verify, because mxBytesSigner adds a domain prefix
const isRawValid = await ed25519Verifier(signature, message, addr.publicKey);
console.log(isValid, isRawValid); // true falseWhat just happened: Every Algorand signer — transaction, logic sig, program
data, mx bytes — applies a domain-separation prefix before signing. When verifying
a signature that came out of one of those higher-level signers, reproduce the exact
bytes that were signed with the matching bytesForSigning.* helper. Verifying
against the raw user message will return false.
Use the pinned Noble backend explicitly
Import the noble-prefixed variants when you want a guaranteed implementation.
import {
nobleEd25519Generator,
nobleEd25519Verifier,
type RawEd25519Signer,
type RawEd25519Verifier,
} from "@algorandfoundation/algokit-utils/crypto";
const { ed25519Pubkey, rawEd25519Signer } = nobleEd25519Generator();
const signer: RawEd25519Signer = rawEd25519Signer;
const verify: RawEd25519Verifier = nobleEd25519Verifier;What just happened: nobleEd25519Generator and nobleEd25519Verifier are the
current concrete backings for ed25519Generator and ed25519Verifier. The package
documents that the default exports may change in future versions — if your code
depends on specific behaviour of the Noble implementation (constant-time
guarantees, exact error messages, etc.) import the noble* symbol directly so
future default-repointing cannot break you.