mirror of
https://github.com/thegeeklab/drone-s3-sync.git
synced 2024-11-16 02:50:40 +00:00
439 lines
13 KiB
Go
439 lines
13 KiB
Go
|
// Package v4 implements signing for AWS V4 signer
|
||
|
package v4
|
||
|
|
||
|
import (
|
||
|
"crypto/hmac"
|
||
|
"crypto/sha256"
|
||
|
"encoding/hex"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/aws/aws-sdk-go/aws"
|
||
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||
|
"github.com/aws/aws-sdk-go/aws/request"
|
||
|
"github.com/aws/aws-sdk-go/private/protocol/rest"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
authHeaderPrefix = "AWS4-HMAC-SHA256"
|
||
|
timeFormat = "20060102T150405Z"
|
||
|
shortTimeFormat = "20060102"
|
||
|
)
|
||
|
|
||
|
var ignoredHeaders = rules{
|
||
|
blacklist{
|
||
|
mapRule{
|
||
|
"Content-Length": struct{}{},
|
||
|
"User-Agent": struct{}{},
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
// requiredSignedHeaders is a whitelist for build canonical headers.
|
||
|
var requiredSignedHeaders = rules{
|
||
|
whitelist{
|
||
|
mapRule{
|
||
|
"Cache-Control": struct{}{},
|
||
|
"Content-Disposition": struct{}{},
|
||
|
"Content-Encoding": struct{}{},
|
||
|
"Content-Language": struct{}{},
|
||
|
"Content-Md5": struct{}{},
|
||
|
"Content-Type": struct{}{},
|
||
|
"Expires": struct{}{},
|
||
|
"If-Match": struct{}{},
|
||
|
"If-Modified-Since": struct{}{},
|
||
|
"If-None-Match": struct{}{},
|
||
|
"If-Unmodified-Since": struct{}{},
|
||
|
"Range": struct{}{},
|
||
|
"X-Amz-Acl": struct{}{},
|
||
|
"X-Amz-Copy-Source": struct{}{},
|
||
|
"X-Amz-Copy-Source-If-Match": struct{}{},
|
||
|
"X-Amz-Copy-Source-If-Modified-Since": struct{}{},
|
||
|
"X-Amz-Copy-Source-If-None-Match": struct{}{},
|
||
|
"X-Amz-Copy-Source-If-Unmodified-Since": struct{}{},
|
||
|
"X-Amz-Copy-Source-Range": struct{}{},
|
||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{},
|
||
|
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||
|
"X-Amz-Grant-Full-control": struct{}{},
|
||
|
"X-Amz-Grant-Read": struct{}{},
|
||
|
"X-Amz-Grant-Read-Acp": struct{}{},
|
||
|
"X-Amz-Grant-Write": struct{}{},
|
||
|
"X-Amz-Grant-Write-Acp": struct{}{},
|
||
|
"X-Amz-Metadata-Directive": struct{}{},
|
||
|
"X-Amz-Mfa": struct{}{},
|
||
|
"X-Amz-Request-Payer": struct{}{},
|
||
|
"X-Amz-Server-Side-Encryption": struct{}{},
|
||
|
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{},
|
||
|
"X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||
|
"X-Amz-Server-Side-Encryption-Customer-Key": struct{}{},
|
||
|
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||
|
"X-Amz-Storage-Class": struct{}{},
|
||
|
"X-Amz-Website-Redirect-Location": struct{}{},
|
||
|
},
|
||
|
},
|
||
|
patterns{"X-Amz-Meta-"},
|
||
|
}
|
||
|
|
||
|
// allowedHoisting is a whitelist for build query headers. The boolean value
|
||
|
// represents whether or not it is a pattern.
|
||
|
var allowedQueryHoisting = inclusiveRules{
|
||
|
blacklist{requiredSignedHeaders},
|
||
|
patterns{"X-Amz-"},
|
||
|
}
|
||
|
|
||
|
type signer struct {
|
||
|
Request *http.Request
|
||
|
Time time.Time
|
||
|
ExpireTime time.Duration
|
||
|
ServiceName string
|
||
|
Region string
|
||
|
CredValues credentials.Value
|
||
|
Credentials *credentials.Credentials
|
||
|
Query url.Values
|
||
|
Body io.ReadSeeker
|
||
|
Debug aws.LogLevelType
|
||
|
Logger aws.Logger
|
||
|
|
||
|
isPresign bool
|
||
|
formattedTime string
|
||
|
formattedShortTime string
|
||
|
|
||
|
signedHeaders string
|
||
|
canonicalHeaders string
|
||
|
canonicalString string
|
||
|
credentialString string
|
||
|
stringToSign string
|
||
|
signature string
|
||
|
authorization string
|
||
|
notHoist bool
|
||
|
signedHeaderVals http.Header
|
||
|
}
|
||
|
|
||
|
// Sign requests with signature version 4.
|
||
|
//
|
||
|
// Will sign the requests with the service config's Credentials object
|
||
|
// Signing is skipped if the credentials is the credentials.AnonymousCredentials
|
||
|
// object.
|
||
|
func Sign(req *request.Request) {
|
||
|
// If the request does not need to be signed ignore the signing of the
|
||
|
// request if the AnonymousCredentials object is used.
|
||
|
if req.Config.Credentials == credentials.AnonymousCredentials {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
region := req.ClientInfo.SigningRegion
|
||
|
if region == "" {
|
||
|
region = aws.StringValue(req.Config.Region)
|
||
|
}
|
||
|
|
||
|
name := req.ClientInfo.SigningName
|
||
|
if name == "" {
|
||
|
name = req.ClientInfo.ServiceName
|
||
|
}
|
||
|
|
||
|
s := signer{
|
||
|
Request: req.HTTPRequest,
|
||
|
Time: req.Time,
|
||
|
ExpireTime: req.ExpireTime,
|
||
|
Query: req.HTTPRequest.URL.Query(),
|
||
|
Body: req.Body,
|
||
|
ServiceName: name,
|
||
|
Region: region,
|
||
|
Credentials: req.Config.Credentials,
|
||
|
Debug: req.Config.LogLevel.Value(),
|
||
|
Logger: req.Config.Logger,
|
||
|
notHoist: req.NotHoist,
|
||
|
}
|
||
|
|
||
|
req.Error = s.sign()
|
||
|
req.SignedHeaderVals = s.signedHeaderVals
|
||
|
}
|
||
|
|
||
|
func (v4 *signer) sign() error {
|
||
|
if v4.ExpireTime != 0 {
|
||
|
v4.isPresign = true
|
||
|
}
|
||
|
|
||
|
if v4.isRequestSigned() {
|
||
|
if !v4.Credentials.IsExpired() {
|
||
|
// If the request is already signed, and the credentials have not
|
||
|
// expired yet ignore the signing request.
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// The credentials have expired for this request. The current signing
|
||
|
// is invalid, and needs to be request because the request will fail.
|
||
|
if v4.isPresign {
|
||
|
v4.removePresign()
|
||
|
// Update the request's query string to ensure the values stays in
|
||
|
// sync in the case retrieving the new credentials fails.
|
||
|
v4.Request.URL.RawQuery = v4.Query.Encode()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var err error
|
||
|
v4.CredValues, err = v4.Credentials.Get()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if v4.isPresign {
|
||
|
v4.Query.Set("X-Amz-Algorithm", authHeaderPrefix)
|
||
|
if v4.CredValues.SessionToken != "" {
|
||
|
v4.Query.Set("X-Amz-Security-Token", v4.CredValues.SessionToken)
|
||
|
} else {
|
||
|
v4.Query.Del("X-Amz-Security-Token")
|
||
|
}
|
||
|
} else if v4.CredValues.SessionToken != "" {
|
||
|
v4.Request.Header.Set("X-Amz-Security-Token", v4.CredValues.SessionToken)
|
||
|
}
|
||
|
|
||
|
v4.build()
|
||
|
|
||
|
if v4.Debug.Matches(aws.LogDebugWithSigning) {
|
||
|
v4.logSigningInfo()
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
const logSignInfoMsg = `DEBUG: Request Signiture:
|
||
|
---[ CANONICAL STRING ]-----------------------------
|
||
|
%s
|
||
|
---[ STRING TO SIGN ]--------------------------------
|
||
|
%s%s
|
||
|
-----------------------------------------------------`
|
||
|
const logSignedURLMsg = `
|
||
|
---[ SIGNED URL ]------------------------------------
|
||
|
%s`
|
||
|
|
||
|
func (v4 *signer) logSigningInfo() {
|
||
|
signedURLMsg := ""
|
||
|
if v4.isPresign {
|
||
|
signedURLMsg = fmt.Sprintf(logSignedURLMsg, v4.Request.URL.String())
|
||
|
}
|
||
|
msg := fmt.Sprintf(logSignInfoMsg, v4.canonicalString, v4.stringToSign, signedURLMsg)
|
||
|
v4.Logger.Log(msg)
|
||
|
}
|
||
|
|
||
|
func (v4 *signer) build() {
|
||
|
|
||
|
v4.buildTime() // no depends
|
||
|
v4.buildCredentialString() // no depends
|
||
|
|
||
|
unsignedHeaders := v4.Request.Header
|
||
|
if v4.isPresign {
|
||
|
if !v4.notHoist {
|
||
|
urlValues := url.Values{}
|
||
|
urlValues, unsignedHeaders = buildQuery(allowedQueryHoisting, unsignedHeaders) // no depends
|
||
|
for k := range urlValues {
|
||
|
v4.Query[k] = urlValues[k]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
v4.buildCanonicalHeaders(ignoredHeaders, unsignedHeaders)
|
||
|
v4.buildCanonicalString() // depends on canon headers / signed headers
|
||
|
v4.buildStringToSign() // depends on canon string
|
||
|
v4.buildSignature() // depends on string to sign
|
||
|
|
||
|
if v4.isPresign {
|
||
|
v4.Request.URL.RawQuery += "&X-Amz-Signature=" + v4.signature
|
||
|
} else {
|
||
|
parts := []string{
|
||
|
authHeaderPrefix + " Credential=" + v4.CredValues.AccessKeyID + "/" + v4.credentialString,
|
||
|
"SignedHeaders=" + v4.signedHeaders,
|
||
|
"Signature=" + v4.signature,
|
||
|
}
|
||
|
v4.Request.Header.Set("Authorization", strings.Join(parts, ", "))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (v4 *signer) buildTime() {
|
||
|
v4.formattedTime = v4.Time.UTC().Format(timeFormat)
|
||
|
v4.formattedShortTime = v4.Time.UTC().Format(shortTimeFormat)
|
||
|
|
||
|
if v4.isPresign {
|
||
|
duration := int64(v4.ExpireTime / time.Second)
|
||
|
v4.Query.Set("X-Amz-Date", v4.formattedTime)
|
||
|
v4.Query.Set("X-Amz-Expires", strconv.FormatInt(duration, 10))
|
||
|
} else {
|
||
|
v4.Request.Header.Set("X-Amz-Date", v4.formattedTime)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (v4 *signer) buildCredentialString() {
|
||
|
v4.credentialString = strings.Join([]string{
|
||
|
v4.formattedShortTime,
|
||
|
v4.Region,
|
||
|
v4.ServiceName,
|
||
|
"aws4_request",
|
||
|
}, "/")
|
||
|
|
||
|
if v4.isPresign {
|
||
|
v4.Query.Set("X-Amz-Credential", v4.CredValues.AccessKeyID+"/"+v4.credentialString)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func buildQuery(r rule, header http.Header) (url.Values, http.Header) {
|
||
|
query := url.Values{}
|
||
|
unsignedHeaders := http.Header{}
|
||
|
for k, h := range header {
|
||
|
if r.IsValid(k) {
|
||
|
query[k] = h
|
||
|
} else {
|
||
|
unsignedHeaders[k] = h
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return query, unsignedHeaders
|
||
|
}
|
||
|
func (v4 *signer) buildCanonicalHeaders(r rule, header http.Header) {
|
||
|
var headers []string
|
||
|
headers = append(headers, "host")
|
||
|
for k, v := range header {
|
||
|
canonicalKey := http.CanonicalHeaderKey(k)
|
||
|
if !r.IsValid(canonicalKey) {
|
||
|
continue // ignored header
|
||
|
}
|
||
|
|
||
|
lowerCaseKey := strings.ToLower(k)
|
||
|
headers = append(headers, lowerCaseKey)
|
||
|
|
||
|
if v4.signedHeaderVals == nil {
|
||
|
v4.signedHeaderVals = make(http.Header)
|
||
|
}
|
||
|
v4.signedHeaderVals[lowerCaseKey] = v
|
||
|
}
|
||
|
sort.Strings(headers)
|
||
|
|
||
|
v4.signedHeaders = strings.Join(headers, ";")
|
||
|
|
||
|
if v4.isPresign {
|
||
|
v4.Query.Set("X-Amz-SignedHeaders", v4.signedHeaders)
|
||
|
}
|
||
|
|
||
|
headerValues := make([]string, len(headers))
|
||
|
for i, k := range headers {
|
||
|
if k == "host" {
|
||
|
headerValues[i] = "host:" + v4.Request.URL.Host
|
||
|
} else {
|
||
|
headerValues[i] = k + ":" +
|
||
|
strings.Join(v4.Request.Header[http.CanonicalHeaderKey(k)], ",")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
v4.canonicalHeaders = strings.Join(headerValues, "\n")
|
||
|
}
|
||
|
|
||
|
func (v4 *signer) buildCanonicalString() {
|
||
|
v4.Request.URL.RawQuery = strings.Replace(v4.Query.Encode(), "+", "%20", -1)
|
||
|
uri := v4.Request.URL.Opaque
|
||
|
if uri != "" {
|
||
|
uri = "/" + strings.Join(strings.Split(uri, "/")[3:], "/")
|
||
|
} else {
|
||
|
uri = v4.Request.URL.Path
|
||
|
}
|
||
|
if uri == "" {
|
||
|
uri = "/"
|
||
|
}
|
||
|
|
||
|
if v4.ServiceName != "s3" {
|
||
|
uri = rest.EscapePath(uri, false)
|
||
|
}
|
||
|
|
||
|
v4.canonicalString = strings.Join([]string{
|
||
|
v4.Request.Method,
|
||
|
uri,
|
||
|
v4.Request.URL.RawQuery,
|
||
|
v4.canonicalHeaders + "\n",
|
||
|
v4.signedHeaders,
|
||
|
v4.bodyDigest(),
|
||
|
}, "\n")
|
||
|
}
|
||
|
|
||
|
func (v4 *signer) buildStringToSign() {
|
||
|
v4.stringToSign = strings.Join([]string{
|
||
|
authHeaderPrefix,
|
||
|
v4.formattedTime,
|
||
|
v4.credentialString,
|
||
|
hex.EncodeToString(makeSha256([]byte(v4.canonicalString))),
|
||
|
}, "\n")
|
||
|
}
|
||
|
|
||
|
func (v4 *signer) buildSignature() {
|
||
|
secret := v4.CredValues.SecretAccessKey
|
||
|
date := makeHmac([]byte("AWS4"+secret), []byte(v4.formattedShortTime))
|
||
|
region := makeHmac(date, []byte(v4.Region))
|
||
|
service := makeHmac(region, []byte(v4.ServiceName))
|
||
|
credentials := makeHmac(service, []byte("aws4_request"))
|
||
|
signature := makeHmac(credentials, []byte(v4.stringToSign))
|
||
|
v4.signature = hex.EncodeToString(signature)
|
||
|
}
|
||
|
|
||
|
func (v4 *signer) bodyDigest() string {
|
||
|
hash := v4.Request.Header.Get("X-Amz-Content-Sha256")
|
||
|
if hash == "" {
|
||
|
if v4.isPresign && v4.ServiceName == "s3" {
|
||
|
hash = "UNSIGNED-PAYLOAD"
|
||
|
} else if v4.Body == nil {
|
||
|
hash = hex.EncodeToString(makeSha256([]byte{}))
|
||
|
} else {
|
||
|
hash = hex.EncodeToString(makeSha256Reader(v4.Body))
|
||
|
}
|
||
|
v4.Request.Header.Add("X-Amz-Content-Sha256", hash)
|
||
|
}
|
||
|
return hash
|
||
|
}
|
||
|
|
||
|
// isRequestSigned returns if the request is currently signed or presigned
|
||
|
func (v4 *signer) isRequestSigned() bool {
|
||
|
if v4.isPresign && v4.Query.Get("X-Amz-Signature") != "" {
|
||
|
return true
|
||
|
}
|
||
|
if v4.Request.Header.Get("Authorization") != "" {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// unsign removes signing flags for both signed and presigned requests.
|
||
|
func (v4 *signer) removePresign() {
|
||
|
v4.Query.Del("X-Amz-Algorithm")
|
||
|
v4.Query.Del("X-Amz-Signature")
|
||
|
v4.Query.Del("X-Amz-Security-Token")
|
||
|
v4.Query.Del("X-Amz-Date")
|
||
|
v4.Query.Del("X-Amz-Expires")
|
||
|
v4.Query.Del("X-Amz-Credential")
|
||
|
v4.Query.Del("X-Amz-SignedHeaders")
|
||
|
}
|
||
|
|
||
|
func makeHmac(key []byte, data []byte) []byte {
|
||
|
hash := hmac.New(sha256.New, key)
|
||
|
hash.Write(data)
|
||
|
return hash.Sum(nil)
|
||
|
}
|
||
|
|
||
|
func makeSha256(data []byte) []byte {
|
||
|
hash := sha256.New()
|
||
|
hash.Write(data)
|
||
|
return hash.Sum(nil)
|
||
|
}
|
||
|
|
||
|
func makeSha256Reader(reader io.ReadSeeker) []byte {
|
||
|
hash := sha256.New()
|
||
|
start, _ := reader.Seek(0, 1)
|
||
|
defer reader.Seek(start, 0)
|
||
|
|
||
|
io.Copy(hash, reader)
|
||
|
return hash.Sum(nil)
|
||
|
}
|