signature_v4.go (221 lines of code) (raw):
package sls
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/pkg/errors"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
const (
emptyStringSha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
signerV4ProductName = "sls"
ISO8601 = "20060102T150405Z"
authorizationV4SigningSaltFigure = "aliyun_v4_request"
authorizationAlgorithmV4 = "SLS4-HMAC-SHA256"
authorizationV4SecretKeyPrefix = "aliyun_v4"
)
var (
errSignerV4MissingRegion = errors.New("sign version v4 require a valid region")
)
// SignerV4 sign version v4, a non-empty region is required
type SignerV4 struct {
accessKeyID string
accessKeySecret string
region string
}
func NewSignerV4(accessKeyID, accessKeySecret, region string) *SignerV4 {
return &SignerV4{
accessKeyID: accessKeyID,
accessKeySecret: accessKeySecret,
region: region,
}
}
func (s *SignerV4) isSignedHeader(key string) bool {
return strings.HasPrefix(key, "x-log-") ||
strings.HasPrefix(key, "x-acs-") ||
strings.EqualFold(key, HTTPHeaderHost) ||
strings.EqualFold(key, HTTPHeaderContentType)
}
func (s *SignerV4) Sign(method, uri string, headers map[string]string, body []byte) error {
if s.region == "" {
return errSignerV4MissingRegion
}
uri, urlParams, err := s.parseUri(uri)
if err != nil {
return err
}
dateTime, ok := headers[HTTPHeaderLogDate]
if !ok {
return fmt.Errorf("can't find '%s' header", HTTPHeaderLogDate)
}
date := dateTime[:8]
// Host should not contain schema here.
if host, ok := headers[HTTPHeaderHost]; ok {
if strings.HasPrefix(host, "http://") {
headers[HTTPHeaderHost] = host[len("http://"):]
} else if strings.HasPrefix(host, "https://") {
headers[HTTPHeaderHost] = host[len("https://"):]
}
}
contentLength := len(body)
var sha256Payload string
if contentLength != 0 {
sha256Payload = fmt.Sprintf("%x", sha256.Sum256(body))
} else {
sha256Payload = emptyStringSha256
}
headers[HTTPHeaderLogContentSha256] = sha256Payload
headers[HTTPHeaderContentLength] = strconv.Itoa(contentLength)
// Canonical headers
signedHeadersStr, canonicalHeaderStr := s.buildCanonicalHeaders(headers)
// CanonicalRequest
canonReq := s.buildCanonicalRequest(method, uri, sha256Payload, canonicalHeaderStr, signedHeadersStr, urlParams)
scope := s.buildScope(date, s.region)
// SignKey + signMessage => signature
strToSign := s.buildSignMessage(canonReq, dateTime, scope)
key, err := s.buildSigningKey(s.accessKeySecret, s.region, date)
if err != nil {
return err
}
hash, err := s.hmacSha256([]byte(strToSign), key)
if err != nil {
return err
}
signature := hex.EncodeToString(hash)
headers[HTTPHeaderAuthorization] = s.buildAuthorization(s.accessKeyID, signature, scope)
return nil
}
func (s *SignerV4) buildCanonicalHeaders(headers map[string]string) (string, string) {
var headerKeys []string
signed := make(map[string]string)
for k, v := range headers {
key := strings.ToLower(k)
if s.isSignedHeader(key) {
signed[key] = v
headerKeys = append(headerKeys, key)
}
}
sort.Strings(headerKeys)
var canonicalHeaders strings.Builder
var signedHeaders strings.Builder
n := len(headerKeys)
for i := 0; i < n; i++ {
canonicalHeaders.WriteString(headerKeys[i])
canonicalHeaders.WriteRune(':')
canonicalHeaders.WriteString(signed[headerKeys[i]])
canonicalHeaders.WriteRune('\n')
if i > 0 {
signedHeaders.WriteRune(';')
}
signedHeaders.WriteString(headerKeys[i])
}
return signedHeaders.String(), canonicalHeaders.String()
}
func (s *SignerV4) parseUri(uriWithQuery string) (string, map[string]string, error) {
u, err := url.Parse(uriWithQuery)
if err != nil {
return "", nil, err
}
urlParams := make(map[string]string)
for k, vals := range u.Query() {
if len(vals) == 0 {
urlParams[k] = ""
} else {
urlParams[k] = vals[0] // param val should at most one value
}
}
return u.Path, urlParams, nil
}
func dateTimeISO8601() string {
return time.Now().In(gmtLoc).Format(ISO8601)
}
func (s *SignerV4) buildCanonicalRequest(method, uri, sha256Payload, canonicalHeaders, signedHeaders string, urlParams map[string]string) string {
builder := strings.Builder{}
builder.WriteString(method)
builder.WriteRune('\n')
builder.WriteString(uri)
builder.WriteRune('\n')
// Url params
canonParams := make(map[string]string)
var queryKeys []string
for k, v := range urlParams {
canonParams[k] = s.percentEncode(v)
queryKeys = append(queryKeys, k)
}
sort.Strings(queryKeys)
n := len(queryKeys)
for i := 0; i < n; i++ {
if i > 0 {
builder.WriteRune('&')
}
builder.WriteString(queryKeys[i])
v := canonParams[queryKeys[i]]
if len(v) != 0 {
builder.WriteRune('=')
builder.WriteString(v)
}
}
builder.WriteRune('\n')
builder.WriteString(canonicalHeaders)
builder.WriteRune('\n')
builder.WriteString(signedHeaders)
builder.WriteRune('\n')
builder.WriteString(sha256Payload)
return builder.String()
}
func (s *SignerV4) percentEncode(uri string) string {
u := url.QueryEscape(uri)
u = strings.ReplaceAll(u, "+", "%20")
return u
}
func (s *SignerV4) buildScope(date, region string) string {
var builder strings.Builder
builder.WriteString(date)
builder.WriteRune('/')
builder.WriteString(region)
builder.WriteRune('/')
builder.WriteString(signerV4ProductName)
builder.WriteRune('/')
builder.WriteString(authorizationV4SigningSaltFigure)
return builder.String()
}
func (s *SignerV4) buildSignMessage(canonReq, dateTime, scope string) string {
var builder strings.Builder
builder.WriteString(authorizationAlgorithmV4)
builder.WriteRune('\n')
builder.WriteString(dateTime)
builder.WriteRune('\n')
builder.WriteString(scope)
builder.WriteRune('\n')
builder.WriteString(fmt.Sprintf("%x", sha256.Sum256([]byte(canonReq))))
return builder.String()
}
func (s *SignerV4) hmacSha256(message, key []byte) ([]byte, error) {
hmacHasher := hmac.New(sha256.New, key)
_, err := hmacHasher.Write(message)
if err != nil {
return nil, err
}
return hmacHasher.Sum(nil), nil
}
func (s *SignerV4) buildSigningKey(accessKeySecret, region, date string) ([]byte, error) {
signDate, err := s.hmacSha256([]byte(date), []byte(authorizationV4SecretKeyPrefix+accessKeySecret))
if err != nil {
return nil, err
}
signRegion, err := s.hmacSha256([]byte(region), signDate)
if err != nil {
return nil, err
}
signService, err := s.hmacSha256([]byte(signerV4ProductName), signRegion)
if err != nil {
return nil, err
}
signAll, err := s.hmacSha256([]byte(authorizationV4SigningSaltFigure), signService)
if err != nil {
return nil, err
}
return signAll, nil
}
func (s *SignerV4) buildAuthorization(accessKeyID, signature, scope string) string {
return fmt.Sprintf("SLS4-HMAC-SHA256 Credential=%s/%s,Signature=%s", accessKeyID, scope, signature)
}