package signer import ( "crypto/ecdsa" "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "net/url" "strconv" "strings" "time" "gitlink.org.cn/cloudream/common/utils/sort2" ) var ErrSignatureMismatch = fmt.Errorf("signature mismatch") var ErrExpired = fmt.Errorf("signature expired") const ( DateQueryKey = "x-jcs-date" ExpiresQueryKey = "x-jcs-expires" SignatureQueryKey = "x-jcs-signature" AccessKeyIDQueryKey = "x-jcs-access-key-id" AlgorithmQueryKey = "x-jcs-algorithm" AlgorithmValue = "ECDSA256" DateFormat = "20060102T150405Z" ) type SignKey struct { privateKey *ecdsa.PrivateKey accessKeyID string } func PresignURL(key *SignKey, method string, u *url.URL, signingTime time.Time, expiresSeconds int) error { date := signingTime.Format(DateFormat) queries := u.Query() queries.Set(DateQueryKey, date) queries.Set(ExpiresQueryKey, fmt.Sprintf("%v", expiresSeconds)) queries.Set(AccessKeyIDQueryKey, key.accessKeyID) queries.Set(AlgorithmQueryKey, AlgorithmValue) ss := makeStringToSign(method, u.Host, u.Path, queries) ssHash := sha256.Sum256([]byte(ss)) sign, err := ecdsa.SignASN1(rand.Reader, key.privateKey, ssHash[:]) if err != nil { return err } queries.Set(SignatureQueryKey, hex.EncodeToString(sign)) u.RawQuery = queries.Encode() return nil } type VerifyKey struct { publicKey *ecdsa.PublicKey } func NewVerifyKey(publicKey *ecdsa.PublicKey) *VerifyKey { return &VerifyKey{publicKey: publicKey} } func GetAccessKeyID(u *url.URL) string { return u.Query().Get(AccessKeyIDQueryKey) } func VerifyPresigned(key *VerifyKey, method string, u *url.URL) error { queries := u.Query() dateStr := queries.Get(DateQueryKey) if dateStr == "" { return fmt.Errorf("missing date in query string") } date, err := time.Parse(DateFormat, dateStr) if err != nil { return fmt.Errorf("invalid date format: %v", err) } expiresStr := queries.Get(ExpiresQueryKey) if expiresStr == "" { return fmt.Errorf("missing expires in query string") } expires, err := strconv.Atoi(expiresStr) if err != nil { return fmt.Errorf("invalid expires format: %v", err) } if time.Now().After(date.Add(time.Duration(expires) * time.Second)) { return ErrExpired } signatureStr := queries.Get(SignatureQueryKey) signature, err := hex.DecodeString(signatureStr) if err != nil { return fmt.Errorf("invalid signature format: %v", err) } queries.Del(SignatureQueryKey) ss := makeStringToSign(method, u.Host, u.Path, queries) ssHash := sha256.Sum256([]byte(ss)) if !ecdsa.VerifyASN1(key.publicKey, ssHash[:], signature) { return ErrSignatureMismatch } return nil } func makeStringToSign(method string, host string, path string, queries url.Values) string { type queryKv struct { Key string Values []string } var queryKvs []queryKv for k, vs := range queries { vs = sort2.SortAsc(vs) queryKvs = append(queryKvs, queryKv{Key: k, Values: vs}) } queryKvs = sort2.Sort(queryKvs, func(left, right queryKv) int { return strings.Compare(left.Key, right.Key) }) var str strings.Builder str.WriteString(method) str.WriteByte('\n') str.WriteString(host) str.WriteByte('\n') str.WriteString(path) str.WriteByte('\n') for _, kv := range queryKvs { str.WriteString(kv.Key) str.WriteByte('=') for i, v := range kv.Values { if i > 0 { str.WriteByte(',') } str.WriteString(v) } str.WriteByte('\n') } return str.String() }