Skip to content

KMS: implement MessageType=DIGEST for Sign and Verify - #10202

Open
the-gabe wants to merge 4 commits into
getmoto:masterfrom
the-gabe:kms-sign-digest
Open

KMS: implement MessageType=DIGEST for Sign and Verify#10202
the-gabe wants to merge 4 commits into
getmoto:masterfrom
the-gabe:kms-sign-digest

Conversation

@the-gabe

Copy link
Copy Markdown

Sign and Verify parsed MessageType and then ignored it (responses.py documented DIGEST as "not yet implemented"), so a DIGEST request had its digest hashed AGAIN. The resulting signature verifies against sha256(digest) instead of the digest — meaning any client that signs with MessageType=DIGEST (the documented way to sign large messages, and what real-world KMS JWT signers do) produced signatures that fail external verification against GetPublicKey, while identical code works on AWS.

Per API_Sign.html / API_Verify.html:

  • DIGEST skips the hashing step; implemented with cryptography's Prehashed for RSASSA_PSS_, RSASSA_PKCS1_V1_5_ and ECDSA_*.
  • "the length of the Message value must match the length of hashed messages for the specified signing algorithm" — enforced on both operations, failing with ValidationException ("Digest is invalid length for algorithm ...").
  • The MessageType enum is RAW | DIGEST | EXTERNAL_MU; an invalid value fails the enum constraint, and EXTERNAL_MU (ML-DSA keys only, not implemented in moto) is rejected with a message that says so instead of calling the value invalid.
  • "The message type does not need to be the same as the one used for signing": sign(RAW, message) verifies as (DIGEST, hash(message)) and vice versa — covered by tests both ways, plus a byte-equality test for the deterministic RSASSA_PKCS1_V1_5 algorithms and an external verification test against GetPublicKey over the original message.

The existing digest tests encoded the old behaviour: they signed DIGEST and verified RAW with the same bytes (which only passes when both sides re-hash), and paired SHA-256-sized digests with SHA_384/512 algorithms (which real KMS rejects on length). They now hash with the signing algorithm's own hash and verify as DIGEST.

Not changed here: moto returns SignatureValid=false for a failed verification, where real KMS raises KMSInvalidSignatureException — a pre-existing, moto-wide divergence left for a separate discussion.

RSAPrivateKey and ECDSAPrivateKey (and the AbstractPrivateKey contract)
gain a message_type parameter. With "DIGEST" the message is treated as
the already-computed hash and signed/verified via cryptography's
Prehashed, after validating that its length matches the signing
algorithm's hash — "the length of the Message value must match the
length of hashed messages for the specified signing algorithm"
(API_Sign.html), failing with AWS's wording: "Digest is invalid length
for algorithm ...". The Verify path applies the same validation rather
than quietly reporting an invalid signature.

Defaults to "RAW", so behaviour is unchanged until callers pass the new
parameter.
KmsBackend.sign/verify accept message_type and hand it to the key
wrapper. Also drops the "MessageType-parameter DIGEST is not yet
implemented" note from the Verify docstring, since the wrappers now
implement it. Default stays "RAW"; the responses layer does not pass
the parameter yet, so behaviour is still unchanged.
Both operations parsed MessageType and ignored it, so a DIGEST request
had its digest hashed AGAIN — the signature verified against
sha256(digest) instead of the digest, and any client signing with
MessageType=DIGEST (the documented way to sign large messages) produced
signatures that fail external verification against GetPublicKey while
identical code works on AWS.

The responses layer now validates the parameter against the real enum
(RAW | DIGEST | EXTERNAL_MU per API_Sign.html — EXTERNAL_MU is a valid
value but only meaningful for ML-DSA keys, which moto does not
implement, and is rejected with a message that says so) and passes it
through to the backend.

The existing digest tests encoded the old behaviour and are corrected in
the same commit: they signed DIGEST and verified RAW with the same bytes
(which only passes when both sides re-hash), and paired SHA-256-sized
digests with SHA_384/512 algorithms (which real KMS rejects on length).
They now compute the digest with the signing algorithm's own hash and
verify as DIGEST.
The contracts from API_Sign.html / API_Verify.html that would have
caught the re-hashing behaviour:

- RSASSA_PKCS1_V1_5 is deterministic, so sign(RAW, message) and
  sign(DIGEST, hash(message)) must be byte-identical.
- "The message type does not need to be the same as the one used for
  signing": RAW-signed verifies as DIGEST and vice versa, across PSS,
  PKCS1v15 and ECDSA.
- A DIGEST signature verifies OUTSIDE KMS with GetPublicKey's key over
  the original message.
- A digest whose length does not match the signing algorithm's hash is
  rejected on both Sign and Verify with "Digest is invalid length for
  algorithm ...".
- EXTERNAL_MU is accepted as an enum value and rejected as unsupported
  (ML-DSA keys), rather than being called invalid.
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