77e69b9cf3
Signed-off-by: Olivier Gambier <olivier@docker.com>
472 lines
14 KiB
Go
472 lines
14 KiB
Go
package aws
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// AWS specifies that the parameters in a signed request must
|
|
// be provided in the natural order of the keys. This is distinct
|
|
// from the natural order of the encoded value of key=value.
|
|
// Percent and gocheck.Equals affect the sorting order.
|
|
func EncodeSorted(values url.Values) string {
|
|
// preallocate the arrays for perfomance
|
|
keys := make([]string, 0, len(values))
|
|
sarray := make([]string, 0, len(values))
|
|
for k, _ := range values {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
for _, k := range keys {
|
|
for _, v := range values[k] {
|
|
sarray = append(sarray, Encode(k)+"="+Encode(v))
|
|
}
|
|
}
|
|
|
|
return strings.Join(sarray, "&")
|
|
}
|
|
|
|
type V2Signer struct {
|
|
auth Auth
|
|
service ServiceInfo
|
|
host string
|
|
}
|
|
|
|
var b64 = base64.StdEncoding
|
|
|
|
func NewV2Signer(auth Auth, service ServiceInfo) (*V2Signer, error) {
|
|
u, err := url.Parse(service.Endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &V2Signer{auth: auth, service: service, host: u.Host}, nil
|
|
}
|
|
|
|
func (s *V2Signer) Sign(method, path string, params map[string]string) {
|
|
params["AWSAccessKeyId"] = s.auth.AccessKey
|
|
params["SignatureVersion"] = "2"
|
|
params["SignatureMethod"] = "HmacSHA256"
|
|
if s.auth.Token() != "" {
|
|
params["SecurityToken"] = s.auth.Token()
|
|
}
|
|
// AWS specifies that the parameters in a signed request must
|
|
// be provided in the natural order of the keys. This is distinct
|
|
// from the natural order of the encoded value of key=value.
|
|
// Percent and gocheck.Equals affect the sorting order.
|
|
var keys, sarray []string
|
|
for k, _ := range params {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
sarray = append(sarray, Encode(k)+"="+Encode(params[k]))
|
|
}
|
|
joined := strings.Join(sarray, "&")
|
|
payload := method + "\n" + s.host + "\n" + path + "\n" + joined
|
|
hash := hmac.New(sha256.New, []byte(s.auth.SecretKey))
|
|
hash.Write([]byte(payload))
|
|
signature := make([]byte, b64.EncodedLen(hash.Size()))
|
|
b64.Encode(signature, hash.Sum(nil))
|
|
|
|
params["Signature"] = string(signature)
|
|
}
|
|
|
|
func (s *V2Signer) SignRequest(req *http.Request) error {
|
|
req.ParseForm()
|
|
req.Form.Set("AWSAccessKeyId", s.auth.AccessKey)
|
|
req.Form.Set("SignatureVersion", "2")
|
|
req.Form.Set("SignatureMethod", "HmacSHA256")
|
|
if s.auth.Token() != "" {
|
|
req.Form.Set("SecurityToken", s.auth.Token())
|
|
}
|
|
|
|
payload := req.Method + "\n" + req.URL.Host + "\n" + req.URL.Path + "\n" + EncodeSorted(req.Form)
|
|
hash := hmac.New(sha256.New, []byte(s.auth.SecretKey))
|
|
hash.Write([]byte(payload))
|
|
signature := make([]byte, b64.EncodedLen(hash.Size()))
|
|
b64.Encode(signature, hash.Sum(nil))
|
|
|
|
req.Form.Set("Signature", string(signature))
|
|
|
|
req.URL.RawQuery = req.Form.Encode()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Common date formats for signing requests
|
|
const (
|
|
ISO8601BasicFormat = "20060102T150405Z"
|
|
ISO8601BasicFormatShort = "20060102"
|
|
)
|
|
|
|
type Route53Signer struct {
|
|
auth Auth
|
|
}
|
|
|
|
func NewRoute53Signer(auth Auth) *Route53Signer {
|
|
return &Route53Signer{auth: auth}
|
|
}
|
|
|
|
// Creates the authorize signature based on the date stamp and secret key
|
|
func (s *Route53Signer) getHeaderAuthorize(message string) string {
|
|
hmacSha256 := hmac.New(sha256.New, []byte(s.auth.SecretKey))
|
|
hmacSha256.Write([]byte(message))
|
|
cryptedString := hmacSha256.Sum(nil)
|
|
|
|
return base64.StdEncoding.EncodeToString(cryptedString)
|
|
}
|
|
|
|
// Adds all the required headers for AWS Route53 API to the request
|
|
// including the authorization
|
|
func (s *Route53Signer) Sign(req *http.Request) {
|
|
date := time.Now().UTC().Format(time.RFC1123)
|
|
delete(req.Header, "Date")
|
|
req.Header.Set("Date", date)
|
|
|
|
authHeader := fmt.Sprintf("AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=%s,Signature=%s",
|
|
s.auth.AccessKey, "HmacSHA256", s.getHeaderAuthorize(date))
|
|
|
|
req.Header.Set("Host", req.Host)
|
|
req.Header.Set("X-Amzn-Authorization", authHeader)
|
|
req.Header.Set("Content-Type", "application/xml")
|
|
if s.auth.Token() != "" {
|
|
req.Header.Set("X-Amz-Security-Token", s.auth.Token())
|
|
}
|
|
}
|
|
|
|
/*
|
|
The V4Signer encapsulates all of the functionality to sign a request with the AWS
|
|
Signature Version 4 Signing Process. (http://goo.gl/u1OWZz)
|
|
*/
|
|
type V4Signer struct {
|
|
auth Auth
|
|
serviceName string
|
|
region Region
|
|
// Add the x-amz-content-sha256 header
|
|
IncludeXAmzContentSha256 bool
|
|
}
|
|
|
|
/*
|
|
Return a new instance of a V4Signer capable of signing AWS requests.
|
|
*/
|
|
func NewV4Signer(auth Auth, serviceName string, region Region) *V4Signer {
|
|
return &V4Signer{
|
|
auth: auth,
|
|
serviceName: serviceName,
|
|
region: region,
|
|
IncludeXAmzContentSha256: false,
|
|
}
|
|
}
|
|
|
|
/*
|
|
Sign a request according to the AWS Signature Version 4 Signing Process. (http://goo.gl/u1OWZz)
|
|
|
|
The signed request will include an "x-amz-date" header with a current timestamp if a valid "x-amz-date"
|
|
or "date" header was not available in the original request. In addition, AWS Signature Version 4 requires
|
|
the "host" header to be a signed header, therefor the Sign method will manually set a "host" header from
|
|
the request.Host.
|
|
|
|
The signed request will include a new "Authorization" header indicating that the request has been signed.
|
|
|
|
Any changes to the request after signing the request will invalidate the signature.
|
|
*/
|
|
func (s *V4Signer) Sign(req *http.Request) {
|
|
req.Header.Set("host", req.Host) // host header must be included as a signed header
|
|
t := s.requestTime(req) // Get request time
|
|
|
|
payloadHash := ""
|
|
|
|
if _, ok := req.Form["X-Amz-Expires"]; ok {
|
|
// We are authenticating the the request by using query params
|
|
// (also known as pre-signing a url, http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html)
|
|
payloadHash = "UNSIGNED-PAYLOAD"
|
|
req.Header.Del("x-amz-date")
|
|
|
|
req.Form["X-Amz-SignedHeaders"] = []string{s.signedHeaders(req.Header)}
|
|
req.Form["X-Amz-Algorithm"] = []string{"AWS4-HMAC-SHA256"}
|
|
req.Form["X-Amz-Credential"] = []string{s.auth.AccessKey + "/" + s.credentialScope(t)}
|
|
req.Form["X-Amz-Date"] = []string{t.Format(ISO8601BasicFormat)}
|
|
req.URL.RawQuery = req.Form.Encode()
|
|
} else {
|
|
payloadHash = s.payloadHash(req)
|
|
if s.IncludeXAmzContentSha256 {
|
|
req.Header.Set("x-amz-content-sha256", payloadHash) // x-amz-content-sha256 contains the payload hash
|
|
}
|
|
}
|
|
creq := s.canonicalRequest(req, payloadHash) // Build canonical request
|
|
sts := s.stringToSign(t, creq) // Build string to sign
|
|
signature := s.signature(t, sts) // Calculate the AWS Signature Version 4
|
|
auth := s.authorization(req.Header, t, signature) // Create Authorization header value
|
|
|
|
if _, ok := req.Form["X-Amz-Expires"]; ok {
|
|
req.Form["X-Amz-Signature"] = []string{signature}
|
|
} else {
|
|
req.Header.Set("Authorization", auth) // Add Authorization header to request
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *V4Signer) SignRequest(req *http.Request) error {
|
|
s.Sign(req)
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
requestTime method will parse the time from the request "x-amz-date" or "date" headers.
|
|
If the "x-amz-date" header is present, that will take priority over the "date" header.
|
|
If neither header is defined or we are unable to parse either header as a valid date
|
|
then we will create a new "x-amz-date" header with the current time.
|
|
*/
|
|
func (s *V4Signer) requestTime(req *http.Request) time.Time {
|
|
|
|
// Get "x-amz-date" header
|
|
date := req.Header.Get("x-amz-date")
|
|
|
|
// Attempt to parse as ISO8601BasicFormat
|
|
t, err := time.Parse(ISO8601BasicFormat, date)
|
|
if err == nil {
|
|
return t
|
|
}
|
|
|
|
// Attempt to parse as http.TimeFormat
|
|
t, err = time.Parse(http.TimeFormat, date)
|
|
if err == nil {
|
|
req.Header.Set("x-amz-date", t.Format(ISO8601BasicFormat))
|
|
return t
|
|
}
|
|
|
|
// Get "date" header
|
|
date = req.Header.Get("date")
|
|
|
|
// Attempt to parse as http.TimeFormat
|
|
t, err = time.Parse(http.TimeFormat, date)
|
|
if err == nil {
|
|
return t
|
|
}
|
|
|
|
// Create a current time header to be used
|
|
t = time.Now().UTC()
|
|
req.Header.Set("x-amz-date", t.Format(ISO8601BasicFormat))
|
|
return t
|
|
}
|
|
|
|
/*
|
|
canonicalRequest method creates the canonical request according to Task 1 of the AWS Signature Version 4 Signing Process. (http://goo.gl/eUUZ3S)
|
|
|
|
CanonicalRequest =
|
|
HTTPRequestMethod + '\n' +
|
|
CanonicalURI + '\n' +
|
|
CanonicalQueryString + '\n' +
|
|
CanonicalHeaders + '\n' +
|
|
SignedHeaders + '\n' +
|
|
HexEncode(Hash(Payload))
|
|
|
|
payloadHash is optional; use the empty string and it will be calculated from the request
|
|
*/
|
|
func (s *V4Signer) canonicalRequest(req *http.Request, payloadHash string) string {
|
|
if payloadHash == "" {
|
|
payloadHash = s.payloadHash(req)
|
|
}
|
|
c := new(bytes.Buffer)
|
|
fmt.Fprintf(c, "%s\n", req.Method)
|
|
fmt.Fprintf(c, "%s\n", s.canonicalURI(req.URL))
|
|
fmt.Fprintf(c, "%s\n", s.canonicalQueryString(req.URL))
|
|
fmt.Fprintf(c, "%s\n\n", s.canonicalHeaders(req.Header))
|
|
fmt.Fprintf(c, "%s\n", s.signedHeaders(req.Header))
|
|
fmt.Fprintf(c, "%s", payloadHash)
|
|
return c.String()
|
|
}
|
|
|
|
func (s *V4Signer) canonicalURI(u *url.URL) string {
|
|
u = &url.URL{Path: u.Path}
|
|
canonicalPath := u.String()
|
|
|
|
slash := strings.HasSuffix(canonicalPath, "/")
|
|
canonicalPath = path.Clean(canonicalPath)
|
|
|
|
if canonicalPath == "" || canonicalPath == "." {
|
|
canonicalPath = "/"
|
|
}
|
|
|
|
if canonicalPath != "/" && slash {
|
|
canonicalPath += "/"
|
|
}
|
|
|
|
return canonicalPath
|
|
}
|
|
|
|
func (s *V4Signer) canonicalQueryString(u *url.URL) string {
|
|
keyValues := make(map[string]string, len(u.Query()))
|
|
keys := make([]string, len(u.Query()))
|
|
|
|
key_i := 0
|
|
for k, vs := range u.Query() {
|
|
k = url.QueryEscape(k)
|
|
|
|
a := make([]string, len(vs))
|
|
for idx, v := range vs {
|
|
v = url.QueryEscape(v)
|
|
a[idx] = fmt.Sprintf("%s=%s", k, v)
|
|
}
|
|
|
|
keyValues[k] = strings.Join(a, "&")
|
|
keys[key_i] = k
|
|
key_i++
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
query := make([]string, len(keys))
|
|
for idx, key := range keys {
|
|
query[idx] = keyValues[key]
|
|
}
|
|
|
|
query_str := strings.Join(query, "&")
|
|
|
|
// AWS V4 signing requires that the space characters
|
|
// are encoded as %20 instead of +. On the other hand
|
|
// golangs url.QueryEscape as well as url.Values.Encode()
|
|
// both encode the space as a + character. See:
|
|
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
|
// https://github.com/golang/go/issues/4013
|
|
// https://groups.google.com/forum/#!topic/golang-nuts/BB443qEjPIk
|
|
|
|
return strings.Replace(query_str, "+", "%20", -1)
|
|
}
|
|
|
|
func (s *V4Signer) canonicalHeaders(h http.Header) string {
|
|
i, a, lowerCase := 0, make([]string, len(h)), make(map[string][]string)
|
|
|
|
for k, v := range h {
|
|
lowerCase[strings.ToLower(k)] = v
|
|
}
|
|
|
|
var keys []string
|
|
for k := range lowerCase {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
for _, k := range keys {
|
|
v := lowerCase[k]
|
|
for j, w := range v {
|
|
v[j] = strings.Trim(w, " ")
|
|
}
|
|
sort.Strings(v)
|
|
a[i] = strings.ToLower(k) + ":" + strings.Join(v, ",")
|
|
i++
|
|
}
|
|
return strings.Join(a, "\n")
|
|
}
|
|
|
|
func (s *V4Signer) signedHeaders(h http.Header) string {
|
|
i, a := 0, make([]string, len(h))
|
|
for k, _ := range h {
|
|
a[i] = strings.ToLower(k)
|
|
i++
|
|
}
|
|
sort.Strings(a)
|
|
return strings.Join(a, ";")
|
|
}
|
|
|
|
func (s *V4Signer) payloadHash(req *http.Request) string {
|
|
var b []byte
|
|
if req.Body == nil {
|
|
b = []byte("")
|
|
} else {
|
|
var err error
|
|
b, err = ioutil.ReadAll(req.Body)
|
|
if err != nil {
|
|
// TODO: I REALLY DON'T LIKE THIS PANIC!!!!
|
|
panic(err)
|
|
}
|
|
}
|
|
req.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
|
return s.hash(string(b))
|
|
}
|
|
|
|
/*
|
|
stringToSign method creates the string to sign accorting to Task 2 of the AWS Signature Version 4 Signing Process. (http://goo.gl/es1PAu)
|
|
|
|
StringToSign =
|
|
Algorithm + '\n' +
|
|
RequestDate + '\n' +
|
|
CredentialScope + '\n' +
|
|
HexEncode(Hash(CanonicalRequest))
|
|
*/
|
|
func (s *V4Signer) stringToSign(t time.Time, creq string) string {
|
|
w := new(bytes.Buffer)
|
|
fmt.Fprint(w, "AWS4-HMAC-SHA256\n")
|
|
fmt.Fprintf(w, "%s\n", t.Format(ISO8601BasicFormat))
|
|
fmt.Fprintf(w, "%s\n", s.credentialScope(t))
|
|
fmt.Fprintf(w, "%s", s.hash(creq))
|
|
return w.String()
|
|
}
|
|
|
|
func (s *V4Signer) credentialScope(t time.Time) string {
|
|
return fmt.Sprintf("%s/%s/%s/aws4_request", t.Format(ISO8601BasicFormatShort), s.region.Name, s.serviceName)
|
|
}
|
|
|
|
/*
|
|
signature method calculates the AWS Signature Version 4 according to Task 3 of the AWS Signature Version 4 Signing Process. (http://goo.gl/j0Yqe1)
|
|
|
|
signature = HexEncode(HMAC(derived-signing-key, string-to-sign))
|
|
*/
|
|
func (s *V4Signer) signature(t time.Time, sts string) string {
|
|
h := s.hmac(s.derivedKey(t), []byte(sts))
|
|
return fmt.Sprintf("%x", h)
|
|
}
|
|
|
|
/*
|
|
derivedKey method derives a signing key to be used for signing a request.
|
|
|
|
kSecret = Your AWS Secret Access Key
|
|
kDate = HMAC("AWS4" + kSecret, Date)
|
|
kRegion = HMAC(kDate, Region)
|
|
kService = HMAC(kRegion, Service)
|
|
kSigning = HMAC(kService, "aws4_request")
|
|
*/
|
|
func (s *V4Signer) derivedKey(t time.Time) []byte {
|
|
h := s.hmac([]byte("AWS4"+s.auth.SecretKey), []byte(t.Format(ISO8601BasicFormatShort)))
|
|
h = s.hmac(h, []byte(s.region.Name))
|
|
h = s.hmac(h, []byte(s.serviceName))
|
|
h = s.hmac(h, []byte("aws4_request"))
|
|
return h
|
|
}
|
|
|
|
/*
|
|
authorization method generates the authorization header value.
|
|
*/
|
|
func (s *V4Signer) authorization(header http.Header, t time.Time, signature string) string {
|
|
w := new(bytes.Buffer)
|
|
fmt.Fprint(w, "AWS4-HMAC-SHA256 ")
|
|
fmt.Fprintf(w, "Credential=%s/%s, ", s.auth.AccessKey, s.credentialScope(t))
|
|
fmt.Fprintf(w, "SignedHeaders=%s, ", s.signedHeaders(header))
|
|
fmt.Fprintf(w, "Signature=%s", signature)
|
|
return w.String()
|
|
}
|
|
|
|
// hash method calculates the sha256 hash for a given string
|
|
func (s *V4Signer) hash(in string) string {
|
|
h := sha256.New()
|
|
fmt.Fprintf(h, "%s", in)
|
|
return fmt.Sprintf("%x", h.Sum(nil))
|
|
}
|
|
|
|
// hmac method calculates the sha256 hmac for a given slice of bytes
|
|
func (s *V4Signer) hmac(key, data []byte) []byte {
|
|
h := hmac.New(sha256.New, key)
|
|
h.Write(data)
|
|
return h.Sum(nil)
|
|
}
|