124 lines
2.9 KiB
Go
124 lines
2.9 KiB
Go
package wayne
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
HeaderService = "X-Wayne-Service"
|
|
HeaderTimestamp = "X-Wayne-Timestamp"
|
|
HeaderNonce = "X-Wayne-Nonce"
|
|
HeaderSignature = "X-Wayne-Signature"
|
|
|
|
SignaturePrefix = "sha256="
|
|
)
|
|
|
|
var (
|
|
ErrMissingSecret = errors.New("wayne service api secret key is empty")
|
|
ErrMissingHeader = errors.New("wayne service header value is empty")
|
|
)
|
|
|
|
type SignedHeaders struct {
|
|
Service string
|
|
Timestamp string
|
|
Nonce string
|
|
Signature string
|
|
}
|
|
|
|
func BodySHA256Hex(body []byte) string {
|
|
sum := sha256.Sum256(body)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func SigningPayload(method, uri, timestamp, nonce string, body []byte) string {
|
|
return strings.Join([]string{
|
|
strings.ToUpper(strings.TrimSpace(method)),
|
|
uri,
|
|
timestamp,
|
|
nonce,
|
|
BodySHA256Hex(body),
|
|
}, "\n")
|
|
}
|
|
|
|
func Sign(secret, method, uri, timestamp, nonce string, body []byte) (string, error) {
|
|
secret = secretValue(secret)
|
|
if secret == "" {
|
|
return "", ErrMissingSecret
|
|
}
|
|
payload := SigningPayload(method, uri, timestamp, nonce, body)
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
_, _ = mac.Write([]byte(payload))
|
|
return SignaturePrefix + hex.EncodeToString(mac.Sum(nil)), nil
|
|
}
|
|
|
|
func Verify(secret, signature, method, uri, timestamp, nonce string, body []byte) bool {
|
|
expected, err := Sign(secret, method, uri, timestamp, nonce, body)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return subtle.ConstantTimeCompare([]byte(normalizeSignature(signature)), []byte(expected)) == 1
|
|
}
|
|
|
|
func BuildSignedHeaders(service, secret, method, uri string, body []byte, now time.Time) (SignedHeaders, error) {
|
|
service = strings.TrimSpace(service)
|
|
if service == "" {
|
|
return SignedHeaders{}, fmt.Errorf("%w: service", ErrMissingHeader)
|
|
}
|
|
timestamp := strconv.FormatInt(now.Unix(), 10)
|
|
nonce, err := NewNonce()
|
|
if err != nil {
|
|
return SignedHeaders{}, err
|
|
}
|
|
signature, err := Sign(secret, method, uri, timestamp, nonce, body)
|
|
if err != nil {
|
|
return SignedHeaders{}, err
|
|
}
|
|
return SignedHeaders{
|
|
Service: service,
|
|
Timestamp: timestamp,
|
|
Nonce: nonce,
|
|
Signature: signature,
|
|
}, nil
|
|
}
|
|
|
|
func (h SignedHeaders) Apply(req *http.Request) {
|
|
req.Header.Set(HeaderService, h.Service)
|
|
req.Header.Set(HeaderTimestamp, h.Timestamp)
|
|
req.Header.Set(HeaderNonce, h.Nonce)
|
|
req.Header.Set(HeaderSignature, h.Signature)
|
|
}
|
|
|
|
func NewNonce() (string, error) {
|
|
buf := make([]byte, 16)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(buf), nil
|
|
}
|
|
|
|
func normalizeSignature(signature string) string {
|
|
signature = strings.TrimSpace(signature)
|
|
if strings.HasPrefix(signature, SignaturePrefix) {
|
|
return signature
|
|
}
|
|
return SignaturePrefix + signature
|
|
}
|
|
|
|
func secretValue(secret string) string {
|
|
secret = strings.TrimSpace(secret)
|
|
if _, value, ok := strings.Cut(secret, ":"); ok {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
return secret
|
|
}
|