chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user