github.com/gophercloud/gophercloud@v1.11.0/openstack/identity/v3/extensions/ec2tokens/requests.go (about) 1 package ec2tokens 2 3 import ( 4 "crypto/hmac" 5 "crypto/sha1" 6 "crypto/sha256" 7 "encoding/hex" 8 "fmt" 9 "math/rand" 10 "net/url" 11 "sort" 12 "strings" 13 "time" 14 15 "github.com/gophercloud/gophercloud" 16 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" 17 ) 18 19 const ( 20 // EC2CredentialsAwsRequestV4 is a constant, used to generate AWS 21 // Credential V4. 22 EC2CredentialsAwsRequestV4 = "aws4_request" 23 // EC2CredentialsHmacSha1V2 is a HMAC SHA1 signature method. Used to 24 // generate AWS Credential V2. 25 EC2CredentialsHmacSha1V2 = "HmacSHA1" 26 // EC2CredentialsHmacSha256V2 is a HMAC SHA256 signature method. Used 27 // to generate AWS Credential V2. 28 EC2CredentialsHmacSha256V2 = "HmacSHA256" 29 // EC2CredentialsAwsHmacV4 is an AWS signature V4 signing method. 30 // More details: 31 // https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html 32 EC2CredentialsAwsHmacV4 = "AWS4-HMAC-SHA256" 33 // EC2CredentialsTimestampFormatV4 is an AWS signature V4 timestamp 34 // format. 35 EC2CredentialsTimestampFormatV4 = "20060102T150405Z" 36 // EC2CredentialsDateFormatV4 is an AWS signature V4 date format. 37 EC2CredentialsDateFormatV4 = "20060102" 38 ) 39 40 // AuthOptions represents options for authenticating a user using EC2 credentials. 41 type AuthOptions struct { 42 // Access is the EC2 Credential Access ID. 43 Access string `json:"access" required:"true"` 44 // Secret is the EC2 Credential Secret, used to calculate signature. 45 // Not used, when a Signature is is. 46 Secret string `json:"-"` 47 // Host is a HTTP request Host header. Used to calculate an AWS 48 // signature V2. For signature V4 set the Host inside Headers map. 49 // Optional. 50 Host string `json:"host"` 51 // Path is a HTTP request path. Optional. 52 Path string `json:"path"` 53 // Verb is a HTTP request method. Optional. 54 Verb string `json:"verb"` 55 // Headers is a map of HTTP request headers. Optional. 56 Headers map[string]string `json:"headers"` 57 // Region is a region name to calculate an AWS signature V4. Optional. 58 Region string `json:"-"` 59 // Service is a service name to calculate an AWS signature V4. Optional. 60 Service string `json:"-"` 61 // Params is a map of GET method parameters. Optional. 62 Params map[string]string `json:"params"` 63 // AllowReauth allows Gophercloud to re-authenticate automatically 64 // if/when your token expires. 65 AllowReauth bool `json:"-"` 66 // Signature can be either a []byte (encoded to base64 automatically) or 67 // a string. You can set the singature explicitly, when you already know 68 // it. In this case default Params won't be automatically set. Optional. 69 Signature interface{} `json:"signature"` 70 // BodyHash is a HTTP request body sha256 hash. When nil and Signature 71 // is not set, a random hash is generated. Optional. 72 BodyHash *string `json:"body_hash"` 73 // Timestamp is a timestamp to calculate a V4 signature. Optional. 74 Timestamp *time.Time `json:"-"` 75 // Token is a []byte string (encoded to base64 automatically) which was 76 // signed by an EC2 secret key. Used by S3 tokens for validation only. 77 // Token must be set with a Signature. If a Signature is not provided, 78 // a Token will be generated automatically along with a Signature. 79 Token []byte `json:"token,omitempty"` 80 } 81 82 // EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string 83 // for an AWS signature V2. 84 // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L133 85 func EC2CredentialsBuildCanonicalQueryStringV2(params map[string]string) string { 86 var keys []string 87 for k := range params { 88 keys = append(keys, k) 89 } 90 sort.Strings(keys) 91 92 var pairs []string 93 for _, k := range keys { 94 pairs = append(pairs, fmt.Sprintf("%s=%s", k, url.QueryEscape(params[k]))) 95 } 96 97 return strings.Join(pairs, "&") 98 } 99 100 // EC2CredentialsBuildStringToSignV2 builds a string to sign an AWS signature 101 // V2. 102 // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L148 103 func EC2CredentialsBuildStringToSignV2(opts AuthOptions) []byte { 104 stringToSign := strings.Join([]string{ 105 opts.Verb, 106 opts.Host, 107 opts.Path, 108 }, "\n") 109 110 return []byte(strings.Join([]string{ 111 stringToSign, 112 EC2CredentialsBuildCanonicalQueryStringV2(opts.Params), 113 }, "\n")) 114 } 115 116 // EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string 117 // for an AWS signature V4. 118 // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L244 119 func EC2CredentialsBuildCanonicalQueryStringV4(verb string, params map[string]string) string { 120 if verb == "POST" { 121 return "" 122 } 123 return EC2CredentialsBuildCanonicalQueryStringV2(params) 124 } 125 126 // EC2CredentialsBuildCanonicalHeadersV4 builds a canonical string based on 127 // "headers" map and "signedHeaders" string parameters. 128 // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L216 129 func EC2CredentialsBuildCanonicalHeadersV4(headers map[string]string, signedHeaders string) string { 130 headersLower := make(map[string]string, len(headers)) 131 for k, v := range headers { 132 headersLower[strings.ToLower(k)] = v 133 } 134 135 var headersList []string 136 for _, h := range strings.Split(signedHeaders, ";") { 137 if v, ok := headersLower[h]; ok { 138 headersList = append(headersList, h+":"+v) 139 } 140 } 141 142 return strings.Join(headersList, "\n") + "\n" 143 } 144 145 // EC2CredentialsBuildSignatureKeyV4 builds a HMAC 256 signature key based on 146 // input parameters. 147 // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L169 148 func EC2CredentialsBuildSignatureKeyV4(secret, region, service string, date time.Time) []byte { 149 kDate := sumHMAC256([]byte("AWS4"+secret), []byte(date.Format(EC2CredentialsDateFormatV4))) 150 kRegion := sumHMAC256(kDate, []byte(region)) 151 kService := sumHMAC256(kRegion, []byte(service)) 152 return sumHMAC256(kService, []byte(EC2CredentialsAwsRequestV4)) 153 } 154 155 // EC2CredentialsBuildStringToSignV4 builds an AWS v4 signature string to sign 156 // based on input parameters. 157 // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L251 158 func EC2CredentialsBuildStringToSignV4(opts AuthOptions, signedHeaders string, bodyHash string, date time.Time) []byte { 159 scope := strings.Join([]string{ 160 date.Format(EC2CredentialsDateFormatV4), 161 opts.Region, 162 opts.Service, 163 EC2CredentialsAwsRequestV4, 164 }, "/") 165 166 canonicalRequest := strings.Join([]string{ 167 opts.Verb, 168 opts.Path, 169 EC2CredentialsBuildCanonicalQueryStringV4(opts.Verb, opts.Params), 170 EC2CredentialsBuildCanonicalHeadersV4(opts.Headers, signedHeaders), 171 signedHeaders, 172 bodyHash, 173 }, "\n") 174 hash := sha256.Sum256([]byte(canonicalRequest)) 175 176 return []byte(strings.Join([]string{ 177 EC2CredentialsAwsHmacV4, 178 date.Format(EC2CredentialsTimestampFormatV4), 179 scope, 180 hex.EncodeToString(hash[:]), 181 }, "\n")) 182 } 183 184 // EC2CredentialsBuildSignatureV4 builds an AWS v4 signature based on input 185 // parameters. 186 // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L285..L286 187 func EC2CredentialsBuildSignatureV4(key []byte, stringToSign []byte) string { 188 return hex.EncodeToString(sumHMAC256(key, stringToSign)) 189 } 190 191 // EC2CredentialsBuildAuthorizationHeaderV4 builds an AWS v4 Authorization 192 // header based on auth parameters, date and signature 193 func EC2CredentialsBuildAuthorizationHeaderV4(opts AuthOptions, signedHeaders string, signature string, date time.Time) string { 194 return fmt.Sprintf("%s Credential=%s/%s/%s/%s/%s, SignedHeaders=%s, Signature=%s", 195 EC2CredentialsAwsHmacV4, 196 opts.Access, 197 date.Format(EC2CredentialsDateFormatV4), 198 opts.Region, 199 opts.Service, 200 EC2CredentialsAwsRequestV4, 201 signedHeaders, 202 signature) 203 } 204 205 // ToTokenV3ScopeMap is a dummy method to satisfy tokens.AuthOptionsBuilder 206 // interface. 207 func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { 208 return nil, nil 209 } 210 211 // ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder 212 // interface in the v3 tokens package. 213 func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) { 214 return nil, nil 215 } 216 217 // CanReauth is a method method to satisfy tokens.AuthOptionsBuilder interface 218 func (opts *AuthOptions) CanReauth() bool { 219 return opts.AllowReauth 220 } 221 222 // ToTokenV3CreateMap formats an AuthOptions into a create request. 223 func (opts *AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) { 224 b, err := gophercloud.BuildRequestBody(opts, "credentials") 225 if err != nil { 226 return nil, err 227 } 228 229 if opts.Signature != nil { 230 return b, nil 231 } 232 233 // calculate signature, when it is not set 234 c, _ := b["credentials"].(map[string]interface{}) 235 h := interfaceToMap(c, "headers") 236 p := interfaceToMap(c, "params") 237 238 // detect and process a signature v2 239 if v, ok := p["SignatureVersion"]; ok && v == "2" { 240 if _, ok := c["body_hash"]; ok { 241 delete(c, "body_hash") 242 } 243 if _, ok := c["headers"]; ok { 244 delete(c, "headers") 245 } 246 if v, ok := p["SignatureMethod"]; ok { 247 // params is a map of strings 248 strToSign := EC2CredentialsBuildStringToSignV2(*opts) 249 switch v { 250 case EC2CredentialsHmacSha1V2: 251 // keystone uses this method only when HmacSHA256 is not available on the server side 252 // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L151..L156 253 c["signature"] = sumHMAC1([]byte(opts.Secret), strToSign) 254 return b, nil 255 case EC2CredentialsHmacSha256V2: 256 c["signature"] = sumHMAC256([]byte(opts.Secret), strToSign) 257 return b, nil 258 } 259 return nil, fmt.Errorf("unsupported signature method: %s", v) 260 } 261 return nil, fmt.Errorf("signature method must be provided") 262 } else if ok { 263 return nil, fmt.Errorf("unsupported signature version: %s", v) 264 } 265 266 // it is not a signature v2, but a signature v4 267 date := time.Now().UTC() 268 if opts.Timestamp != nil { 269 date = *opts.Timestamp 270 } 271 if v, _ := c["body_hash"]; v == nil { 272 // when body_hash is not set, generate a random one 273 c["body_hash"] = randomBodyHash() 274 } 275 276 signedHeaders, _ := h["X-Amz-SignedHeaders"] 277 278 stringToSign := EC2CredentialsBuildStringToSignV4(*opts, signedHeaders, c["body_hash"].(string), date) 279 key := EC2CredentialsBuildSignatureKeyV4(opts.Secret, opts.Region, opts.Service, date) 280 c["signature"] = EC2CredentialsBuildSignatureV4(key, stringToSign) 281 h["X-Amz-Date"] = date.Format(EC2CredentialsTimestampFormatV4) 282 h["Authorization"] = EC2CredentialsBuildAuthorizationHeaderV4(*opts, signedHeaders, c["signature"].(string), date) 283 284 // token is only used for S3 tokens validation and will be removed when using EC2 validation 285 c["token"] = stringToSign 286 287 return b, nil 288 } 289 290 // Create authenticates and either generates a new token from EC2 credentials 291 func Create(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { 292 b, err := opts.ToTokenV3CreateMap(nil) 293 if err != nil { 294 r.Err = err 295 return 296 } 297 298 // delete "token" element, since it is used in s3tokens 299 deleteBodyElements(b, "token") 300 301 resp, err := c.Post(ec2tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{ 302 MoreHeaders: map[string]string{"X-Auth-Token": ""}, 303 OkCodes: []int{200}, 304 }) 305 _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) 306 return 307 } 308 309 // ValidateS3Token authenticates an S3 request using EC2 credentials. Doesn't 310 // generate a new token ID, but returns a tokens.CreateResult. 311 func ValidateS3Token(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { 312 b, err := opts.ToTokenV3CreateMap(nil) 313 if err != nil { 314 r.Err = err 315 return 316 } 317 318 // delete unused element, since it is used in ec2tokens only 319 deleteBodyElements(b, "body_hash", "headers", "host", "params", "path", "verb") 320 321 resp, err := c.Post(s3tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{ 322 MoreHeaders: map[string]string{"X-Auth-Token": ""}, 323 OkCodes: []int{200}, 324 }) 325 _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) 326 return 327 } 328 329 // The following are small helper functions used to help build the signature. 330 331 // sumHMAC1 is a func to implement the HMAC SHA1 signature method. 332 func sumHMAC1(key []byte, data []byte) []byte { 333 hash := hmac.New(sha1.New, key) 334 hash.Write(data) 335 return hash.Sum(nil) 336 } 337 338 // sumHMAC256 is a func to implement the HMAC SHA256 signature method. 339 func sumHMAC256(key []byte, data []byte) []byte { 340 hash := hmac.New(sha256.New, key) 341 hash.Write(data) 342 return hash.Sum(nil) 343 } 344 345 // randomBodyHash is a func to generate a random sha256 hexdigest. 346 func randomBodyHash() string { 347 h := make([]byte, 64) 348 rand.Read(h) 349 return hex.EncodeToString(h) 350 } 351 352 // interfaceToMap is a func used to represent a "credentials" map element as a 353 // "map[string]string" 354 func interfaceToMap(c map[string]interface{}, key string) map[string]string { 355 // convert map[string]interface{} to map[string]string 356 m := make(map[string]string) 357 if v, _ := c[key].(map[string]interface{}); v != nil { 358 for k, v := range v { 359 m[k] = v.(string) 360 } 361 } 362 363 c[key] = m 364 365 return m 366 } 367 368 // deleteBodyElements deletes map body elements 369 func deleteBodyElements(b map[string]interface{}, elements ...string) { 370 if c, ok := b["credentials"].(map[string]interface{}); ok { 371 for _, k := range elements { 372 if _, ok := c[k]; ok { 373 delete(c, k) 374 } 375 } 376 } 377 }