Skip to content

Instantly share code, notes, and snippets.

@prog893
Last active April 11, 2024 15:29
Show Gist options
  • Save prog893/4496c499a8f820f1aae71bcd8756b286 to your computer and use it in GitHub Desktop.
Save prog893/4496c499a8f820f1aae71bcd8756b286 to your computer and use it in GitHub Desktop.
CloudFront Signed URL generator in Python

CloudFront Signed URL generator in Python

For signed cookies, refer here

Usage

from cloudfront_signed_url import generate_cloudfront_signed_url

generate_cloudfront_signed_url("https://your-cf-domain.com/path/to/file.txt", 3600)

Prerequisites

  • Configured CloudFront Distribution
  • An Origin access identity and a CloudFront key
  • Origin and Behavior configured to Restrict Viewer Access

Reference: http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html

Parameters

  • Make sure you have a CloudFront key pair and key group created and associated with distribution behavior, details here)
  • Replace newlines with \\n in private key and save the result to an SSM Parameter Store paramater named CF_SIGNED_URL_PRIVATE_KEY
  • Save key ID (key pair ID) to a parameter named CF_SIGNED_URL_KEY_ID

Notes

  • Unlike S3 pre-signed URLs, you can use a link generated once multiple times, as long as it is still valid (TTL).
  • You can modify make_policy to make other policies (not per-url, but broader clauses), or even signed cookies (Policy, Signature, Key-Pair-Id are generated in the same way for cookies too).

Dependencies

  • cryptography
  • boto3 (only for SSM)
import base64
import datetime
import json
import boto3
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
ssm_client = boto3.client('ssm')
# CloudFront secret from SSM
CF_SIGNED_URL_KEY_PAIR_ID = ssm_client.get_parameter(f"CF_SIGNED_URL_KEY_PAIR_ID")
CF_SIGNED_URL_PRIVATE_KEY = ssm_client.get_parameter(f"CF_SIGNED_URL_PRIVATE_KEY")
# key pair id is expected to be str
# private key is expected to be bytes
# fix possible escaped newlines
CF_SIGNED_URL_PRIVATE_KEY = CF_SIGNED_URL_PRIVATE_KEY.replace(b'\\n', b'\n')
# or, define your own
def generate_cloudfront_signature(message: bytes, private_key: bytes):
private_key_signer = serialization.load_pem_private_key(
private_key,
password=None,
backend=default_backend()
)
return private_key_signer.sign(message, padding.PKCS1v15(), hashes.SHA1())
def make_cloudfront_policy(resource: str, expire_epoch_time: int):
policy = {
'Statement': [{
'Resource': resource,
'Condition': {
'DateLessThan': {
'AWS:EpochTime': expire_epoch_time
}
}
}]
}
return json.dumps(policy).replace(" ", "")
def url_base64_encode(data: bytes):
return base64.b64encode(data).replace(b'+', b'-').replace(b'=', b'_').replace(b'/', b'~').decode('utf-8')
def url_base64_decode(data: bytes):
return base64.b64encode(data).replace(b'-', b'+').replace(b'_', b'=').replace(b'~', b'/').decode('utf-8')
def generate_cloudfront_signed_url(url: str, expire_seconds: int):
expire_epoch_time = (datetime.datetime.now() + datetime.timedelta(seconds=expire_seconds)).timestamp()
expire_epoch_time = int(expire_epoch_time)
policy = make_cloudfront_policy(url, expire_epoch_time)
signature = generate_cloudfront_signature(policy.encode('utf-8'), CF_SIGNED_URL_PRIVATE_KEY)
signed_url = f"{url}?" \
f"Policy={url_base64_encode(policy.encode('utf-8'))}&" \
f"Signature={url_base64_encode(signature)}&" \
f"Key-Pair-Id={CF_SIGNED_URL_KEY_PAIR_ID}"
return signed_url
@dpmccabe
Copy link

Landed here via Google search. There are a bunch of things wrong with this gist, among them CF_SIGNED_URL_SECRET being undefined and generate_signature having the wrong argument order, and more. Until this functionality is added to boto3, it's better just to use botocore.

@prog893
Copy link
Author

prog893 commented Oct 28, 2022

@dpmccabe Hi there, thank you for your interest. I have rewritten the code and tested it against real CF distributions, you can take a look if you still need this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment