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  }