github.com/aavshr/aws-sdk-go@v1.41.3/service/cloudfront/sign/policy.go (about) 1 package sign 2 3 import ( 4 "bytes" 5 "crypto" 6 "crypto/rand" 7 "crypto/rsa" 8 "crypto/sha1" 9 "encoding/base64" 10 "encoding/json" 11 "fmt" 12 "io" 13 "net/url" 14 "strings" 15 "time" 16 "unicode" 17 ) 18 19 // An AWSEpochTime wraps a time value providing JSON serialization needed for 20 // AWS Policy epoch time fields. 21 type AWSEpochTime struct { 22 time.Time 23 } 24 25 // NewAWSEpochTime returns a new AWSEpochTime pointer wrapping the Go time provided. 26 func NewAWSEpochTime(t time.Time) *AWSEpochTime { 27 return &AWSEpochTime{t} 28 } 29 30 // MarshalJSON serializes the epoch time as AWS Profile epoch time. 31 func (t AWSEpochTime) MarshalJSON() ([]byte, error) { 32 return []byte(fmt.Sprintf(`{"AWS:EpochTime":%d}`, t.UTC().Unix())), nil 33 } 34 35 // UnmarshalJSON unserializes AWS Profile epoch time. 36 func (t *AWSEpochTime) UnmarshalJSON(data []byte) error { 37 var epochTime struct { 38 Sec int64 `json:"AWS:EpochTime"` 39 } 40 err := json.Unmarshal(data, &epochTime) 41 if err != nil { 42 return err 43 } 44 t.Time = time.Unix(epochTime.Sec, 0).UTC() 45 return nil 46 } 47 48 // An IPAddress wraps an IPAddress source IP providing JSON serialization information 49 type IPAddress struct { 50 SourceIP string `json:"AWS:SourceIp"` 51 } 52 53 // A Condition defines the restrictions for how a signed URL can be used. 54 type Condition struct { 55 // Optional IP address mask the signed URL must be requested from. 56 IPAddress *IPAddress `json:"IpAddress,omitempty"` 57 58 // Optional date that the signed URL cannot be used until. It is invalid 59 // to make requests with the signed URL prior to this date. 60 DateGreaterThan *AWSEpochTime `json:",omitempty"` 61 62 // Required date that the signed URL will expire. A DateLessThan is required 63 // sign cloud front URLs 64 DateLessThan *AWSEpochTime `json:",omitempty"` 65 } 66 67 // A Statement is a collection of conditions for resources 68 type Statement struct { 69 // The Web or RTMP resource the URL will be signed for 70 Resource string 71 72 // The set of conditions for this resource 73 Condition Condition 74 } 75 76 // A Policy defines the resources that a signed will be signed for. 77 // 78 // See the following page for more information on how policies are constructed. 79 // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement 80 type Policy struct { 81 // List of resource and condition statements. 82 // Signed URLs should only provide a single statement. 83 Statements []Statement `json:"Statement"` 84 } 85 86 // Override for testing to mock out usage of crypto/rand.Reader 87 var randReader = rand.Reader 88 89 // Sign will sign a policy using an RSA private key. It will return a base 64 90 // encoded signature and policy if no error is encountered. 91 // 92 // The signature and policy should be added to the signed URL following the 93 // guidelines in: 94 // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html 95 func (p *Policy) Sign(privKey *rsa.PrivateKey) (b64Signature, b64Policy []byte, err error) { 96 if err = p.Validate(); err != nil { 97 return nil, nil, err 98 } 99 100 // Build and escape the policy 101 b64Policy, jsonPolicy, err := encodePolicy(p) 102 if err != nil { 103 return nil, nil, err 104 } 105 awsEscapeEncoded(b64Policy) 106 107 // Build and escape the signature 108 b64Signature, err = signEncodedPolicy(randReader, jsonPolicy, privKey) 109 if err != nil { 110 return nil, nil, err 111 } 112 awsEscapeEncoded(b64Signature) 113 114 return b64Signature, b64Policy, nil 115 } 116 117 // Validate verifies that the policy is valid and usable, and returns an 118 // error if there is a problem. 119 func (p *Policy) Validate() error { 120 if len(p.Statements) == 0 { 121 return fmt.Errorf("at least one policy statement is required") 122 } 123 for i, s := range p.Statements { 124 if s.Resource == "" { 125 return fmt.Errorf("statement at index %d does not have a resource", i) 126 } 127 if !isASCII(s.Resource) { 128 return fmt.Errorf("unable to sign resource, [%s]. "+ 129 "Resources must only contain ascii characters. "+ 130 "Hostnames with unicode should be encoded as Punycode, (e.g. golang.org/x/net/idna), "+ 131 "and URL unicode path/query characters should be escaped.", s.Resource) 132 } 133 } 134 135 return nil 136 } 137 138 // CreateResource constructs, validates, and returns a resource URL string. An 139 // error will be returned if unable to create the resource string. 140 func CreateResource(scheme, u string) (string, error) { 141 scheme = strings.ToLower(scheme) 142 143 if scheme == "http" || scheme == "https" || scheme == "http*" || scheme == "*" { 144 return u, nil 145 } 146 147 if scheme == "rtmp" { 148 parsed, err := url.Parse(u) 149 if err != nil { 150 return "", fmt.Errorf("unable to parse rtmp URL, err: %s", err) 151 } 152 153 rtmpURL := strings.TrimLeft(parsed.Path, "/") 154 if parsed.RawQuery != "" { 155 rtmpURL = fmt.Sprintf("%s?%s", rtmpURL, parsed.RawQuery) 156 } 157 158 return rtmpURL, nil 159 } 160 161 return "", fmt.Errorf("invalid URL scheme must be http, https, or rtmp. Provided: %s", scheme) 162 } 163 164 // NewCannedPolicy returns a new Canned Policy constructed using the resource 165 // and expires time. This can be used to generate the basic model for a Policy 166 // that can be then augmented with additional conditions. 167 // 168 // See the following page for more information on how policies are constructed. 169 // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement 170 func NewCannedPolicy(resource string, expires time.Time) *Policy { 171 return &Policy{ 172 Statements: []Statement{ 173 { 174 Resource: resource, 175 Condition: Condition{ 176 DateLessThan: NewAWSEpochTime(expires), 177 }, 178 }, 179 }, 180 } 181 } 182 183 // encodePolicy encodes the Policy as JSON and also base 64 encodes it. 184 func encodePolicy(p *Policy) (b64Policy, jsonPolicy []byte, err error) { 185 jsonPolicy, err = encodePolicyJSON(p) 186 if err != nil { 187 return nil, nil, fmt.Errorf("failed to encode policy, %s", err.Error()) 188 } 189 // Remove leading and trailing white space, JSON encoding will note include 190 // whitespace within the encoding. 191 jsonPolicy = bytes.TrimSpace(jsonPolicy) 192 193 b64Policy = make([]byte, base64.StdEncoding.EncodedLen(len(jsonPolicy))) 194 base64.StdEncoding.Encode(b64Policy, jsonPolicy) 195 return b64Policy, jsonPolicy, nil 196 } 197 198 // signEncodedPolicy will sign and base 64 encode the JSON encoded policy. 199 func signEncodedPolicy(randReader io.Reader, jsonPolicy []byte, privKey *rsa.PrivateKey) ([]byte, error) { 200 hash := sha1.New() 201 if _, err := bytes.NewReader(jsonPolicy).WriteTo(hash); err != nil { 202 return nil, fmt.Errorf("failed to calculate signing hash, %s", err.Error()) 203 } 204 205 sig, err := rsa.SignPKCS1v15(randReader, privKey, crypto.SHA1, hash.Sum(nil)) 206 if err != nil { 207 return nil, fmt.Errorf("failed to sign policy, %s", err.Error()) 208 } 209 210 b64Sig := make([]byte, base64.StdEncoding.EncodedLen(len(sig))) 211 base64.StdEncoding.Encode(b64Sig, sig) 212 return b64Sig, nil 213 } 214 215 // special characters to be replaced with awsEscapeEncoded 216 var invalidEncodedChar = map[byte]byte{ 217 '+': '-', 218 '=': '_', 219 '/': '~', 220 } 221 222 // awsEscapeEncoded will replace base64 encoding's special characters to be URL safe. 223 func awsEscapeEncoded(b []byte) { 224 for i, v := range b { 225 if r, ok := invalidEncodedChar[v]; ok { 226 b[i] = r 227 } 228 } 229 } 230 231 func isASCII(u string) bool { 232 for _, c := range u { 233 if c > unicode.MaxASCII { 234 return false 235 } 236 } 237 return true 238 }