Skip to content

Instantly share code, notes, and snippets.

@mrwacky42
Created April 21, 2017 20:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrwacky42/34ae049f49e62f1a529b26948a9d6d3d to your computer and use it in GitHub Desktop.
Save mrwacky42/34ae049f49e62f1a529b26948a9d6d3d to your computer and use it in GitHub Desktop.
#!/usr/bin/perl -w
# Copyright 2008 Amazon Technologies, Inc. Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy of the License at:
#
# http://aws.amazon.com/apache2.0
#
# This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
=head1 cfsign.pl
cfsign.pl - A tool to generate and verify AWS CloudFront signed URLs
=head1 SYNOPSIS
This script uses an existing RSA key pair to sign and verify AWS CloudFront signed URLs
View the script source for details as to which CPAN packages are required beforehand.
For help, try:
cfsign.pl --help
URL signing examples:
cfsign.pl --action encode --url http://images.my-website.com/gallery1.zip --policy sample_policy.json --private-key privkey.pem --key-pair-id mykey
cfsign.pl --action encode --url http://images.my-website.com/gallery1.zip --expires 1257439868 --private-key privkey.pem --key-pair-id mykey
Stream signing example:
cfsign.pl --action encode --stream videos/myvideo.mp4 --expires 1257439868 --private-key privkey.pem --key-pair-id mykey
URL decode example:
cfsign.pl --action decode --url "http//mydist.cloudfront.net/?Signature=AGO-PgxkYo99MkJFHvjfGXjG1QDEXeaDb4Qtzmy85wqyJjK7eKojQWa4BCRcow__&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovLypicmFkbS5qcGciLCJDb25kaXRpb24iOnsiSXBBZGRyZXNzIjp7IkFXUzpTb3VyY2VJcCI6IjEwLjUyLjE3LjkvMCJ9LCJEYXRlR3JlYXRlclRoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTI1MjUyMDgzMH19fV19Cg__&Key-Pair-Id=mykey"
To generate an RSA key pair, you can use openssl and the following commands:
# Generate a 1024bit key pair
openssl genrsa -out private-key.pem 1024
openssl rsa -in private-key.pem -pubout -out public-key.pem
=head1 OPTIONS
=over 8
=item B<--help>
Print a help message and exits.
=item B<--action> [action]
The action to execute. action can be one of:
encode - Generate a signed URL (using a canned policy or a user policy)
decode - Decode a signed URL
=item B<--url>
The URL to en/decode
=item B<--stream>
The stream to en/decode
=item B<--private-key>
The path to your private key.
=item B<--key-pair-id>
The AWS Portal assigned key pair identifier.
=item B<--policy>
The CloudFront policy document.
=item B<--expires>
The Unix epoch time when the URL is to expire. If both this option and
the --policy option are specified, --policy will be used. Otherwise, this
option alone will use a canned policy.
=back
=cut
use strict;
use warnings;
# you might need to use CPAN to get these modules.
# run perl -MCPAN -e "install <module>" to get them.
# The openssl command line will also need to be in your $PATH.
use File::Temp qw/tempfile/;
use Getopt::Long;
use IPC::Open2;
use MIME::Base64 qw(encode_base64 decode_base64);
use Pod::Usage;
use URI;
my $CANNED_POLICY
= '{"Statement":[{"Resource":"<RESOURCE>","Condition":{"DateLessThan":{"AWS:EpochTime":<EXPIRES>}}}]}';
my $POLICY_PARAM = "Policy";
my $EXPIRES_PARAM = "Expires";
my $SIGNATURE_PARAM = "Signature";
my $KEY_PAIR_ID_PARAM = "Key-Pair-Id";
my $verbose = 0;
my $policy_filename = "";
my $expires_epoch = 0;
my $action = "";
my $help = 0;
my $key_pair_id = "";
my $url = "";
my $stream = "";
my $private_key_filename = "";
my $result = GetOptions("action=s" => \$action,
"policy=s" => \$policy_filename,
"expires=i" => \$expires_epoch,
"private-key=s" => \$private_key_filename,
"key-pair-id=s" => \$key_pair_id,
"verbose" => \$verbose,
"help" => \$help,
"url=s" => \$url,
"stream=s" => \$stream,
);
if ($help or !$result) {
pod2usage(1);
exit;
}
if ($url eq "" and $stream eq "") {
print STDERR "Must include a stream or a URL to encode or decode with the --stream or --url option\n";
exit;
}
if ($url ne "" and $stream ne "") {
print STDERR "Only one of --url and --stream may be specified\n";
exit;
}
if ($url ne "" and !is_url_valid($url)) {
exit;
}
if ($stream ne "") {
exit unless is_stream_valid($stream);
# The signing mechanism is identical, so from here on just pretend we're
# dealing with a URL
$url = $stream;
}
if ($action eq "encode") {
# The encode action will generate a private content URL given a base URL,
# a policy file (or an expires timestamp) and a key pair id parameter
my $private_key;
my $public_key;
my $public_key_file;
my $policy;
if ($policy_filename eq "") {
if ($expires_epoch == 0) {
print STDERR "Must include policy filename with --policy argument or an expires" .
"time using --expires\n";
}
$policy = $CANNED_POLICY;
$policy =~ s/<EXPIRES>/$expires_epoch/g;
$policy =~ s/<RESOURCE>/$url/g;
} else {
if (! -e $policy_filename) {
print STDERR "Policy file $policy_filename does not exist\n";
exit;
}
$expires_epoch = 0; # ignore if set
$policy = read_file($policy_filename);
}
if ($private_key_filename eq "") {
print STDERR "You must specific the path to your private key file with --private-key\n";
exit;
}
if (! -e $private_key_filename) {
print STDERR "Private key file $private_key_filename does not exist\n";
exit;
}
if ($key_pair_id eq "") {
print STDERR "You must specify an AWS portal key pair id with --key-pair-id\n";
exit;
}
my $encoded_policy = url_safe_base64_encode($policy);
my $signature = rsa_sha1_sign($policy, $private_key_filename);
my $encoded_signature = url_safe_base64_encode($signature);
my $generated_url = create_url($url, $encoded_policy, $encoded_signature, $key_pair_id, $expires_epoch);
if ($stream ne "") {
print "Encoded stream (for use within a swf):\n" . $generated_url . "\n";
print "Encoded and escaped stream (for use on a webpage):\n" . escape_url_for_webpage($generated_url) . "\n";
} else {
print "Encoded URL:\n" . $generated_url . "\n";
}
} elsif ($action eq "decode") {
my $decoded = decode_url($url);
if (!$decoded) {
print STDERR "Improperly formed URL\n";
exit;
}
print_decoded_url($decoded);
} else {
# No action specified, print help. But only if this is run as a program (caller will be empty)
pod2usage(1) unless caller();
}
# Decode a private content URL into its component parts
sub decode_url {
my $url = shift;
if ($url =~ /(.*)\?(.*)/) {
my $base_url = $1;
my $params = $2;
my @unparsed_params = split(/&/, $params);
my %params = ();
foreach my $param (@unparsed_params) {
my ($key, $val) = split(/=/, $param);
$params{$key} = $val;
}
my $encoded_signature = "";
if (exists $params{$SIGNATURE_PARAM}) {
$encoded_signature = $params{"Signature"};
} else {
print STDERR "Missing Signature URL parameter\n";
return 0;
}
my $encoded_policy = "";
if (exists $params{$POLICY_PARAM}) {
$encoded_policy = $params{$POLICY_PARAM};
} else {
if (!exists $params{$EXPIRES_PARAM}) {
print STDERR "Either the Policy or Expires URL parameter needs to be specified\n";
return 0;
}
my $expires = $params{$EXPIRES_PARAM};
my $policy = $CANNED_POLICY;
$policy =~ s/<EXPIRES>/$expires/g;
my $url_without_cf_params = $url;
$url_without_cf_params =~ s/$SIGNATURE_PARAM=[^&]*&?//g;
$url_without_cf_params =~ s/$POLICY_PARAM=[^&]*&?//g;
$url_without_cf_params =~ s/$EXPIRES_PARAM=[^&]*&?//g;
$url_without_cf_params =~ s/$KEY_PAIR_ID_PARAM=[^&]*&?//g;
if ($url_without_cf_params =~ /(.*)\?$/) {
$url_without_cf_params = $1;
}
$policy =~ s/<RESOURCE>/$url_without_cf_params/g;
$encoded_policy = url_safe_base64_encode($policy);
}
my $key = "";
if (exists $params{$KEY_PAIR_ID_PARAM}) {
$key = $params{$KEY_PAIR_ID_PARAM};
} else {
print STDERR "Missing $KEY_PAIR_ID_PARAM parameter\n";
return 0;
}
my $policy = url_safe_base64_decode($encoded_policy);
my %ret = ();
$ret{"base_url"} = $base_url;
$ret{"policy"} = $policy;
$ret{"key"} = $key;
return \%ret;
} else {
return 0;
}
}
# Print a decoded URL out
sub print_decoded_url {
my $decoded = shift;
print "Base URL: \n" . $decoded->{"base_url"} . "\n";
print "Policy: \n" . $decoded->{"policy"} . "\n";
print "Key: \n" . $decoded->{"key"} . "\n";
}
# Encode a string with base 64 encoding and replace some invalid URL characters
sub url_safe_base64_encode {
my ($value) = @_;
my $result = encode_base64($value);
$result =~ tr|+=/|-_~|;
return $result;
}
# Decode a string with base 64 encoding
sub url_safe_base64_decode {
my ($value) = @_;
$value =~ tr|-_~|+=/|;
my $result = decode_base64($value);
return $result;
}
# Create a private content URL
sub create_url {
my ($path, $policy, $signature, $key_pair_id, $expires) = @_;
my $result;
my $separator = $path =~ /\?/ ? '&' : '?';
if ($expires) {
$result = "$path$separator$EXPIRES_PARAM=$expires&$SIGNATURE_PARAM=$signature&$KEY_PAIR_ID_PARAM=$key_pair_id";
} else {
$result = "$path$separator$POLICY_PARAM=$policy&$SIGNATURE_PARAM=$signature&$KEY_PAIR_ID_PARAM=$key_pair_id";
}
$result =~ s/\n//g;
return $result;
}
# Sign a document with given private key file.
# The first argument is the document to sign
# The second argument is the name of the private key file
sub rsa_sha1_sign {
my ($to_sign, $pvkFile) = @_;
return write_to_program("openssl sha1 -sign $pvkFile", $to_sign);
}
# Helper function to write data to a program
sub write_to_program {
my ($prog, $data) = @_;
my $pid = open2(*README, *WRITEME, $prog);
print WRITEME $data;
close WRITEME;
# slurp entire contents of output into scalar
my $output;
local $/ = undef;
$output = <README>;
close README;
waitpid($pid, 0);
return $output;
}
# Read a file into a string and return the string
sub read_file {
my ($file) = @_;
open(INFILE, "<$file") or die("Failed to open $file: $!");
my $str = join('', <INFILE>);
close INFILE;
return $str;
}
sub is_url_valid {
my ($url) = @_;
# HTTP distributions start with http[s]:// and are the correct thing to sign
if ($url =~ /^https?:\/\//) {
return 1;
} else {
print STDERR "CloudFront requires absolute URLs for HTTP distributions\n";
return 0;
}
}
sub is_stream_valid {
my ($stream) = @_;
if ($stream =~ /^rtmp:\/\// or $stream =~ /^\/?cfx\/st/) {
print STDERR "Streaming distributions require that only the stream name is signed.\n";
print STDERR "The stream name is everything after, but not including, cfx/st/\n";
return 0;
} else {
return 1;
}
}
# flash requires that the query parameters in the stream name are url
# encoded when passed in through javascript, etc. This sub handles the minimal
# required url encoding.
sub escape_url_for_webpage {
my ($url) = @_;
$url =~ s/\?/%3F/g;
$url =~ s/=/%3D/g;
$url =~ s/&/%26/g;
return $url;
}
1;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment