118 lines
2.7 KiB
Go
118 lines
2.7 KiB
Go
package oss
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/md5"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
pubKeyURLHeader = "X-Oss-Pub-Key-Url"
|
|
authorizationHeader = "Authorization"
|
|
)
|
|
|
|
type VerifyInput struct {
|
|
Path string
|
|
RawQuery string
|
|
Body []byte
|
|
Authorization string
|
|
PubKeyURLHdr string
|
|
}
|
|
|
|
func VerifyCallback(in VerifyInput) error {
|
|
pubKeyURLBase64 := in.PubKeyURLHdr
|
|
if pubKeyURLBase64 == "" {
|
|
return fmt.Errorf("missing %s", pubKeyURLHeader)
|
|
}
|
|
pubKeyURLBytes, err := base64.StdEncoding.DecodeString(pubKeyURLBase64)
|
|
if err != nil {
|
|
return fmt.Errorf("decode pub key url: %w", err)
|
|
}
|
|
pubKeyURL := string(pubKeyURLBytes)
|
|
if !isTrustedPubKeyURL(pubKeyURL) {
|
|
return fmt.Errorf("untrusted pub key url: %s", pubKeyURL)
|
|
}
|
|
|
|
pubKeyPEM, err := fetchPublicKey(pubKeyURL)
|
|
if err != nil {
|
|
return fmt.Errorf("fetch pub key: %w", err)
|
|
}
|
|
|
|
authBase64 := in.Authorization
|
|
if authBase64 == "" {
|
|
return fmt.Errorf("missing %s", authorizationHeader)
|
|
}
|
|
signature, err := base64.StdEncoding.DecodeString(authBase64)
|
|
if err != nil {
|
|
return fmt.Errorf("decode authorization: %w", err)
|
|
}
|
|
|
|
authString, err := buildAuthString(in.Path, in.RawQuery, in.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
digest := md5.Sum([]byte(authString))
|
|
|
|
block, _ := pem.Decode(pubKeyPEM)
|
|
if block == nil {
|
|
return fmt.Errorf("invalid pub key pem")
|
|
}
|
|
pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
if err != nil {
|
|
return fmt.Errorf("parse pub key: %w", err)
|
|
}
|
|
pubKey, ok := pubInterface.(*rsa.PublicKey)
|
|
if !ok {
|
|
return fmt.Errorf("pub key is not rsa")
|
|
}
|
|
if err := rsa.VerifyPKCS1v15(pubKey, crypto.MD5, digest[:], signature); err != nil {
|
|
return fmt.Errorf("verify signature: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildAuthString(path, query string, body []byte) (string, error) {
|
|
decodedPath, err := url.PathUnescape(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("unescape path: %w", err)
|
|
}
|
|
if query == "" {
|
|
return decodedPath + "\n" + string(body), nil
|
|
}
|
|
return decodedPath + "?" + query + "\n" + string(body), nil
|
|
}
|
|
|
|
func isTrustedPubKeyURL(raw string) bool {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if u.Scheme != "http" && u.Scheme != "https" {
|
|
return false
|
|
}
|
|
host := strings.ToLower(u.Hostname())
|
|
return host == "gosspublic.alicdn.com" || strings.HasSuffix(host, ".alicdn.com")
|
|
}
|
|
|
|
func fetchPublicKey(pubKeyURL string) ([]byte, error) {
|
|
client := &http.Client{Timeout: 3 * time.Second}
|
|
resp, err := client.Get(pubKeyURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("pub key http status %d", resp.StatusCode)
|
|
}
|
|
return io.ReadAll(resp.Body)
|
|
}
|