diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index e3beeb1fd0a..4cd42d1360e 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -1,5 +1,6 @@ import base64 import fnmatch +import hashlib import ipaddress import re from typing import Any, Dict, List, Optional, Set, Tuple, TypeVar, Union @@ -600,7 +601,7 @@ def random_rsa_key_pair() -> Dict[str, str]: encoding=serialization.Encoding.OpenSSH, format=serialization.PublicFormat.OpenSSH, ) - fingerprint = public_key_fingerprint(public_key) + fingerprint = public_key_fingerprint(public_key, is_created=True) return { "fingerprint": fingerprint, @@ -753,14 +754,19 @@ def public_key_parse( return public_key -def public_key_fingerprint(public_key: Union[RSAPublicKey, Ed25519PublicKey]) -> str: - # TODO: Use different fingerprint calculation methods based on key type and source - # see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/verify-keys.html#how-ec2-key-fingerprints-are-calculated +def public_key_fingerprint( + public_key: Union[RSAPublicKey, Ed25519PublicKey], is_created: bool = False +) -> str: key_data = public_key.public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) - fingerprint_hex = md5_hash(key_data).hexdigest() + if isinstance(public_key, Ed25519PublicKey): + fingerprint_hex = hashlib.sha256(key_data).hexdigest() + elif is_created: + fingerprint_hex = hashlib.sha1(key_data).hexdigest() + else: + fingerprint_hex = md5_hash(key_data).hexdigest() fingerprint = re.sub(r"([a-f0-9]{2})(?!$)", r"\1:", fingerprint_hex) return fingerprint diff --git a/tests/test_ec2/test_key_pairs.py b/tests/test_ec2/test_key_pairs.py index b5da292043e..1113edc5068 100644 --- a/tests/test_ec2/test_key_pairs.py +++ b/tests/test_ec2/test_key_pairs.py @@ -15,7 +15,10 @@ AAAAC3NzaC1lZDI1NTE5AAAAIEwsSB9HbTeKCdkSlMZeTq9jZggaPJUwAsUi/7wakB+B \ moto@getmoto""" -ED25519_PUBLIC_KEY_FINGERPRINT = "6c:d9:cf:90:d7:f7:bc:46:83:9e:f5:56:aa:e1:13:38" +ED25519_PUBLIC_KEY_FINGERPRINT = ( + "6e:bc:8a:45:e5:fa:ba:e4:c9:a4:a3:f6:7b:da:05:16:fb:cc:88:66:05" + ":33:b8:a1:c2:80:9a:6b:06:ac:38:76" +) RSA_PUBLIC_KEY_OPENSSH = b"""\ ssh-rsa \ @@ -122,8 +125,8 @@ def test_key_pairs_create_dryrun_boto3(): @mock_aws -@pytest.mark.parametrize("key_type", ["rsa", "ed25519"]) -def test_key_pairs_create_boto3(key_type): +@pytest.mark.parametrize("key_type, fingerprint_len", [("rsa", 59), ("ed25519", 95)]) +def test_key_pairs_create_boto3(key_type: str, fingerprint_len: int): ec2 = boto3.resource("ec2", "us-west-1") client = boto3.client("ec2", "us-west-1") @@ -147,6 +150,10 @@ def test_key_pairs_create_boto3(key_type): assert "KeyPairId" in kps[0] assert kps[0]["KeyName"] == key_name assert "KeyFingerprint" in kps[0] + # Naive check for hash type + # SHA1 - 40 + 19 `:` symbols + # SHA256 - 64 + 15 `:` symbols + assert len(kps[0]["KeyFingerprint"]) == fingerprint_len assert isinstance(kps[0]["CreateTime"], datetime) diff --git a/tests/test_ec2/test_utils.py b/tests/test_ec2/test_utils.py index 0952766558f..771c3a8307c 100644 --- a/tests/test_ec2/test_utils.py +++ b/tests/test_ec2/test_utils.py @@ -2,6 +2,10 @@ from copy import deepcopy from unittest.mock import patch +import pytest +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from pytest import raises from moto.ec2 import utils @@ -13,9 +17,8 @@ def test_random_key_pair(): key_pair = utils.random_rsa_key_pair() check_private_key(key_pair["material"], "rsa") - # AWS uses MD5 fingerprints, which are 47 characters long, *not* SHA1 - # fingerprints with 59 characters. - assert len(key_pair["fingerprint"]) == 47 + # AWS uses SHA1 for created by Amazon fingerprints, which are 59 characters long + assert len(key_pair["fingerprint"]) == 59 key_pair = utils.random_ed25519_key_pair() check_private_key(key_pair["material"], "ed25519") @@ -62,3 +65,24 @@ def test_gen_moto_amis(): # with drop=False, it should raise KeyError because of the missing key with raises(KeyError, match="'Public'"): utils.gen_moto_amis(images, drop_images_missing_keys=False) + + +@pytest.mark.parametrize("is_created, fingerprint_len", [(True, 59), (False, 47)]) +def test_public_key_fingerprint__rsa(is_created: bool, fingerprint_len: int): + private_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ) + public_key = private_key.public_key() + + fingerprint = utils.public_key_fingerprint(public_key, is_created=is_created) + + assert len(fingerprint) == fingerprint_len + + +def test_public_key_fingerprint__ed25519(): + private_key = Ed25519PrivateKey.generate() + public_key = private_key.public_key() + + fingerprint = utils.public_key_fingerprint(public_key) + + assert len(fingerprint) == 95