From ef3e9531de2d7fc71e0f587a13790a4312fcc71c Mon Sep 17 00:00:00 2001 From: Daniel Pinheiro Date: Mon, 21 Sep 2026 15:21:21 +0000 Subject: [PATCH 1/2] feat: Add support for SHA256 in the CloudFront signers --- gems/aws-sdk-cloudfront/CHANGELOG.md | 2 + .../lib/aws-sdk-cloudfront/cookie_signer.rb | 10 +- .../lib/aws-sdk-cloudfront/signer.rb | 41 +++++- .../lib/aws-sdk-cloudfront/url_signer.rb | 10 +- .../spec/cookie_signer_spec.rb | 47 +++++++ gems/aws-sdk-cloudfront/spec/sep_dsa_key | 9 ++ gems/aws-sdk-cloudfront/spec/sep_ecdsa_key | 5 + .../spec/sep_ecdsa_public_key | 4 + gems/aws-sdk-cloudfront/spec/sep_rsa_key | 16 +++ .../spec/sep_rsa_public_key | 6 + gems/aws-sdk-cloudfront/spec/signer_spec.rb | 71 ++++++++++ .../spec/url_signer_spec.rb | 131 ++++++++++++++++++ 12 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 gems/aws-sdk-cloudfront/spec/sep_dsa_key create mode 100644 gems/aws-sdk-cloudfront/spec/sep_ecdsa_key create mode 100644 gems/aws-sdk-cloudfront/spec/sep_ecdsa_public_key create mode 100644 gems/aws-sdk-cloudfront/spec/sep_rsa_key create mode 100644 gems/aws-sdk-cloudfront/spec/sep_rsa_public_key create mode 100644 gems/aws-sdk-cloudfront/spec/signer_spec.rb diff --git a/gems/aws-sdk-cloudfront/CHANGELOG.md b/gems/aws-sdk-cloudfront/CHANGELOG.md index 78ec50c9e23..123577fc8f8 100644 --- a/gems/aws-sdk-cloudfront/CHANGELOG.md +++ b/gems/aws-sdk-cloudfront/CHANGELOG.md @@ -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 raise a clear error otherwise. + 1.154.0 (2026-09-11) ------------------ diff --git a/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/cookie_signer.rb b/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/cookie_signer.rb index 7f00bfb4e5d..eb505f5549a 100644 --- a/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/cookie_signer.rb +++ b/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/cookie_signer.rb @@ -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 diff --git a/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/signer.rb b/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/signer.rb index 674f55da4f4..a70fcb8a9da 100644 --- a/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/signer.rb +++ b/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/signer.rb @@ -8,13 +8,17 @@ module Aws module CloudFront module Signer + SUPPORTED_HASH_ALGORITHMS = %w[SHA1 SHA256].freeze + # @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 @@ -90,12 +94,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 @@ -121,6 +131,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] @@ -131,6 +151,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 diff --git a/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/url_signer.rb b/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/url_signer.rb index 9ef30f95a97..754561a0c7e 100644 --- a/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/url_signer.rb +++ b/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/url_signer.rb @@ -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 diff --git a/gems/aws-sdk-cloudfront/spec/cookie_signer_spec.rb b/gems/aws-sdk-cloudfront/spec/cookie_signer_spec.rb index b8bc93f7736..1790c2a2716 100644 --- a/gems/aws-sdk-cloudfront/spec/cookie_signer_spec.rb +++ b/gems/aws-sdk-cloudfront/spec/cookie_signer_spec.rb @@ -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 diff --git a/gems/aws-sdk-cloudfront/spec/sep_dsa_key b/gems/aws-sdk-cloudfront/spec/sep_dsa_key new file mode 100644 index 00000000000..9ed56ce3685 --- /dev/null +++ b/gems/aws-sdk-cloudfront/spec/sep_dsa_key @@ -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----- diff --git a/gems/aws-sdk-cloudfront/spec/sep_ecdsa_key b/gems/aws-sdk-cloudfront/spec/sep_ecdsa_key new file mode 100644 index 00000000000..4b13d33be7e --- /dev/null +++ b/gems/aws-sdk-cloudfront/spec/sep_ecdsa_key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrYlqAmw042lSYigS +/hH1a8NamwSmZHOnIfkoDPMjHbKhRANCAAS70BK5GOWO0HlQcodlwYOKG45Ng7cd +agIYyRgZUEId3gsFY43KtQPcgKLUQ1NPOJyuFcYCtki460UsOV1ThTxP +-----END PRIVATE KEY----- diff --git a/gems/aws-sdk-cloudfront/spec/sep_ecdsa_public_key b/gems/aws-sdk-cloudfront/spec/sep_ecdsa_public_key new file mode 100644 index 00000000000..f95d4388e9d --- /dev/null +++ b/gems/aws-sdk-cloudfront/spec/sep_ecdsa_public_key @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu9ASuRjljtB5UHKHZcGDihuOTYO3 +HWoCGMkYGVBCHd4LBWONyrUD3ICi1ENTTzicrhXGArZIuOtFLDldU4U8Tw== +-----END PUBLIC KEY----- diff --git a/gems/aws-sdk-cloudfront/spec/sep_rsa_key b/gems/aws-sdk-cloudfront/spec/sep_rsa_key new file mode 100644 index 00000000000..c795cc92a3b --- /dev/null +++ b/gems/aws-sdk-cloudfront/spec/sep_rsa_key @@ -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----- diff --git a/gems/aws-sdk-cloudfront/spec/sep_rsa_public_key b/gems/aws-sdk-cloudfront/spec/sep_rsa_public_key new file mode 100644 index 00000000000..253c33a44eb --- /dev/null +++ b/gems/aws-sdk-cloudfront/spec/sep_rsa_public_key @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9V3AbaAks3cqqpghX4dF3i4rg +rZROm11G7HAG5ewFCEs20DSs4DtRcEQQzHXX2MjkP9pwz8y4YACthCr/nRIuP2BH +S4NnGzmsY34Up+snMDPgudNi7PwrcRyAnKrOHELOwU5RX5N9oMuzqpC0207bSBjF +mrAQEBy4WyWbLQ6drwIDAQAB +-----END PUBLIC KEY----- diff --git a/gems/aws-sdk-cloudfront/spec/signer_spec.rb b/gems/aws-sdk-cloudfront/spec/signer_spec.rb new file mode 100644 index 00000000000..1df318ae53e --- /dev/null +++ b/gems/aws-sdk-cloudfront/spec/signer_spec.rb @@ -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 diff --git a/gems/aws-sdk-cloudfront/spec/url_signer_spec.rb b/gems/aws-sdk-cloudfront/spec/url_signer_spec.rb index 94a20666229..711b9637b5d 100644 --- a/gems/aws-sdk-cloudfront/spec/url_signer_spec.rb +++ b/gems/aws-sdk-cloudfront/spec/url_signer_spec.rb @@ -103,6 +103,137 @@ module CloudFront expect(url).to eq(expected_url) 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 } + let(:active) { 1_767_200_000 } + + def rsa_signer(hash_algorithm = nil) + UrlSigner.new(key_pair_id: 'K1TESTKEY', private_key_path: "#{key_dir}/sep_rsa_key", hash_algorithm: hash_algorithm) + end + + def query_params(url) + URI.decode_www_form(URI(url).query).to_h + end + + def cloudfront_base64_decode(str) + Base64.decode64(str.tr('-_~', '+=/')) + end + + def canned_policy(resource) + %({"Statement":[{"Resource":"#{resource}","Condition":{"DateLessThan":{"AWS:EpochTime":#{expires}}}}]}) + end + + it 'canned-policy-url-basic' do + url = rsa_signer.signed_url('https://d111111abcdef8.cloudfront.net/image.jpg', expires: expires) + expect(url).to start_with('https://d111111abcdef8.cloudfront.net/image.jpg?') + expect(query_params(url)).to eq( + 'Expires' => '1767290400', + 'Signature' => 'iONoMLnhiCy9q1~WB9GkR2DiHz18I85i3o6kZ64REf-fCSOg-AyXEZiq7fJuS~DT-kbZXjVpgIQqI4sCTcBW9XpO6dyJ5sh8Igk3V~OVncS9acGVnI~ZhHBWiGhU8GmkEMAhn6R2RGO-wKGClrXdJGEUE26XoALdHUzHbmU6AGI_', + 'Key-Pair-Id' => 'K1TESTKEY' + ) + end + + it 'canned-policy-url-basic-sha256' do + url = rsa_signer('SHA256').signed_url('https://d111111abcdef8.cloudfront.net/image.jpg', expires: expires) + expect(query_params(url)).to eq( + 'Expires' => '1767290400', + 'Signature' => 'LiC~LakvNvZtR~AsTcirQ0CAsy-YIZmpHmI9uImK4xJmrhVdJULhWmRt3bXO7qqw2gJDECZN-xC~bKWEKcJ9Vgs1IgpRdMkY6XGDKZ1XHBdNbd~0v5UiRf4zXwVMRqoynkQPQcihkze7RkDBsOoYHh9jDdtO1iDm0QZ1Qp~cxro_', + 'Key-Pair-Id' => 'K1TESTKEY', + 'Hash-Algorithm' => 'SHA256' + ) + end + + it 'canned-policy-url-existing-params' do + url = rsa_signer.signed_url('https://d111111abcdef8.cloudfront.net/image.jpg?size=large', expires: expires) + expect(query_params(url)).to eq( + 'size' => 'large', + 'Expires' => '1767290400', + 'Signature' => 'kx5dxV5cSW50EuCMtYi9JI0D5H97if~zNqwKxZtS7FPc3NV5oi~zCb45ti8ve-YDYw45hzN4lceF6zhW~STVWa7r7fC9wi5XBG1T6ZT2b9R0wMWmGxsy9uVGXFn6JqcjQMsq09MnWrPezK0qFQ2X6pV1nGQqLQZ2sgO~FSlEoCo_', + 'Key-Pair-Id' => 'K1TESTKEY' + ) + end + + it 'canned-policy-url-existing-params-sha256' do + url = rsa_signer('SHA256').signed_url('https://d111111abcdef8.cloudfront.net/image.jpg?size=large', expires: expires) + expect(query_params(url)).to eq( + 'size' => 'large', + 'Expires' => '1767290400', + 'Signature' => 'g8vzqbTu5PxBzAXD3zowGFNHNlyaGZn7thMiCk27sTfvdy5CMAbECVz-aucgqrKYmqsz1xY7ZoxoMxOizzXWzfzTyJ6k38YwW6QMyJWT7B8pHStQrq0DGFSCnXqgRDXUDzkpIe8mHqyRWtgnFbdFQlbPweykfl7Eo4RJadveryQ_', + 'Key-Pair-Id' => 'K1TESTKEY', + 'Hash-Algorithm' => 'SHA256' + ) + end + + it 'custom-policy-url-with-active-date' do + policy = %({"Statement":[{"Resource":"https://d111111abcdef8.cloudfront.net/video.mp4","Condition":{"DateLessThan":{"AWS:EpochTime":#{expires}},"DateGreaterThan":{"AWS:EpochTime":#{active}}}}]}) + url = rsa_signer.signed_url('https://d111111abcdef8.cloudfront.net/video.mp4', policy: policy) + params = query_params(url) + expect(params.keys).to eq(%w[Policy Signature Key-Pair-Id]) + expect(params['Signature']).to eq('aF0WSOV6dkNn-D0HJ~-EwLG69-4QZZ9dv1PgAZEsER~WXkaNa1pBM2aTVXrskBWVHUU6hc1nD6ZKYgBS5Sb9JluFVT0MbQWR~DrtUGu8ugCsnfzUe6ov38nFPBwQICY~jnoQZEXxO2ZkQrluOO4~jWOpqAGrCw5tkdHAnb9gHYQ_') + end + + it 'custom-policy-url-with-active-date-sha256' do + policy = %({"Statement":[{"Resource":"https://d111111abcdef8.cloudfront.net/video.mp4","Condition":{"DateLessThan":{"AWS:EpochTime":#{expires}},"DateGreaterThan":{"AWS:EpochTime":#{active}}}}]}) + url = rsa_signer('SHA256').signed_url('https://d111111abcdef8.cloudfront.net/video.mp4', policy: policy) + params = query_params(url) + expect(params.keys).to eq(%w[Policy Signature Key-Pair-Id Hash-Algorithm]) + expect(params['Signature']).to eq('RKoXpBb3IFyONXBnGGPj6021LmYyaphTYom0DsDID20xlCaK8gvNpcbYw493LloPhtDdGTdyadyuR59ok8eSPpJReRGbHbw8FDjGSXE9JWsVayzI7t7ZPGfPtKgK~ij0AekXDNQNFNUdwuSE0aR5nQmuPhw7CfDHCMpTWmoKPgE_') + expect(params['Hash-Algorithm']).to eq('SHA256') + end + + it 'custom-policy-url-with-ip-range' do + policy = %({"Statement":[{"Resource":"https://d111111abcdef8.cloudfront.net/document.pdf","Condition":{"DateLessThan":{"AWS:EpochTime":#{expires}},"IpAddress":{"AWS:SourceIp":"192.168.0.0/24"}}}]}) + url = rsa_signer.signed_url('https://d111111abcdef8.cloudfront.net/document.pdf', policy: policy) + expect(query_params(url)['Signature']).to eq('fq~vePR~kmc5kWDveZOJ3j6PSYRsHSSanN1shBBpj2rQwvVQPU8e9MRwUDizOYHLnuAXQG89Xm7kXL03HIX0uzELcY2Q6Bw68BcGQHq-nyC2U6h0Uv277pg~FIwatR84y1FZiswIVmZwfPmpkvR0X~1skuooCPGiztAXREe-hsg_') + end + + it 'custom-policy-url-with-ip-range-sha256' do + policy = %({"Statement":[{"Resource":"https://d111111abcdef8.cloudfront.net/document.pdf","Condition":{"DateLessThan":{"AWS:EpochTime":#{expires}},"IpAddress":{"AWS:SourceIp":"192.168.0.0/24"}}}]}) + url = rsa_signer('SHA256').signed_url('https://d111111abcdef8.cloudfront.net/document.pdf', policy: policy) + expect(query_params(url)['Signature']).to eq('aKDzeIIfHviQRbCu9jxlFpOenTNQHMnt8HL2V2cqYP0vqc-KpRBuez2k4LZAzmfH0sIiQuDMZZc1D149XYRVAjEZoqr3vzGzWmrpzZJV-CSHs-Scau1cCNdVXqswj~QX8SzOZtGsRSKb4bomqOWxSIUu~Sybjf5b12GXBcYRCH4_') + end + + it 'custom-policy-url-with-wildcard' do + policy = canned_policy('https://d111111abcdef8.cloudfront.net/images/*') + url = rsa_signer.signed_url('https://d111111abcdef8.cloudfront.net/images/photo.jpg', policy: policy) + expect(url).to start_with('https://d111111abcdef8.cloudfront.net/images/photo.jpg?Policy=') + expect(query_params(url)['Signature']).to eq('nP~GtuBVUSEUDMXW1ZTGTx~BUQfjl7FsWBQBKwnTNhHlBAeYlzdbMGMphViPQbjawdYt1Z~iBHDr0ctooQyhEdsXwZPQKmRJQm3yIxmbvnuCzB2qdMDVGRKH8l3PnfAJErrgUVIzb6m3KUdtIDvSkLV~Mb0tyQJwZ6QE4BdlEmQ_') + end + + it 'custom-policy-url-with-wildcard-sha256' do + policy = canned_policy('https://d111111abcdef8.cloudfront.net/images/*') + url = rsa_signer('SHA256').signed_url('https://d111111abcdef8.cloudfront.net/images/photo.jpg', policy: policy) + expect(query_params(url)['Signature']).to eq('FoaoRDqA7H91xD8CL2zk2Ztm6WVx3kIunCdqvzwMCUdsduwVWEH8elbBZc2qqWy3ZixDBBEHegeJnZbgp~nHTsYtr~jv6R-8~BnmmvH3KNyjH7wgyKKtzKpteNXYacWgHYabkoreyHC4ek~JMjGo1o0gTiIeL8YCV3ueKK3Lqtk_') + end + + it 'ecdsa-canned-policy-url-sha256' do + signer = UrlSigner.new(key_pair_id: 'K2ECDSATEST', private_key_path: "#{key_dir}/sep_ecdsa_key", hash_algorithm: 'SHA256') + url = signer.signed_url('https://d111111abcdef8.cloudfront.net/video.mp4', expires: expires) + params = query_params(url) + expect(params.keys).to eq(%w[Expires Signature Key-Pair-Id Hash-Algorithm]) + expect(params['Hash-Algorithm']).to eq('SHA256') + + public_key = OpenSSL::PKey.read(File.read("#{key_dir}/sep_ecdsa_public_key")) + signature = cloudfront_base64_decode(params['Signature']) + policy = canned_policy('https://d111111abcdef8.cloudfront.net/video.mp4') + expect(public_key.verify(OpenSSL::Digest.new('SHA256'), signature, policy)).to be(true) + end + + it 'ecdsa-canned-policy-url-sha1-accept' do + signer = UrlSigner.new(key_pair_id: 'K2ECDSATEST', private_key_path: "#{key_dir}/sep_ecdsa_key") + url = signer.signed_url('https://d111111abcdef8.cloudfront.net/video.mp4', expires: expires) + params = query_params(url) + expect(params.keys).to eq(%w[Expires Signature Key-Pair-Id]) + + public_key = OpenSSL::PKey.read(File.read("#{key_dir}/sep_ecdsa_public_key")) + signature = cloudfront_base64_decode(params['Signature']) + policy = canned_policy('https://d111111abcdef8.cloudfront.net/video.mp4') + expect(public_key.verify(OpenSSL::Digest.new('SHA1'), signature, policy)).to be(true) + end + end end end end From b3a491aa4159bac5f926bca577bdfdb72d04a2f9 Mon Sep 17 00:00:00 2001 From: Daniel Pinheiro Date: Tue, 22 Sep 2026 23:54:33 +0000 Subject: [PATCH 2/2] refactor: Address PR feedback --- gems/aws-sdk-cloudfront/CHANGELOG.md | 2 +- .../lib/aws-sdk-cloudfront/signer.rb | 3 ++- gems/aws-sdk-cloudfront/spec/url_signer_spec.rb | 11 +++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/gems/aws-sdk-cloudfront/CHANGELOG.md b/gems/aws-sdk-cloudfront/CHANGELOG.md index 123577fc8f8..5016d0583df 100644 --- a/gems/aws-sdk-cloudfront/CHANGELOG.md +++ b/gems/aws-sdk-cloudfront/CHANGELOG.md @@ -1,7 +1,7 @@ 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 raise a clear error otherwise. +* 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) ------------------ diff --git a/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/signer.rb b/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/signer.rb index a70fcb8a9da..67e7d1d9f0a 100644 --- a/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/signer.rb +++ b/gems/aws-sdk-cloudfront/lib/aws-sdk-cloudfront/signer.rb @@ -8,6 +8,7 @@ module Aws module CloudFront module Signer + # @api private SUPPORTED_HASH_ALGORITHMS = %w[SHA1 SHA256].freeze # @option options [String] :key_pair_id @@ -82,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]) diff --git a/gems/aws-sdk-cloudfront/spec/url_signer_spec.rb b/gems/aws-sdk-cloudfront/spec/url_signer_spec.rb index 711b9637b5d..11d01d88b7e 100644 --- a/gems/aws-sdk-cloudfront/spec/url_signer_spec.rb +++ b/gems/aws-sdk-cloudfront/spec/url_signer_spec.rb @@ -73,6 +73,17 @@ module CloudFront expect(url).to eq(expected_url) end + it 'minifies the custom policy before signing' do + policy = { + 'Statement' => [ + 'Resource' => 'images/image.jpg', + 'Condition' => { 'DateLessThan' => { 'AWS:EpochTime' => expires } } + ] + } + url = 'http://abc.cloudfront.net/images/image.jpg' + expect(signer.signed_url(url, policy: JSON.pretty_generate(policy))).to eq(signer.signed_url(url, policy: policy.to_json)) + end + it 'can generate signed urls with canned policy' do url = signer.signed_url('https://abc.cloudfront.net/images/image.jpg?color=red', expires: expires) expected_url = 'https://abc.cloudfront.net/images/image.jpg?'\