Skip to main content
Say, for example, that a lending startup in Pune decides to sign its agent records with ML-DSA-65, the post-quantum algorithm, because a regulator asked how long its evidence will stay trustworthy. An engineer changes one setting. The deploy goes out. Every record now says “signed with ML-DSA-65”. The compliance team writes it into a board note: from this week, the record of a ₹2,00,000 loan disbursement proposed by an agent carries a post-quantum signature. Nobody noticed that the key directory still held the old Ed25519 key files. The setting changed. The keys did not. What happens next depends on one small detail of how signing libraries work, and it is the detail this essay is about.

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 it dilithium3 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.

The takeaway

A signature label is a promise about the bytes beneath it. If the code can sign with one algorithm while writing down another, the label is decoration. The guard is a few lines long, and its job is to make that mismatch loud: one algorithm chosen, one kind of key accepted, and an error that tells you exactly which two disagreed. Read more: Cryptography, Choose a signature provider, Choosing signatures.