Techdebt: Remove ECDSA dependency (#7356)

This commit is contained in:
Bert Blommers 2024-02-18 15:30:33 +00:00 committed by GitHub
parent 3f65f94675
commit a54cb2937f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 146 additions and 54 deletions

View File

@ -6,4 +6,4 @@ readthedocs-sphinx-search
docker docker
openapi_spec_validator openapi_spec_validator
PyYAML>=5.1 PyYAML>=5.1
python-jose[cryptography]>=3.1.0,<4.0.0 joserfc>=0.9.0

View File

@ -8,7 +8,7 @@ import typing
from collections import OrderedDict from collections import OrderedDict
from typing import Any, Dict, List, Optional, Set, Tuple from typing import Any, Dict, List, Optional, Set, Tuple
from jose import jws from joserfc import jwk, jwt
from moto.core.base_backend import BackendDict, BaseBackend from moto.core.base_backend import BackendDict, BaseBackend
from moto.core.common_models import BaseModel from moto.core.common_models import BaseModel
@ -444,7 +444,7 @@ class CognitoIdpUserPool(BaseModel):
with open( with open(
os.path.join(os.path.dirname(__file__), "resources/jwks-private.json") os.path.join(os.path.dirname(__file__), "resources/jwks-private.json")
) as f: ) as f:
self.json_web_key = json.loads(f.read()) self.json_web_key = jwk.RSAKey.import_key(json.loads(f.read()))
@property @property
def backend(self) -> "CognitoIdpBackend": def backend(self) -> "CognitoIdpBackend":
@ -543,10 +543,10 @@ class CognitoIdpUserPool(BaseModel):
"username" if token_use == "access" else "cognito:username": username, "username" if token_use == "access" else "cognito:username": username,
} }
payload.update(extra_data or {}) payload.update(extra_data or {})
headers = {"kid": "dummy"} # KID as present in jwks-public.json headers = {"kid": "dummy", "alg": "RS256"} # KID as present in jwks-public.json
return ( return (
jws.sign(payload, self.json_web_key, headers, algorithm="RS256"), jwt.encode(headers, payload, self.json_web_key),
expires_in, expires_in,
) )

View File

@ -669,31 +669,66 @@ def generate_instance_identity_document(instance: Any) -> Dict[str, Any]:
return document return document
def _convert_rfc4716(data: bytes) -> bytes:
"""Convert an RFC 4716 public key to OpenSSH authorized_keys format"""
# Normalize line endings and join continuation lines
data_normalized = data.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
data_joined = data_normalized.replace(b"\\\n", b"")
lines = data_joined.splitlines()
# Trim header and footer
if lines[0] != b"---- BEGIN SSH2 PUBLIC KEY ----":
raise ValueError("Invalid RFC4716 header line")
if lines[-1] != b"---- END SSH2 PUBLIC KEY ----":
raise ValueError("Invalid RFC4716 footer line")
lines = lines[1:-1]
# Leading lines containing a colon are headers
headers = {}
num_header_lines = 0
for line in lines:
if b":" not in line:
break
num_header_lines += 1
header_name, header_value = line.split(b": ")
headers[header_name.lower()] = header_value
# Remaining lines are key data
data_lines = lines[num_header_lines:]
b64_key = b"".join(data_lines)
# Extract the algo name from the binary packet
packet = base64.b64decode(b64_key)
alg_len = int.from_bytes(packet[:4], "big")
alg = packet[4 : 4 + alg_len]
result_parts = [alg, b64_key]
if b"comment" in headers:
result_parts.append(headers[b"comment"])
return b" ".join(result_parts)
def public_key_parse( def public_key_parse(
key_material: Union[str, bytes] key_material: Union[str, bytes]
) -> Union[RSAPublicKey, Ed25519PublicKey]: ) -> Union[RSAPublicKey, Ed25519PublicKey]:
# These imports take ~.5s; let's keep them local
import sshpubkeys.exceptions
from sshpubkeys.keys import SSHKey
try: try:
if not isinstance(key_material, bytes): if isinstance(key_material, str):
key_material = key_material.encode("ascii") key_material = key_material.encode("ascii")
key_material = base64.b64decode(key_material)
decoded_key = base64.b64decode(key_material) if key_material.startswith(b"---- BEGIN SSH2 PUBLIC KEY ----"):
public_key = SSHKey(decoded_key.decode("ascii")) # cryptography doesn't parse RFC4716 key format, so we have to convert it first
except (sshpubkeys.exceptions.InvalidKeyException, UnicodeDecodeError): key_material = _convert_rfc4716(key_material)
public_key = serialization.load_ssh_public_key(key_material)
if not isinstance(public_key, (RSAPublicKey, Ed25519PublicKey)):
raise ValueError("bad key")
except UnicodeDecodeError:
raise ValueError("bad key") raise ValueError("bad key")
if public_key.rsa: return public_key
return public_key.rsa
# `cryptography` currently does not support RSA RFC4716/SSH2 format, otherwise we could get rid of `sshpubkeys` and
# simply use `load_ssh_public_key()`
if public_key.key_type == b"ssh-ed25519":
return serialization.load_ssh_public_key(decoded_key) # type: ignore[return-value]
raise ValueError("bad key")
def public_key_fingerprint(public_key: Union[RSAPublicKey, Ed25519PublicKey]) -> str: def public_key_fingerprint(public_key: Union[RSAPublicKey, Ed25519PublicKey]) -> str:

View File

@ -44,13 +44,11 @@ moto = py.typed
[options.extras_require] [options.extras_require]
all = all =
python-jose[cryptography]>=3.1.0,<4.0.0 joserfc>=0.9.0
ecdsa!=0.15
docker>=3.0.0 docker>=3.0.0
graphql-core graphql-core
PyYAML>=5.1 PyYAML>=5.1
cfn-lint>=0.40.0 cfn-lint>=0.40.0
sshpubkeys>=3.1.0
openapi-spec-validator>=0.5.0 openapi-spec-validator>=0.5.0
pyparsing>=3.0.7 pyparsing>=3.0.7
jsondiff>=1.1.2 jsondiff>=1.1.2
@ -59,13 +57,11 @@ all =
setuptools setuptools
multipart multipart
proxy = proxy =
python-jose[cryptography]>=3.1.0,<4.0.0 joserfc>=0.9.0
ecdsa!=0.15
docker>=2.5.1 docker>=2.5.1
graphql-core graphql-core
PyYAML>=5.1 PyYAML>=5.1
cfn-lint>=0.40.0 cfn-lint>=0.40.0
sshpubkeys>=3.1.0
openapi-spec-validator>=0.5.0 openapi-spec-validator>=0.5.0
pyparsing>=3.0.7 pyparsing>=3.0.7
jsondiff>=1.1.2 jsondiff>=1.1.2
@ -74,13 +70,11 @@ proxy =
setuptools setuptools
multipart multipart
server = server =
python-jose[cryptography]>=3.1.0,<4.0.0 joserfc>=0.9.0
ecdsa!=0.15
docker>=3.0.0 docker>=3.0.0
graphql-core graphql-core
PyYAML>=5.1 PyYAML>=5.1
cfn-lint>=0.40.0 cfn-lint>=0.40.0
sshpubkeys>=3.1.0
openapi-spec-validator>=0.5.0 openapi-spec-validator>=0.5.0
pyparsing>=3.0.7 pyparsing>=3.0.7
jsondiff>=1.1.2 jsondiff>=1.1.2
@ -94,8 +88,7 @@ acmpca =
amp = amp =
apigateway = apigateway =
PyYAML>=5.1 PyYAML>=5.1
python-jose[cryptography]>=3.1.0,<4.0.0 joserfc>=0.9.0
ecdsa!=0.15
openapi-spec-validator>=0.5.0 openapi-spec-validator>=0.5.0
apigatewayv2 = apigatewayv2 =
PyYAML>=5.1 PyYAML>=5.1
@ -112,13 +105,11 @@ batch_simple =
budgets = budgets =
ce = ce =
cloudformation = cloudformation =
python-jose[cryptography]>=3.1.0,<4.0.0 joserfc>=0.9.0
ecdsa!=0.15
docker>=3.0.0 docker>=3.0.0
graphql-core graphql-core
PyYAML>=5.1 PyYAML>=5.1
cfn-lint>=0.40.0 cfn-lint>=0.40.0
sshpubkeys>=3.1.0
openapi-spec-validator>=0.5.0 openapi-spec-validator>=0.5.0
pyparsing>=3.0.7 pyparsing>=3.0.7
jsondiff>=1.1.2 jsondiff>=1.1.2
@ -133,8 +124,7 @@ codecommit =
codepipeline = codepipeline =
cognitoidentity = cognitoidentity =
cognitoidp = cognitoidp =
python-jose[cryptography]>=3.1.0,<4.0.0 joserfc>=0.9.0
ecdsa!=0.15
comprehend = comprehend =
config = config =
databrew = databrew =
@ -150,7 +140,7 @@ dynamodbstreams =
docker>=3.0.0 docker>=3.0.0
py-partiql-parser==0.5.1 py-partiql-parser==0.5.1
ebs = ebs =
ec2 = sshpubkeys>=3.1.0 ec2 =
ec2instanceconnect = ec2instanceconnect =
ecr = ecr =
ecs = ecs =
@ -204,8 +194,7 @@ redshiftdata =
rekognition = rekognition =
resourcegroups = resourcegroups =
resourcegroupstaggingapi = resourcegroupstaggingapi =
python-jose[cryptography]>=3.1.0,<4.0.0 joserfc>=0.9.0
ecdsa!=0.15
docker>=3.0.0 docker>=3.0.0
graphql-core graphql-core
PyYAML>=5.1 PyYAML>=5.1

View File

@ -13,13 +13,17 @@ import boto3
import pytest import pytest
import requests import requests
from botocore.exceptions import ClientError, ParamValidationError from botocore.exceptions import ClientError, ParamValidationError
from jose import jws, jwt from joserfc import jwk, jws, jwt
import moto.cognitoidp.models import moto.cognitoidp.models
from moto import mock_aws, settings from moto import cognitoidp, mock_aws, settings
from moto.cognitoidp.utils import create_id from moto.cognitoidp.utils import create_id
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
from moto.core import set_initial_no_auth_action_count from moto.core import set_initial_no_auth_action_count
from moto.utilities.utils import load_resource
private_key = load_resource(cognitoidp.__name__, "resources/jwks-private.json")
PUBLIC_KEY = jwk.RSAKey.import_key(private_key)
@mock_aws @mock_aws
@ -1543,7 +1547,8 @@ def test_group_in_access_token():
ChallengeResponses={"USERNAME": username, "NEW_PASSWORD": new_password}, ChallengeResponses={"USERNAME": username, "NEW_PASSWORD": new_password},
) )
claims = jwt.get_unverified_claims(result["AuthenticationResult"]["AccessToken"]) payload = jwt.decode(result["AuthenticationResult"]["AccessToken"], PUBLIC_KEY)
claims = payload.claims
assert claims["cognito:groups"] == [group_name] assert claims["cognito:groups"] == [group_name]
@ -1604,7 +1609,8 @@ def test_other_attributes_in_id_token():
ChallengeResponses={"USERNAME": username, "NEW_PASSWORD": new_password}, ChallengeResponses={"USERNAME": username, "NEW_PASSWORD": new_password},
) )
claims = jwt.get_unverified_claims(result["AuthenticationResult"]["IdToken"]) payload = jwt.decode(result["AuthenticationResult"]["IdToken"], PUBLIC_KEY)
claims = payload.claims
assert claims["cognito:groups"] == [group_name] assert claims["cognito:groups"] == [group_name]
assert claims["custom:myattr"] == "some val" assert claims["custom:myattr"] == "some val"
@ -2957,7 +2963,7 @@ def test_token_legitimacy():
path = "../../moto/cognitoidp/resources/jwks-public.json" path = "../../moto/cognitoidp/resources/jwks-public.json"
with open(os.path.join(os.path.dirname(__file__), path)) as f: with open(os.path.join(os.path.dirname(__file__), path)) as f:
json_web_key = json.loads(f.read())["keys"][0] json_web_key = jwk.RSAKey.import_key(json.loads(f.read())["keys"][0])
for auth_flow in ["ADMIN_NO_SRP_AUTH", "ADMIN_USER_PASSWORD_AUTH"]: for auth_flow in ["ADMIN_NO_SRP_AUTH", "ADMIN_USER_PASSWORD_AUTH"]:
outputs = authentication_flow(conn, auth_flow) outputs = authentication_flow(conn, auth_flow)
@ -2968,14 +2974,14 @@ def test_token_legitimacy():
issuer = ( issuer = (
f"https://cognito-idp.us-west-2.amazonaws.com/{outputs['user_pool_id']}" f"https://cognito-idp.us-west-2.amazonaws.com/{outputs['user_pool_id']}"
) )
id_claims = json.loads(jws.verify(id_token, json_web_key, "RS256")) id_claims = jwt.decode(id_token, json_web_key, ["RS256"]).claims
assert id_claims["iss"] == issuer assert id_claims["iss"] == issuer
assert id_claims["aud"] == client_id assert id_claims["aud"] == client_id
assert id_claims["token_use"] == "id" assert id_claims["token_use"] == "id"
assert id_claims["cognito:username"] == username assert id_claims["cognito:username"] == username
for k, v in outputs["additional_fields"].items(): for k, v in outputs["additional_fields"].items():
assert id_claims[k] == v assert id_claims[k] == v
access_claims = json.loads(jws.verify(access_token, json_web_key, "RS256")) access_claims = jwt.decode(access_token, json_web_key, ["RS256"]).claims
assert access_claims["iss"] == issuer assert access_claims["iss"] == issuer
assert access_claims["client_id"] == client_id assert access_claims["client_id"] == client_id
assert access_claims["token_use"] == "access" assert access_claims["token_use"] == "access"
@ -4938,8 +4944,10 @@ if not settings.TEST_SERVER_MODE:
def verify_kid_header(token): def verify_kid_header(token):
"""Verifies the kid-header is corresponds with the public key""" """Verifies the kid-header is corresponds with the public key"""
headers = jwt.get_unverified_headers(token) if isinstance(token, str):
kid = headers["kid"] token = token.encode("ascii")
sig = jws.extract_compact(token)
kid = sig.headers()["kid"]
key_index = -1 key_index = -1
keys = fetch_public_keys() keys = fetch_public_keys()

View File

@ -27,7 +27,7 @@ A3t8mL7r91aM5q6QOQm219lctFM8O7HRJnDgmhGpnjRwE1LyKktWTbgFZ4SNWU2X\
qusUO07jKuSxzPumXBeU+JEtx0J1tqZwJlpGt2R+0qN7nKnPl2+hx \ qusUO07jKuSxzPumXBeU+JEtx0J1tqZwJlpGt2R+0qN7nKnPl2+hx \
moto@github.com""" moto@github.com"""
RSA_PUBLIC_KEY_RFC4716 = b"""\ RSA_PUBLIC_KEY_RFC4716_1 = b"""\
---- BEGIN SSH2 PUBLIC KEY ---- ---- BEGIN SSH2 PUBLIC KEY ----
AAAAB3NzaC1yc2EAAAADAQABAAABAQDusXfgTE4eBP50NglSzCSEGnIL6+cr6m3H6cZANO AAAAB3NzaC1yc2EAAAADAQABAAABAQDusXfgTE4eBP50NglSzCSEGnIL6+cr6m3H6cZANO
Q+P1o/W4BdtcAL3sor4iGi7SOeJgo8kweyMQrhrt6HaKGgromRiz37LQx4YIAcBi4Zd023 Q+P1o/W4BdtcAL3sor4iGi7SOeJgo8kweyMQrhrt6HaKGgromRiz37LQx4YIAcBi4Zd023
@ -38,6 +38,44 @@ wJlpGt2R+0qN7nKnPl2+hx
---- END SSH2 PUBLIC KEY ---- ---- END SSH2 PUBLIC KEY ----
""" """
RSA_PUBLIC_KEY_RFC4716_2 = b"""\
---- BEGIN SSH2 PUBLIC KEY ----
cOmmENt: moto@github.com
AAAAB3NzaC1yc2EAAAADAQABAAABAQDusXfgTE4eBP50NglSzCSEGnIL6+cr6m3H6cZANO
Q+P1o/W4BdtcAL3sor4iGi7SOeJgo8kweyMQrhrt6HaKGgromRiz37LQx4YIAcBi4Zd023
mO/V7Rc2Chh18mWgLSmA6ng+j37ip6452zxtv0jHAz9pJolbKBpJzbZlPN45ZCTk9ck0fS
VHRl6VRSSPQcpqi65XpRf+35zNOCGCc1mAOOTmw59Q2a6A3t8mL7r91aM5q6QOQm219lct
FM8O7HRJnDgmhGpnjRwE1LyKktWTbgFZ4SNWU2XqusUO07jKuSxzPumXBeU+JEtx0J1tqZ
wJlpGt2R+0qN7nKnPl2+hx
---- END SSH2 PUBLIC KEY ----
"""
RSA_PUBLIC_KEY_RFC4716_3 = b"""\
---- BEGIN SSH2 PUBLIC KEY ----
Comment: "1024-bit RSA, converted from OpenSSH by me@example.com"
x-command: /home/me/bin/lock-in-guest.sh
AAAAB3NzaC1yc2EAAAADAQABAAABAQDusXfgTE4eBP50NglSzCSEGnIL6+cr6m3H6cZANO
Q+P1o/W4BdtcAL3sor4iGi7SOeJgo8kweyMQrhrt6HaKGgromRiz37LQx4YIAcBi4Zd023
mO/V7Rc2Chh18mWgLSmA6ng+j37ip6452zxtv0jHAz9pJolbKBpJzbZlPN45ZCTk9ck0fS
VHRl6VRSSPQcpqi65XpRf+35zNOCGCc1mAOOTmw59Q2a6A3t8mL7r91aM5q6QOQm219lct
FM8O7HRJnDgmhGpnjRwE1LyKktWTbgFZ4SNWU2XqusUO07jKuSxzPumXBeU+JEtx0J1tqZ
wJlpGt2R+0qN7nKnPl2+hx
---- END SSH2 PUBLIC KEY ----
"""
RSA_PUBLIC_KEY_RFC4716_4 = b"""\
---- BEGIN SSH2 PUBLIC KEY ----
Comment: This is my public key for use on \
servers which I don't like.
AAAAB3NzaC1yc2EAAAADAQABAAABAQDusXfgTE4eBP50NglSzCSEGnIL6+cr6m3H6cZANO
Q+P1o/W4BdtcAL3sor4iGi7SOeJgo8kweyMQrhrt6HaKGgromRiz37LQx4YIAcBi4Zd023
mO/V7Rc2Chh18mWgLSmA6ng+j37ip6452zxtv0jHAz9pJolbKBpJzbZlPN45ZCTk9ck0fS
VHRl6VRSSPQcpqi65XpRf+35zNOCGCc1mAOOTmw59Q2a6A3t8mL7r91aM5q6QOQm219lct
FM8O7HRJnDgmhGpnjRwE1LyKktWTbgFZ4SNWU2XqusUO07jKuSxzPumXBeU+JEtx0J1tqZ
wJlpGt2R+0qN7nKnPl2+hx
---- END SSH2 PUBLIC KEY ----
"""
RSA_PUBLIC_KEY_FINGERPRINT = "6a:49:07:1c:7e:bd:d2:bd:96:25:fe:b5:74:83:ae:fd" RSA_PUBLIC_KEY_FINGERPRINT = "6a:49:07:1c:7e:bd:d2:bd:96:25:fe:b5:74:83:ae:fd"
DSA_PUBLIC_KEY_OPENSSH = b"""ssh-dss \ DSA_PUBLIC_KEY_OPENSSH = b"""ssh-dss \
@ -157,10 +195,20 @@ def test_key_pairs_delete_exist_boto3():
"public_key,fingerprint", "public_key,fingerprint",
[ [
(RSA_PUBLIC_KEY_OPENSSH, RSA_PUBLIC_KEY_FINGERPRINT), (RSA_PUBLIC_KEY_OPENSSH, RSA_PUBLIC_KEY_FINGERPRINT),
(RSA_PUBLIC_KEY_RFC4716, RSA_PUBLIC_KEY_FINGERPRINT), (RSA_PUBLIC_KEY_RFC4716_1, RSA_PUBLIC_KEY_FINGERPRINT),
(RSA_PUBLIC_KEY_RFC4716_2, RSA_PUBLIC_KEY_FINGERPRINT),
(RSA_PUBLIC_KEY_RFC4716_3, RSA_PUBLIC_KEY_FINGERPRINT),
(RSA_PUBLIC_KEY_RFC4716_4, RSA_PUBLIC_KEY_FINGERPRINT),
(ED25519_PUBLIC_KEY_OPENSSH, ED25519_PUBLIC_KEY_FINGERPRINT), (ED25519_PUBLIC_KEY_OPENSSH, ED25519_PUBLIC_KEY_FINGERPRINT),
], ],
ids=["rsa-openssh", "rsa-rfc4716", "ed25519"], ids=[
"rsa-openssh",
"rsa-rfc4716-1",
"rsa-rfc4716-2",
"rsa-rfc4716-3",
"rsa-rfc4716-4",
"ed25519",
],
) )
def test_key_pairs_import_boto3(public_key, fingerprint): def test_key_pairs_import_boto3(public_key, fingerprint):
client = boto3.client("ec2", "us-west-1") client = boto3.client("ec2", "us-west-1")
@ -188,6 +236,18 @@ def test_key_pairs_import_boto3(public_key, fingerprint):
assert kp1["KeyName"] in all_names assert kp1["KeyName"] in all_names
@mock_aws
def test_key_pairs_import_invalid_key():
client = boto3.client("ec2", "us-west-1")
with pytest.raises(ClientError) as exc:
client.import_key_pair(
KeyName="sth", PublicKeyMaterial="---- BEGIN SSH2 PUBLIC KEY ----\nsth"
)
err = exc.value.response["Error"]
assert err["Code"] == "InvalidKeyPair.Format"
@mock_aws @mock_aws
def test_key_pairs_import_exist_boto3(): def test_key_pairs_import_exist_boto3():
client = boto3.client("ec2", "us-west-1") client = boto3.client("ec2", "us-west-1")