Skip to content

Commit 2a9c7c3

Browse files
committed
add minimum key size checks and warnings
1 parent 80465a4 commit 2a9c7c3

File tree

5 files changed

+63
-3
lines changed

5 files changed

+63
-3
lines changed

src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/authentication.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from dynamodb_encryption_sdk.exceptions import InvalidAlgorithmError, SignatureVerificationError, SigningError
2929
from dynamodb_encryption_sdk.identifiers import LOGGER_NAME, EncryptionKeyType, KeyEncodingType
30+
from dynamodb_encryption_sdk.internal.identifiers import MinimumKeySizes
3031
from dynamodb_encryption_sdk.internal.validators import callable_validator
3132

3233
from .primitives import load_rsa_key
@@ -37,7 +38,6 @@
3738
# We only actually need these imports when running the mypy checks
3839
pass
3940

40-
4141
__all__ = ("JavaAuthenticator", "JavaMac", "JavaSignature", "JAVA_AUTHENTICATOR")
4242
_LOGGER = logging.getLogger(LOGGER_NAME)
4343

@@ -138,6 +138,9 @@ def load_key(self, key, key_type, key_encoding):
138138
if not (key_type is EncryptionKeyType.SYMMETRIC and key_encoding is KeyEncodingType.RAW):
139139
raise ValueError("Key type must be symmetric and encoding must be raw.")
140140

141+
if len(key) * 8 < MinimumKeySizes.HMAC.value:
142+
_LOGGER.warning("HMAC keys smaller than %d bits are unsafe" % MinimumKeySizes.HMAC.value)
143+
141144
return key
142145

143146
def validate_algorithm(self, algorithm):

src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/primitives.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
WrappingError,
3636
)
3737
from dynamodb_encryption_sdk.identifiers import LOGGER_NAME, EncryptionKeyType, KeyEncodingType
38+
from dynamodb_encryption_sdk.internal.identifiers import MinimumKeySizes
3839
from dynamodb_encryption_sdk.internal.validators import callable_validator
3940

4041
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
@@ -451,7 +452,12 @@ def load_rsa_key(key, key_type, key_encoding):
451452
if key_type is EncryptionKeyType.PRIVATE:
452453
kwargs["password"] = None
453454

454-
return loader(**kwargs)
455+
loaded_key = loader(**kwargs)
456+
457+
if loaded_key.key_size < MinimumKeySizes.RSA.value:
458+
_LOGGER.warning("RSA keys smaller than %d bits are unsafe" % MinimumKeySizes.RSA.value)
459+
460+
return loaded_key
455461

456462

457463
_KEY_LOADERS = {rsa: load_rsa_key}

src/dynamodb_encryption_sdk/internal/identifiers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,21 @@
3232
"SignatureValues",
3333
"MaterialDescriptionKeys",
3434
"MaterialDescriptionValues",
35+
"MinimumKeySizes",
3536
)
3637

3738
#: Encoding to use for all text values.
3839
#: This is noted here for consistency but should not be changed.
3940
TEXT_ENCODING = "utf-8"
4041

4142

43+
class MinimumKeySizes(Enum):
44+
"""Minimum safe key sizes for algorithms."""
45+
46+
RSA = 2048
47+
HMAC = 128
48+
49+
4250
class ReservedAttributes(Enum):
4351
"""Item attributes reserved for use by DynamoDBEncryptionClient"""
4452

test/functional/delegated_keys/test_jce.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@
1111
# ANY KIND, either express or implied. See the License for the specific
1212
# language governing permissions and limitations under the License.
1313
"""Functional test suite for ``dynamodb_encryption_sdk.delegated_keys.jce``."""
14+
from __future__ import division
15+
1416
import pytest
1517
from cryptography.hazmat.backends import default_backend
1618
from cryptography.hazmat.primitives import serialization
1719

1820
from dynamodb_encryption_sdk.delegated_keys.jce import JceNameLocalDelegatedKey
21+
from dynamodb_encryption_sdk.internal.crypto.jce_bridge.authentication import JAVA_AUTHENTICATOR
22+
from dynamodb_encryption_sdk.internal.identifiers import MinimumKeySizes
23+
24+
from ..functional_test_utils import capturing_logger
1925

2026
pytestmark = [pytest.mark.functional, pytest.mark.local]
2127

@@ -26,7 +32,7 @@ def _find_aes_key_length(key):
2632

2733
def _find_rsa_key_length(key):
2834
loaded_key = serialization.load_der_private_key(data=key, password=None, backend=default_backend())
29-
return loaded_key._key_size
35+
return loaded_key.key_size
3036

3137

3238
@pytest.mark.parametrize(
@@ -49,3 +55,28 @@ def test_generate_correct_key_length(algorithm, requested_bits, expected_bits, l
4955
test = JceNameLocalDelegatedKey.generate(algorithm, requested_bits)
5056

5157
assert length_finder(test.key) == expected_bits
58+
59+
60+
def build_short_key_cases():
61+
for algorithm in JAVA_AUTHENTICATOR:
62+
if algorithm.upper().startswith("HMAC"):
63+
message = "HMAC keys smaller than {} bits are unsafe".format(MinimumKeySizes.HMAC.value)
64+
yield (algorithm, MinimumKeySizes.HMAC.value, False, message)
65+
yield (algorithm, MinimumKeySizes.HMAC.value - 1, True, message)
66+
67+
elif algorithm.upper().endswith("RSA"):
68+
message = "RSA keys smaller than {} bits are unsafe".format(MinimumKeySizes.RSA.value)
69+
yield (algorithm, MinimumKeySizes.RSA.value, False, message)
70+
yield (algorithm, MinimumKeySizes.RSA.value // 2, True, message)
71+
72+
message = "RSA keys smaller than {} bits are unsafe".format(MinimumKeySizes.RSA.value)
73+
yield ("RSA", MinimumKeySizes.RSA.value, False, message)
74+
yield ("RSA", MinimumKeySizes.RSA.value // 2, True, message)
75+
76+
77+
@pytest.mark.parametrize("algorithm, key_bits, too_short, error_message", build_short_key_cases())
78+
def test_warn_on_short_keys(capturing_logger, algorithm, key_bits, too_short, error_message):
79+
_test = JceNameLocalDelegatedKey.generate(algorithm, key_bits)
80+
81+
logging_results = capturing_logger.getvalue()
82+
assert (too_short and error_message in logging_results) or (not too_short and error_message not in logging_results)

test/functional/functional_test_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515

1616
import copy
1717
import itertools
18+
import logging
1819
from collections import defaultdict
1920
from decimal import Decimal
2021

2122
import boto3
2223
import pytest
24+
import six
2325
from boto3.dynamodb.types import Binary
2426
from moto import mock_dynamodb2
2527

@@ -641,3 +643,13 @@ def client_cycle_batch_items_check_paginators(
641643
e_scan_result = e_client.scan(TableName=table_name, ConsistentRead=True)
642644
assert not raw_scan_result["Items"]
643645
assert not e_scan_result["Items"]
646+
647+
648+
@pytest.fixture
649+
def capturing_logger():
650+
output_stream = six.StringIO()
651+
handler = logging.StreamHandler(stream=output_stream)
652+
logger = logging.getLogger()
653+
logger.setLevel(logging.DEBUG)
654+
logger.addHandler(handler)
655+
return output_stream

0 commit comments

Comments
 (0)