Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
77b6101
Support ecdsa cloudfront
Sep 28, 2026
23f8d3c
Add CloudFront URL sign command and refactor signing logic
Oct 1, 2026
d6ae904
Add URL sign functional test suite and test key files
Oct 1, 2026
76742e6
Add changelog entries for CloudFront sign
Oct 1, 2026
24607cb
Refactor CloudFront sign validation and improve backward compat
Sep 28, 2026
5379221
Remove strict validation for CloudFront sign command inputs for backw…
Sep 29, 2026
4dfb5aa
Add input validation and wildcard support for sign command
Sep 29, 2026
0fc6289
Refactor date parsing, add key validation and sign tests
Oct 1, 2026
e7529bc
Simplify sign validation and consolidate sign changelog entries
Oct 1, 2026
ba8fa64
Fix sign.py: add AwsCrtError import, remove extra _validate_signing_a…
Oct 1, 2026
e975da6
Fix test_sign.py: update mock paths to cloudfront.sign module, trim T…
Oct 1, 2026
038553c
Move sign-cookies botocore method and cookie tests to feat-cloudfront…
Oct 1, 2026
64c5e37
Move PEM-leak tests to feat-log-redaction
Oct 1, 2026
376ff73
Address comment: remove left over from splitting PRs
Oct 14, 2026
341fbc7
Address comment: drop signed cookie wording from CloudFrontSigner doc…
Oct 14, 2026
54abf1b
Address feedback: Passing keep_blank_values=True to parse_qsl
Oct 14, 2026
24a753e
address feedback: fix comment
Oct 14, 2026
7e12417
Address feedback: double wrapping error message
Oct 14, 2026
5987d1b
Address feedback: signin example
Oct 14, 2026
0ab6cee
Address feedback
Oct 22, 2026
90cb690
Add testing
Oct 22, 2026
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
5 changes: 5 additions & 0 deletions .changes/next-release/bugfix-cloudfront-privatekey.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "bugfix",
"category": "``cloudfront``",
"description": "Fix ``aws cloudfront sign`` to support ``fileb://`` private keys and PEM files that contain other blocks, such as a certificate."
}
5 changes: 5 additions & 0 deletions .changes/next-release/enhancement-cloudfront-sign.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"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."
Comment thread
luigi617 marked this conversation as resolved.
}
56 changes: 34 additions & 22 deletions awscli/botocore/signers.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,43 +356,54 @@ def generate_presigned_url(


class CloudFrontSigner:
'''A signer to create a signed CloudFront URL.
'''A signer to create signed CloudFront URLs.

First you create a cloudfront signer based on a normalized RSA signer::
First you create a cloudfront signer based on a normalized signer::

import rsa
def rsa_signer(message):
private_key = open('private_key.pem', 'r').read()
return rsa.sign(
message,
rsa.PrivateKey.load_pkcs1(private_key.encode('utf8')),
'SHA-1') # CloudFront requires SHA-1 hash
'SHA-1') # SHA-1 is CloudFront's default verification hash
cf_signer = CloudFrontSigner(key_id, rsa_signer)

To sign with a canned policy::

signed_url = cf_signer.generate_signed_url(
signed_url = cf_signer.generate_presigned_url(
url, date_less_than=datetime(2015, 12, 1))

To sign with a custom policy::

signed_url = cf_signer.generate_signed_url(url, policy=my_policy)
signed_url = cf_signer.generate_presigned_url(url, policy=my_policy)
'''

def __init__(self, key_id, rsa_signer):
def __init__(self, key_id, rsa_signer, hash_algorithm=None):
Comment thread
luigi617 marked this conversation as resolved.
"""Create a CloudFrontSigner.

:type key_id: str
:param key_id: The CloudFront Key Pair ID

:type rsa_signer: callable
:param rsa_signer: An RSA signer.
:param rsa_signer: An RSA or ECDSA signer.
Its only input parameter will be the message to be signed,
and its output will be the signed content as a binary string.
The hash algorithm needed by CloudFront is SHA-1.
The signer is responsible for hashing the message with the
algorithm declared by ``hash_algorithm`` (SHA-1 when it is
``None``). Name is kept as ``rsa_signer`` for backward
compatibility.

:type hash_algorithm: str
:param hash_algorithm: The hash algorithm CloudFront must use to verify
the signature, emitted as the ``Hash-Algorithm`` query parameter
of signed URLs. CloudFront defaults to SHA-1 when it is absent,
so leave this as ``None`` for SHA-1 signatures and set it to
``SHA256`` for SHA-256 signatures.
"""
self.key_id = key_id
self.rsa_signer = rsa_signer
self.hash_algorithm = hash_algorithm

def generate_presigned_url(self, url, date_less_than=None, policy=None):
"""Creates a signed CloudFront URL based on given parameters.
Expand All @@ -409,6 +420,12 @@ def generate_presigned_url(self, url, date_less_than=None, policy=None):
:rtype: str
:return: The signed URL.
"""
params = self._generate_signing_params(url, date_less_than, policy)
return self._build_url(
url, [f'{name}={value}' for name, value in params.items()]
)

def _generate_signing_params(self, resource, date_less_than, policy):
if (
date_less_than is not None
and policy is not None
Expand All @@ -419,25 +436,20 @@ def generate_presigned_url(self, url, date_less_than=None, policy=None):
raise ValueError(e)
if date_less_than is not None:
# We still need to build a canned policy for signing purpose
policy = self.build_policy(url, date_less_than)
policy = self.build_policy(resource, date_less_than)
if isinstance(policy, str):
policy = policy.encode('utf8')
params = {}
if date_less_than is not None:
params = [f'Expires={int(datetime2timestamp(date_less_than))}']
params['Expires'] = str(int(datetime2timestamp(date_less_than)))
else:
params = [
'Policy={}'.format(self._url_b64encode(policy).decode('utf8'))
]
params['Policy'] = self._url_b64encode(policy).decode('utf8')
signature = self.rsa_signer(policy)
params.extend(
[
'Signature={}'.format(
self._url_b64encode(signature).decode('utf8')
),
f'Key-Pair-Id={self.key_id}',
]
)
return self._build_url(url, params)
params['Signature'] = self._url_b64encode(signature).decode('utf8')
params['Key-Pair-Id'] = self.key_id
if self.hash_algorithm is not None:
params['Hash-Algorithm'] = self.hash_algorithm
return params

def _build_url(self, base_url, extra_params):
separator = '&' if '?' in base_url else '?'
Expand Down
Loading
Loading