ROOTVIELROOTVIEL
← Academy

Module 7 · Labs & experiments

Lab — sign and verify a message (Ed25519)

A guided, self-contained lab on synthetic data. You produce an Ed25519 key pair, sign exact bytes, verify, and flip a single byte to see the signature fail — building the muscle memory that "verifies" means "these bytes came from this key unaltered", and nothing about whether the request was wise or allowed.

Learning objective

Run a complete sign/verify loop with Ed25519: generate a key pair, sign a canonical message, verify it with the public key, then watch verification reject a tampered copy — and read the result as authenticity and integrity, never as authorization.

Track
Engineering
Module
7 · Labs & experiments
Prerequisites
Digital signatures · Hash functions · Node.js 16+ (built-in crypto)

Lab goal

This is a teaching exercise on synthetic data, run entirely on your own machine with no network, no external service and no real keys or secrets. By the end you will have signed a message, verified it, and confirmed that any change to the signed bytes breaks verification. That is the whole trust model of a signature: authenticity (who authored these bytes) and integrity (that they were not altered). It is not authorization — a signature never says the action is correct, safe or permitted.

Note

Keys you generate here live only in memory for the length of the run. Never reuse lab keys for anything real, and never paste a production private key into an exercise.

Setup

  1. 1Confirm Node.js 16 or newer: node --version. Ed25519 is in the built-in crypto module, so there is nothing to install.
  2. 2Create an empty working folder and a file lab1.js inside it.
  3. 3Paste the code below into lab1.js and run node lab1.js.

Step by step

  1. 1Generate an Ed25519 key pair; the private key is the signing capability, the public key is the verification capability anyone may hold.
  2. 2Canonicalize the message to exact bytes — here a JSON object with sorted keys — so signer and verifier agree byte-for-byte.
  3. 3Sign the canonical bytes with the private key to get a signature.
  4. 4Verify (message, signature) against the public key: this must accept.
  5. 5Tamper with one byte of the message and verify again against the same signature: this must reject.
lab1.js — synthetic sign/verify loop (Node.js crypto)
const crypto = require('crypto'); // 1) Key pair — lives only in memory for this run. const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519'); // 2) Canonical bytes: sorted-key JSON so both sides agree exactly. function canonical(obj) { return Buffer.from(JSON.stringify(obj, Object.keys(obj).sort())); } const message = { action: 'rotate-key', scope: 'vault:lab', note: 'synthetic' }; const msgBytes = canonical(message); // 3) Sign (Ed25519 takes null as the algorithm argument). const signature = crypto.sign(null, msgBytes, privateKey); // 4) Verify the genuine message -> expect true. const okGenuine = crypto.verify(null, msgBytes, publicKey, signature); // 5) Tamper: change one field, keep the old signature -> expect false. const tampered = canonical({ ...message, scope: 'vault:prod' }); const okTampered = crypto.verify(null, tampered, publicKey, signature); console.log('signature length (bytes):', signature.length); console.log('verify genuine :', okGenuine); console.log('verify tampered:', okTampered);

Expected result

expected console output
signature length (bytes): 64 verify genuine : true verify tampered: false

The 64-byte signature verifies against the exact bytes it was made over and fails against anything else. Changing scope from vault:lab to vault:prod — a single meaningful edit — is enough to make verification return false. This is integrity in action: the verifier learns the bytes were altered without needing to know how.

Common mistakes

Note

Passing an algorithm string instead of null to sign/verify throws for Ed25519 — the curve already fixes the hash, so the algorithm argument must be null.

  • ▸Signing a non-canonical string: if signer and verifier serialize the object differently (key order, whitespace), a genuine message can fail. Always canonicalize both sides the same way.
  • ▸Treating "verify = true" as "request approved". Verification proves authorship and integrity only; the allow/deny decision is a separate, later step.
  • ▸Reusing one key pair across contexts. In real systems bind the key to a context (domain separation) and keep private keys in a KMS/HSM; here the keys are disposable lab material.

Exercise

Note

Extend the lab: instead of changing a field, keep the message identical but flip exactly one bit of the signature (e.g. signature[0] ^= 1) before verifying. Predict the result and explain what it demonstrates about integrity.

Expected direction: Verification returns false. Ed25519 verification checks an algebraic relation between the public key, the message and the full signature, so corrupting any bit of the signature breaks that relation just as corrupting the message does. It demonstrates that integrity covers the signature itself, not only the payload — there is no "almost valid" signature.

Check your understanding

What two things does a successful Ed25519 verification establish?
Authenticity (the bytes were produced by the holder of the matching private key) and integrity (the bytes were not altered since signing). Nothing about whether the action is correct, safe or authorized.
Why must you pass null as the algorithm argument to crypto.sign/crypto.verify for Ed25519?
Ed25519 fixes its own hash internally, so Node expects null rather than a separate digest algorithm; passing a string throws.
Why does canonicalizing the message matter for sign and verify?
Signer and verifier must hash the exact same bytes; differing serialization (key order, whitespace) makes a genuine message fail verification.

References

  1. [1]RFC 8032 — EdDSA / Ed25519
  2. [2]Node.js crypto — sign / verify (Ed25519)
  3. [3]NIST FIPS 186-5 — Digital Signature Standard

References point to public, well-known technical sources for further study.

Related articles