Skip to content

Commit 989d5e3

Browse files
luigi617luigi liu
andauthored
[cloudfront] Upgrade sign command: ECDSA, SHA-256, wildcard policies, input validation (#10716)
* Support ecdsa cloudfront * Add CloudFront URL sign command and refactor signing logic * Add URL sign functional test suite and test key files * Add changelog entries for CloudFront sign * Refactor CloudFront sign validation and improve backward compat * Remove strict validation for CloudFront sign command inputs for backward compatability * Add input validation and wildcard support for sign command * Refactor date parsing, add key validation and sign tests * Simplify sign validation and consolidate sign changelog entries * Fix sign.py: add AwsCrtError import, remove extra _validate_signing_args, restore SHA1 error handling * Fix test_sign.py: update mock paths to cloudfront.sign module, trim TestSignBackwardCompatibility * Move sign-cookies botocore method and cookie tests to feat-cloudfront-cookies * Move PEM-leak tests to feat-log-redaction * Address comment: remove left over from splitting PRs * Address comment: drop signed cookie wording from CloudFrontSigner docstring * Address feedback: Passing keep_blank_values=True to parse_qsl * address feedback: fix comment * Address feedback: double wrapping error message * Address feedback: signin example * Address feedback * Add testing --------- Co-authored-by: luigi liu <luigiliu@amazon.com>
1 parent 16a15f6 commit 989d5e3

19 files changed

Lines changed: 2236 additions & 420 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "bugfix",
3+
"category": "``cloudfront``",
4+
"description": "Fix ``aws cloudfront sign`` to support ``fileb://`` private keys and PEM files that contain other blocks, such as a certificate."
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "enhancement",
3+
"category": "``cloudfront``",
4+
"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."
5+
}

‎awscli/botocore/signers.py‎

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -356,43 +356,54 @@ def generate_presigned_url(
356356

357357

358358
class CloudFrontSigner:
359-
'''A signer to create a signed CloudFront URL.
359+
'''A signer to create signed CloudFront URLs.
360360
361-
First you create a cloudfront signer based on a normalized RSA signer::
361+
First you create a cloudfront signer based on a normalized signer::
362362
363363
import rsa
364364
def rsa_signer(message):
365365
private_key = open('private_key.pem', 'r').read()
366366
return rsa.sign(
367367
message,
368368
rsa.PrivateKey.load_pkcs1(private_key.encode('utf8')),
369-
'SHA-1') # CloudFront requires SHA-1 hash
369+
'SHA-1') # SHA-1 is CloudFront's default verification hash
370370
cf_signer = CloudFrontSigner(key_id, rsa_signer)
371371
372372
To sign with a canned policy::
373373
374-
signed_url = cf_signer.generate_signed_url(
374+
signed_url = cf_signer.generate_presigned_url(
375375
url, date_less_than=datetime(2015, 12, 1))
376376
377377
To sign with a custom policy::
378378
379-
signed_url = cf_signer.generate_signed_url(url, policy=my_policy)
379+
signed_url = cf_signer.generate_presigned_url(url, policy=my_policy)
380380
'''
381381

382-
def __init__(self, key_id, rsa_signer):
382+
def __init__(self, key_id, rsa_signer, hash_algorithm=None):
383383
"""Create a CloudFrontSigner.
384384
385385
:type key_id: str
386386
:param key_id: The CloudFront Key Pair ID
387387
388388
:type rsa_signer: callable
389-
:param rsa_signer: An RSA signer.
389+
:param rsa_signer: An RSA or ECDSA signer.
390390
Its only input parameter will be the message to be signed,
391391
and its output will be the signed content as a binary string.
392-
The hash algorithm needed by CloudFront is SHA-1.
392+
The signer is responsible for hashing the message with the
393+
algorithm declared by ``hash_algorithm`` (SHA-1 when it is
394+
``None``). Name is kept as ``rsa_signer`` for backward
395+
compatibility.
396+
397+
:type hash_algorithm: str
398+
:param hash_algorithm: The hash algorithm CloudFront must use to verify
399+
the signature, emitted as the ``Hash-Algorithm`` query parameter
400+
of signed URLs. CloudFront defaults to SHA-1 when it is absent,
401+
so leave this as ``None`` for SHA-1 signatures and set it to
402+
``SHA256`` for SHA-256 signatures.
393403
"""
394404
self.key_id = key_id
395405
self.rsa_signer = rsa_signer
406+
self.hash_algorithm = hash_algorithm
396407

397408
def generate_presigned_url(self, url, date_less_than=None, policy=None):
398409
"""Creates a signed CloudFront URL based on given parameters.
@@ -409,6 +420,12 @@ def generate_presigned_url(self, url, date_less_than=None, policy=None):
409420
:rtype: str
410421
:return: The signed URL.
411422
"""
423+
params = self._generate_signing_params(url, date_less_than, policy)
424+
return self._build_url(
425+
url, [f'{name}={value}' for name, value in params.items()]
426+
)
427+
428+
def _generate_signing_params(self, resource, date_less_than, policy):
412429
if (
413430
date_less_than is not None
414431
and policy is not None
@@ -419,25 +436,20 @@ def generate_presigned_url(self, url, date_less_than=None, policy=None):
419436
raise ValueError(e)
420437
if date_less_than is not None:
421438
# We still need to build a canned policy for signing purpose
422-
policy = self.build_policy(url, date_less_than)
439+
policy = self.build_policy(resource, date_less_than)
423440
if isinstance(policy, str):
424441
policy = policy.encode('utf8')
442+
params = {}
425443
if date_less_than is not None:
426-
params = [f'Expires={int(datetime2timestamp(date_less_than))}']
444+
params['Expires'] = str(int(datetime2timestamp(date_less_than)))
427445
else:
428-
params = [
429-
'Policy={}'.format(self._url_b64encode(policy).decode('utf8'))
430-
]
446+
params['Policy'] = self._url_b64encode(policy).decode('utf8')
431447
signature = self.rsa_signer(policy)
432-
params.extend(
433-
[
434-
'Signature={}'.format(
435-
self._url_b64encode(signature).decode('utf8')
436-
),
437-
f'Key-Pair-Id={self.key_id}',
438-
]
439-
)
440-
return self._build_url(url, params)
448+
params['Signature'] = self._url_b64encode(signature).decode('utf8')
449+
params['Key-Pair-Id'] = self.key_id
450+
if self.hash_algorithm is not None:
451+
params['Hash-Algorithm'] = self.hash_algorithm
452+
return params
441453

442454
def _build_url(self, base_url, extra_params):
443455
separator = '&' if '?' in base_url else '?'

0 commit comments

Comments
 (0)