Skip to content

Commit

Permalink
Auth: introduce ec2 credentials auth support
Browse files Browse the repository at this point in the history
  • Loading branch information
kayrus committed Mar 21, 2020
1 parent 83f764e commit 32da65f
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 0 deletions.
22 changes: 22 additions & 0 deletions openstack/identity/v3/extensions/ec2tokens/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
Package tokens provides information and interaction with the EC2 token API
resource for the OpenStack Identity service.
For more information, see:
https://docs.openstack.org/api-ref/identity/v2-ext/
Example to Create a Token From an EC2 access and secret keys
var authOptions tokens.AuthOptionsBuilder
authOptions = &ec2tokens.Credentials{
Access: "a7f1e798b7c2417cba4a02de97dc3cdc",
Secret: "18f4f6761ada4e3795fa5273c30349b9",
}
token, err := ec2tokens.Create(identityClient, authOptions).ExtractToken()
if err != nil {
panic(err)
}
*/
package ec2tokens
263 changes: 263 additions & 0 deletions openstack/identity/v3/extensions/ec2tokens/requests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
package ec2tokens

import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"math/rand"
"net/url"
"sort"
"strings"
"time"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
)

const (
aws4Request = "aws4_request"
v2AlgoSha1 = "HmacSHA1"
v2AlgoSha256 = "HmacSHA256"
v4Algo = "AWS4-HMAC-SHA256"
iso8601utc = "20060102T150405Z"
rfc3339utc = "2006-01-02T15:04:05Z"
yyyymmdd = "20060102"
)

// Credentials represents options for authenticating a user using EC2 credentials.
type Credentials struct {
// EC2 access ID
Access string `json:"access" required:"true"`
// EC2 access secret, used to calculate signature only
Secret string `json:"-" required:"true"`
// Optional parameters
Host string `json:"host"`
Path string `json:"path"`
Verb string `json:"verb"`
Headers *map[string]string `json:"headers,omitempty"`
Region string `json:"-"`
Service string `json:"-"`
Params Params `json:"params"`
AllowReauth bool `json:"-"`
signature
}

type signature struct {
// can be either a []byte (encoded to base64 automatically) or a string
Signature interface{} `json:"signature"`
BodyHash *string `json:"body_hash,omitempty"`
timestamp time.Time `json:"-"`
}

type unexported struct {
Date string `json:"X-Amz-Date,omitempty"`
Algorithm string `json:"X-Amz-Algorithm,omitempty"`
Credential string `json:"X-Amz-Credential,omitempty"`
}

type Params struct {
unexported
SignedHeaders *string `json:"X-Amz-SignedHeaders,omitempty"`
// signature v2
SignatureMethod string `json:"SignatureMethod,omitempty"`
SignatureVersion string `json:"SignatureVersion,omitempty"`
}

func canonicalQs(opts *Credentials) string {
var params map[string]string
s, _ := json.Marshal(opts.Params)
json.Unmarshal(s, &params)
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, "&")
}

func paramsToQs(opts *Credentials) string {
strToSign := strings.Join([]string{
opts.Verb,
opts.Host,
opts.Path,
}, "\n")

return strings.Join([]string{
strToSign,
canonicalQs(opts),
}, "\n")
}

func paramsToQs4(opts *Credentials) string {
if opts.Verb == "POST" {
return ""
}
return canonicalQs(opts)
}

func canonicalHeaders(opts *Credentials) string {
headersLower := make(map[string]string, len(*opts.Headers))
for k, v := range *opts.Headers {
headersLower[strings.ToLower(k)] = v
}

var headersList []string
for _, h := range strings.Split(*opts.Params.SignedHeaders, ";") {
if v, ok := headersLower[h]; !ok {
continue
} else {
headersList = append(headersList, h+":"+v)
}
}

return strings.Join(headersList, "\n") + "\n"
}

func sumHMAC1(key []byte, data []byte) []byte {
hash := hmac.New(sha1.New, key)
hash.Write(data)
return hash.Sum(nil)
}

func sumHMAC256(key []byte, data []byte) []byte {
hash := hmac.New(sha256.New, key)
hash.Write(data)
return hash.Sum(nil)
}

func v4SignatureKey(secret, date, region, service string) []byte {
kDate := sumHMAC256([]byte("AWS4"+secret), []byte(date))
kRegion := sumHMAC256(kDate, []byte(region))
kService := sumHMAC256(kRegion, []byte(service))
return sumHMAC256(kService, []byte(aws4Request))
}

func v4Signature(opts *Credentials) string {
scope := strings.Join([]string{
opts.timestamp.Format(yyyymmdd),
opts.Region,
opts.Service,
aws4Request,
}, "/")

canonicalRequest := strings.Join([]string{
opts.Verb,
opts.Path,
paramsToQs4(opts),
canonicalHeaders(opts),
*opts.Params.SignedHeaders,
*opts.BodyHash,
}, "\n")
t := sha256.Sum256([]byte(canonicalRequest))

strToSign := strings.Join([]string{
v4Algo,
opts.Params.Date,
scope,
hex.EncodeToString(t[:]),
}, "\n")

key := v4SignatureKey(opts.Secret, opts.timestamp.Format(yyyymmdd), opts.Region, opts.Service)

return hex.EncodeToString(sumHMAC256(key, []byte(strToSign)))
}

func generateBodyHash() *string {
h := make([]byte, 64)
rand.Read(h)
bodyHash := hex.EncodeToString(h)
return &bodyHash
}

// ToTokenV3CreateMap builds a scope request body from AuthOptions.
func (opts *Credentials) ToTokenV3ScopeMap() (map[string]interface{}, error) {
return nil, nil
}

func (opts *Credentials) CanReauth() bool {
return opts.AllowReauth
}

func CalculateSignature(opts *Credentials) error {
switch opts.Params.SignatureVersion {
case "2": // signature v2
if opts.Headers != nil {
opts.Headers = nil
}
if opts.Params.SignedHeaders != nil {
opts.Params.SignedHeaders = nil
}
strToSign := paramsToQs(opts)
switch opts.Params.SignatureMethod {
case v2AlgoSha1:
// 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
opts.Signature = sumHMAC1([]byte(opts.Secret), []byte(strToSign))
case v2AlgoSha256:
opts.Signature = sumHMAC256([]byte(opts.Secret), []byte(strToSign))
default:
return fmt.Errorf("unsupported signature method: %s", opts.Params.SignatureMethod)
}
case "": // signature v4
opts.timestamp = time.Now().UTC()
opts.BodyHash = generateBodyHash()
opts.Params.Date = opts.timestamp.Format(iso8601utc)
opts.Params.Algorithm = v4Algo
opts.Params.Credential = strings.Join([]string{
opts.Access,
opts.timestamp.Format(yyyymmdd),
opts.Region,
opts.Service,
aws4Request,
}, "/")
if opts.Headers == nil {
h := make(map[string]string)
opts.Headers = &h
}
if opts.Params.SignedHeaders == nil {
opts.Params.SignedHeaders = new(string)
}
opts.Signature = v4Signature(opts)
default:
return fmt.Errorf("unsupported signature version: %s", opts.Params.SignatureVersion)
}
return nil
}

// ToCredentialsCreateMap formats a Credentials into a create request.
func (opts Credentials) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) {
err := CalculateSignature(&opts)
if err != nil {
return nil, err
}

return gophercloud.BuildRequestBody(opts, "credentials")
}

// 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
}

resp, err := c.Post(ec2tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{
MoreHeaders: map[string]string{"X-Auth-Token": ""},
OkCodes: []int{200},
})
r.Err = err
if resp != nil {
r.Header = resp.Header
}
return
}
7 changes: 7 additions & 0 deletions openstack/identity/v3/extensions/ec2tokens/urls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ec2tokens

import "github.com/gophercloud/gophercloud"

func ec2tokensURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("ec2tokens")
}

0 comments on commit 32da65f

Please sign in to comment.