Repository navigation
Conversation
…ard compatability
…rgs, restore SHA1 error handling
…estSignBackwardCompatibility
| ''' | ||
|
|
||
| def __init__(self, key_id, rsa_signer): | ||
| def __init__(self, key_id, rsa_signer, hash_algorithm=None): |
There was a problem hiding this comment.
why do we still call this param rsa_signer if it can be either rsa signer or ecsda signer? is it for backward compatibility reasons?
There was a problem hiding this comment.
yeah, it is not technically for backward compatibility because even if we change the name to signer, CLI user won't see any behavioral change, I kept rsa_signer only because I wasn't sure if I can make the assumption that botocore module isn't being used by others and it is a breaking change for python team so keeping the same name can make python team port process it easier.
|
|
||
| class ECDSASigner(_KeySigner): | ||
| _P256_COORDINATE_LENGTH = 32 | ||
| _SUPPORTED_HASH_ALGORITHMS = (None, 'SHA256') |
There was a problem hiding this comment.
question: why do we have None as valid choice here but then down below hard code to sha256?
There was a problem hiding this comment.
None means the user didn't pass --hash-algorithm. _SUPPORTED_HASH_ALGORITHMS lists the inputs we accept, not the algorithms we sign with. It is used only for fast failing if parameter given is invalid
GarrettBeatty
left a comment
There was a problem hiding this comment.
The crypto checks out. I checked RSA SHA-1 and SHA-256 and ECDSA (SEC1, PKCS#8, and files with an EC PARAMETERS block first) signatures with openssl dgst -verify on canned, custom and wildcard policies. ECDSA output is DER, and 400 random P-256 keys loaded without a false "not P-256" rejection. RSA SHA-1 output is byte-identical to v2 for canned, custom, query-string, epoch and offset-date inputs.
Blocker: five generate_signed_cookies unit tests are still on this branch after the split, so CI fails on this PR alone (5 failed, 165 passed in tests/unit/botocore/test_signers.py). The other comments are smaller.
| policy='{}', | ||
| ) | ||
|
|
||
| def test_generate_signed_cookies_with_expire_time(self): |
There was a problem hiding this comment.
Blocking: test_generate_signed_cookies_with_expire_time, _with_custom_policy, _with_hash_algorithm, _signs_the_policy and _requires_one_of_date_or_policy (lines 712–773) call CloudFrontSigner.generate_signed_cookies. That method is only added in #10715, so all five fail with AttributeError on this branch. Please move them to #10715 with the method.
|
|
||
| class CloudFrontSigner: | ||
| '''A signer to create a signed CloudFront URL. | ||
| '''A signer to create signed CloudFront URLs and signed cookies. |
There was a problem hiding this comment.
Nit: the docstring says "signed CloudFront URLs and signed cookies", but this PR doesn't add cookie support. Suggest keeping "signed CloudFront URLs" here and updating it in #10715.
| { | ||
| "type": "enhancement", | ||
| "category": "``cloudfront``", | ||
| "description": "``aws cloudfront sign`` now supports ECDSA (P-256) private keys, SHA-256 signing with ``--hash-algorithm``, and wildcard policies with ``--policy-resource``, and rejects input that would produce a URL CloudFront rejects." |
There was a problem hiding this comment.
This is a behavior change, so it should be in the changelog. --url values that aren't absolute http(s) URLs used to sign with rc 0 and now fail with rc 252. Examples: rtmp-stream-name.mp4, videos/clip.mp4, https://host:99999/a.jpg. The same goes for a later --date-greater-than and URLs with spaces. RTMP distributions are gone and CloudFront would have rejected these URLs, so rejecting them is reasonable. But scripts that relied on rc 0 will break, and the PR description says "Exit codes are unchanged". Can we mention in the entry that non-http(s) resources are now rejected?
There was a problem hiding this comment.
As discussed offline, fast fail for invalid case are not breaking change. PR description updated
| def _get_policy_resource(self, args): | ||
| _validate_resource(args.url, 'url', allow_wildcards=False) | ||
| signing_params = _SIGNING_QUERY_PARAMS.intersection( | ||
| name for name, _ in parse_qsl(urlsplit(args.url).query) |
There was a problem hiding this comment.
parse_qsl drops blank values by default, so the duplicate-parameter check misses ?Expires and ?Signature=. Both still sign and produce a URL with duplicate parameters:
aws cloudfront sign --url 'https://d1.cloudfront.net/a.jpg?Signature=' ...
# -> ...a.jpg?Signature=&Expires=...&Signature=...
| name for name, _ in parse_qsl(urlsplit(args.url).query) | |
| name | |
| for name, _ in parse_qsl( | |
| urlsplit(args.url).query, keep_blank_values=True | |
| ) |
| _SUPPORTED_HASH_ALGORITHMS = (None, 'SHA256') | ||
|
|
||
| def __init__(self, priv_key, hash_algorithm=None): | ||
| coords = priv_key.get_public_coords() |
There was a problem hiding this comment.
On Linux, this "does not include its public key" error is only reachable for PKCS#8 keys. A SEC1 key exported with openssl ec -no_public (BEGIN EC PRIVATE KEY) fails earlier in EC.new_key_from_der_data with AWS_ERROR_CAL_UNSUPPORTED_KEY_FORMAT. from_pem swallows that error, so the user gets the generic "Unsupported key type or invalid private key" message. Could we detect that case for the EC PRIVATE KEY label and give the specific message?
There was a problem hiding this comment.
Detecting this means parsing the SEC1 DER ourselves, which seems like a lot of code for a rare case (-no_public keys). The key is still rejected with a clear error. I'd rather leave it for now, or pick it up in a follow-up if it comes up.
|
|
||
| @skip_if_windows('The Windows CRT derives the missing EC public key.') | ||
| def test_ec_private_key_without_public_key(self): | ||
| # A P-256 key exported with ``openssl ec -no_public``. |
There was a problem hiding this comment.
The fixture is PKCS#8 (BEGIN PRIVATE KEY), but openssl ec -no_public writes SEC1 (BEGIN EC PRIVATE KEY). Suggest something like openssl ec -no_public | openssl pkcs8 -topk8 -nocrypt, and adding a SEC1 case if the SEC1 error message is handled (see the comment on ECDSASigner.__init__).
There was a problem hiding this comment.
Comment fixed and did not add SEC1 as I leave it for now
|
|
||
| class _KeySigner: | ||
| def sign(self, message): | ||
| try: |
There was a problem hiding this comment.
AwsCrtError subclasses Exception, not RuntimeError, so a CRT error raised as AwsCrtError from ECDSASigner._sign skips this wrapper. RSASigner._sign already catches both. Also, the SHA-1 RuntimeError from RSASigner._sign gets wrapped here a second time, so the message reads "Failed to sign the CloudFront policy with the private key: Failed to sign the URL using the SHA1 hash algorithm: …". Suggest catching (AwsCrtError, RuntimeError) here and raising the SHA-1 error as ValueError directly so it isn't wrapped twice.
| ), | ||
| }, | ||
| { | ||
| 'name': 'hash-algorithm', |
There was a problem hiding this comment.
Optional: --hash-algorithm sha256 fails with "Found invalid choice 'sha256'". Since this is a CLI-only argument, accepting any case would be friendlier, but keeping it as is matches other choices arguments.
There was a problem hiding this comment.
CloudFront uses the uppercase form and the error already tells the user what to do, I'd leave it as is
|
|
||
| https://d111111abcdef8.cloudfront.net/private-content/private-file.html?Expires=1577836800&Signature=nEXK7Kby47XKeZQKVc6pwkif6oZc-JWSpDkH0UH7EBGGqvgurkecCbgL5VfUAXyLQuJxFwRQWscz-owcq9KpmewCXrXQbPaJZNi9XSNwf4YKurPDQYaRQawKoeenH0GFteRf9ELK-Bs3nljTLjtbgzIUt7QJNKXcWr8AuUYikzGdJ4-qzx6WnxXfH~fxg4-GGl6l2kgCpXUB6Jx6K~Y3kpVOdzUPOIqFLHAnJojbhxqrVejomZZ2XrquDvNUCCIbePGnR3d24UPaLXG4FKOqNEaWDIBXu7jUUPwOyQCvpt-GNvjRJxqWf93uMobeMOiVYahb-e0KItiQewGcm0eLZQ__&Key-Pair-Id=APKAEIBAERJR2EXAMPLE | ||
|
|
||
| **To sign a CloudFront URL using SHA-256** |
There was a problem hiding this comment.
The signatures in the SHA-256 and wildcard examples are 128 bytes, which means a 1024-bit key. The first example's is 256 bytes (2048-bit), and the text says all three use the same cf-signer-priv-key.pem. CloudFront key groups need 2048-bit RSA keys, so could we regenerate these with the same 2048-bit key? Also, ECDSA support is a headline feature of this PR but has no example; worth adding one.
Issue #, if available:
Description of changes:
Description of changes
Brings
aws cloudfront signin line with the shared CloudFront URL Signer SEP.New features
signaccepts ECDSA private keys in addition to RSA. The key type is detected from the PEM. Supported formats: PKCS#1, PKCS#8, and SEC1.--hash-algorithm SHA1|SHA256option. RSA still defaults to SHA-1, so existing signed URLs stay byte-identical. SHA-256 addsHash-Algorithm=SHA256so CloudFront verifies with the right hash. ECDSA keys always use SHA-256.*in the resource now selects a custom policy, so the signature works for every matching URL. With the new--policy-resourceoption, you sign a wildcard resource and apply the signature to a specific--url.Bug fixes
--private-key fileb://...now works. Previously it failed and printed the key in the error message.EC PARAMETERSblock fromopenssl ecparam) and keys with a UTF-8 byte order mark are now accepted.http(s)://scheme, with a#fragment, with non-ASCII characters, or already containing signing parameters--date-greater-thanat or after--date-less-thanRefactor
cloudfrontcustomizations (create-invalidation --paths,create-distribution/update-distribution --default-root-object,--origin-domain-name) moved into acloudfront/package with no behavior change.Testing
CloudFrontSignerHash-Algorithm, and base64 encoding.MalformedPolicy,AccessDenied,MissingKey).create-invalidation,create-distribution, andupdate-distributionsend identical requests to v2.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.