Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gems/aws-sdk-cloudfront/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Unreleased Changes
------------------

* Feature - CloudFront signers now support SHA-256 signatures via the `:hash_algorithm` option (`'SHA1'` or `'SHA256'`, defaults to `'SHA1'`). Signers also validate that the private key is RSA or ECDSA (P-256), and custom policies are now minified before signing.

1.154.0 (2026-09-11)
------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ module CloudFront
#
# signer = Aws::CloudFront::CookieSigner.new(
# key_pair_id: "cf-keypair-id",
# private_key_path: "./unit_test_dummy_key"
# private_key_path: "./private_key.pem"
# )
# cookies = signer.signed_cookie(url,
# policy: policy.to_json
# )
#
# Pass `hash_algorithm: 'SHA256'` to sign with SHA-256 instead of SHA-1:
#
# signer = Aws::CloudFront::CookieSigner.new(
# key_pair_id: "cf-keypair-id",
# private_key_path: "./private_key.pem",
# hash_algorithm: "SHA256"
# )
#
class CookieSigner
include Signer

Expand Down
44 changes: 41 additions & 3 deletions gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/signer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@
module Aws
module CloudFront
module Signer
# @api private
SUPPORTED_HASH_ALGORITHMS = %w[SHA1 SHA256].freeze
Comment thread
jterapin marked this conversation as resolved.

# @option options [String] :key_pair_id
# @option options [String] :private_key
# @option options [String] :private_key_path
# @option options [String] :hash_algorithm ('SHA1') 'SHA1' or 'SHA256'
def initialize(options = {})
@key_pair_id = key_pair_id(options)
@cipher = OpenSSL::Digest.new('SHA1')
@private_key = OpenSSL::PKey.read(private_key(options))
@hash_algorithm = hash_algorithm(options)
@cipher = OpenSSL::Digest.new(@hash_algorithm)
@private_key = load_private_key(private_key(options))
end

private
Expand Down Expand Up @@ -78,7 +83,7 @@ def resource(scheme, url)
def signature(params = {})
signature_content = {}
if params[:policy]
policy = params[:policy].gsub('/\s/s', '')
policy = params[:policy].gsub(/\s/, '')
signature_content['Policy'] = encode(policy)
elsif params[:resource] && params[:expires]
policy = canned_policy(params[:resource], params[:expires])
Expand All @@ -90,12 +95,18 @@ def signature(params = {})

signature_content['Signature'] = encode(sign_policy(policy))
signature_content['Key-Pair-Id'] = @key_pair_id
# omitted for SHA1 to keep existing signed URLs and cookies unchanged
signature_content['Hash-Algorithm'] = @hash_algorithm if @hash_algorithm == 'SHA256'
signature_content
end

# create the signature string with policy signed
def sign_policy(policy)
@private_key.sign(@cipher, policy)
rescue OpenSSL::PKey::PKeyError => e
msg = "failed to sign with #{@hash_algorithm}: #{e.message}"
msg += ", consider `hash_algorithm: 'SHA256'`" if @hash_algorithm == 'SHA1'
raise ArgumentError, msg
end

# create canned policy that used for signing
Expand All @@ -121,6 +132,16 @@ def key_pair_id(options)
options[:key_pair_id]
end

def hash_algorithm(options)
algorithm = (options[:hash_algorithm] || 'SHA1').to_s.upcase
unless SUPPORTED_HASH_ALGORITHMS.include?(algorithm)
msg = ":hash_algorithm must be one of #{SUPPORTED_HASH_ALGORITHMS.join(', ')}"
raise ArgumentError, msg
end

algorithm
end

def private_key(options)
if options[:private_key]
options[:private_key]
Expand All @@ -131,6 +152,23 @@ def private_key(options)
raise ArgumentError, msg
end
end

def load_private_key(pem)
key = OpenSSL::PKey.read(pem)
case key
when OpenSSL::PKey::RSA
key
when OpenSSL::PKey::EC
curve = key.group.curve_name
raise ArgumentError, "unsupported ECDSA curve `#{curve}', must be prime256v1" unless curve == 'prime256v1'

key
else
raise ArgumentError, "unsupported private key type #{key.class}, must be RSA or ECDSA"
end
rescue OpenSSL::PKey::PKeyError
raise ArgumentError, 'invalid private key, must be a PEM-encoded RSA or ECDSA private key'
end
end
end
end
10 changes: 9 additions & 1 deletion gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/url_signer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ module CloudFront
#
# signer = Aws::CloudFront::UrlSigner.new(
# key_pair_id: "cf-keypair-id",
# private_key_path: "./unit_test_dummy_key"
# private_key_path: "./private_key.pem"
# )
# url = signer.signed_url(url,
# policy: policy.to_json
# )
#
# Pass `hash_algorithm: 'SHA256'` to sign with SHA-256 instead of SHA-1:
#
# signer = Aws::CloudFront::UrlSigner.new(
# key_pair_id: "cf-keypair-id",
# private_key_path: "./private_key.pem",
# hash_algorithm: "SHA256"
# )
#
class UrlSigner
include Signer

Expand Down
47 changes: 47 additions & 0 deletions gems/aws-sdk-cloudfront/spec/cookie_signer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,53 @@ module CloudFront
expect(cookie['CloudFront-Key-Pair-Id']).to eq('CF_KEYPAIR_ID')
end
end

# test vectors from the CloudFront URL and Cookie Signer SEP
describe 'SEP test cases' do
let(:key_dir) { File.dirname(__FILE__) }
let(:expires) { 1_767_290_400 }

def rsa_signer(hash_algorithm = nil)
CookieSigner.new(key_pair_id: 'K1TESTKEY', private_key_path: "#{key_dir}/sep_rsa_key", hash_algorithm: hash_algorithm)
end

it 'canned-policy-cookies' do
cookies = rsa_signer.signed_cookie('https://d111111abcdef8.cloudfront.net/image.jpg', expires: expires)
expect(cookies).to eq(
'CloudFront-Expires' => '1767290400',
'CloudFront-Signature' => 'iONoMLnhiCy9q1~WB9GkR2DiHz18I85i3o6kZ64REf-fCSOg-AyXEZiq7fJuS~DT-kbZXjVpgIQqI4sCTcBW9XpO6dyJ5sh8Igk3V~OVncS9acGVnI~ZhHBWiGhU8GmkEMAhn6R2RGO-wKGClrXdJGEUE26XoALdHUzHbmU6AGI_',
'CloudFront-Key-Pair-Id' => 'K1TESTKEY'
)
end

it 'canned-policy-cookies-sha256' do
cookies = rsa_signer('SHA256').signed_cookie('https://d111111abcdef8.cloudfront.net/image.jpg', expires: expires)
expect(cookies).to eq(
'CloudFront-Expires' => '1767290400',
'CloudFront-Signature' => 'LiC~LakvNvZtR~AsTcirQ0CAsy-YIZmpHmI9uImK4xJmrhVdJULhWmRt3bXO7qqw2gJDECZN-xC~bKWEKcJ9Vgs1IgpRdMkY6XGDKZ1XHBdNbd~0v5UiRf4zXwVMRqoynkQPQcihkze7RkDBsOoYHh9jDdtO1iDm0QZ1Qp~cxro_',
'CloudFront-Key-Pair-Id' => 'K1TESTKEY',
'CloudFront-Hash-Algorithm' => 'SHA256'
)
end

it 'custom-policy-cookies' do
policy = %({"Statement":[{"Resource":"https://d111111abcdef8.cloudfront.net/*","Condition":{"DateLessThan":{"AWS:EpochTime":#{expires}},"IpAddress":{"AWS:SourceIp":"10.0.0.0/8"}}}]})
cookies = rsa_signer.signed_cookie(nil, policy: policy)
expect(cookies).to eq(
'CloudFront-Policy' => 'eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9kMTExMTExYWJjZGVmOC5jbG91ZGZyb250Lm5ldC8qIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNzY3MjkwNDAwfSwiSXBBZGRyZXNzIjp7IkFXUzpTb3VyY2VJcCI6IjEwLjAuMC4wLzgifX19XX0_',
'CloudFront-Signature' => 'r28Jnd0t9aq7cu0k9jGWl4L0YsRxgueZtGRw5oEEspU9-eIPGM~ZGMQh36~5HpKC5c67cZjDgJcsqrCacmTHMZZx613gbeYAsx2-hEatU8URiuNHnVp4hPV3HqtbuZ6Din9iEZUpOBYVg6DWGEFJRCQ7SPouBhhJdYDZZOPHGpA_',
'CloudFront-Key-Pair-Id' => 'K1TESTKEY'
)
end

it 'custom-policy-cookies-sha256' do
policy = %({"Statement":[{"Resource":"https://d111111abcdef8.cloudfront.net/*","Condition":{"DateLessThan":{"AWS:EpochTime":#{expires}},"IpAddress":{"AWS:SourceIp":"10.0.0.0/8"}}}]})
cookies = rsa_signer('SHA256').signed_cookie(nil, policy: policy)
expect(cookies['CloudFront-Signature']).to eq('OzeonPh-NS8g3trdsFAo5LrMBEx1ef05GvdmWuLai6AaBLP63PVJRUGySYmGfQ-NqQ02geWzo7aZS7XFtkr4X1z9VTbQMfmzZftbuRXoP5ZDhFzVnSX3DeoEW8jP3BrOLHJsKwFCY5alIR4zO6LpqFmR5vVmqMYpRcbafg3~X18_')
expect(cookies['CloudFront-Hash-Algorithm']).to eq('SHA256')
expect(cookies).to_not have_key('CloudFront-Expires')
end
end
end
end
end
9 changes: 9 additions & 0 deletions gems/aws-sdk-cloudfront/spec/sep_dsa_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PRIVATE KEY-----
MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAP4RyUV/qd4E/QPJ7IrjogSq073W
dinSable3aQYBeLsKVcnUg9n6Jfpw6o1WxsANZK8U6+vnG/hfc3OUKANcqaM57U6
z3dTaRUR/SS85MLNLeofsnqtYVYNYgrqWpim6BJ6fG0ur9D7l9qwQTPCS6Jgw+/S
543zKvTDUvh2ilztAhUAipnpPmXU/H9o6HUo7b6oULtaFZ8CgYAIGh6nNlHaDspr
vGKBTmdLWZsH6mtTQGjCfDQ3MkFJoutbfhjCWt5SY+BjvTbfF3qF/LyKdvr29Ps6
L9lbFbCwnae3J1lpZMB2bE0iH8AiIxtl4Y4TG2QfqS1bmL2pm+KoCJabcw5pDbEs
Vv2Y4KN0Avi/8Xm6uwNIjjAvkxGi7AQWAhQPQOfoCwk7SSHhTGzsePLRb7VheA==
-----END PRIVATE KEY-----
5 changes: 5 additions & 0 deletions gems/aws-sdk-cloudfront/spec/sep_ecdsa_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrYlqAmw042lSYigS
/hH1a8NamwSmZHOnIfkoDPMjHbKhRANCAAS70BK5GOWO0HlQcodlwYOKG45Ng7cd
agIYyRgZUEId3gsFY43KtQPcgKLUQ1NPOJyuFcYCtki460UsOV1ThTxP
-----END PRIVATE KEY-----
4 changes: 4 additions & 0 deletions gems/aws-sdk-cloudfront/spec/sep_ecdsa_public_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu9ASuRjljtB5UHKHZcGDihuOTYO3
HWoCGMkYGVBCHd4LBWONyrUD3ICi1ENTTzicrhXGArZIuOtFLDldU4U8Tw==
-----END PUBLIC KEY-----
16 changes: 16 additions & 0 deletions gems/aws-sdk-cloudfront/spec/sep_rsa_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAL1XcBtoCSzdyqqm
CFfh0XeLiuCtlE6bXUbscAbl7AUISzbQNKzgO1FwRBDMddfYyOQ/2nDPzLhgAK2E
Kv+dEi4/YEdLg2cbOaxjfhSn6ycwM+C502Ls/CtxHICcqs4cQs7BTlFfk32gy7Oq
kLTbTttIGMWasBAQHLhbJZstDp2vAgMBAAECgYAoujMqIgG/PeIHPPmUdcWJ0mFI
HO5GzlKNG/So9zICjxsmqjh6ay03Qk/R0TkT+dSUjEufcoNVsYjTyhc5rn6nR0SK
UL2IG5r6yFzlh+27u0/CzR1x61zWj01FwI8tacyR1tskyt7jqvT6eYFb1in2BDlE
qdNgTxVrjVBlxU6hkQJBAON8RGwO3g5Pxt0k0yQ6uSu3xj1/2uq4njKcoDgyFcNp
M7ZFW8SX0ZZw3wqSPlzs7mRI79b+G+M97nBw78kdKBkCQQDVEy4WnA3VRSlBRjVH
HwLyK7B9FaRJi70cb0JePnazEZS74FeT6BFW0HsNNeq+awCEcvemKpsOa8nr8QgM
K00HAkEA2keyS9GURz1Lf4VHSHtElOt5QCe/wvw1aDEcF/APK/t1UE+LN7/Jr0ZM
7pLXXklGklneMXiQ/+K8OY5Ut7DPeQJAATjy8r5Cdg7HhdBZTecnpSwK/yy4nJNo
qlkZEGFbXPuk1s8asYaLUuwvSIwepKkIf7oJIbLs4NBNgEUJvsgg0QJBAJx2KEC9
KMs82U4qjLE5vA87r4s4llGgvR+QsYfiwr8LUMlN47DLGrjNpb3JZ58j9Cw3zFPh
yLL0lGtqf49VRDs=
-----END PRIVATE KEY-----
6 changes: 6 additions & 0 deletions gems/aws-sdk-cloudfront/spec/sep_rsa_public_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9V3AbaAks3cqqpghX4dF3i4rg
rZROm11G7HAG5ewFCEs20DSs4DtRcEQQzHXX2MjkP9pwz8y4YACthCr/nRIuP2BH
S4NnGzmsY34Up+snMDPgudNi7PwrcRyAnKrOHELOwU5RX5N9oMuzqpC0207bSBjF
mrAQEBy4WyWbLQ6drwIDAQAB
-----END PUBLIC KEY-----
71 changes: 71 additions & 0 deletions gems/aws-sdk-cloudfront/spec/signer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

require_relative 'spec_helper'

module Aws
module CloudFront
describe Signer do
let(:key_dir) { File.dirname(__FILE__) }

describe ':hash_algorithm' do
it 'defaults to SHA1' do
signer = UrlSigner.new(key_pair_id: 'K', private_key_path: "#{key_dir}/sep_rsa_key")
expect(signer.instance_variable_get(:@hash_algorithm)).to eq('SHA1')
end

it 'accepts SHA1 and SHA256' do
%w[SHA1 SHA256 sha256].each do |alg|
expect do
UrlSigner.new(key_pair_id: 'K', private_key_path: "#{key_dir}/sep_rsa_key", hash_algorithm: alg)
end.to_not raise_error
end
end

it 'raises on an unsupported hash algorithm' do
expect do
UrlSigner.new(key_pair_id: 'K', private_key_path: "#{key_dir}/sep_rsa_key", hash_algorithm: 'SHA384')
end.to raise_error(ArgumentError, /:hash_algorithm must be one of SHA1, SHA256/)
end
end

describe 'private key validation' do
it 'accepts a PKCS#8 RSA key' do
expect do
UrlSigner.new(key_pair_id: 'K', private_key_path: "#{key_dir}/sep_rsa_key")
end.to_not raise_error
end

it 'accepts a PKCS#8 ECDSA P-256 key' do
expect do
UrlSigner.new(key_pair_id: 'K', private_key_path: "#{key_dir}/sep_ecdsa_key")
end.to_not raise_error
end

it 'accepts a SEC1 ECDSA P-256 key' do
expect do
UrlSigner.new(key_pair_id: 'K', private_key_path: "#{key_dir}/ecdsa_dummy_key")
end.to_not raise_error
end

it 'raises on an unsupported key type' do
expect do
UrlSigner.new(key_pair_id: 'K', private_key_path: "#{key_dir}/sep_dsa_key")
end.to raise_error(ArgumentError, /unsupported private key type OpenSSL::PKey::DSA/)
end

it 'raises on an ECDSA key that is not P-256' do
key = OpenSSL::PKey::EC.generate('secp384r1').to_pem
expect do
UrlSigner.new(key_pair_id: 'K', private_key: key)
end.to raise_error(ArgumentError, /unsupported ECDSA curve `secp384r1'/)
end

it 'raises on an invalid key' do
expect do
UrlSigner.new(key_pair_id: 'K', private_key: 'not a key')
end.to raise_error(ArgumentError, /invalid private key/)
end
end
end
end
end
Loading
Loading