« Back to Index

[AWS CloudFront Signed-Cookie Access]

View original Gist on GitHub

Tags: #aws #cloudfront #cookie #planz

1. Automated Script.sh

#!/usr/bin/env bash
#
# chmod +x ./planz.sh
#
# For usage try:
# 	./planz.sh -h
# 	./planz.sh help

file=/tmp/planz_credentials

function generate_policy_template {
  # <<- is different from << in that you can indent the contents
  # but you can only indent using tabs (not spaces)
	cat > policy.json <<-EOF
	{
		"Statement":[
			{
				"Resource":"https://plan-z.buzzfeed.com/*",
				"Condition":{
					"DateLessThan":{
						"AWS:EpochTime": {{date}}
					}
				}
			}
		]
	}
	EOF
}

function init {
  local cookie_ttl_hours="+${1:-2}H" # default to two hours cookie ttl

  local CLOUDFRONT_PRIVATE_KEY_CONTAINER=./cloudfront-keypair.tgz.asc
  local CLOUDFRONT_PRIVATE_KEY_ID=APKAIT3GGGOSP434T2JQ
  local CLOUDFRONT_PRIVATE_KEY="pk-$CLOUDFRONT_PRIVATE_KEY_ID.pem"

  if [ ! -f "$CLOUDFRONT_PRIVATE_KEY_CONTAINER" ]; then
    echo "You need the AWS CloudFront keypair file: $CLOUDFRONT_PRIVATE_KEY_CONTAINER"
    exit 1
  fi

  rm "$file" 2>/dev/null
  rm policy.json 2>/dev/null
  rm policy.json.backup 2>/dev/null

  generate_policy_template

  sed -i ".backup" "s/{{date}}/$(date -u -v "$cookie_ttl_hours" +%s)/" policy.json
  
  # Linux version
  # sed -i ".backup" "s/{{date}}/$(date -d "today 1 hour" +%s)/" policy.json

  if [ ! -f "./$CLOUDFRONT_PRIVATE_KEY" ]; then
    gpg -d "$CLOUDFRONT_PRIVATE_KEY_CONTAINER" | tar -zxv -C ./ --strip-components=3
  fi

  # shellcheck disable=SC2155,SC2002
  export POLICY=$(cat policy.json | tr -d " \t\n\r" | base64 | tr '+=/' '-_~')

  # shellcheck disable=SC2155,SC2002
  export SIGNATURE=$(cat policy.json | tr -d " \t\n\r" | openssl sha1 -sign $CLOUDFRONT_PRIVATE_KEY | base64 | tr '+=/' '-_~')

  {
    echo "CLOUDFRONT_PRIVATE_KEY=$CLOUDFRONT_PRIVATE_KEY"
    echo "ID=$CLOUDFRONT_PRIVATE_KEY_ID"
    echo "POLICY=$POLICY"
    echo "SIGNATURE=$SIGNATURE"
  } >> "$file"

  cat "$file"
}

function web {
  cat "$file"
}

function req {
  local path=$1

  # shellcheck disable=SC2155
  local id=$(web | awk 'NR == 2 { print $0 }' | cut -d "=" -f 2)

  # shellcheck disable=SC2155
  local policy=$(web | awk 'NR == 3 { print $0 }' | cut -d "=" -f 2)

  # shellcheck disable=SC2155
  local signature=$(web | awk 'NR == 4 { print $0 }' | cut -d "=" -f 2)

  curl -v \
    -H "Cookie: CloudFront-Policy=$policy" \
    -H "Cookie: CloudFront-Signature=$signature" \
    -H "Cookie: CloudFront-Key-Pair-Id=$id" \
    "https://plan-z.buzzfeed.com$path"
}

if [ "$1" == "clear" ]; then
  rm "$file"
fi

if [ "$1" == "init" ]; then
  init "$2"
fi

if [ "$1" == "web" ]; then
  web
fi

if [ "$1" == "req" ]; then
  req "$2"
fi

if [[ "$1" =~ help|-h ]]; then
  printf "Usage:\n\t./planz.sh clear\n\t./planz.sh init [cookie_ttl_hours: 2]\n\t./planz.sh web\n\t./planz.sh req /robots.txt\n\t./planz.sh req /robots.txt 1>/dev/null\n"
fi
export CLOUDFRONT_KEY_PAIR_ID="APKAIT3GGGOSP434T2JQ"
export CLOUDFRONT_PRIVATE_KEY="pk-APKAIT3GGGOSP434T2JQ.pem"
export POLICY=$(cat policy.json | tr -d " \t\n\r" | base64 | tr '+=/' '-_~')
export SIGNATURE=$(cat policy.json | tr -d " \t\n\r" | openssl sha1 -sign $CLOUDFRONT_PRIVATE_KEY | base64 | tr '+=/' '-_~')

curl -v -o /dev/null \
  -H "Cookie: CloudFront-Policy=$POLICY" \
  -H "Cookie: CloudFront-Signature=$SIGNATURE" \
  -H "Cookie: CloudFront-Key-Pair-Id=$CLOUDFRONT_KEY_PAIR_ID" \
  https://plan-z.buzzfeed.com/robots.txt

policy.json

{
   "Statement":[
      {
         "Resource":"https://your.domain.com/*",
         "Condition":{
            "DateLessThan":{
               "AWS:EpochTime": 1504357200 // some future timestamp
            }
         }
      }
   ]
}

signed-cookies.rb

require "base64"
require "json"
require "openssl"

def cookie_data(resource, expiry)
  raw_policy = policy(resource, expiry)

  {
    "CloudFront-Policy" => safe_base64(raw_policy),
    "CloudFront-Signature" => sign(raw_policy),
    "CloudFront-Key-Pair-Id" => ENV["CLOUDFRONT_KEY_PAIR_ID"]
  }
end

def policy(url, expiry)
  {
     "Statement"=> [
        {
           "Resource" => url,
           "Condition"=>{
              "DateLessThan" =>{"AWS:EpochTime"=> expiry.utc.to_i}
           }
        }
     ]
  }.to_json.gsub(/\s+/,'')
end

def safe_base64(data)
  # equivalent to:
  # cat policy.json | tr -d " \t\n\r" | base64 | tr '+=/' '-_~'
  # remove all linebreaks/whitespace, base64 encode, strip invalid characters

  Base64.strict_encode64(data).tr("+=/", "-_~")
end

def sign(data)
  # equivalent to:
  # cat policy.json | openssl sha1 -sign ./pk-APKAIT3GGGOSP434T2JQ.pem | base64 | tr '+=/' '-_~'

  digest = OpenSSL::Digest::SHA1.new
  key    = OpenSSL::PKey::RSA.new File.read(ENV["CLOUDFRONT_PRIVATE_KEY"])
  result = key.sign digest, data
  safe_base64(result)
end

printf "curl -v -o /dev/null "
cookie_data("https://your.domain.com/*", Time.now + (60*60)).each do |k,v|
  # printf "Set-Cookie: Domain=domain.com; Path=/; Secure; HttpOnly; %s=%s\n\n", k, v
  printf "-H 'Cookie: %s=%s' ", k, v
end

# execute with:
# eval $(ruby signed-cookies.rb) https://your.domain.com/robots.txt