package signature import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "strconv" "testing" "time" ) type mockNonceStore struct { seen map[string]bool } func newMockNonceStore() *mockNonceStore { return &mockNonceStore{seen: map[string]bool{}} } func (m *mockNonceStore) SetIfNotExists(_ context.Context, appId, nonce string, _ int64) (bool, error) { key := appId + ":" + nonce if m.seen[key] { return true, nil } m.seen[key] = true return false, nil } func makeSignature(secret, stringToSign string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(stringToSign)) return hex.EncodeToString(mac.Sum(nil)) } func TestValidateSuccess(t *testing.T) { conf := SignatureConf{ AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"}, ValidWindowSeconds: 300, } v := NewValidator(conf, newMockNonceStore()) ts := strconv.FormatInt(time.Now().Unix(), 10) nonce := fmt.Sprintf("%x", time.Now().UnixNano()) sts := BuildStringToSign("POST", "/v1/public/order/create", "", []byte(`{"plan_id":1}`), "web-client", ts, nonce) sig := makeSignature("uB4G,XxL2{7b", sts) if err := v.Validate(context.Background(), "web-client", ts, nonce, sig, sts); err != nil { t.Fatalf("expected success, got %v", err) } } func TestValidateExpired(t *testing.T) { conf := SignatureConf{ AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"}, ValidWindowSeconds: 300, } v := NewValidator(conf, newMockNonceStore()) ts := strconv.FormatInt(time.Now().Unix()-400, 10) nonce := "abc" sts := BuildStringToSign("GET", "/v1/public/user/info", "", nil, "web-client", ts, nonce) sig := makeSignature("uB4G,XxL2{7b", sts) if err := v.Validate(context.Background(), "web-client", ts, nonce, sig, sts); err != ErrSignatureExpired { t.Fatalf("expected ErrSignatureExpired, got %v", err) } } func TestValidateReplay(t *testing.T) { conf := SignatureConf{ AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"}, ValidWindowSeconds: 300, } v := NewValidator(conf, newMockNonceStore()) ts := strconv.FormatInt(time.Now().Unix(), 10) nonce := "same-nonce-replay" sts := BuildStringToSign("GET", "/v1/public/user/info", "", nil, "web-client", ts, nonce) sig := makeSignature("uB4G,XxL2{7b", sts) _ = v.Validate(context.Background(), "web-client", ts, nonce, sig, sts) if err := v.Validate(context.Background(), "web-client", ts, nonce, sig, sts); err != ErrSignatureReplay { t.Fatalf("expected ErrSignatureReplay, got %v", err) } } func TestValidateInvalidSignature(t *testing.T) { conf := SignatureConf{ AppSecrets: map[string]string{"web-client": "uB4G,XxL2{7b"}, ValidWindowSeconds: 300, } v := NewValidator(conf, newMockNonceStore()) ts := strconv.FormatInt(time.Now().Unix(), 10) nonce := "nonce-invalid-sig" sts := BuildStringToSign("POST", "/v1/public/order/create", "", []byte(`{"plan_id":1}`), "web-client", ts, nonce) if err := v.Validate(context.Background(), "web-client", ts, nonce, "badsignature", sts); err != ErrSignatureInvalid { t.Fatalf("expected ErrSignatureInvalid, got %v", err) } } func TestBuildStringToSignCanonicalQuery(t *testing.T) { got := BuildStringToSign( "get", "/v1/public/order/list", "b=2&a=1&a=3&c=", nil, "web-client", "1700000000", "nonce-1", ) want := "GET\n/v1/public/order/list\na=1&b=2&c=\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\nweb-client\n1700000000\nnonce-1" if got != want { t.Fatalf("unexpected stringToSign\nwant: %s\ngot: %s", want, got) } }