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  }