-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
KMS: Fix double hashing during sign/verify operations #7878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
tests/integration/test_kms.py
Outdated
def test_invalid_key_usage(self, kms_client, kms_create_key): | ||
key_id = kms_create_key(KeyUsage="ENCRYPT_DECRYPT", KeySpec="RSA_4096")["KeyId"] | ||
with pytest.raises(ClientError) as exc: | ||
kms_client.sign( | ||
MessageType="RAW", | ||
Message="hello", | ||
KeyId=key_id, | ||
SigningAlgorithm="RSASSA_PSS_SHA_256", | ||
) | ||
assert exc.match("InvalidKeyUsageException") | ||
|
||
key_id = kms_create_key(KeyUsage="SIGN_VERIFY", KeySpec="RSA_4096")["KeyId"] | ||
with pytest.raises(ClientError) as exc: | ||
kms_client.encrypt( | ||
Plaintext="hello", | ||
KeyId=key_id, | ||
EncryptionAlgorithm="RSAES_OAEP_SHA_256", | ||
) | ||
assert exc.match("InvalidKeyUsageException") | ||
|
||
@pytest.mark.parametrize( | ||
"key_spec,algo", | ||
[ | ||
("SYMMETRIC_DEFAULT", "SYMMETRIC_DEFAULT"), | ||
("RSA_2048", "RSAES_OAEP_SHA_256"), | ||
("ECC_NIST_P256", "RSAES_OAEP_SHA_1"), | ||
("ECC_SECG_P256K1", "RSAES_OAEP_SHA_256"), | ||
# ("HMAC_256", "SYMMETRIC_DEFAULT"), # currently not supported in LocalStack | ||
# ("SM2", "SM2PKE"), # currently not supported in LocalStack | ||
], | ||
) | ||
def test_encrypt_decrypt(self, kms_client, kms_create_key, key_spec, algo): | ||
key_id = kms_create_key(KeyUsage="ENCRYPT_DECRYPT", KeySpec=key_spec)["KeyId"] | ||
message = b"test message 123 !%$@ 1234567890" | ||
algo = "RSASSA_PSS_SHA_256" if key_type == "rsa" else "ECDSA_SHA_256" | ||
kwargs = {"KeyId": key_id, "Message": message, "SigningAlgorithm": algo} | ||
|
||
signature_for_plaintext = kms_client.sign(MessageType="RAW", **kwargs)["Signature"] | ||
signature_for_digest = kms_client.sign(MessageType="DIGEST", **kwargs)["Signature"] | ||
assert signature_for_plaintext != signature_for_digest | ||
|
||
# These following blocks basically test that MessageType="DIGEST" results in a different outcome | ||
# from the default MessageType="RAW". As long as both Sign and Verify are called with the same MessageType, | ||
# everything should work, while if this parameter mismatches between such two calls - the verifications is | ||
# supposed to fail. | ||
# | ||
# There is a possibility that I do not understand how digests work, so can't write better tests. If we have | ||
# any issues with the digests - know that the current approach is not a calculated design, but rather a guess. | ||
signature = kms_client.sign(MessageType="RAW", **kwargs)["Signature"] | ||
assert kms_client.verify(MessageType="RAW", Signature=signature, **kwargs)["SignatureValid"] | ||
|
||
signature = kms_client.sign(MessageType="RAW", **kwargs)["Signature"] | ||
with pytest.raises(kms_client.exceptions.KMSInvalidSignatureException): | ||
assert not kms_client.verify(MessageType="DIGEST", Signature=signature, **kwargs)[ | ||
"SignatureValid" | ||
] | ||
|
||
signature = kms_client.sign(MessageType="DIGEST", **kwargs)["Signature"] | ||
with pytest.raises(kms_client.exceptions.KMSInvalidSignatureException): | ||
assert not kms_client.verify(MessageType="RAW", Signature=signature, **kwargs)[ | ||
"SignatureValid" | ||
] | ||
|
||
signature = kms_client.sign(MessageType="DIGEST", **kwargs)["Signature"] | ||
assert kms_client.verify(MessageType="DIGEST", Signature=signature, **kwargs)[ | ||
"SignatureValid" | ||
] | ||
ciphertext = kms_client.encrypt( | ||
KeyId=key_id, Plaintext=base64.b64encode(message), EncryptionAlgorithm=algo | ||
)["CiphertextBlob"] | ||
plaintext = kms_client.decrypt( | ||
KeyId=key_id, CiphertextBlob=ciphertext, EncryptionAlgorithm=algo | ||
)["Plaintext"] | ||
assert base64.b64decode(plaintext) == message |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New tests to cover Encrypt and Decrypt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome set of changes @viren-nadkarni ! 🚀 Also big kudos to @paullallier for the detailed analysis of the issue and the initial implementation 🙌
Changes LGTM, only one small comment related to error handling.. In a future iteration, we could maybe introduce a few snapshots.
# Ensure bad digest raises during signing | ||
with pytest.raises(ClientError) as exc: | ||
kms_client.sign(MessageType="DIGEST", Message=plaintext, **kwargs) | ||
assert exc.match("ValidationException") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small nit: Since the tests are already AWS validated, we could also do a snapshot.match(..)
here, which would also assert the other elements of the error response. (no need to change now, though, we could tackle the snapshots in a follow-up iteration 👍 ).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed 👍
Thanks for getting this one into the codebase, @viren-nadkarni. |
This PR is based on investigation done by @paullallier in #6815 (comment)
LocalStack KMS Sign and Verify operations were double hashing the message payloads. This PR reworks these operations to rely on
cryptography
to perform hashing if required based on the value ofMessageType
argument.The integration tests are also refactored to make the correct assertions.
Also checked that signatures produced by KMS:Sign can be correctly verified with OpenSSL (i.e. outside of the KMS:Verify operation).
Supersedes #7817
Fixes #6815, #7158