From d539d775978289249f509429a742a91535f0700c Mon Sep 17 00:00:00 2001 From: Quest Henkart Date: Wed, 30 Mar 2016 17:41:34 -0700 Subject: [PATCH 1/2] service/cloudfront/sign: Add CookieSigner for CloudFront resources Adds a CookieSigner for signing cookies for client user agents to use when requesting protected resources from CloudFront. --- service/cloudfront/sign/policy.go | 2 +- service/cloudfront/sign/sign_cookie.go | 241 ++++++++++++++++++ .../sign/sign_cookie_example_test.go | 163 ++++++++++++ service/cloudfront/sign/sign_cookie_test.go | 83 ++++++ 4 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 service/cloudfront/sign/sign_cookie.go create mode 100644 service/cloudfront/sign/sign_cookie_example_test.go create mode 100644 service/cloudfront/sign/sign_cookie_test.go diff --git a/service/cloudfront/sign/policy.go b/service/cloudfront/sign/policy.go index 8d392eb89fc..fc103c86e17 100644 --- a/service/cloudfront/sign/policy.go +++ b/service/cloudfront/sign/policy.go @@ -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 } diff --git a/service/cloudfront/sign/sign_cookie.go b/service/cloudfront/sign/sign_cookie.go new file mode 100644 index 00000000000..9b2deadf14b --- /dev/null +++ b/service/cloudfront/sign/sign_cookie.go @@ -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 +} diff --git a/service/cloudfront/sign/sign_cookie_example_test.go b/service/cloudfront/sign/sign_cookie_example_test.go new file mode 100644 index 00000000000..3683360e8b3 --- /dev/null +++ b/service/cloudfront/sign/sign_cookie_example_test.go @@ -0,0 +1,163 @@ +package sign + +import ( + "fmt" + "io" + "math/rand" + "net/http" + "time" +) + +func examplePEMReader() io.Reader { + reader, err := generatePEM(randReader, nil) + if err != nil { + panic(fmt.Sprintf("Unexpected pem generation err %v", err)) + } + + return reader +} + +func ExampleCookieSigner_Sign() { + origRandReader := randReader + randReader = newRandomReader(rand.New(rand.NewSource(1))) + defer func() { + randReader = origRandReader + }() + + // Load your private key so it can be used by the CookieSigner + // To load private key from file use `sign.LoadPEMPrivKeyFile`. + privKey, err := LoadPEMPrivKey(examplePEMReader()) + if err != nil { + fmt.Println("failed to load private key", err) + return + } + + cookieSigner := NewCookieSigner("keyID", privKey) + + // Use the signer to sign the URL + cookies, err := cookieSigner.Sign("http://example.com/somepath/*", testSignTime.Add(30*time.Minute)) + if err != nil { + fmt.Println("failed to sign cookies with policy,", err) + return + } + + printExampleCookies(cookies) + // Output: + // Cookies: + // CloudFront-Policy: eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL2V4YW1wbGUuY29tL3NvbWVwYXRoLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjEyNTc4OTU4MDB9fX1dfQ__, , , false + // CloudFront-Signature: o~jvj~CFkvGZB~yYED3elicKZag-CRijy8yD2E5yF1s7VNV7kNeQWC7MDtEcBQ8-eh7Xgjh0wMPQdAVdh09gBObd-hXDpKUyh8YKxogj~oloV~8KOvqE5xzWiKcqjdfJjmT5iEqIui~H1ExYjyKjgir79npmlyYkaJS5s62EQa8_, , , false + // CloudFront-Key-Pair-Id: keyID, , , false +} + +func ExampleCookieSigner_SignWithPolicy() { + origRandReader := randReader + randReader = newRandomReader(rand.New(rand.NewSource(1))) + defer func() { + randReader = origRandReader + }() + + // Sign cookie to be valid for 30 minutes from now, expires one hour + // from now, and restricted to the 192.0.2.0/24 IP address range. + // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html + p := &Policy{ + // Only a single policy statement can be used with CloudFront + // cookie signatures. + Statements: []Statement{{ + // Read the provided documentation on how to set this correctly, + // you'll probably want to use wildcards + Resource: "http://sub.cloudfront.com", + Condition: Condition{ + // Optional IP source address range + IPAddress: &IPAddress{SourceIP: "192.0.2.0/24"}, + // Optional date URL is not valid until + DateGreaterThan: &AWSEpochTime{testSignTime.Add(30 * time.Minute)}, + // Required date the URL will expire after + DateLessThan: &AWSEpochTime{testSignTime.Add(1 * time.Hour)}, + }, + }, + }, + } + + // Load your private key so it can be used by the CookieSigner + // To load private key from file use `sign.LoadPEMPrivKeyFile`. + privKey, err := LoadPEMPrivKey(examplePEMReader()) + if err != nil { + fmt.Println("failed to load private key", err) + return + } + + // Key ID that represents the key pair associated with the private key + keyID := "privateKeyID" + + // Set credentials to the CookieSigner. + cookieSigner := NewCookieSigner(keyID, privKey) + + // Avoid adding an Expire or MaxAge. See provided AWS Documentation for + // more info. + cookies, err := cookieSigner.SignWithPolicy(p) + if err != nil { + fmt.Println("failed to sign cookies with policy,", err) + return + } + + printExampleCookies(cookies) + // Output: + // Cookies: + // CloudFront-Policy: eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cDovL3N1Yi5jbG91ZGZyb250LmNvbSIsIkNvbmRpdGlvbiI6eyJJcEFkZHJlc3MiOnsiQVdTOlNvdXJjZUlwIjoiMTkyLjAuMi4wLzI0In0sIkRhdGVHcmVhdGVyVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxMjU3ODk1ODAwfSwiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjEyNTc4OTc2MDB9fX1dfQ__, , , false + // CloudFront-Signature: JaWdcbr98colrDAhOpkyxqCZev2IAxURu1RKKo1wS~sI5XdNXWYbZJs2FdpbJ475ZvmhZ1-r4ENUqBXAlRfPfOc21Hm4~24jRmPTO3512D4uuJHrPVxSfgeGuFeigfCGWAqyfYYH1DsFl5JQDpzetsNI3ZhGRkQb8V-oYFanddg_, , , false + // CloudFront-Key-Pair-Id: privateKeyID, , , false +} + +func ExampleCookieSigner_SignOptions() { + origRandReader := randReader + randReader = newRandomReader(rand.New(rand.NewSource(1))) + defer func() { + randReader = origRandReader + }() + + // Load your private key so it can be used by the CookieSigner + // To load private key from file use `sign.LoadPEMPrivKeyFile`. + privKey, err := LoadPEMPrivKey(examplePEMReader()) + if err != nil { + fmt.Println("failed to load private key", err) + return + } + + // Create the CookieSigner with options set. These options can be set + // directly with cookieSigner.Opts. These values can be overriden on + // individual Sign and SignWithProfile calls. + cookieSigner := NewCookieSigner("keyID", privKey, func(o *CookieOptions) { + //provide an optional struct fields to specify other options + o.Path = "/" + + // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html + o.Domain = ".cNameAssociatedWithMyDistribution.com" + + // Make sure your app/site can handle https payloads, otherwise + // set this to false. + o.Secure = true + }) + + // Use the signer to sign the URL + cookies, err := cookieSigner.Sign("http*://*", testSignTime.Add(30*time.Minute), func(o *CookieOptions) { + o.Path = "/mypath/" + }) + if err != nil { + fmt.Println("failed to sign cookies with policy,", err) + return + } + + printExampleCookies(cookies) + // Output: + // Cookies: + // CloudFront-Policy: eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cCo6Ly8qIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxMjU3ODk1ODAwfX19XX0_, /mypath/, .cNameAssociatedWithMyDistribution.com, true + // CloudFront-Signature: Yco06vgowwvSYgTSY9XbXpBcTlUlqpyyYXgRhus3nfnC74A7oQ~fMBH0we-rGxvph8ZyHnTxC5ubbPKSzo3EHUm2IcQeEo4p6WCgZZMzCuLlkpeMKhMAkCqX7rmUfkXhTslBHe~ylcmaZqo-hdnOiWrXk2U974ZQbbt5cOjwQG0_, /mypath/, .cNameAssociatedWithMyDistribution.com, true + // CloudFront-Key-Pair-Id: keyID, /mypath/, .cNameAssociatedWithMyDistribution.com, true +} + +func printExampleCookies(cookies []*http.Cookie) { + fmt.Println("Cookies:") + for _, c := range cookies { + fmt.Printf("%s: %s, %s, %s, %t\n", c.Name, c.Value, c.Path, c.Domain, c.Secure) + } +} diff --git a/service/cloudfront/sign/sign_cookie_test.go b/service/cloudfront/sign/sign_cookie_test.go new file mode 100644 index 00000000000..3bcd8672f91 --- /dev/null +++ b/service/cloudfront/sign/sign_cookie_test.go @@ -0,0 +1,83 @@ +package sign + +import ( + "crypto/rsa" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNewCookieSigner(t *testing.T) { + privKey, err := rsa.GenerateKey(randReader, 1024) + if err != nil { + t.Fatalf("Unexpected priv key error, %#v", err) + } + + signer := NewCookieSigner("keyID", privKey) + assert.Equal(t, "keyID", signer.keyID) + assert.Equal(t, privKey, signer.privKey) +} + +func TestSignCookie(t *testing.T) { + privKey, err := rsa.GenerateKey(randReader, 1024) + assert.NoError(t, err) + + signer := NewCookieSigner("keyID", privKey) + cookies, err := signer.Sign("http*://*", time.Now().Add(1*time.Hour)) + + assert.NoError(t, err) + assert.Equal(t, CookiePolicyName, cookies[0].Name) + assert.Equal(t, CookieSignatureName, cookies[1].Name) + assert.Equal(t, CookieKeyIDName, cookies[2].Name) +} + +func TestSignCookie_WithPolicy(t *testing.T) { + privKey, err := rsa.GenerateKey(randReader, 1024) + assert.NoError(t, err) + + p := &Policy{ + Statements: []Statement{ + { + Resource: "*", + Condition: Condition{ + DateLessThan: &AWSEpochTime{time.Now().Add(1 * time.Hour)}, + }, + }, + }, + } + + signer := NewCookieSigner("keyID", privKey) + cookies, err := signer.SignWithPolicy(p) + + assert.NoError(t, err) + assert.Equal(t, CookiePolicyName, cookies[0].Name) + assert.Equal(t, CookieSignatureName, cookies[1].Name) + assert.Equal(t, CookieKeyIDName, cookies[2].Name) +} + +func TestSignCookie_WithCookieOptions(t *testing.T) { + privKey, err := rsa.GenerateKey(randReader, 1024) + assert.NoError(t, err) + + expires := time.Now().Add(1 * time.Hour) + + signer := NewCookieSigner("keyID", privKey) + cookies, err := signer.Sign("https://example.com/*", expires, func(o *CookieOptions) { + o.Path = "/" + o.Domain = ".example.com" + o.Secure = true + + }) + + assert.NoError(t, err) + assert.Equal(t, CookiePolicyName, cookies[0].Name) + assert.Equal(t, CookieSignatureName, cookies[1].Name) + assert.Equal(t, CookieKeyIDName, cookies[2].Name) + + for _, c := range cookies { + assert.Equal(t, "/", c.Path) + assert.Equal(t, ".example.com", c.Domain) + assert.True(t, c.Secure) + } +} From d73620a77da8165c0d08a33e9005b492dabefb95 Mon Sep 17 00:00:00 2001 From: Jason Del Ponte Date: Mon, 11 Apr 2016 15:08:40 -0700 Subject: [PATCH 2/2] Add example for CloudFront Cookie signing --- example/signCloudFrontCookies/signCookies.go | 74 ++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 example/signCloudFrontCookies/signCookies.go diff --git a/example/signCloudFrontCookies/signCookies.go b/example/signCloudFrontCookies/signCookies.go new file mode 100644 index 00000000000..8f6a68b3583 --- /dev/null +++ b/example/signCloudFrontCookies/signCookies.go @@ -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 -id -r -g +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)) +}