Files
story/internal/deviceauth/verifier_test.go
T

122 lines
4.3 KiB
Go

package deviceauth
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
type fakeRepository struct {
identity Identity
err error
}
func (r fakeRepository) FindByPublicID(context.Context, string) (Identity, error) {
return r.identity, r.err
}
type memoryReplayGuard struct{ claimed map[string]bool }
func (g *memoryReplayGuard) Claim(_ context.Context, deviceID, nonce string, _ time.Duration) (bool, error) {
key := deviceID + nonce
if g.claimed[key] {
return false, nil
}
g.claimed[key] = true
return true, nil
}
func TestVerifierAcceptsValidRequestAndRejectsReplay(t *testing.T) {
secret := []byte("01234567890123456789012345678901")
now := time.Date(2026, 8, 23, 12, 0, 0, 0, time.UTC)
verifier, err := NewVerifier(
fakeRepository{identity: Identity{ID: "internal", PublicID: "device-1", TenantID: "tenant-1", Status: "registered", CredentialRef: "local://one"}},
StaticSecretResolver{"local://one": secret},
&memoryReplayGuard{claimed: map[string]bool{}},
5*time.Minute,
)
if err != nil {
t.Fatal(err)
}
verifier.now = func() time.Time { return now }
request := signedRequest(secret, now, "0123456789abcdef", `{"firmware":"1"}`)
identity, err := verifier.Verify(request)
if err != nil || identity.PublicID != "device-1" {
t.Fatalf("identity = %#v, err = %v", identity, err)
}
if body := readTestBody(t, request); body != `{"firmware":"1"}` {
t.Fatalf("restored body = %q", body)
}
request = signedRequest(secret, now, "0123456789abcdef", `{"firmware":"1"}`)
if _, err := verifier.Verify(request); !errors.Is(err, ErrReplayDetected) {
t.Fatalf("replay error = %v", err)
}
}
func TestVerifierRejectsTamperExpiredAndInactive(t *testing.T) {
secret := []byte("01234567890123456789012345678901")
now := time.Date(2026, 8, 23, 12, 0, 0, 0, time.UTC)
repository := fakeRepository{identity: Identity{PublicID: "device-1", TenantID: "tenant-1", Status: "registered", CredentialRef: "local://one"}}
verifier, _ := NewVerifier(repository, StaticSecretResolver{"local://one": secret}, &memoryReplayGuard{claimed: map[string]bool{}}, time.Minute)
verifier.now = func() time.Time { return now }
tampered := signedRequest(secret, now, "0123456789abcdef", `{}`)
tampered.Header.Set(HeaderContentSHA256, strings.Repeat("0", 64))
if _, err := verifier.Verify(tampered); !errors.Is(err, ErrCredentialInvalid) {
t.Fatalf("tamper error = %v", err)
}
expired := signedRequest(secret, now.Add(-2*time.Minute), "fedcba9876543210", `{}`)
if _, err := verifier.Verify(expired); !errors.Is(err, ErrRequestExpired) {
t.Fatalf("expired error = %v", err)
}
verifier.repository = fakeRepository{identity: Identity{PublicID: "device-1", TenantID: "tenant-1", Status: "revoked", CredentialRef: "local://one"}}
inactive := signedRequest(secret, now, "abcdef0123456789", `{}`)
if _, err := verifier.Verify(inactive); !errors.Is(err, ErrDeviceInactive) {
t.Fatalf("inactive error = %v", err)
}
}
func TestIdentityContextAndEmptyBodyDigest(t *testing.T) {
expected := Identity{ID: "internal", PublicID: "device", TenantID: "tenant", Status: "registered"}
if _, ok := FromContext(context.Background()); ok {
t.Fatal("empty context must not contain a device identity")
}
actual, ok := FromContext(WithIdentity(context.Background(), expected))
if !ok || actual != expected {
t.Fatalf("identity=%+v ok=%t", actual, ok)
}
digest := sha256.Sum256(nil)
if EmptyBodyDigest() != hex.EncodeToString(digest[:]) {
t.Fatalf("empty body digest=%s", EmptyBodyDigest())
}
}
func signedRequest(secret []byte, timestamp time.Time, nonce, body string) *http.Request {
request := httptest.NewRequest(http.MethodPost, "/device/v2/example?x=1", strings.NewReader(body))
digest := sha256.Sum256([]byte(body))
digestText := hex.EncodeToString(digest[:])
timestampText := timestamp.Format(time.RFC3339)
request.Header.Set(HeaderDeviceID, "device-1")
request.Header.Set(HeaderTimestamp, timestampText)
request.Header.Set(HeaderNonce, nonce)
request.Header.Set(HeaderContentSHA256, digestText)
request.Header.Set(HeaderSignature, SignatureHeader(secret, request.Method, request.URL.RequestURI(), timestampText, nonce, digestText))
return request
}
func readTestBody(t *testing.T, request *http.Request) string {
t.Helper()
body, err := io.ReadAll(request.Body)
if err != nil {
t.Fatal(err)
}
return string(body)
}