oss/signer/v4.go (288 lines of code) (raw):
package signer
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
const (
// headers
contentSha256Header = "x-oss-content-sha256"
iso8601DatetimeFormat = "20060102T150405Z"
iso8601DateFormat = "20060102"
algorithmV4 = "OSS4-HMAC-SHA256"
unsignedPayload = "UNSIGNED-PAYLOAD"
)
var noEscape [256]bool
func init() {
for i := 0; i < len(noEscape); i++ {
noEscape[i] = (i >= 'A' && i <= 'Z') ||
(i >= 'a' && i <= 'z') ||
(i >= '0' && i <= '9') ||
i == '-' ||
i == '.' ||
i == '_' ||
i == '~'
}
}
func toString(p *string) (v string) {
if p == nil {
return v
}
return *p
}
func escapePath(path string, encodeSep bool) string {
var buf bytes.Buffer
for i := 0; i < len(path); i++ {
c := path[i]
if noEscape[c] || (c == '/' && !encodeSep) {
buf.WriteByte(c)
} else {
fmt.Fprintf(&buf, "%%%02X", c)
}
}
return buf.String()
}
func isDefaultSignedHeader(low string) bool {
if strings.HasPrefix(low, ossHeaderPreifx) ||
low == "content-type" ||
low == "content-md5" {
return true
}
return false
}
func getCommonAdditionalHeaders(header http.Header, additionalHeaders []string) []string {
var keys []string
for _, k := range additionalHeaders {
lowK := strings.ToLower(k)
if isDefaultSignedHeader(lowK) {
//default signed header, skip
continue
} else if header.Get(lowK) != "" {
keys = append(keys, lowK)
}
}
sort.Strings(keys)
return keys
}
type SignerV4 struct {
}
func (s *SignerV4) calcStringToSign(datetime, scope, canonicalRequest string) string {
/**
StringToSign
"OSS4-HMAC-SHA256" + "\n" +
TimeStamp + "\n" +
Scope + "\n" +
Hex(SHA256Hash(Canonical Request))
*/
hash256 := sha256.New()
hash256.Write([]byte(canonicalRequest))
hashValue := hash256.Sum(nil)
canonicalHash := hex.EncodeToString(hashValue)
return "OSS4-HMAC-SHA256" + "\n" +
datetime + "\n" +
scope + "\n" +
canonicalHash
}
func (s *SignerV4) calcCanonicalRequest(signingCtx *SigningContext, additionalHeaders []string) string {
request := signingCtx.Request
/*
Canonical Request
HTTP Verb + "\n" +
Canonical URI + "\n" +
Canonical Query String + "\n" +
Canonical Headers + "\n" +
Additional Headers + "\n" +
Hashed PayLoad
*/
//Canonical Uri
uri := "/"
if signingCtx.Bucket != nil {
uri += *signingCtx.Bucket + "/"
}
if signingCtx.Key != nil {
uri += *signingCtx.Key
}
canonicalUri := escapePath(uri, false)
//Canonical Query
query := strings.Replace(request.URL.RawQuery, "+", "%20", -1)
values := make(map[string]string)
var params []string
for query != "" {
var key string
key, query, _ = strings.Cut(query, "&")
if key == "" {
continue
}
key, value, _ := strings.Cut(key, "=")
values[key] = value
params = append(params, key)
}
sort.Strings(params)
var buf strings.Builder
for _, k := range params {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
if len(values[k]) > 0 {
buf.WriteByte('=')
buf.WriteString(values[k])
}
}
canonicalQuery := buf.String()
//Canonical Headers
var headers []string
buf.Reset()
addHeadersMap := make(map[string]bool)
for _, k := range additionalHeaders {
addHeadersMap[strings.ToLower(k)] = true
}
for k := range request.Header {
lowk := strings.ToLower(k)
if isDefaultSignedHeader(lowk) {
headers = append(headers, lowk)
} else if _, ok := addHeadersMap[lowk]; ok {
headers = append(headers, lowk)
}
}
sort.Strings(headers)
for _, k := range headers {
headerValues := make([]string, len(request.Header.Values(k)))
for i, v := range request.Header.Values(k) {
headerValues[i] = strings.TrimSpace(v)
}
buf.WriteString(k)
buf.WriteString(":")
buf.WriteString(strings.Join(headerValues, ","))
buf.WriteString("\n")
}
canonicalHeaders := buf.String()
//Additional Headers
canonicalAdditionalHeaders := strings.Join(additionalHeaders, ";")
hashPayload := unsignedPayload
if val := request.Header.Get(contentSha256Header); val != "" {
hashPayload = val
}
buf.Reset()
buf.WriteString(request.Method)
buf.WriteString("\n")
buf.WriteString(canonicalUri)
buf.WriteString("\n")
buf.WriteString(canonicalQuery)
buf.WriteString("\n")
buf.WriteString(canonicalHeaders)
buf.WriteString("\n")
buf.WriteString(canonicalAdditionalHeaders)
buf.WriteString("\n")
buf.WriteString(hashPayload)
return buf.String()
}
func buildScope(date, region, product string) string {
return fmt.Sprintf("%s/%s/%s/aliyun_v4_request", date, region, product)
}
func (s *SignerV4) calcSignature(sk, date, region, product, stringToSign string) string {
hmacHash := func() hash.Hash { return sha256.New() }
signingKey := "aliyun_v4" + sk
h1 := hmac.New(func() hash.Hash { return sha256.New() }, []byte(signingKey))
io.WriteString(h1, date)
h1Key := h1.Sum(nil)
h2 := hmac.New(hmacHash, h1Key)
io.WriteString(h2, region)
h2Key := h2.Sum(nil)
h3 := hmac.New(hmacHash, h2Key)
io.WriteString(h3, product)
h3Key := h3.Sum(nil)
h4 := hmac.New(hmacHash, h3Key)
io.WriteString(h4, "aliyun_v4_request")
h4Key := h4.Sum(nil)
h := hmac.New(hmacHash, h4Key)
io.WriteString(h, stringToSign)
signature := hex.EncodeToString(h.Sum(nil))
return signature
}
func (s *SignerV4) authHeader(ctx context.Context, signingCtx *SigningContext) error {
request := signingCtx.Request
cred := signingCtx.Credentials
// Date
if signingCtx.Time.IsZero() {
signingCtx.Time = time.Now().Add(signingCtx.ClockOffset)
}
utcTime := signingCtx.Time.UTC()
datetime := utcTime.Format(iso8601DatetimeFormat)
date := utcTime.Format(iso8601DateFormat)
request.Header.Set(ossDateHeader, datetime)
request.Header.Set(dateHeader, utcTime.Format(http.TimeFormat))
// Credentials information
if cred.SecurityToken != "" {
request.Header.Set(securityTokenHeader, cred.SecurityToken)
}
// Other Headers
request.Header.Set(contentSha256Header, unsignedPayload)
// Scope
region := toString(signingCtx.Region)
product := toString(signingCtx.Product)
scope := buildScope(date, region, product)
additionalHeaders := getCommonAdditionalHeaders(request.Header, signingCtx.AdditionalHeaders)
// CanonicalRequest
canonicalRequest := s.calcCanonicalRequest(signingCtx, additionalHeaders)
// StringToSign
stringToSign := s.calcStringToSign(datetime, scope, canonicalRequest)
signingCtx.StringToSign = stringToSign
// Signature
signature := s.calcSignature(cred.AccessKeySecret, date, region, product, stringToSign)
// credential
var buf strings.Builder
buf.WriteString("OSS4-HMAC-SHA256 Credential=")
buf.WriteString(cred.AccessKeyID + "/" + scope)
if len(additionalHeaders) > 0 {
buf.WriteString(",AdditionalHeaders=")
buf.WriteString(strings.Join(additionalHeaders, ";"))
}
buf.WriteString(",Signature=")
buf.WriteString(signature)
request.Header.Set(authorizationHeader, buf.String())
//fmt.Printf("canonicalRequest:\n%s\n", canonicalRequest)
//fmt.Printf("stringToSign:\n%s\n", stringToSign)
return nil
}
func (s *SignerV4) authQuery(ctx context.Context, signingCtx *SigningContext) error {
request := signingCtx.Request
cred := signingCtx.Credentials
// Date
now := time.Now().UTC()
if signingCtx.Time.IsZero() {
signingCtx.Time = now.Add(defaultExpiresDuration)
}
if signingCtx.signTime != nil {
now = signingCtx.signTime.UTC()
}
datetime := now.Format(iso8601DatetimeFormat)
date := now.Format(iso8601DateFormat)
expires := signingCtx.Time.Unix() - now.Unix()
// Scope
region := toString(signingCtx.Region)
product := toString(signingCtx.Product)
scope := buildScope(date, region, product)
additionalHeaders := getCommonAdditionalHeaders(request.Header, signingCtx.AdditionalHeaders)
// Credentials information
query, _ := url.ParseQuery(request.URL.RawQuery)
if cred.SecurityToken != "" {
query.Add("x-oss-security-token", cred.SecurityToken)
}
query.Add("x-oss-signature-version", algorithmV4)
query.Add("x-oss-date", datetime)
query.Add("x-oss-expires", fmt.Sprintf("%v", expires))
query.Add("x-oss-credential", fmt.Sprintf("%s/%s", cred.AccessKeyID, scope))
if len(additionalHeaders) > 0 {
query.Add("x-oss-additional-headers", strings.Join(additionalHeaders, ";"))
}
request.URL.RawQuery = query.Encode()
// CanonicalRequest
canonicalRequest := s.calcCanonicalRequest(signingCtx, additionalHeaders)
// StringToSign
stringToSign := s.calcStringToSign(datetime, scope, canonicalRequest)
signingCtx.StringToSign = stringToSign
//fmt.Printf("canonicalRequest:\n%s\n", canonicalRequest)
//fmt.Printf("stringToSign:\n%s\n", stringToSign)
// Signature
signature := s.calcSignature(cred.AccessKeySecret, date, region, product, stringToSign)
// Authorization query
query.Add("x-oss-signature", signature)
request.URL.RawQuery = strings.Replace(query.Encode(), "+", "%20", -1)
return nil
}
func (s *SignerV4) Sign(ctx context.Context, signingCtx *SigningContext) error {
if signingCtx == nil {
return fmt.Errorf("SigningContext is null.")
}
if signingCtx.Credentials == nil || !signingCtx.Credentials.HasKeys() {
return fmt.Errorf("SigningContext.Credentials is null or empty.")
}
if signingCtx.Request == nil {
return fmt.Errorf("SigningContext.Request is null.")
}
if signingCtx.AuthMethodQuery {
return s.authQuery(ctx, signingCtx)
}
return s.authHeader(ctx, signingCtx)
}
func (s *SignerV4) IsSignedHeader(additionalHeaders []string, h string) bool {
return isDefaultSignedHeader(strings.ToLower(h)) || ContainsStr(additionalHeaders, h)
}
// ContainsStr Used to check if the string is in the slice
func ContainsStr(slice []string, str string) bool {
for _, item := range slice {
if strings.ToLower(str) == strings.ToLower(item) {
return true
}
}
return false
}