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