forked from gophercloud/gophercloud
/
requests.go
377 lines (332 loc) · 13 KB
/
requests.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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
package ec2tokens
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/rand"
"net/url"
"sort"
"strings"
"time"
"github.com/bizflycloud/gophercloud"
"github.com/bizflycloud/gophercloud/openstack/identity/v3/tokens"
)
const (
// EC2CredentialsAwsRequestV4 is a constant, used to generate AWS
// Credential V4.
EC2CredentialsAwsRequestV4 = "aws4_request"
// EC2CredentialsHmacSha1V2 is a HMAC SHA1 signature method. Used to
// generate AWS Credential V2.
EC2CredentialsHmacSha1V2 = "HmacSHA1"
// EC2CredentialsHmacSha256V2 is a HMAC SHA256 signature method. Used
// to generate AWS Credential V2.
EC2CredentialsHmacSha256V2 = "HmacSHA256"
// EC2CredentialsAwsHmacV4 is an AWS signature V4 signing method.
// More details:
// https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
EC2CredentialsAwsHmacV4 = "AWS4-HMAC-SHA256"
// EC2CredentialsTimestampFormatV4 is an AWS signature V4 timestamp
// format.
EC2CredentialsTimestampFormatV4 = "20060102T150405Z"
// EC2CredentialsDateFormatV4 is an AWS signature V4 date format.
EC2CredentialsDateFormatV4 = "20060102"
)
// AuthOptions represents options for authenticating a user using EC2 credentials.
type AuthOptions struct {
// Access is the EC2 Credential Access ID.
Access string `json:"access" required:"true"`
// Secret is the EC2 Credential Secret, used to calculate signature.
// Not used, when a Signature is is.
Secret string `json:"-"`
// Host is a HTTP request Host header. Used to calculate an AWS
// signature V2. For signature V4 set the Host inside Headers map.
// Optional.
Host string `json:"host"`
// Path is a HTTP request path. Optional.
Path string `json:"path"`
// Verb is a HTTP request method. Optional.
Verb string `json:"verb"`
// Headers is a map of HTTP request headers. Optional.
Headers map[string]string `json:"headers"`
// Region is a region name to calculate an AWS signature V4. Optional.
Region string `json:"-"`
// Service is a service name to calculate an AWS signature V4. Optional.
Service string `json:"-"`
// Params is a map of GET method parameters. Optional.
Params map[string]string `json:"params"`
// AllowReauth allows Gophercloud to re-authenticate automatically
// if/when your token expires.
AllowReauth bool `json:"-"`
// Signature can be either a []byte (encoded to base64 automatically) or
// a string. You can set the singature explicitly, when you already know
// it. In this case default Params won't be automatically set. Optional.
Signature interface{} `json:"signature"`
// BodyHash is a HTTP request body sha256 hash. When nil and Signature
// is not set, a random hash is generated. Optional.
BodyHash *string `json:"body_hash"`
// Timestamp is a timestamp to calculate a V4 signature. Optional.
Timestamp *time.Time `json:"-"`
// Token is a []byte string (encoded to base64 automatically) which was
// signed by an EC2 secret key. Used by S3 tokens for validation only.
// Token must be set with a Signature. If a Signature is not provided,
// a Token will be generated automatically along with a Signature.
Token []byte `json:"token,omitempty"`
}
// EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string
// for an AWS signature V2.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L133
func EC2CredentialsBuildCanonicalQueryStringV2(params map[string]string) string {
var keys []string
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var pairs []string
for _, k := range keys {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, url.QueryEscape(params[k])))
}
return strings.Join(pairs, "&")
}
// EC2CredentialsBuildStringToSignV2 builds a string to sign an AWS signature
// V2.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L148
func EC2CredentialsBuildStringToSignV2(opts AuthOptions) []byte {
stringToSign := strings.Join([]string{
opts.Verb,
opts.Host,
opts.Path,
}, "\n")
return []byte(strings.Join([]string{
stringToSign,
EC2CredentialsBuildCanonicalQueryStringV2(opts.Params),
}, "\n"))
}
// EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string
// for an AWS signature V4.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L244
func EC2CredentialsBuildCanonicalQueryStringV4(verb string, params map[string]string) string {
if verb == "POST" {
return ""
}
return EC2CredentialsBuildCanonicalQueryStringV2(params)
}
// EC2CredentialsBuildCanonicalHeadersV4 builds a canonical string based on
// "headers" map and "signedHeaders" string parameters.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L216
func EC2CredentialsBuildCanonicalHeadersV4(headers map[string]string, signedHeaders string) string {
headersLower := make(map[string]string, len(headers))
for k, v := range headers {
headersLower[strings.ToLower(k)] = v
}
var headersList []string
for _, h := range strings.Split(signedHeaders, ";") {
if v, ok := headersLower[h]; ok {
headersList = append(headersList, h+":"+v)
}
}
return strings.Join(headersList, "\n") + "\n"
}
// EC2CredentialsBuildSignatureKeyV4 builds a HMAC 256 signature key based on
// input parameters.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L169
func EC2CredentialsBuildSignatureKeyV4(secret, region, service string, date time.Time) []byte {
kDate := sumHMAC256([]byte("AWS4"+secret), []byte(date.Format(EC2CredentialsDateFormatV4)))
kRegion := sumHMAC256(kDate, []byte(region))
kService := sumHMAC256(kRegion, []byte(service))
return sumHMAC256(kService, []byte(EC2CredentialsAwsRequestV4))
}
// EC2CredentialsBuildStringToSignV4 builds an AWS v4 signature string to sign
// based on input parameters.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L251
func EC2CredentialsBuildStringToSignV4(opts AuthOptions, signedHeaders string, bodyHash string, date time.Time) []byte {
scope := strings.Join([]string{
date.Format(EC2CredentialsDateFormatV4),
opts.Region,
opts.Service,
EC2CredentialsAwsRequestV4,
}, "/")
canonicalRequest := strings.Join([]string{
opts.Verb,
opts.Path,
EC2CredentialsBuildCanonicalQueryStringV4(opts.Verb, opts.Params),
EC2CredentialsBuildCanonicalHeadersV4(opts.Headers, signedHeaders),
signedHeaders,
bodyHash,
}, "\n")
hash := sha256.Sum256([]byte(canonicalRequest))
return []byte(strings.Join([]string{
EC2CredentialsAwsHmacV4,
date.Format(EC2CredentialsTimestampFormatV4),
scope,
hex.EncodeToString(hash[:]),
}, "\n"))
}
// EC2CredentialsBuildSignatureV4 builds an AWS v4 signature based on input
// parameters.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L285..L286
func EC2CredentialsBuildSignatureV4(key []byte, stringToSign []byte) string {
return hex.EncodeToString(sumHMAC256(key, stringToSign))
}
// EC2CredentialsBuildAuthorizationHeaderV4 builds an AWS v4 Authorization
// header based on auth parameters, date and signature
func EC2CredentialsBuildAuthorizationHeaderV4(opts AuthOptions, signedHeaders string, signature string, date time.Time) string {
return fmt.Sprintf("%s Credential=%s/%s/%s/%s/%s, SignedHeaders=%s, Signature=%s",
EC2CredentialsAwsHmacV4,
opts.Access,
date.Format(EC2CredentialsDateFormatV4),
opts.Region,
opts.Service,
EC2CredentialsAwsRequestV4,
signedHeaders,
signature)
}
// ToTokenV3ScopeMap is a dummy method to satisfy tokens.AuthOptionsBuilder
// interface.
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
return nil, nil
}
// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder
// interface in the v3 tokens package.
func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) {
return nil, nil
}
// CanReauth is a method method to satisfy tokens.AuthOptionsBuilder interface
func (opts *AuthOptions) CanReauth() bool {
return opts.AllowReauth
}
// ToTokenV3CreateMap formats an AuthOptions into a create request.
func (opts *AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) {
b, err := gophercloud.BuildRequestBody(opts, "credentials")
if err != nil {
return nil, err
}
if opts.Signature != nil {
return b, nil
}
// calculate signature, when it is not set
c, _ := b["credentials"].(map[string]interface{})
h := interfaceToMap(c, "headers")
p := interfaceToMap(c, "params")
// detect and process a signature v2
if v, ok := p["SignatureVersion"]; ok && v == "2" {
if _, ok := c["body_hash"]; ok {
delete(c, "body_hash")
}
if _, ok := c["headers"]; ok {
delete(c, "headers")
}
if v, ok := p["SignatureMethod"]; ok {
// params is a map of strings
strToSign := EC2CredentialsBuildStringToSignV2(*opts)
switch v {
case EC2CredentialsHmacSha1V2:
// keystone uses this method only when HmacSHA256 is not available on the server side
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L151..L156
c["signature"] = sumHMAC1([]byte(opts.Secret), strToSign)
return b, nil
case EC2CredentialsHmacSha256V2:
c["signature"] = sumHMAC256([]byte(opts.Secret), strToSign)
return b, nil
}
return nil, fmt.Errorf("unsupported signature method: %s", v)
}
return nil, fmt.Errorf("signature method must be provided")
} else if ok {
return nil, fmt.Errorf("unsupported signature version: %s", v)
}
// it is not a signature v2, but a signature v4
date := time.Now().UTC()
if opts.Timestamp != nil {
date = *opts.Timestamp
}
if v, _ := c["body_hash"]; v == nil {
// when body_hash is not set, generate a random one
c["body_hash"] = randomBodyHash()
}
signedHeaders, _ := h["X-Amz-SignedHeaders"]
stringToSign := EC2CredentialsBuildStringToSignV4(*opts, signedHeaders, c["body_hash"].(string), date)
key := EC2CredentialsBuildSignatureKeyV4(opts.Secret, opts.Region, opts.Service, date)
c["signature"] = EC2CredentialsBuildSignatureV4(key, stringToSign)
h["X-Amz-Date"] = date.Format(EC2CredentialsTimestampFormatV4)
h["Authorization"] = EC2CredentialsBuildAuthorizationHeaderV4(*opts, signedHeaders, c["signature"].(string), date)
// token is only used for S3 tokens validation and will be removed when using EC2 validation
c["token"] = stringToSign
return b, nil
}
// Create authenticates and either generates a new token from EC2 credentials
func Create(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) {
b, err := opts.ToTokenV3CreateMap(nil)
if err != nil {
r.Err = err
return
}
// delete "token" element, since it is used in s3tokens
deleteBodyElements(b, "token")
resp, err := c.Post(ec2tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{
MoreHeaders: map[string]string{"X-Auth-Token": ""},
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// ValidateS3Token authenticates an S3 request using EC2 credentials. Doesn't
// generate a new token ID, but returns a tokens.CreateResult.
func ValidateS3Token(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) {
b, err := opts.ToTokenV3CreateMap(nil)
if err != nil {
r.Err = err
return
}
// delete unused element, since it is used in ec2tokens only
deleteBodyElements(b, "body_hash", "headers", "host", "params", "path", "verb")
resp, err := c.Post(s3tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{
MoreHeaders: map[string]string{"X-Auth-Token": ""},
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// The following are small helper functions used to help build the signature.
// sumHMAC1 is a func to implement the HMAC SHA1 signature method.
func sumHMAC1(key []byte, data []byte) []byte {
hash := hmac.New(sha1.New, key)
hash.Write(data)
return hash.Sum(nil)
}
// sumHMAC256 is a func to implement the HMAC SHA256 signature method.
func sumHMAC256(key []byte, data []byte) []byte {
hash := hmac.New(sha256.New, key)
hash.Write(data)
return hash.Sum(nil)
}
// randomBodyHash is a func to generate a random sha256 hexdigest.
func randomBodyHash() string {
h := make([]byte, 64)
rand.Read(h)
return hex.EncodeToString(h)
}
// interfaceToMap is a func used to represent a "credentials" map element as a
// "map[string]string"
func interfaceToMap(c map[string]interface{}, key string) map[string]string {
// convert map[string]interface{} to map[string]string
m := make(map[string]string)
if v, _ := c[key].(map[string]interface{}); v != nil {
for k, v := range v {
m[k] = v.(string)
}
}
c[key] = m
return m
}
// deleteBodyElements deletes map body elements
func deleteBodyElements(b map[string]interface{}, elements ...string) {
if c, ok := b["credentials"].(map[string]interface{}); ok {
for _, k := range elements {
if _, ok := c[k]; ok {
delete(c, k)
}
}
}
}