1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
|
// Package sign provides utilities to generate signed URLs for Amazon CloudFront.
//
// More information about signed URLs and their structure can be found at:
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-canned-policy.html
//
// To sign a URL create a [URLSigner] with your private key and credential pair
// key ID. Once you have a URLSigner instance you can call [URLSigner.Sign] or
// [URLSigner.SignWithPolicy] to sign the URLs.
//
// Example:
//
// // Load our key from a PEM block.
// privKey, err := sign.LoadPEMPrivKey(block)
// if err != nil {
// log.Fatalf("Failed to load private key, err: %s\n", err.Error())
// }
//
// // Create our signer. Keys loaded via the LoadPEMPrivKey* family of APIs
// // implement crypto.Signer and can be passed to this directly.
// signer := sign.NewURLSigner(keyID, privKey)
//
// // Sign URL to be valid for 1 hour from now.
// signedURL, err := signer.Sign(rawURL, time.Now().Add(1*time.Hour))
// if err != nil {
// log.Fatalf("Failed to sign url, err: %s\n", err.Error())
// }
package sign
import (
"crypto"
"fmt"
"net/url"
"strings"
"time"
)
// An URLSigner provides URL signing utilities to sign URLs for Amazon CloudFront
// resources. Using a private key and Credential Key Pair key ID the URLSigner
// only needs to be created once per Credential Key Pair key ID and private key.
//
// The signer is safe to use concurrently.
type URLSigner struct {
keyID string
signer crypto.Signer
}
// NewURLSigner constructs and returns a new URLSigner to be used to for signing
// Amazon CloudFront URL resources with.
func NewURLSigner(keyID string, signer crypto.Signer) *URLSigner {
return &URLSigner{
keyID: keyID,
signer: signer,
}
}
// Sign will sign a single URL to expire at the time of expires sign using the
// Amazon CloudFront default Canned Policy. The URL will be signed with the
// private key and Credential Key Pair Key ID previously provided to URLSigner.
//
// This is the default method of signing Amazon CloudFront URLs. If extra policy
// conditions are need other than URL expiry use SignWithPolicy instead.
//
// Example:
//
// // Sign URL to be valid for 1 hour from now.
// signer := sign.NewURLSigner(keyID, privKey)
// signedURL, err := signer.Sign(rawURL, time.Now().Add(1*time.Hour))
// if err != nil {
// log.Fatalf("Failed to sign url, err: %s\n", err.Error())
// }
func (s URLSigner) Sign(url string, expires time.Time) (string, error) {
scheme, cleanedURL, err := cleanURLScheme(url)
if err != nil {
return "", err
}
resource, err := CreateResource(scheme, url)
if err != nil {
return "", err
}
return signURL(scheme, cleanedURL, s.keyID, NewCannedPolicy(resource, expires), false, s.signer)
}
// SignWithPolicy will sign a URL with the Policy provided. The URL will be
// signed with the private key and Credential Key Pair Key ID previously provided to URLSigner.
//
// Use this signing method if you are looking to sign a URL with more than just
// the URL's expiry time, or reusing Policies between multiple URL signings.
// If only the expiry time is needed you can use Sign and provide just the
// URL's expiry time. A minimum of at least one policy statement is required for a signed URL.
//
// Note: It is not safe to use Polices between multiple signers concurrently
//
// Example:
//
// // Sign URL 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.
// policy := &sign.Policy{
// Statements: []sign.Statement{
// {
// Resource: rawURL,
// 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)},
// },
// },
// },
// }
//
// signer := sign.NewURLSigner(keyID, privKey)
// signedURL, err := signer.SignWithPolicy(rawURL, policy)
// if err != nil {
// log.Fatalf("Failed to sign url, err: %s\n", err.Error())
// }
func (s URLSigner) SignWithPolicy(url string, p *Policy) (string, error) {
scheme, cleanedURL, err := cleanURLScheme(url)
if err != nil {
return "", err
}
return signURL(scheme, cleanedURL, s.keyID, p, true, s.signer)
}
func signURL(scheme, url, keyID string, p *Policy, customPolicy bool, signer crypto.Signer) (string, error) {
// Validation URL elements
if err := validateURL(url); err != nil {
return "", err
}
b64Signature, b64Policy, err := p.Sign(signer)
if err != nil {
return "", err
}
// build and return signed URL
builtURL := buildSignedURL(url, keyID, p, customPolicy, b64Policy, b64Signature)
if scheme == "rtmp" {
return buildRTMPURL(builtURL)
}
return builtURL, nil
}
func buildSignedURL(baseURL, keyID string, p *Policy, customPolicy bool, b64Policy, b64Signature []byte) string {
pred := "?"
if strings.Contains(baseURL, "?") {
pred = "&"
}
signedURL := baseURL + pred
if customPolicy {
signedURL += "Policy=" + string(b64Policy)
} else {
signedURL += fmt.Sprintf("Expires=%d", p.Statements[0].Condition.DateLessThan.UTC().Unix())
}
signedURL += fmt.Sprintf("&Signature=%s&Key-Pair-Id=%s", string(b64Signature), keyID)
return signedURL
}
func buildRTMPURL(u string) (string, error) {
parsed, err := url.Parse(u)
if err != nil {
return "", fmt.Errorf("unable to parse rtmp signed URL, err: %s", err)
}
rtmpURL := strings.TrimLeft(parsed.Path, "/")
if parsed.RawQuery != "" {
rtmpURL = fmt.Sprintf("%s?%s", rtmpURL, parsed.RawQuery)
}
return rtmpURL, nil
}
func cleanURLScheme(u string) (scheme, cleanedURL string, err error) {
parts := strings.SplitN(u, "://", 2)
if len(parts) != 2 {
return "", "", fmt.Errorf("invalid URL, missing scheme and domain/path")
}
scheme = strings.Replace(parts[0], "*", "", 1)
cleanedURL = fmt.Sprintf("%s://%s", scheme, parts[1])
return strings.ToLower(scheme), cleanedURL, nil
}
var illegalQueryParms = []string{"Expires", "Policy", "Signature", "Key-Pair-Id"}
func validateURL(u string) error {
parsed, err := url.Parse(u)
if err != nil {
return fmt.Errorf("unable to parse URL, err: %s", err.Error())
}
if parsed.Scheme == "" {
return fmt.Errorf("URL missing valid scheme, %s", u)
}
q := parsed.Query()
for _, p := range illegalQueryParms {
if _, ok := q[p]; ok {
return fmt.Errorf("%s cannot be a query parameter for a signed URL", p)
}
}
return nil
}
|