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 22, 2020
1 parent 83f764e commit a1edc2b
Show file tree
Hide file tree
Showing 5 changed files with 559 additions and 1 deletion.
13 changes: 12 additions & 1 deletion openstack/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/gophercloud/gophercloud"
tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
"github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens"
tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
"github.com/gophercloud/gophercloud/openstack/utils"
)
Expand Down Expand Up @@ -224,7 +225,13 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
return err
}
} else {
result := tokens3.Create(v3Client, opts)
var result tokens3.CreateResult
switch opts.(type) {
case *ec2tokens.AuthOptions:
result = ec2tokens.Create(v3Client, opts)
default:
result = tokens3.Create(v3Client, opts)
}

err = client.SetTokenAndAuthResult(result)
if err != nil {
Expand Down Expand Up @@ -255,6 +262,10 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
o := *ot
o.AllowReauth = false
tao = &o
case *ec2tokens.AuthOptions:
o := *ot
o.AllowReauth = false
tao = &o
default:
tao = opts
}
Expand Down
41 changes: 41 additions & 0 deletions openstack/identity/v3/extensions/ec2tokens/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
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.AuthOptions{
Access: "a7f1e798b7c2417cba4a02de97dc3cdc",
Secret: "18f4f6761ada4e3795fa5273c30349b9",
}
token, err := ec2tokens.Create(identityClient, authOptions).ExtractToken()
if err != nil {
panic(err)
}
Example to auth a client using EC2 access and secret keys
client, err := openstack.NewClient("http://localhost:5000/v3")
if err != nil {
panic(err)
}
var authOptions tokens.AuthOptionsBuilder
authOptions = &ec2tokens.AuthOptions{
Access: "a7f1e798b7c2417cba4a02de97dc3cdc",
Secret: "18f4f6761ada4e3795fa5273c30349b9",
AllowReauth: true,
}
err = openstack.AuthenticateV3(client, authOptions, gophercloud.EndpointOpts{})
if err != nil {
panic(err)
}
*/
package ec2tokens
279 changes: 279 additions & 0 deletions openstack/identity/v3/extensions/ec2tokens/requests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
package ec2tokens

import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"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"
)

// AuthOptions represents options for authenticating a user using EC2 credentials.
type AuthOptions struct {
// EC2 access ID.
Access string `json:"access" required:"true"`
// EC2 access secret, used to calculate signature only.
// Not used, when a Signature is provided.
Secret string `json:"-"`
// Optional parameters.
Host string `json:"host"`
Path string `json:"path"`
Verb string `json:"verb"`
Headers map[string]string `json:"headers"`
Region string `json:"-"`
Service string `json:"-"`
Params map[string]string `json:"params"`
// Allows Gophercloud to re-authenticate automatically if/when your
// token expires.
AllowReauth bool `json:"-"`
// Can be either a []byte (encoded to base64 automatically) or a
// string. You can set the singature explicitly, when you already know
// it.
Signature interface{} `json:"signature"`
// Optional random hash, when signature is nil.
BodyHash *string `json:"body_hash"`
// Optional timestamp to calculate a V4 signature.
Timestamp *time.Time `json:"-"`
}

func canonicalQsV2(params map[string]string) string {
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 stringToSignV2(opts AuthOptions, params map[string]string) string {
stringToSign := strings.Join([]string{
opts.Verb,
opts.Host,
opts.Path,
}, "\n")

return strings.Join([]string{
stringToSign,
canonicalQsV2(params),
}, "\n")
}

func canonicalQsV4(verb string, params map[string]string) string {
if verb == "POST" {
return ""
}
return canonicalQsV2(params)
}

func canonicalHeadersV4(headers map[string]string, signedHeaders string) string {
headersLower := make(map[string]string, len(headers))
for k, v := range headers {
headersLower[strings.ToLower(k)] = v
}

var headersList []string
for _, h := range strings.Split(signedHeaders, ";") {
if v, ok := headersLower[h]; ok {
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 signatureKeyV4(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 signatureV4(opts AuthOptions, params map[string]string, date time.Time, bodyHash string) string {
scope := strings.Join([]string{
date.Format(yyyymmdd),
opts.Region,
opts.Service,
aws4Request,
}, "/")

canonicalRequest := strings.Join([]string{
opts.Verb,
opts.Path,
canonicalQsV4(opts.Verb, params),
canonicalHeadersV4(opts.Headers, params["X-Amz-SignedHeaders"]),
params["X-Amz-SignedHeaders"],
bodyHash,
}, "\n")
hash := sha256.Sum256([]byte(canonicalRequest))

strToSign := strings.Join([]string{
v4Algo,
date.Format(iso8601utc),
scope,
hex.EncodeToString(hash[:]),
}, "\n")

key := signatureKeyV4(opts.Secret, date.Format(yyyymmdd), opts.Region, opts.Service)

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

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

func paramsToMap(c map[string]interface{}) map[string]string {
// convert map[string]interface{} to map[string]string
p := make(map[string]string)
if v, _ := c["params"].(map[string]interface{}); v != nil {
for k, v := range v {
p[k] = v.(string)
}
}

c["params"] = p

return p
}

// ToTokenV3CreateMap is a dummy method to satisfy tokens.AuthOptionsBuilder interface
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
return nil, nil
}

// CanReauth is a method method to satisfy tokens.AuthOptionsBuilder interface
func (opts *AuthOptions) CanReauth() bool {
return opts.AllowReauth
}

// ToAuthOptionsCreateMap formats an AuthOptions into a create request.
func (opts *AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) {
b, err := gophercloud.BuildRequestBody(opts, "credentials")
if err != nil {
return nil, err
}

if opts.Signature != nil {
return b, nil
}

// calculate signature, when it is not set
c, _ := b["credentials"].(map[string]interface{})
p := paramsToMap(c)

if v, ok := p["SignatureVersion"]; ok && v == "2" {
// signature v2
if _, ok := c["body_hash"]; ok {
delete(c, "body_hash")
}
if _, ok := c["headers"]; ok {
delete(c, "headers")
}
if v, ok := p["SignatureMethod"]; ok {
// params is a map of strings
strToSign := stringToSignV2(*opts, p)
switch v {
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
c["signature"] = sumHMAC1([]byte(opts.Secret), []byte(strToSign))
return b, nil
case v2AlgoSha256:
c["signature"] = sumHMAC256([]byte(opts.Secret), []byte(strToSign))
return b, nil
}
return nil, fmt.Errorf("unsupported signature method: %s", v)
}
return nil, fmt.Errorf("signature method must be provided")
} else if ok {
return nil, fmt.Errorf("unsupported signature version: %s", v)
}

// signature v4
date := time.Now().UTC()
if opts.Timestamp != nil {
date = *opts.Timestamp
}
if v, _ := c["body_hash"]; v == nil {
// when body_hash is not set, generate a random one
c["body_hash"] = randomBodyHash()
}
if v, _ := c["headers"]; v == nil {
// when headers is not set, make an empty map
h := make(map[string]string)
c["headers"] = &h
}
if _, ok := p["X-Amz-SignedHeaders"]; !ok {
// when X-Amz-SignedHeaders is not set, make an empty string
p["X-Amz-SignedHeaders"] = ""
}
p["X-Amz-Date"] = date.Format(iso8601utc)
p["X-Amz-Algorithm"] = v4Algo
p["X-Amz-Credential"] = strings.Join([]string{
opts.Access,
date.Format(yyyymmdd),
opts.Region,
opts.Service,
aws4Request,
}, "/")
c["signature"] = signatureV4(*opts, p, date, c["body_hash"].(string))

return b, nil
}

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

0 comments on commit a1edc2b

Please sign in to comment.