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

Techdebt: Remove ECDSA dependency #7356

Merged
merged 2 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ readthedocs-sphinx-search
docker
openapi_spec_validator
PyYAML>=5.1
python-jose[cryptography]>=3.1.0,<4.0.0
joserfc>=0.9.0
8 changes: 4 additions & 4 deletions moto/cognitoidp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from collections import OrderedDict
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.common_models import BaseModel
Expand Down Expand Up @@ -444,7 +444,7 @@ def __init__(
with open(
os.path.join(os.path.dirname(__file__), "resources/jwks-private.json")
) as f:
self.json_web_key = json.loads(f.read())
self.json_web_key = jwk.RSAKey.import_key(json.loads(f.read()))

@property
def backend(self) -> "CognitoIdpBackend":
Expand Down Expand Up @@ -543,10 +543,10 @@ def create_jwt(
"username" if token_use == "access" else "cognito:username": username,
}
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 (
jws.sign(payload, self.json_web_key, headers, algorithm="RS256"),
jwt.encode(headers, payload, self.json_web_key),
expires_in,
)

Expand Down
67 changes: 51 additions & 16 deletions moto/ec2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,31 +669,66 @@ def generate_instance_identity_document(instance: Any) -> Dict[str, Any]:
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(
key_material: Union[str, bytes]
) -> Union[RSAPublicKey, Ed25519PublicKey]:
# These imports take ~.5s; let's keep them local
import sshpubkeys.exceptions
from sshpubkeys.keys import SSHKey

try:
if not isinstance(key_material, bytes):
if isinstance(key_material, str):
key_material = key_material.encode("ascii")
key_material = base64.b64decode(key_material)

decoded_key = base64.b64decode(key_material)
public_key = SSHKey(decoded_key.decode("ascii"))
except (sshpubkeys.exceptions.InvalidKeyException, UnicodeDecodeError):
raise ValueError("bad key")
if key_material.startswith(b"---- BEGIN SSH2 PUBLIC KEY ----"):
# cryptography doesn't parse RFC4716 key format, so we have to convert it first
key_material = _convert_rfc4716(key_material)

if public_key.rsa:
return public_key.rsa
public_key = serialization.load_ssh_public_key(key_material)

# `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]
if not isinstance(public_key, (RSAPublicKey, Ed25519PublicKey)):
raise ValueError("bad key")
except UnicodeDecodeError:
raise ValueError("bad key")

raise ValueError("bad key")
return public_key


def public_key_fingerprint(public_key: Union[RSAPublicKey, Ed25519PublicKey]) -> str:
Expand Down
29 changes: 9 additions & 20 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,11 @@ moto = py.typed

[options.extras_require]
all =
python-jose[cryptography]>=3.1.0,<4.0.0
ecdsa!=0.15
joserfc>=0.9.0
docker>=3.0.0
graphql-core
PyYAML>=5.1
cfn-lint>=0.40.0
sshpubkeys>=3.1.0
openapi-spec-validator>=0.5.0
pyparsing>=3.0.7
jsondiff>=1.1.2
Expand All @@ -59,13 +57,11 @@ all =
setuptools
multipart
proxy =
python-jose[cryptography]>=3.1.0,<4.0.0
ecdsa!=0.15
joserfc>=0.9.0
docker>=2.5.1
graphql-core
PyYAML>=5.1
cfn-lint>=0.40.0
sshpubkeys>=3.1.0
openapi-spec-validator>=0.5.0
pyparsing>=3.0.7
jsondiff>=1.1.2
Expand All @@ -74,13 +70,11 @@ proxy =
setuptools
multipart
server =
python-jose[cryptography]>=3.1.0,<4.0.0
ecdsa!=0.15
joserfc>=0.9.0
docker>=3.0.0
graphql-core
PyYAML>=5.1
cfn-lint>=0.40.0
sshpubkeys>=3.1.0
openapi-spec-validator>=0.5.0
pyparsing>=3.0.7
jsondiff>=1.1.2
Expand All @@ -94,10 +88,9 @@ acmpca =
amp =
apigateway =
PyYAML>=5.1
python-jose[cryptography]>=3.1.0,<4.0.0
ecdsa!=0.15
joserfc>=0.9.0
openapi-spec-validator>=0.5.0
apigatewayv2 =
apigatewayv2 =
PyYAML>=5.1
openapi-spec-validator>=0.5.0
applicationautoscaling =
Expand All @@ -112,13 +105,11 @@ batch_simple =
budgets =
ce =
cloudformation =
python-jose[cryptography]>=3.1.0,<4.0.0
ecdsa!=0.15
joserfc>=0.9.0
docker>=3.0.0
graphql-core
PyYAML>=5.1
cfn-lint>=0.40.0
sshpubkeys>=3.1.0
openapi-spec-validator>=0.5.0
pyparsing>=3.0.7
jsondiff>=1.1.2
Expand All @@ -133,8 +124,7 @@ codecommit =
codepipeline =
cognitoidentity =
cognitoidp =
python-jose[cryptography]>=3.1.0,<4.0.0
ecdsa!=0.15
joserfc>=0.9.0
comprehend =
config =
databrew =
Expand All @@ -150,7 +140,7 @@ dynamodbstreams =
docker>=3.0.0
py-partiql-parser==0.5.1
ebs =
ec2 = sshpubkeys>=3.1.0
ec2 =
ec2instanceconnect =
ecr =
ecs =
Expand Down Expand Up @@ -204,8 +194,7 @@ redshiftdata =
rekognition =
resourcegroups =
resourcegroupstaggingapi =
python-jose[cryptography]>=3.1.0,<4.0.0
ecdsa!=0.15
joserfc>=0.9.0
docker>=3.0.0
graphql-core
PyYAML>=5.1
Expand Down
26 changes: 17 additions & 9 deletions tests/test_cognitoidp/test_cognitoidp.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@
import pytest
import requests
from botocore.exceptions import ClientError, ParamValidationError
from jose import jws, jwt
from joserfc import jwk, jws, jwt

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.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
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
Expand Down Expand Up @@ -1543,7 +1547,8 @@ def test_group_in_access_token():
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]


Expand Down Expand Up @@ -1604,7 +1609,8 @@ def test_other_attributes_in_id_token():
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["custom:myattr"] == "some val"

Expand Down Expand Up @@ -2957,7 +2963,7 @@ def test_token_legitimacy():

path = "../../moto/cognitoidp/resources/jwks-public.json"
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"]:
outputs = authentication_flow(conn, auth_flow)
Expand All @@ -2968,14 +2974,14 @@ def test_token_legitimacy():
issuer = (
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["aud"] == client_id
assert id_claims["token_use"] == "id"
assert id_claims["cognito:username"] == username
for k, v in outputs["additional_fields"].items():
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["client_id"] == client_id
assert access_claims["token_use"] == "access"
Expand Down Expand Up @@ -4938,8 +4944,10 @@ def test_idtoken_contains_kid_header():

def verify_kid_header(token):
"""Verifies the kid-header is corresponds with the public key"""
headers = jwt.get_unverified_headers(token)
kid = headers["kid"]
if isinstance(token, str):
token = token.encode("ascii")
sig = jws.extract_compact(token)
kid = sig.headers()["kid"]

key_index = -1
keys = fetch_public_keys()
Expand Down
66 changes: 63 additions & 3 deletions tests/test_ec2/test_key_pairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
qusUO07jKuSxzPumXBeU+JEtx0J1tqZwJlpGt2R+0qN7nKnPl2+hx \
moto@github.com"""

RSA_PUBLIC_KEY_RFC4716 = b"""\
RSA_PUBLIC_KEY_RFC4716_1 = b"""\
---- BEGIN SSH2 PUBLIC KEY ----
AAAAB3NzaC1yc2EAAAADAQABAAABAQDusXfgTE4eBP50NglSzCSEGnIL6+cr6m3H6cZANO
Q+P1o/W4BdtcAL3sor4iGi7SOeJgo8kweyMQrhrt6HaKGgromRiz37LQx4YIAcBi4Zd023
Expand All @@ -38,6 +38,44 @@
---- 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"

DSA_PUBLIC_KEY_OPENSSH = b"""ssh-dss \
Expand Down Expand Up @@ -157,10 +195,20 @@ def test_key_pairs_delete_exist_boto3():
"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),
],
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):
client = boto3.client("ec2", "us-west-1")
Expand Down Expand Up @@ -188,6 +236,18 @@ def test_key_pairs_import_boto3(public_key, fingerprint):
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
def test_key_pairs_import_exist_boto3():
client = boto3.client("ec2", "us-west-1")
Expand Down