Skip to content

Commit

Permalink
Merge pull request #626 from aws/pr614SignCookies
Browse files Browse the repository at this point in the history
Add CookieSigner for CloudFront resources
  • Loading branch information
jasdel committed Apr 11, 2016
2 parents dc2fcd1 + d73620a commit 09a34f2
Show file tree
Hide file tree
Showing 5 changed files with 562 additions and 1 deletion.
74 changes: 74 additions & 0 deletions example/signCloudFrontCookies/signCookies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"flag"
"fmt"
"io/ioutil"
"net/http"
"time"

"github.com/aws/aws-sdk-go/service/cloudfront/sign"
)

// Usage example:
// go run main.go -file <privkey file> -id <keyId> -r <resource pattern> -g <object to get>
func main() {
var keyFile string // Private key PEM file
var keyID string // Key pair ID of CloudFront key pair
var resource string // CloudFront resource pattern
var object string // S3 object frontented by CloudFront

flag.StringVar(&keyFile, "file", "", "private key file")
flag.StringVar(&keyID, "id", "", "key pair id")
flag.StringVar(&resource, "r", "", "resource to request")
flag.StringVar(&object, "g", "", "object to get")
flag.Parse()

// Load the PEM file into memory so it can be used by the signer
privKey, err := sign.LoadPEMPrivKeyFile(keyFile)
if err != nil {
fmt.Println("failed to load key,", err)
return
}

// Create the new CookieSigner to get signed cookies for CloudFront
// resource requests
signer := sign.NewCookieSigner(keyID, privKey)

// Get the cookies for the resource. These will be used
// to make the requests with
cookies, err := signer.Sign(resource, time.Now().Add(1*time.Hour))
if err != nil {
fmt.Println("failed to sign cookies", err)
return
}

// Use the cookies in a http.Client to show how they allow the client
// to request resources from CloudFront.
req, err := http.NewRequest("GET", object, nil)
fmt.Println("Cookies:")
for _, c := range cookies {
fmt.Printf("%s=%s;\n", c.Name, c.Value)
req.AddCookie(c)
}

// Send and handle the response. For a successful response the object's
// content will be written to stdout. The same process could be applied
// to a http service written cookies to the response but useing
// http.SetCookie(w, c,) on the ResponseWriter.
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("failed to send request", err)
return
}
defer resp.Body.Close()

b, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("failed to read requested body", err)
return
}

fmt.Println("Response:", resp.Status)
fmt.Println(string(b))
}
2 changes: 1 addition & 1 deletion service/cloudfront/sign/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (p *Policy) Validate() error {
func CreateResource(scheme, u string) (string, error) {
scheme = strings.ToLower(scheme)

if scheme == "http" || scheme == "https" {
if scheme == "http" || scheme == "https" || scheme == "http*" || scheme == "*" {
return u, nil
}

Expand Down
241 changes: 241 additions & 0 deletions service/cloudfront/sign/sign_cookie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package sign

import (
"crypto/rsa"
"fmt"
"net/http"
"strings"
"time"
)

const (
// CookiePolicyName name of the policy cookie
CookiePolicyName = "CloudFront-Policy"
// CookieSignatureName name of the signature cookie
CookieSignatureName = "CloudFront-Signature"
// CookieKeyIDName name of the signing Key ID cookie
CookieKeyIDName = "CloudFront-Key-Pair-Id"
)

// A CookieOptions optional additonal options that can be applied to the signed
// cookies.
type CookieOptions struct {
Path string
Domain string
Secure bool
}

// apply will integration the options provided into the base cookie options
// a new copy will be returned. The base CookieOption will not be modified.
func (o CookieOptions) apply(opts ...func(*CookieOptions)) CookieOptions {
if len(opts) == 0 {
return o
}

for _, opt := range opts {
opt(&o)
}

return o
}

// A CookieSigner provides signing utilities to sign Cookies for Amazon CloudFront
// resources. Using a private key and Credential Key Pair key ID the CookieSigner
// only needs to be created once per Credential Key Pair key ID and private key.
//
// More information about signed Cookies and their structure can be found at:
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html
//
// To sign a Cookie, create a CookieSigner with your private key and credential
// pair key ID. Once you have a CookieSigner instance you can call Sign or
// SignWithPolicy to sign the URLs.
//
// The signer is safe to use concurrently, but the optional cookies options
// are not safe to modify concurrently.
type CookieSigner struct {
keyID string
privKey *rsa.PrivateKey

Opts CookieOptions
}

// NewCookieSigner constructs and returns a new CookieSigner to be used to for
// signing Amazon CloudFront URL resources with.
func NewCookieSigner(keyID string, privKey *rsa.PrivateKey, opts ...func(*CookieOptions)) *CookieSigner {
signer := &CookieSigner{
keyID: keyID,
privKey: privKey,
Opts: CookieOptions{}.apply(opts...),
}

return signer
}

// Sign returns the cookies needed to allow user agents to make arbetrary
// requests to cloudfront for the resource(s) defined by the policy.
//
// Sign will create a CloudFront policy with only a resource and condition of
// DateLessThan equal to the expires time provided.
//
// The returned slice cookies should all be added to the Client's cookies or
// server's response.
//
// Example:
// s := NewCookieSigner(keyID, privKey)
//
// // Get Signed cookies for a resource that will expire in 1 hour
// cookies, err := s.Sign("*", time.Now().Add(1 * time.Hour))
// if err != nil {
// fmt.Println("failed to create signed cookies", err)
// return
// }
//
// // Or get Signed cookies for a resource that will expire in 1 hour
// // and set path and domain of cookies
// cookies, err := s.Sign("*", time.Now().Add(1 * time.Hour), func(o *sign.CookieOptions) {
// o.Path = "/"
// o.Domain = ".example.com"
// })
// if err != nil {
// fmt.Println("failed to create signed cookies", err)
// return
// }
//
// // Server Response via http.ResponseWriter
// for _, c := range cookies {
// http.SetCookie(w, c)
// }
//
// // Client request via the cookie jar
// if client.CookieJar != nil {
// for _, c := range cookies {
// client.Cookie(w, c)
// }
// }
func (s CookieSigner) Sign(u string, expires time.Time, opts ...func(*CookieOptions)) ([]*http.Cookie, error) {
scheme, err := cookieURLScheme(u)
if err != nil {
return nil, err
}

resource, err := CreateResource(scheme, u)
if err != nil {
return nil, err
}

p := NewCannedPolicy(resource, expires)
return createCookies(p, s.keyID, s.privKey, s.Opts.apply(opts...))
}

// Returns and validates the URL's scheme.
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html#private-content-custom-policy-statement-cookies
func cookieURLScheme(u string) (string, error) {
parts := strings.SplitN(u, "://", 2)
if len(parts) != 2 {
return "", fmt.Errorf("invalid cookie URL, missing scheme")
}

scheme := strings.ToLower(parts[0])
if scheme != "http" && scheme != "https" && scheme != "http*" {
return "", fmt.Errorf("invalid cookie URL scheme. Expect http, https, or http*. Go, %s", scheme)
}

return scheme, nil
}

// SignWithPolicy returns the cookies needed to allow user agents to make
// arbetrairy requets to cloudfront for the resource(s) defined by the policy.
//
// The returned slice cookies should all be added to the Client's cookies or
// server's response.
//
// Example:
// s := NewCookieSigner(keyID, privKey)
//
// policy := &sign.Policy{
// Statements: []sign.Statement{
// {
// // Read the provided documentation on how to set this
// // correctly, you'll probably want to use wildcards.
// Resource: RawCloudFrontURL,
// Condition: sign.Condition{
// // Optional IP source address range
// IPAddress: &sign.IPAddress{SourceIP: "192.0.2.0/24"},
// // Optional date URL is not valid until
// DateGreaterThan: &sign.AWSEpochTime{time.Now().Add(30 * time.Minute)},
// // Required date the URL will expire after
// DateLessThan: &sign.AWSEpochTime{time.Now().Add(1 * time.Hour)},
// },
// },
// },
// }
//
// // Get Signed cookies for a resource that will expire in 1 hour
// cookies, err := s.SignWithPolicy(policy)
// if err != nil {
// fmt.Println("failed to create signed cookies", err)
// return
// }
//
// // Or get Signed cookies for a resource that will expire in 1 hour
// // and set path and domain of cookies
// cookies, err := s.Sign(policy, func(o *sign.CookieOptions) {
// o.Path = "/"
// o.Domain = ".example.com"
// })
// if err != nil {
// fmt.Println("failed to create signed cookies", err)
// return
// }
//
// // Server Response via http.ResponseWriter
// for _, c := range cookies {
// http.SetCookie(w, c)
// }
//
// // Client request via the cookie jar
// if client.CookieJar != nil {
// for _, c := range cookies {
// client.Cookie(w, c)
// }
// }
func (s CookieSigner) SignWithPolicy(p *Policy, opts ...func(*CookieOptions)) ([]*http.Cookie, error) {
return createCookies(p, s.keyID, s.privKey, s.Opts.apply(opts...))
}

// Prepares the cookies to be attached to the header. An (optional) options
// struct is provided in case people don't want to manually edit their cookies.
func createCookies(p *Policy, keyID string, privKey *rsa.PrivateKey, opt CookieOptions) ([]*http.Cookie, error) {
b64Sig, b64Policy, err := p.Sign(privKey)
if err != nil {
return nil, err
}

// Creates proper cookies
cPolicy := &http.Cookie{
Name: CookiePolicyName,
Value: string(b64Policy),
HttpOnly: true,
}
cSignature := &http.Cookie{
Name: CookieSignatureName,
Value: string(b64Sig),
HttpOnly: true,
}
cKey := &http.Cookie{
Name: CookieKeyIDName,
Value: keyID,
HttpOnly: true,
}

cookies := []*http.Cookie{cPolicy, cSignature, cKey}

// Applie the cookie options
for _, c := range cookies {
c.Path = opt.Path
c.Domain = opt.Domain
c.Secure = opt.Secure
}

return cookies, nil
}
Loading

0 comments on commit 09a34f2

Please sign in to comment.