The library trusts the key, not your setting
In Node’s crypto library, the sign and verify functions look at the key they are handed and use whatever algorithm that key belongs to. Give them an Ed25519 key and you get an Ed25519 signature, whatever your configuration says. So in the scenario above, nothing fails. The code labelled “ML-DSA-65” calls sign, the library sees an Ed25519 key, and it quietly produces an Ed25519 signature. The record says one algorithm. The bytes are another. The board note is wrong, and no error, test or log line says so. This is the worst kind of security bug: everything works, and the protection you think you bought is not there.The idea: the algorithm is chosen once, and the key must agree
The fix is not clever. Whoever chose the algorithm, the operator, in configuration, should win. The key is just material. If the material does not match the choice, stop. In other words, a key is for one algorithm only, and a signer configured for an algorithm accepts only keys of that algorithm. Never “use whatever the key is”.How Parmana does it
Parmana has two signature providers: Ed25519, the default, and ML-DSA-65 (the code calls itdilithium3 for
historical reasons, and accepts ml-dsa-65 as an alias). Both run one check before every sign and every verify:
the key’s own type must equal the type the provider expects.
If it does not, the call fails closed with an error that names both: the type it expected and the type it received,
and a sentence saying the configured algorithm does not match the key material. It does not fall back, retry with the
other provider, or sign anyway. The check runs in both directions: an Ed25519 key handed to the ML-DSA-65 provider is
refused, and so is an ML-DSA-65 key handed to the Ed25519 provider.
This is recorded as claim 2.13 in the claims register. It is covered by the signature provider unit tests and by a
runnable tutorial that tries both wrong pairings and then shows a correctly matched key still working. The same check
ships in the separate open-source package, @parmana/sign, which raises an error when a key’s type does not match the
provider.
There are neighbours to this check that come from the same instinct, refuse rather than guess:
- Selecting ML-DSA-65 with missing or mismatched key material fails closed. It does not regenerate a key or substitute a different one.
- Key ids are checked before they become file paths. Local keys live as files named after their key id. A key id must match a short set of safe characters (letters, digits, dot, underscore and hyphen), so an id containing ”../” cannot point the key reader at some other file on the disk (claim 2.18).
- An unbuilt key store fails at startup. The configuration accepts names for Azure Key Vault, Google Cloud KMS and HSMs, but none is implemented. Setting one makes the server refuse to start, naming the value. Before this was fixed, setting the AWS KMS value quietly gave you key files on disk, with no error at all.
- The AWS KMS signer accepts only Ed25519 KMS keys. Any other kind of KMS key is refused.
What it does not do
- It checks the type of the key, not whose key it is. An Ed25519 key passes the Ed25519 provider whether it is your current key, a revoked one or a stranger’s. Which key is acceptable is a separate question, answered by key ids, expiry and revocation, and by the public key a verifier chooses to trust.
- It does not change algorithms for you. A verifying process supports one configured primary algorithm at a time. Moving a live deployment from Ed25519 to ML-DSA-65 while still verifying old records is not built; it is listed as future work.
- ML-DSA-65 is not running in production. Every Parmana deployment today, production included, signs with Ed25519. The guard matters most at the moment someone switches, which no deployment has done yet.
- It is not a check on configuration as a whole. Startup validation catches some wrong values, but a correctly typed key with the wrong contents, such as a public key that does not belong to the private key, is caught by other checks, not this one.