Ed448
Verify an Ed448 signature
Ed448 is Ed25519's bigger sibling. Here, @agntn/curves/ed448 verifies its signatures. It doesn't sign, derive keys or export point math.
RFC 8032 calls the curve edwards448. Its equation is x² + y² = 1 + d x²y². The field is p = 2^448 - 2^224 - 1. The curve parameter d is -39081. The cofactor is 4.
The ed448 export holds p, d, l, h = 4n and g. The other export is verify.
Here's the RFC 8032 section 7.4 vector with context foo. Change the message or drop the context, and it fails.
import { verify } from "@agntn/curves/ed448";
const publicKey = "43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a9480";
const signature = "d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b32a89f7d2151f7647f11d8ca2ae279fb842d607217fce6e042f6815ea000c85741de5c8da1144a6a1aba7f96de42505d7a7298524fda538fccbbb754f578c1cad10d54d0d5428407e85dcbc98a49155c13764e66c3c00";
verify(signature, { message: "03", publicKey, context: "666f6f" }); // true
verify(signature, { message: "03", publicKey }); // false
verify(signature, { message: "0300", publicKey, context: "666f6f" }); // false
The public key is 57 bytes, or 114 hex digits. The signature is 114 bytes, R then S. That's 228 hex digits. The message is hex of any length, including empty. Leave 0x off everything.
Ed448 context
The context is part of what got signed. It's 0 to 255 bytes, passed as hex. Leave it out and it's empty.
Got the right message but the wrong context? The signature fails. That includes leaving out a context the signer used. 666f6f is foo in ASCII. Three small bytes, one very different answer.
Ed448ph isn't supported. That variant prehashes with SHAKE256(M, 64). Pass the message itself, not an Ed448ph prehash.
Ed448 verification rules
The verifier follows RFC 8032 section 5.2. It checks the cofactored equation 4SB = 4R + 4kA. The challenge comes from SHAKE256(dom4(0, context) || R || A || M, 114). dom4 starts with ASCII SigEd448.
Encodings must be canonical. A y at or above p fails. So does a sign bit set on x = 0. S at or above l fails too. These return false, not an exception.
A small order key isn't refused. RFC 8032 doesn't ask for that check. The key still has to pass the equation, like any other.
What does verify refuse?
Wrong shapes throw. The messages say what needs fixing.
verify("00", { message: "", publicKey }) // Signature must be 228 hex digits without 0x, R then S
verify(signature, { message: "0x00", publicKey }) // Message must be hex without 0x
verify(signature, { message: "", publicKey: "00" }) // Public key must be 114 hex digits without 0x
verify(signature, { message: "", publicKey, context: "zz" }) // Context must be hex without 0x, at most 255 bytes
OpenPGP Ed448 signatures
OpenPGP uses pure Ed448 with an empty context. That's algorithm 28 in RFC 9580. The default context is already empty, so leave it out.
OpenPGP hashes the signed data itself. Its hash must be at least 512 bits. Feed that digest as the Ed448 message. This isn't Ed448ph.
Ed448 CLI and agent tool
The CLI checks the same context vector.
curves ed448 verify --public-key 43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a9480 --message 03 --signature d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b32a89f7d2151f7647f11d8ca2ae279fb842d607217fce6e042f6815ea000c85741de5c8da1144a6a1aba7f96de42505d7a7298524fda538fccbbb754f578c1cad10d54d0d5428407e85dcbc98a49155c13764e66c3c00 --context 666f6f
{"operation":"verify","valid":true}
Remove --context and the answer changes.
{"operation":"verify","valid":false}
The agent tool is curves_ed448_compute, with operation verify. It takes publicKey, message, signature and context. It's the same tool over MCP, Pi and OMP. The AI SDK gets it from @agntn/curves/ai.
Written from the specs
The implementation follows RFC 8032. All nine Ed448 vectors from section 7.4 run in the tests. Their messages range from empty to 1023 bytes. One uses the context foo.
The tests also cover all 87 Wycheproof Ed448 vectors. Ten more signatures come from OpenSSL 3.6.5, through Node.js 26.12.1 crypto.sign. Their messages sit around SHAKE256's 136-byte block. Their contexts run 1, 57 and 255 bytes.
SHAKE256 lives in this package. It uses the same Keccak-f1600 as the sr25519 Merlin transcripts. @agntn/hashes 0.6.7 doesn't export SHAKE256.
In a local run, verification took about 8 ms here. OpenSSL through node:crypto took about 0.3 ms. Slower, and not hiding it.
Why no signing? Secret multiplication here has to take the same steps for every scalar. Ed448 doesn't have that yet. Verification uses public input only. No signing, no secret to multiply.