Skip to content

sign jwt tokens with ed25519, ecdsa p-256 and rsa pkcs#1 - #631

Merged
kacy merged 1 commit into
mainfrom
crypto-signers
Jul 31, 2026
Merged

sign jwt tokens with ed25519, ecdsa p-256 and rsa pkcs#1#631
kacy merged 1 commit into
mainfrom
crypto-signers

Conversation

@kacy

@kacy kacy commented Jul 31, 2026

Copy link
Copy Markdown
Owner

summary

the runtime verified ed25519, ecdsa and rsa pkcs#1 signatures but could not produce them — a shape inherited from tls, which checks certificates constantly and signs almost never. jwt is the first caller that wants signing as a first-class operation, so this wires the three signers through. ring already had all of them; rsa pkcs#1 is the existing rsa helper with a different encoding constant, ed25519 is a ten-line kernel, and ecdsa p-256 signs with the fixed encoding so the signature comes out as the raw r-and-s form jws carries — the sign path needs none of the der translation the verify path does.

std.crypto.jwt gains sign_rs256, sign_es256 and sign_eddsa, and every algorithm in the module now both signs and verifies. std.crypto.signature exposes the three kernels for anything else that needs them. all signing keys are pkcs#8, which is what encoding.pem_decode yields from a BEGIN PRIVATE KEY file; the ed25519 parser is ring's maybe_unchecked variant because openssl writes pkcs#8 v1, which cannot carry the embedded public key the strict parser insists on.

docs and the crypto index no longer describe the sign/verify asymmetry, and instead say which algorithm to pick when.

what was tested

rsa pkcs#1 v1.5 and ed25519 are deterministic, so for a fixed key and claims there is exactly one valid token — the tests check sign_rs256 and sign_eddsa reproduce, byte for byte, the tokens openssl minted over the same key and claims. that pins the signing half to the same external standard the verifying half was already held to, where a round trip would only prove the module agrees with itself. es256 gets the opposite check: two signatures over the same input must differ (a repeated ecdsa nonce forfeits the private key), both must verify, and a wrong key must not. a garbage private key fails loudly.

make test, make bootstrap-verify (the bootstrap seed is regenerated — the checker and lowering tables changed), make run-examples, and make leak-check all clean.

notes

the per-signature ecdsa nonce is generated inside ring from the system rng; it is never chosen or seen by pith code, which is the property that keeps p-256 keys from leaking through nonce reuse. ed25519 needs no nonce at all, which is why it can be the deterministic one.

the runtime could verify all three but sign none of them, a shape left over
from tls, where certificates are checked constantly and produced never. jwt
is the first caller that wants signing outright, so the three signers are
now wired through: ring already had them, and rsa pkcs#1 reuses the same
helper rsa-pss was using with a different encoding.

ecdsa signs with the fixed encoding, so the signature comes out as the raw
r-and-s form jws carries and the sign path needs none of the der translation
the verify path does. the ed25519 parser is the maybe_unchecked variant
because openssl writes pkcs#8 v1, which cannot carry the public key ring's
strict parser insists on.

jwt gains sign_rs256, sign_es256 and sign_eddsa, and every algorithm in the
module now both signs and verifies. rsa pkcs#1 and ed25519 are deterministic,
so the tests hold the new signers to byte equality with tokens openssl minted
over the same key and claims, rather than settling for a round trip. es256
gets the opposite check: two signatures over the same input must differ,
because a repeated ecdsa nonce forfeits the private key.
@kacy
kacy merged commit 82ffacd into main Jul 31, 2026
2 checks passed
@kacy
kacy deleted the crypto-signers branch July 31, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant