Skip to content
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

Add SHA1 and SHA256 algorithms for EC2 fingerprint calculation #8235

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions moto/ec2/utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming of is_created is a bit confusing for people without any context. Any objections to rename this to something like use_sha1: bool?

A comment in the caller function (random_rsa_key_pair) can then explain why we're using sha1:

# This fingerprint should use sha1 because it's created by EC2
# See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/verify-keys.html#how-ec2-key-fingerprints-are-calculated

) -> 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

Expand Down
13 changes: 10 additions & 3 deletions tests/test_ec2/test_key_pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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")

Expand All @@ -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)


Expand Down
30 changes: 27 additions & 3 deletions tests/test_ec2/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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
Loading