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/gophercloud/gophercloud"
"github.com/gophercloud/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)
}
}
}
}
|