Skip to content

mid-verify accepts the high-s twin of an ES256 signature (ECDSA malleability) #2

Description

@Ttimmahlax

mid-verify's three signature checks (checks.rs: genesis self-signature, chain entries, the JWS) decode the 64-byte r || s with Signature::from_slice and call verify_prehash as given. ECDSA is malleable: for any valid (r, s), (r, n − s) verifies too, so a token can be re-encoded without the key. Anything keyed on the signature bytes (replay windows, dedup, audit trails) sees two identities for one token.

The device side (rusty_esp_mid-core::verify_prehash, Janus) rejects high-s: if sig.normalize_s().is_some() { return Err(Crypto) }. The producers (mid-issuer, kms-client, the device key) already emit low-s via normalize_s() on sign, so a reject in the verifier costs no legitimate token.

Evidence: Janus rusty_esp_mid-core/tests/oracle.rs::high_s_twin_refused_here_and_its_fate_upstream_is_pinned builds a self-issued token, flips s to n − s, and asserts that mid_verify::verify_mid_response accepts the twin (it does, 2026-09-02). The test fails the day this changes, on purpose.

Suggested fix (one line in decode_signature / signature_from_raw, plus a VerifyError::SignatureHighS so the reason is distinguishable):

let sig = Signature::from_slice(bytes).map_err(|_| VerifyError::SignatureInvalidScalars)?;
if sig.normalize_s().is_some() {
    return Err(VerifyError::SignatureHighS);
}

Not folded into PR #1 (the no_std split) because it is a behaviour change and deserves its own review. kms-verifier has not been checked with a twin yet.

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions