Ed25519 Signing
Ed25519 lets anyone holding the public key verify envelopes without being able to create them.
1. Flow
- Generate a keypair once and store the raw 32-byte private seed and public key in two files (the engine does not generate keys).
- Configure
setup.signing = Setup::ed25519(private_path, public_path)?. A missing file is an error, andSetup::run()checks that the private key is a 32-byte seed and that the public key matches it. - Every envelope gets
sig= lowercase hex Ed25519 signature over the v2 message (see Cryptographic Verification). - A verifier with the public key checks the version, freshness, slot and signature.
┌────────────────────────┐
│ Rust origin │ signs the v2 message with the private key
└───────────┬────────────┘
▼
┌────────────────────────┐
│ CDN / proxy │ forwards the envelope
└───────────┬────────────┘
▼
┌────────────────────────┐
│ Verifier (edge, │ checks sig with the public key
│ service or your JS) │
└────────────────────────┘
2. Rust API
src/crypto/ed25519/sign.rs and src/crypto/ed25519/verify.rs:
// anza::crypto::ed25519::sign
pub fn payload(private_key_bytes: &[u8], data: &[u8]) -> anza::Result<String>;
// anza::crypto::ed25519::verify
pub fn check(public_key_bytes: &[u8], data: &[u8], signature_hex: &str) -> bool;
use anza::crypto::{ed25519::{sign, verify}, envelope};
// Raw 32-byte keys, e.g. from ed25519-dalek or read from the files above.
let private_key: [u8; 32] = std::fs::read("keys/origin.key")?.try_into().unwrap();
let public_key: [u8; 32] = std::fs::read("keys/origin.pub")?.try_into().unwrap();
let message = envelope::message(1_724_771_200, "feed", "<ui-card>Live</ui-card>", None);
let signature_hex = sign::payload(&private_key, &message)?;
assert!(verify::check(&public_key, &message, &signature_hex));
check returns false (never an error) for a bad key, bad hex or a wrong
signature. It only checks the signature: use verify_envelope (below) to
also check the version, age and slot.
To create a keypair, use ed25519-dalek (with its rand_core feature) directly:
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
let signing_key = SigningKey::generate(&mut OsRng);
std::fs::write("keys/origin.key", signing_key.to_bytes())?;
std::fs::write("keys/origin.pub", signing_key.verifying_key().to_bytes())?;
3. Verifying on the server
A service or edge that holds only the public key verifies a received envelope
with anza::verify_envelope:
use anza::{verify_envelope, Envelope, VerifyKey, VerifyOptions};
let public_key = std::fs::read("keys/origin.pub")?;
let envelope: Envelope = serde_json::from_str(&body)?;
// Slot "feed", at most 60 seconds old, up to 5 seconds in the future.
verify_envelope(VerifyKey::Ed25519(&public_key), &envelope, &VerifyOptions::new("feed", 60))?;
Every failure is an Error::Crypto naming the check (see
Cryptographic Verification). A public key that is not 32 bytes is
invalid verification key.
4. Verifying in the browser (your code)
The Anza browser library does not do this for you. If you want browser-side
checks, publish the public key yourself (for example in a
<meta name="anza-key"> tag), rebuild the v2 message and verify with Web
Crypto. Ed25519 in crypto.subtle is supported in current Chrome, Firefox and
Safari.
const enc = new TextEncoder();
// "anza-sig-v2\n" field(ts) field(slot) field(html) field(css)
function signatureMessage({ ts, slot, html, css = '' }) {
const parts = [enc.encode('anza-sig-v2\n')];
for (const value of [String(ts), slot, html, css]) {
const bytes = enc.encode(value);
parts.push(enc.encode(`${bytes.length}:`), bytes, enc.encode('\n'));
}
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
let offset = 0;
for (const p of parts) { out.set(p, offset); offset += p.length; }
return out;
}
async function verifyEnvelope(envelope, publicKeyBytes, slot, maxAgeSeconds = 60) {
if (envelope.v !== 2 || !/^[0-9a-f]{128}$/.test(envelope.sig ?? '')) return false;
const now = Math.floor(Date.now() / 1000);
if (envelope.ts > now + 5 || now - envelope.ts > maxAgeSeconds) return false;
if (envelope.slot !== slot) return false;
const key = await crypto.subtle.importKey('raw', publicKeyBytes, { name: 'Ed25519' }, false, ['verify']);
const sig = Uint8Array.from(envelope.sig.match(/../g), (h) => parseInt(h, 16));
return crypto.subtle.verify({ name: 'Ed25519' }, key, sig, signatureMessage(envelope));
}