Skip to content

Commit 32da65f

Browse files
committed
Auth: introduce ec2 credentials auth support
1 parent 83f764e commit 32da65f

File tree

3 files changed

+292
-0
lines changed

3 files changed

+292
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
Package tokens provides information and interaction with the EC2 token API
3+
resource for the OpenStack Identity service.
4+
5+
For more information, see:
6+
https://docs.openstack.org/api-ref/identity/v2-ext/
7+
8+
Example to Create a Token From an EC2 access and secret keys
9+
10+
var authOptions tokens.AuthOptionsBuilder
11+
authOptions = &ec2tokens.Credentials{
12+
Access: "a7f1e798b7c2417cba4a02de97dc3cdc",
13+
Secret: "18f4f6761ada4e3795fa5273c30349b9",
14+
}
15+
16+
token, err := ec2tokens.Create(identityClient, authOptions).ExtractToken()
17+
if err != nil {
18+
panic(err)
19+
}
20+
21+
*/
22+
package ec2tokens
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package ec2tokens
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha1"
6+
"crypto/sha256"
7+
"encoding/hex"
8+
"encoding/json"
9+
"fmt"
10+
"math/rand"
11+
"net/url"
12+
"sort"
13+
"strings"
14+
"time"
15+
16+
"github.com/gophercloud/gophercloud"
17+
"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
18+
)
19+
20+
const (
21+
aws4Request = "aws4_request"
22+
v2AlgoSha1 = "HmacSHA1"
23+
v2AlgoSha256 = "HmacSHA256"
24+
v4Algo = "AWS4-HMAC-SHA256"
25+
iso8601utc = "20060102T150405Z"
26+
rfc3339utc = "2006-01-02T15:04:05Z"
27+
yyyymmdd = "20060102"
28+
)
29+
30+
// Credentials represents options for authenticating a user using EC2 credentials.
31+
type Credentials struct {
32+
// EC2 access ID
33+
Access string `json:"access" required:"true"`
34+
// EC2 access secret, used to calculate signature only
35+
Secret string `json:"-" required:"true"`
36+
// Optional parameters
37+
Host string `json:"host"`
38+
Path string `json:"path"`
39+
Verb string `json:"verb"`
40+
Headers *map[string]string `json:"headers,omitempty"`
41+
Region string `json:"-"`
42+
Service string `json:"-"`
43+
Params Params `json:"params"`
44+
AllowReauth bool `json:"-"`
45+
signature
46+
}
47+
48+
type signature struct {
49+
// can be either a []byte (encoded to base64 automatically) or a string
50+
Signature interface{} `json:"signature"`
51+
BodyHash *string `json:"body_hash,omitempty"`
52+
timestamp time.Time `json:"-"`
53+
}
54+
55+
type unexported struct {
56+
Date string `json:"X-Amz-Date,omitempty"`
57+
Algorithm string `json:"X-Amz-Algorithm,omitempty"`
58+
Credential string `json:"X-Amz-Credential,omitempty"`
59+
}
60+
61+
type Params struct {
62+
unexported
63+
SignedHeaders *string `json:"X-Amz-SignedHeaders,omitempty"`
64+
// signature v2
65+
SignatureMethod string `json:"SignatureMethod,omitempty"`
66+
SignatureVersion string `json:"SignatureVersion,omitempty"`
67+
}
68+
69+
func canonicalQs(opts *Credentials) string {
70+
var params map[string]string
71+
s, _ := json.Marshal(opts.Params)
72+
json.Unmarshal(s, &params)
73+
var keys []string
74+
for k := range params {
75+
keys = append(keys, k)
76+
}
77+
sort.Strings(keys)
78+
79+
var pairs []string
80+
for _, k := range keys {
81+
pairs = append(pairs, fmt.Sprintf("%s=%s", k, url.QueryEscape(params[k])))
82+
}
83+
84+
return strings.Join(pairs, "&")
85+
}
86+
87+
func paramsToQs(opts *Credentials) string {
88+
strToSign := strings.Join([]string{
89+
opts.Verb,
90+
opts.Host,
91+
opts.Path,
92+
}, "\n")
93+
94+
return strings.Join([]string{
95+
strToSign,
96+
canonicalQs(opts),
97+
}, "\n")
98+
}
99+
100+
func paramsToQs4(opts *Credentials) string {
101+
if opts.Verb == "POST" {
102+
return ""
103+
}
104+
return canonicalQs(opts)
105+
}
106+
107+
func canonicalHeaders(opts *Credentials) string {
108+
headersLower := make(map[string]string, len(*opts.Headers))
109+
for k, v := range *opts.Headers {
110+
headersLower[strings.ToLower(k)] = v
111+
}
112+
113+
var headersList []string
114+
for _, h := range strings.Split(*opts.Params.SignedHeaders, ";") {
115+
if v, ok := headersLower[h]; !ok {
116+
continue
117+
} else {
118+
headersList = append(headersList, h+":"+v)
119+
}
120+
}
121+
122+
return strings.Join(headersList, "\n") + "\n"
123+
}
124+
125+
func sumHMAC1(key []byte, data []byte) []byte {
126+
hash := hmac.New(sha1.New, key)
127+
hash.Write(data)
128+
return hash.Sum(nil)
129+
}
130+
131+
func sumHMAC256(key []byte, data []byte) []byte {
132+
hash := hmac.New(sha256.New, key)
133+
hash.Write(data)
134+
return hash.Sum(nil)
135+
}
136+
137+
func v4SignatureKey(secret, date, region, service string) []byte {
138+
kDate := sumHMAC256([]byte("AWS4"+secret), []byte(date))
139+
kRegion := sumHMAC256(kDate, []byte(region))
140+
kService := sumHMAC256(kRegion, []byte(service))
141+
return sumHMAC256(kService, []byte(aws4Request))
142+
}
143+
144+
func v4Signature(opts *Credentials) string {
145+
scope := strings.Join([]string{
146+
opts.timestamp.Format(yyyymmdd),
147+
opts.Region,
148+
opts.Service,
149+
aws4Request,
150+
}, "/")
151+
152+
canonicalRequest := strings.Join([]string{
153+
opts.Verb,
154+
opts.Path,
155+
paramsToQs4(opts),
156+
canonicalHeaders(opts),
157+
*opts.Params.SignedHeaders,
158+
*opts.BodyHash,
159+
}, "\n")
160+
t := sha256.Sum256([]byte(canonicalRequest))
161+
162+
strToSign := strings.Join([]string{
163+
v4Algo,
164+
opts.Params.Date,
165+
scope,
166+
hex.EncodeToString(t[:]),
167+
}, "\n")
168+
169+
key := v4SignatureKey(opts.Secret, opts.timestamp.Format(yyyymmdd), opts.Region, opts.Service)
170+
171+
return hex.EncodeToString(sumHMAC256(key, []byte(strToSign)))
172+
}
173+
174+
func generateBodyHash() *string {
175+
h := make([]byte, 64)
176+
rand.Read(h)
177+
bodyHash := hex.EncodeToString(h)
178+
return &bodyHash
179+
}
180+
181+
// ToTokenV3CreateMap builds a scope request body from AuthOptions.
182+
func (opts *Credentials) ToTokenV3ScopeMap() (map[string]interface{}, error) {
183+
return nil, nil
184+
}
185+
186+
func (opts *Credentials) CanReauth() bool {
187+
return opts.AllowReauth
188+
}
189+
190+
func CalculateSignature(opts *Credentials) error {
191+
switch opts.Params.SignatureVersion {
192+
case "2": // signature v2
193+
if opts.Headers != nil {
194+
opts.Headers = nil
195+
}
196+
if opts.Params.SignedHeaders != nil {
197+
opts.Params.SignedHeaders = nil
198+
}
199+
strToSign := paramsToQs(opts)
200+
switch opts.Params.SignatureMethod {
201+
case v2AlgoSha1:
202+
// keystone uses this method only when HmacSHA256 is not available on the server side
203+
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L151..L156
204+
opts.Signature = sumHMAC1([]byte(opts.Secret), []byte(strToSign))
205+
case v2AlgoSha256:
206+
opts.Signature = sumHMAC256([]byte(opts.Secret), []byte(strToSign))
207+
default:
208+
return fmt.Errorf("unsupported signature method: %s", opts.Params.SignatureMethod)
209+
}
210+
case "": // signature v4
211+
opts.timestamp = time.Now().UTC()
212+
opts.BodyHash = generateBodyHash()
213+
opts.Params.Date = opts.timestamp.Format(iso8601utc)
214+
opts.Params.Algorithm = v4Algo
215+
opts.Params.Credential = strings.Join([]string{
216+
opts.Access,
217+
opts.timestamp.Format(yyyymmdd),
218+
opts.Region,
219+
opts.Service,
220+
aws4Request,
221+
}, "/")
222+
if opts.Headers == nil {
223+
h := make(map[string]string)
224+
opts.Headers = &h
225+
}
226+
if opts.Params.SignedHeaders == nil {
227+
opts.Params.SignedHeaders = new(string)
228+
}
229+
opts.Signature = v4Signature(opts)
230+
default:
231+
return fmt.Errorf("unsupported signature version: %s", opts.Params.SignatureVersion)
232+
}
233+
return nil
234+
}
235+
236+
// ToCredentialsCreateMap formats a Credentials into a create request.
237+
func (opts Credentials) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) {
238+
err := CalculateSignature(&opts)
239+
if err != nil {
240+
return nil, err
241+
}
242+
243+
return gophercloud.BuildRequestBody(opts, "credentials")
244+
}
245+
246+
// Create authenticates and either generates a new token from EC2 credentials
247+
func Create(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) {
248+
b, err := opts.ToTokenV3CreateMap(nil)
249+
if err != nil {
250+
r.Err = err
251+
return
252+
}
253+
254+
resp, err := c.Post(ec2tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{
255+
MoreHeaders: map[string]string{"X-Auth-Token": ""},
256+
OkCodes: []int{200},
257+
})
258+
r.Err = err
259+
if resp != nil {
260+
r.Header = resp.Header
261+
}
262+
return
263+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package ec2tokens
2+
3+
import "github.com/gophercloud/gophercloud"
4+
5+
func ec2tokensURL(c *gophercloud.ServiceClient) string {
6+
return c.ServiceURL("ec2tokens")
7+
}

0 commit comments

Comments
 (0)