500 Internal Server Error. It
was a synthetic test refund, not a customer’s ₹15,000, but the cause was real. Every refund would have failed the same
way.
A few days earlier I had moved the signing key into AWS KMS. That was the right move: the private key is created inside
KMS and never leaves it, so nobody can copy it, me included. The server sends KMS the bytes to sign and gets a signature
back. The tests passed. Then a full refund ran through production, and KMS refused to sign.
The limit nobody hit in testing
KMS refuses a raw Ed25519 message longer than 4096 bytes. The error said so plainly: the message must have length less than or equal to 4096. An Execution Trust Record is not small. It carries the request, the decision, the signed authorization it was released under, what the connector reported and a reference to the approved policy. A full record is well over 4 KB. The earlier KMS tests had only signed a small test record, so the limit was never reached until real traffic reached it. Worse, the failure happened after the refund connector had already been called. An action ran, and the signed record of it could not be made. That ordering problem was recorded as a separate gap, and closed the next day with a signed Execution Intent stored before release. This essay is about the signing.Options that looked easier
The decision is written down in ADR-0010, including what was rejected:- Use the pre-hashed variant of Ed25519, which KMS supports for any size. Its signatures are a different scheme, and every existing Ed25519 verifier, including those used by third parties, would reject them.
- Sign Trust Records with a local key, and keep KMS for small things. Simple, and it puts the key that signs the durable evidence back on a disk, which is what the move to KMS was meant to end.
- Make the record smaller. Connector evidence and request parameters have no fixed size. A record can always grow past the limit.
- Store a marker on each record saying how it was signed. That changes the record format, every verifier’s field mapping and the story for old records.
The idea: sign a short, unambiguous summary
If the message is too long, sign a fixed size commitment to it instead. In Parmana it is 97 bytes: a fixed text prefix naming the scheme and its version, a single zero byte, and the SHA-512 digest of the whole message. Change any byte of the message and the digest changes, so the signature no longer verifies. The signature over those 97 bytes is an ordinary Ed25519 signature. Any Ed25519 library can check it, once it rebuilds the commitment.The rules that make it safe
- Length decides, nothing else. A message of 4096 bytes or fewer is signed raw, exactly as before. Only a longer message signed through KMS uses the commitment. No marker is stored, so no record changed shape and every signature made before the change still verifies.
- No downgrade. A verifier accepts a raw signature for any message, and additionally the commitment form only for a message over 4096 bytes. A commitment signature over a small message is refused. Tests cover the exact boundary, 4096 bytes and 4097 bytes.
- No confusion between forms. The prefix includes a zero byte, and the canonical JSON Parmana signs always begins with an opening brace, so a raw message can never look like a commitment.
- Same key, same message. Accepting both forms for a large message is needed because records signed earlier with local key files were signed raw. Both forms are made by the same key over the same content, so accepting either does not let an attacker forge anything new.
What happened next
The fix went to production the same day. A live refund returned200 with a Trust Record whose canonical form was 4965
bytes. It did not verify as a raw signature and did verify as the commitment, and the offline verifier accepted it
against the public key the live server publishes. Later a repeatable check signed messages of 300, 4096, 5000 and 60,000
bytes through the real key, confirmed that a signature over other data failed, and confirmed that KMS still refuses 5000
raw bytes. That check ran with a limited AWS user from a developer machine.
What it does not do
- A verifier must know the rule. The TypeScript packages, the Python SDK and
@parmana/signimplement it. A verifier you write yourself must accept the commitment form for messages over 4096 bytes, or it will reject large records signed under KMS. The rule is fully specified in ADR-0010. - It only makes signing possible. It does not decide when signing happens. That is the job of the readiness check and the Execution Intent.
- It is Ed25519 under AWS KMS only. KMS signing is Ed25519 only. No other key store is implemented, and hybrid signing does not run with KMS at all.
- Not every path has been seen live. The check above used a local AWS login, not the role production uses. The extra cost is small, one SHA-512 and at most one extra verification, but it was not measured on its own.