-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
policy.go
238 lines (204 loc) · 7.07 KB
/
policy.go
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
package sign
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/url"
"strings"
"time"
"unicode"
)
// An AWSEpochTime wraps a time value providing JSON serialization needed for
// AWS Policy epoch time fields.
type AWSEpochTime struct {
time.Time
}
// NewAWSEpochTime returns a new AWSEpochTime pointer wrapping the Go time provided.
func NewAWSEpochTime(t time.Time) *AWSEpochTime {
return &AWSEpochTime{t}
}
// MarshalJSON serializes the epoch time as AWS Profile epoch time.
func (t AWSEpochTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`{"AWS:EpochTime":%d}`, t.UTC().Unix())), nil
}
// UnmarshalJSON unserializes AWS Profile epoch time.
func (t *AWSEpochTime) UnmarshalJSON(data []byte) error {
var epochTime struct {
Sec int64 `json:"AWS:EpochTime"`
}
err := json.Unmarshal(data, &epochTime)
if err != nil {
return err
}
t.Time = time.Unix(epochTime.Sec, 0).UTC()
return nil
}
// An IPAddress wraps an IPAddress source IP providing JSON serialization information
type IPAddress struct {
SourceIP string `json:"AWS:SourceIp"`
}
// A Condition defines the restrictions for how a signed URL can be used.
type Condition struct {
// Optional IP address mask the signed URL must be requested from.
IPAddress *IPAddress `json:"IpAddress,omitempty"`
// Optional date that the signed URL cannot be used until. It is invalid
// to make requests with the signed URL prior to this date.
DateGreaterThan *AWSEpochTime `json:",omitempty"`
// Required date that the signed URL will expire. A DateLessThan is required
// sign cloud front URLs
DateLessThan *AWSEpochTime `json:",omitempty"`
}
// A Statement is a collection of conditions for resources
type Statement struct {
// The Web or RTMP resource the URL will be signed for
Resource string
// The set of conditions for this resource
Condition Condition
}
// A Policy defines the resources that a signed will be signed for.
//
// See the following page for more information on how policies are constructed.
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement
type Policy struct {
// List of resource and condition statements.
// Signed URLs should only provide a single statement.
Statements []Statement `json:"Statement"`
}
// Override for testing to mock out usage of crypto/rand.Reader
var randReader = rand.Reader
// Sign will sign a policy using an RSA private key. It will return a base 64
// encoded signature and policy if no error is encountered.
//
// The signature and policy should be added to the signed URL following the
// guidelines in:
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html
func (p *Policy) Sign(privKey *rsa.PrivateKey) (b64Signature, b64Policy []byte, err error) {
if err = p.Validate(); err != nil {
return nil, nil, err
}
// Build and escape the policy
b64Policy, jsonPolicy, err := encodePolicy(p)
if err != nil {
return nil, nil, err
}
awsEscapeEncoded(b64Policy)
// Build and escape the signature
b64Signature, err = signEncodedPolicy(randReader, jsonPolicy, privKey)
if err != nil {
return nil, nil, err
}
awsEscapeEncoded(b64Signature)
return b64Signature, b64Policy, nil
}
// Validate verifies that the policy is valid and usable, and returns an
// error if there is a problem.
func (p *Policy) Validate() error {
if len(p.Statements) == 0 {
return fmt.Errorf("at least one policy statement is required")
}
for i, s := range p.Statements {
if s.Resource == "" {
return fmt.Errorf("statement at index %d does not have a resource", i)
}
if !isASCII(s.Resource) {
return fmt.Errorf("unable to sign resource, [%s]. "+
"Resources must only contain ascii characters. "+
"Hostnames with unicode should be encoded as Punycode, (e.g. golang.org/x/net/idna), "+
"and URL unicode path/query characters should be escaped.", s.Resource)
}
}
return nil
}
// CreateResource constructs, validates, and returns a resource URL string. An
// error will be returned if unable to create the resource string.
func CreateResource(scheme, u string) (string, error) {
scheme = strings.ToLower(scheme)
if scheme == "http" || scheme == "https" || scheme == "http*" || scheme == "*" {
return u, nil
}
if scheme == "rtmp" {
parsed, err := url.Parse(u)
if err != nil {
return "", fmt.Errorf("unable to parse rtmp URL, err: %s", err)
}
rtmpURL := strings.TrimLeft(parsed.Path, "/")
if parsed.RawQuery != "" {
rtmpURL = fmt.Sprintf("%s?%s", rtmpURL, parsed.RawQuery)
}
return rtmpURL, nil
}
return "", fmt.Errorf("invalid URL scheme must be http, https, or rtmp. Provided: %s", scheme)
}
// NewCannedPolicy returns a new Canned Policy constructed using the resource
// and expires time. This can be used to generate the basic model for a Policy
// that can be then augmented with additional conditions.
//
// See the following page for more information on how policies are constructed.
// http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-custom-policy.html#private-content-custom-policy-statement
func NewCannedPolicy(resource string, expires time.Time) *Policy {
return &Policy{
Statements: []Statement{
{
Resource: resource,
Condition: Condition{
DateLessThan: NewAWSEpochTime(expires),
},
},
},
}
}
// encodePolicy encodes the Policy as JSON and also base 64 encodes it.
func encodePolicy(p *Policy) (b64Policy, jsonPolicy []byte, err error) {
jsonPolicy, err = encodePolicyJSON(p)
if err != nil {
return nil, nil, fmt.Errorf("failed to encode policy, %s", err.Error())
}
// Remove leading and trailing white space, JSON encoding will note include
// whitespace within the encoding.
jsonPolicy = bytes.TrimSpace(jsonPolicy)
b64Policy = make([]byte, base64.StdEncoding.EncodedLen(len(jsonPolicy)))
base64.StdEncoding.Encode(b64Policy, jsonPolicy)
return b64Policy, jsonPolicy, nil
}
// signEncodedPolicy will sign and base 64 encode the JSON encoded policy.
func signEncodedPolicy(randReader io.Reader, jsonPolicy []byte, privKey *rsa.PrivateKey) ([]byte, error) {
hash := sha1.New()
if _, err := bytes.NewReader(jsonPolicy).WriteTo(hash); err != nil {
return nil, fmt.Errorf("failed to calculate signing hash, %s", err.Error())
}
sig, err := rsa.SignPKCS1v15(randReader, privKey, crypto.SHA1, hash.Sum(nil))
if err != nil {
return nil, fmt.Errorf("failed to sign policy, %s", err.Error())
}
b64Sig := make([]byte, base64.StdEncoding.EncodedLen(len(sig)))
base64.StdEncoding.Encode(b64Sig, sig)
return b64Sig, nil
}
// special characters to be replaced with awsEscapeEncoded
var invalidEncodedChar = map[byte]byte{
'+': '-',
'=': '_',
'/': '~',
}
// awsEscapeEncoded will replace base64 encoding's special characters to be URL safe.
func awsEscapeEncoded(b []byte) {
for i, v := range b {
if r, ok := invalidEncodedChar[v]; ok {
b[i] = r
}
}
}
func isASCII(u string) bool {
for _, c := range u {
if c > unicode.MaxASCII {
return false
}
}
return true
}