chore: establish local platform baseline

This commit is contained in:
Codex Local
2026-08-26 06:41:54 +08:00
commit 29bdf4900f
575 changed files with 95644 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
package deviceauth
import (
"bytes"
"context"
"errors"
"io"
"os"
"path/filepath"
"regexp"
"strings"
)
const deviceSecretReferencePrefix = "device-secret://"
var deviceSecretNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`)
// DirectorySecretResolver resolves a per-device credential from a controlled
// secret mount. It is suitable for Docker/Kubernetes Secrets and cloud KMS CSI
// drivers: the database stores only device-secret://<name>, never secret bytes.
// Files are read for every verification so an atomic secret-mount rotation is
// observed without restarting the API process.
type DirectorySecretResolver struct {
root string
}
func NewDirectorySecretResolver(root string) (*DirectorySecretResolver, error) {
if strings.TrimSpace(root) == "" || !filepath.IsAbs(root) {
return nil, errors.New("device secret directory must be an absolute path")
}
absolute, err := filepath.Abs(filepath.Clean(root))
if err != nil {
return nil, errors.New("resolve device secret directory")
}
resolved, err := filepath.EvalSymlinks(absolute)
if err != nil {
return nil, errors.New("resolve device secret directory")
}
info, err := os.Stat(resolved)
if err != nil || !info.IsDir() {
return nil, errors.New("device secret directory is not readable")
}
return &DirectorySecretResolver{root: resolved}, nil
}
func (r *DirectorySecretResolver) Resolve(ctx context.Context, reference string) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if !strings.HasPrefix(reference, deviceSecretReferencePrefix) {
return nil, ErrCredentialInvalid
}
name := strings.TrimPrefix(reference, deviceSecretReferencePrefix)
if !deviceSecretNamePattern.MatchString(name) || name == "." || name == ".." {
return nil, ErrCredentialInvalid
}
candidate := filepath.Join(r.root, name)
resolved, err := filepath.EvalSymlinks(candidate)
if err != nil || !pathWithin(r.root, resolved) {
return nil, ErrCredentialInvalid
}
file, err := os.Open(resolved)
if err != nil {
return nil, ErrCredentialInvalid
}
defer file.Close()
secret, err := io.ReadAll(io.LimitReader(file, 4097))
if err != nil || len(secret) > 4096 {
return nil, ErrCredentialInvalid
}
secret = bytes.TrimRight(secret, "\r\n")
if len(secret) < 32 {
return nil, ErrCredentialInvalid
}
if err := ctx.Err(); err != nil {
return nil, err
}
return append([]byte(nil), secret...), nil
}
func pathWithin(root, candidate string) bool {
relative, err := filepath.Rel(root, candidate)
return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && !filepath.IsAbs(relative)
}
@@ -0,0 +1,59 @@
package deviceauth
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
)
func TestDirectorySecretResolverReadsPerDeviceSecretAndObservesRotation(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "terminal-001")
first := strings.Repeat("a", 40)
if err := os.WriteFile(path, []byte(first+"\n"), 0o600); err != nil {
t.Fatal(err)
}
resolver, err := NewDirectorySecretResolver(root)
if err != nil {
t.Fatal(err)
}
secret, err := resolver.Resolve(context.Background(), "device-secret://terminal-001")
if err != nil || string(secret) != first {
t.Fatalf("secret=%q err=%v", secret, err)
}
second := strings.Repeat("b", 48)
if err := os.WriteFile(path, []byte(second), 0o600); err != nil {
t.Fatal(err)
}
secret, err = resolver.Resolve(context.Background(), "device-secret://terminal-001")
if err != nil || string(secret) != second {
t.Fatalf("rotated secret=%q err=%v", secret, err)
}
}
func TestDirectorySecretResolverRejectsUnsafeOrInvalidReferences(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "short"), []byte("too-short"), 0o600); err != nil {
t.Fatal(err)
}
resolver, err := NewDirectorySecretResolver(root)
if err != nil {
t.Fatal(err)
}
for _, reference := range []string{
"local-dev://terminal-1", "device-secret://../outside", "device-secret://nested/file", "device-secret://short", "device-secret://missing",
} {
if _, err := resolver.Resolve(context.Background(), reference); !errors.Is(err, ErrCredentialInvalid) {
t.Fatalf("Resolve(%q) error=%v", reference, err)
}
}
}
func TestDirectorySecretResolverRequiresExistingAbsoluteDirectory(t *testing.T) {
if _, err := NewDirectorySecretResolver("relative/secrets"); err == nil {
t.Fatal("relative directory must be rejected")
}
}
+24
View File
@@ -0,0 +1,24 @@
package deviceauth
import "context"
type contextKey string
const identityKey contextKey = "device-identity"
type Identity struct {
ID string
PublicID string
TenantID string
Status string
CredentialRef string
}
func WithIdentity(ctx context.Context, identity Identity) context.Context {
return context.WithValue(ctx, identityKey, identity)
}
func FromContext(ctx context.Context) (Identity, bool) {
identity, ok := ctx.Value(identityKey).(Identity)
return identity, ok
}
+29
View File
@@ -0,0 +1,29 @@
package deviceauth
import (
"context"
"crypto/sha256"
"encoding/hex"
"time"
"github.com/redis/go-redis/v9"
)
type ReplayGuard interface {
Claim(context.Context, string, string, time.Duration) (bool, error)
}
type RedisReplayGuard struct {
client *redis.Client
prefix string
}
func NewRedisReplayGuard(client *redis.Client, prefix string) *RedisReplayGuard {
return &RedisReplayGuard{client: client, prefix: prefix}
}
func (g *RedisReplayGuard) Claim(ctx context.Context, deviceID, nonce string, ttl time.Duration) (bool, error) {
digest := sha256.Sum256([]byte(deviceID + "\x00" + nonce))
key := g.prefix + ":device-nonce:" + hex.EncodeToString(digest[:16])
return g.client.SetNX(ctx, key, "1", ttl).Result()
}
@@ -0,0 +1,29 @@
package deviceauth
import (
"context"
"os"
"testing"
"time"
"github.com/redis/go-redis/v9"
)
func TestRedisReplayGuardClaimsNonceOnce(t *testing.T) {
address := os.Getenv("TEST_REDIS_ADDR")
if address == "" {
t.Skip("TEST_REDIS_ADDR is not set")
}
client := redis.NewClient(&redis.Options{Addr: address})
defer client.Close()
guard := NewRedisReplayGuard(client, "voice-platform-test")
nonce := time.Now().UTC().Format("20060102T150405.000000000")
claimed, err := guard.Claim(context.Background(), "device-test", nonce, time.Minute)
if err != nil || !claimed {
t.Fatalf("first claim = %v, err = %v", claimed, err)
}
claimed, err = guard.Claim(context.Background(), "device-test", nonce, time.Minute)
if err != nil || claimed {
t.Fatalf("second claim = %v, err = %v", claimed, err)
}
}
+32
View File
@@ -0,0 +1,32 @@
package deviceauth
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var ErrDeviceNotFound = errors.New("device identity was not found")
type Repository struct{ pool *pgxpool.Pool }
func NewRepository(pool *pgxpool.Pool) *Repository { return &Repository{pool: pool} }
func (r *Repository) FindByPublicID(ctx context.Context, publicID string) (Identity, error) {
var identity Identity
err := r.pool.QueryRow(ctx, `
SELECT id::text, public_id::text, tenant_id::text, status, credential_ref
FROM device
WHERE public_id = $1::uuid`, publicID,
).Scan(&identity.ID, &identity.PublicID, &identity.TenantID, &identity.Status, &identity.CredentialRef)
if errors.Is(err, pgx.ErrNoRows) {
return Identity{}, ErrDeviceNotFound
}
if err != nil {
return Identity{}, fmt.Errorf("find device identity: %w", err)
}
return identity, nil
}
@@ -0,0 +1,34 @@
package deviceauth
import (
"context"
"errors"
"os"
"testing"
"data-terminal-cloud/internal/database"
)
func TestRepositoryFindsRegisteredDeviceWithoutCredentialMaterial(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := database.Open(ctx, databaseURL, "device-auth-integration-test")
if err != nil {
t.Fatal(err)
}
defer pool.Close()
repository := NewRepository(pool)
identity, err := repository.FindByPublicID(ctx, "10000000-0000-0000-0000-000000000511")
if err != nil {
t.Fatal(err)
}
if identity.Status != "registered" || identity.TenantID != "10000000-0000-0000-0000-000000000001" || identity.CredentialRef == "" {
t.Fatalf("identity = %#v", identity)
}
if _, err := repository.FindByPublicID(ctx, "30000000-0000-0000-0000-000000000511"); !errors.Is(err, ErrDeviceNotFound) {
t.Fatalf("missing device error = %v", err)
}
}
+144
View File
@@ -0,0 +1,144 @@
package deviceauth
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
)
const (
HeaderDeviceID = "X-Device-Id"
HeaderTimestamp = "X-Device-Timestamp"
HeaderNonce = "X-Device-Nonce"
HeaderContentSHA256 = "X-Device-Content-SHA256"
HeaderSignature = "X-Device-Signature"
)
var (
ErrCredentialsMissing = errors.New("device credentials are missing")
ErrCredentialInvalid = errors.New("device credential is invalid")
ErrDeviceInactive = errors.New("device is not active")
ErrRequestExpired = errors.New("device request timestamp is outside the allowed window")
ErrReplayDetected = errors.New("device request nonce was already used")
)
type IdentityRepository interface {
FindByPublicID(context.Context, string) (Identity, error)
}
type SecretResolver interface {
Resolve(context.Context, string) ([]byte, error)
}
type StaticSecretResolver map[string][]byte
func (r StaticSecretResolver) Resolve(_ context.Context, reference string) ([]byte, error) {
secret, ok := r[reference]
if !ok || len(secret) < 32 {
return nil, ErrCredentialInvalid
}
return append([]byte(nil), secret...), nil
}
type Verifier struct {
repository IdentityRepository
secrets SecretResolver
replay ReplayGuard
maxSkew time.Duration
now func() time.Time
}
func NewVerifier(repository IdentityRepository, secrets SecretResolver, replay ReplayGuard, maxSkew time.Duration) (*Verifier, error) {
if repository == nil || secrets == nil || replay == nil || maxSkew <= 0 {
return nil, errors.New("device repository, secret resolver, replay guard and positive clock skew are required")
}
return &Verifier{repository: repository, secrets: secrets, replay: replay, maxSkew: maxSkew, now: time.Now}, nil
}
func (v *Verifier) Verify(request *http.Request) (Identity, error) {
deviceID := strings.TrimSpace(request.Header.Get(HeaderDeviceID))
timestampText := strings.TrimSpace(request.Header.Get(HeaderTimestamp))
nonce := strings.TrimSpace(request.Header.Get(HeaderNonce))
contentDigest := strings.ToLower(strings.TrimSpace(request.Header.Get(HeaderContentSHA256)))
signatureText := strings.TrimSpace(request.Header.Get(HeaderSignature))
if deviceID == "" || timestampText == "" || len(nonce) < 16 || len(nonce) > 128 || contentDigest == "" || signatureText == "" {
return Identity{}, ErrCredentialsMissing
}
timestamp, err := time.Parse(time.RFC3339, timestampText)
if err != nil || absoluteDuration(v.now().Sub(timestamp)) > v.maxSkew {
return Identity{}, ErrRequestExpired
}
body, err := io.ReadAll(request.Body)
if err != nil {
return Identity{}, fmt.Errorf("read signed request body: %w", err)
}
request.Body.Close()
request.Body = io.NopCloser(bytes.NewReader(body))
digest := sha256.Sum256(body)
actualDigest := hex.EncodeToString(digest[:])
if !hmac.Equal([]byte(actualDigest), []byte(contentDigest)) {
return Identity{}, ErrCredentialInvalid
}
identity, err := v.repository.FindByPublicID(request.Context(), deviceID)
if err != nil {
if errors.Is(err, ErrDeviceNotFound) {
return Identity{}, ErrCredentialInvalid
}
return Identity{}, fmt.Errorf("lookup device identity: %w", err)
}
if identity.Status != "registered" || identity.TenantID == "" {
return Identity{}, ErrDeviceInactive
}
secret, err := v.secrets.Resolve(request.Context(), identity.CredentialRef)
if err != nil {
return Identity{}, ErrCredentialInvalid
}
providedSignature, err := base64.RawURLEncoding.DecodeString(signatureText)
if err != nil {
return Identity{}, ErrCredentialInvalid
}
expected := Sign(secret, request.Method, request.URL.RequestURI(), timestampText, nonce, actualDigest)
if !hmac.Equal(providedSignature, expected) {
return Identity{}, ErrCredentialInvalid
}
claimed, err := v.replay.Claim(request.Context(), identity.PublicID, nonce, v.maxSkew*2)
if err != nil {
return Identity{}, fmt.Errorf("claim device request nonce: %w", err)
}
if !claimed {
return Identity{}, ErrReplayDetected
}
return identity, nil
}
func Sign(secret []byte, method, requestURI, timestamp, nonce, contentDigest string) []byte {
canonical := strings.Join([]string{strings.ToUpper(method), requestURI, timestamp, nonce, strings.ToLower(contentDigest)}, "\n")
mac := hmac.New(sha256.New, secret)
_, _ = mac.Write([]byte(canonical))
return mac.Sum(nil)
}
func SignatureHeader(secret []byte, method, requestURI, timestamp, nonce, contentDigest string) string {
return base64.RawURLEncoding.EncodeToString(Sign(secret, method, requestURI, timestamp, nonce, contentDigest))
}
func EmptyBodyDigest() string {
digest := sha256.Sum256(nil)
return hex.EncodeToString(digest[:])
}
func absoluteDuration(duration time.Duration) time.Duration {
if duration < 0 {
return -duration
}
return duration
}
+121
View File
@@ -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)
}