middleware

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 22, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

Signet HTTP Middleware

Production-ready HTTP middleware for Signet authentication with two-step cryptographic verification.

Features

  • Two-Step Verification: Validates master→ephemeral→request signature chain
  • Replay Prevention: Per-token nonce tracking prevents request replay
  • Clock Skew Tolerance: Configurable time window for client/server synchronization
  • Pluggable Storage: Interface-based design supports Redis, databases, or custom backends
  • Multi-Issuer Support: Handle tokens from multiple authorities
  • Observability: Built-in hooks for logging and metrics
  • Thread-Safe: Designed for concurrent request handling
  • Graceful Degradation: Configurable error handling and recovery

Quick Start

Basic Setup
import (
    "net/http"
    "github.com/agentic-research/signet/pkg/http/middleware"
)

// Create middleware with simple configuration
auth := middleware.SignetMiddleware(
    middleware.WithMasterKey(masterPublicKey),
    middleware.WithClockSkew(30*time.Second),
)

// Apply to your handlers
protected := auth(yourHandler)
http.ListenAndServe(":8080", protected)
Accessing Authentication Context
func yourHandler(w http.ResponseWriter, r *http.Request) {
    // Get authentication details from verified requests
    authCtx, ok := middleware.GetAuthContext(r)
    if !ok {
        // Should not happen if middleware is properly configured
        http.Error(w, "No auth context", 500)
        return
    }

    // Access authentication information
    fmt.Printf("Token: %s, Purpose: %s\n", authCtx.TokenID, authCtx.Purpose)
}

Configuration Options

Storage Backends
In-Memory (Default)
// Suitable for single-instance deployments
auth := middleware.SignetMiddleware(
    middleware.WithTokenStore(middleware.NewMemoryTokenStore()),
    middleware.WithNonceStore(middleware.NewMemoryNonceStore()),
)
Redis (Distributed)
// For distributed deployments
tokenStore := middleware.NewRedisTokenStore(redisClient, "signet:tokens:")
nonceStore := middleware.NewRedisNonceStore(redisClient, "signet:nonces:")

auth := middleware.SignetMiddleware(
    middleware.WithTokenStore(tokenStore),
    middleware.WithNonceStore(nonceStore),
)
Key Management
Static Key
// Simple deployments with a single master key
middleware.WithMasterKey(masterPublicKey)
Multi-Issuer
// Support multiple token issuers
provider := middleware.NewMultiKeyProvider()
provider.AddKey("issuer1", publicKey1)
provider.AddKey("issuer2", publicKey2)

middleware.WithKeyProvider(provider)
Dynamic Key Provider
// Implement KeyProvider for dynamic key management
type MyKeyProvider struct {
    // Your implementation
}

func (p *MyKeyProvider) GetMasterKey(ctx context.Context, issuerID string) (ed25519.PublicKey, error) {
    // Fetch from PKI, certificate store, or KMS
    return fetchKeyFromAuthority(issuerID)
}

middleware.WithKeyProvider(&MyKeyProvider{})
Error Handling
// JSON error responses
middleware.WithJSONErrors()

// Custom error handler
middleware.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) {
    // Your custom error response
    status := mapErrorToStatus(err)
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(customErrorResponse(err))
})
Observability
// Integrate with your logging system
type MyLogger struct{}

func (l *MyLogger) Info(msg string, args ...interface{}) {
    // Your logging implementation
}

// Integrate with metrics collection
type MyMetrics struct{}

func (m *MyMetrics) RecordAuthResult(result string, duration time.Duration) {
    // Record to Prometheus, StatsD, etc.
}

auth := middleware.SignetMiddleware(
    middleware.WithLogger(&MyLogger{}),
    middleware.WithMetrics(&MyMetrics{}),
)
Access Control
// Skip authentication for specific paths
middleware.WithSkipPaths("/health", "/metrics", "/public")

// Require specific token purposes
middleware.WithRequiredPurposes("api-access", "admin-access")

Custom Request Canonicalization

For applications requiring custom request signing:

type CustomBuilder struct{}

func (b *CustomBuilder) Build(r *http.Request, proof *header.SignetProof) ([]byte, error) {
    // Include additional headers or parameters in the signed payload
    canonical := fmt.Sprintf("%s|%s|%s|%d",
        r.Method,
        r.URL.String(),
        r.Header.Get("X-Request-ID"),
        proof.Timestamp,
    )
    return []byte(canonical), nil
}

middleware.WithRequestBuilder(&CustomBuilder{})

Interfaces

The middleware is built around clean interfaces for maximum flexibility:

TokenStore

Manages token storage and retrieval. Implement for custom backends.

NonceStore

Handles replay prevention. Must provide atomic check-and-store operations.

KeyProvider

Retrieves master public keys. Enables dynamic key management and rotation.

RequestBuilder

Constructs canonical request representation for signature verification.

Logger & Metrics

Standard interfaces for observability integration.

Security Considerations

  1. Token Storage: In distributed systems, use shared storage (Redis, database) for token consistency
  2. Nonce Tracking: Essential for replay prevention - ensure atomic operations
  3. Clock Skew: Balance between security and usability (30-60 seconds recommended)
  4. Key Rotation: Use KeyProvider interface for dynamic key management
  5. TLS Required: Always use HTTPS in production to prevent token interception

Performance

  • Middleware adds ~1-2ms overhead for in-memory operations
  • Redis-backed stores add network latency (typically 1-5ms)
  • Cryptographic verification is performed using efficient Ed25519 operations
  • Automatic cleanup prevents memory leaks in long-running services

Error Codes

The middleware returns consistent error codes for client handling:

  • MISSING_PROOF - No Signet-Proof header provided
  • INVALID_PROOF - Malformed proof header
  • TOKEN_NOT_FOUND - Unknown or revoked token
  • TOKEN_EXPIRED - Token past expiry time
  • CLOCK_SKEW - Request timestamp outside acceptable bounds
  • REPLAY_DETECTED - Request already processed
  • INVALID_SIGNATURE - Cryptographic verification failed
  • PURPOSE_MISMATCH - Token not authorized for operation

Thread Safety

All provided implementations are thread-safe and suitable for concurrent use. Custom implementations should ensure proper synchronization.

Testing

The package includes comprehensive tests demonstrating various scenarios:

go test ./pkg/http/middleware -v

See example_test.go for usage patterns and signet_test.go for implementation details.

Documentation

Overview

Package middleware provides production-ready HTTP middleware for Signet authentication.

Signet replaces bearer tokens with cryptographic proof-of-possession, implementing two-step verification that ensures both token validity and request authenticity.

Security Properties

The middleware enforces multiple security layers:

  • Two-step cryptographic verification (master→ephemeral→request)
  • Per-request replay prevention via nonce tracking
  • Time-bound tokens with configurable validity windows
  • Clock skew tolerance for distributed systems
  • Purpose-specific token validation

Basic Usage

Create middleware with minimal configuration:

auth := middleware.SignetMiddleware(
    middleware.WithMasterKey(masterPublicKey),
)
protected := auth(yourHandler)

Distributed Systems

For multi-instance deployments, use shared storage:

auth := middleware.SignetMiddleware(
    middleware.WithTokenStore(redisTokenStore),
    middleware.WithNonceStore(redisNonceStore),
    middleware.WithKeyProvider(dynamicKeyProvider),
)

Authentication Context

Verified requests include authentication details:

func handler(w http.ResponseWriter, r *http.Request) {
    authCtx, _ := middleware.GetAuthContext(r)
    log.Printf("Token: %s, Purpose: %s", authCtx.TokenID, authCtx.Purpose)
}

Extensibility

The middleware is built on clean interfaces:

  • TokenStore: Token storage and retrieval
  • NonceStore: Replay prevention
  • KeyProvider: Master key management
  • RequestBuilder: Custom canonicalization
  • Logger & Metrics: Observability integration

Thread Safety

All provided implementations are thread-safe and suitable for concurrent use. Custom implementations should ensure proper synchronization.

Error Handling

The middleware provides consistent error codes for client handling:

  • MISSING_PROOF: No authentication header
  • INVALID_PROOF: Malformed header
  • TOKEN_EXPIRED: Token past validity
  • REPLAY_DETECTED: Request already processed
  • INVALID_SIGNATURE: Verification failed

For detailed examples, see the example_test.go file.

Package middleware provides production-ready HTTP middleware for Signet authentication. This middleware implements two-step cryptographic verification (master→ephemeral→request) with configurable backends for token and nonce storage.

Example (CustomCanonical)

Example_customCanonical demonstrates custom request canonicalization

package main

import (
	"crypto/ed25519"
	"fmt"
	"net/http"

	"github.com/agentic-research/signet/pkg/http/header"
	"github.com/agentic-research/signet/pkg/http/middleware"
)

// CustomRequestBuilder implements custom request canonicalization
type CustomRequestBuilder struct{}

func (b *CustomRequestBuilder) Build(r *http.Request, proof *header.SignetProof) ([]byte, error) {

	canonical := fmt.Sprintf("%s|%s|%s|%d",
		r.Method,
		r.URL.String(),
		r.Header.Get("X-Request-ID"),
		proof.Timestamp,
	)
	return []byte(canonical), nil
}

func main() {
	masterPub, _, _ := ed25519.GenerateKey(nil)

	auth, err := middleware.SignetMiddleware(
		middleware.WithMasterKey(masterPub),
		middleware.WithRequestBuilder(&CustomRequestBuilder{}),
	)
	if err != nil {
		panic(err) // In production, handle this error gracefully
	}

	handler := auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("Custom canonical request"))
	}))

	_ = http.ListenAndServe(":8080", handler)
}
Example (DistributedSetup)

Example_distributedSetup demonstrates setup for distributed systems

package main

import (
	"crypto/ed25519"
	"log"
	"net/http"
	"time"

	"github.com/agentic-research/signet/pkg/http/middleware"
)

func main() {
	// Create Redis-backed stores for distributed deployments
	// (assumes you have a Redis client implementation)
	/*
		redisClient := redis.NewClient(&redis.Options{
			Addr: "localhost:6379",
		})

		tokenStore := middleware.NewRedisTokenStore(redisClient, "signet:tokens:")
		nonceStore := middleware.NewRedisNonceStore(redisClient, "signet:nonces:")
	*/

	// For this example, we'll use memory stores
	tokenStore := middleware.NewMemoryTokenStore()
	nonceStore := middleware.NewMemoryNonceStore()

	// Multi-issuer key provider
	keyProvider := middleware.NewMultiKeyProvider()
	pub1, _, _ := ed25519.GenerateKey(nil)
	pub2, _, _ := ed25519.GenerateKey(nil)
	keyProvider.AddKey("issuer1", pub1)
	keyProvider.AddKey("issuer2", pub2)

	// Create middleware with advanced configuration
	auth, err := middleware.SignetMiddleware(
		middleware.WithTokenStore(tokenStore),
		middleware.WithNonceStore(nonceStore),
		middleware.WithKeyProvider(keyProvider),
		middleware.WithClockSkew(1*time.Minute),
		middleware.WithJSONErrors(),
		middleware.WithSkipPaths("/health", "/metrics"),
		middleware.WithRequiredPurposes("api-access", "admin-access"),
	)
	if err != nil {
		panic(err) // In production, handle this error gracefully
	}

	// Create your application handler
	app := http.NewServeMux()

	// Public endpoints (skipped by middleware)
	app.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`{"status": "healthy"}`))
	})

	// Protected endpoints
	app.Handle("/api/", auth(http.HandlerFunc(apiHandler)))
	app.Handle("/admin/", auth(http.HandlerFunc(adminHandler)))

	_ = http.ListenAndServe(":8080", app)
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
	authCtx, _ := middleware.GetAuthContext(r)
	log.Printf("API request from token %s with purpose %s", authCtx.TokenID, authCtx.Purpose)
	_, _ = w.Write([]byte("API response"))
}

func adminHandler(w http.ResponseWriter, r *http.Request) {
	authCtx, _ := middleware.GetAuthContext(r)

	if authCtx.Purpose != "admin-access" {
		http.Error(w, "Admin access required", http.StatusForbidden)
		return
	}

	_, _ = w.Write([]byte("Admin response"))
}
Example (SimpleUsage)

Example_simpleUsage demonstrates basic middleware setup with a static key

package main

import (
	"crypto/ed25519"
	"net/http"
	"time"

	"github.com/agentic-research/signet/pkg/http/middleware"
)

func main() {
	// Generate or load your master key
	masterPub, _, _ := ed25519.GenerateKey(nil)

	// Create middleware with simple configuration
	auth, err := middleware.SignetMiddleware(
		middleware.WithMasterKey(masterPub),
		middleware.WithClockSkew(30*time.Second),
	)
	if err != nil {
		panic(err) // In production, handle this error gracefully
	}

	// Apply to your handler
	handler := auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Get authentication context
		authCtx, ok := middleware.GetAuthContext(r)
		if !ok {
			// This shouldn't happen if middleware is properly configured
			http.Error(w, "No auth context", http.StatusInternalServerError)
			return
		}

		// Access authenticated request information
		_, _ = w.Write([]byte("Hello, authenticated user! Token: " + authCtx.TokenID))
	}))

	// Use the handler
	_ = http.ListenAndServe(":8080", handler)
}
Example (WithObservability)

Example_withObservability demonstrates integration with logging and metrics

package main

import (
	"crypto/ed25519"
	"log"
	"net/http"
	"time"

	"github.com/agentic-research/signet/pkg/http/middleware"
)

// CustomLogger implements middleware.Logger for integration with your logging system
type CustomLogger struct {
}

func (l *CustomLogger) Debug(msg string, args ...interface{}) {

	log.Printf("[DEBUG] %s %v", msg, args)
}

func (l *CustomLogger) Info(msg string, args ...interface{}) {
	log.Printf("[INFO] %s %v", msg, args)
}

func (l *CustomLogger) Warn(msg string, args ...interface{}) {
	log.Printf("[WARN] %s %v", msg, args)
}

func (l *CustomLogger) Error(msg string, args ...interface{}) {
	log.Printf("[ERROR] %s %v", msg, args)
}

// PrometheusMetrics implements middleware.Metrics for Prometheus integration
type PrometheusMetrics struct {
}

func (m *PrometheusMetrics) RecordAuthResult(result string, duration time.Duration) {

}

func (m *PrometheusMetrics) RecordTokenUsage(tokenID string, purpose string) {

}

func main() {
	masterPub, _, _ := ed25519.GenerateKey(nil)

	// Create middleware with observability
	auth, err := middleware.SignetMiddleware(
		middleware.WithMasterKey(masterPub),
		middleware.WithLogger(&CustomLogger{}),
		middleware.WithMetrics(&PrometheusMetrics{}),
	)
	if err != nil {
		panic(err) // In production, handle this error gracefully
	}

	handler := auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("Authenticated with observability"))
	}))

	_ = http.ListenAndServe(":8080", handler)
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrMissingProof indicates the Signet-Proof header is missing
	ErrMissingProof = errors.New("missing Signet-Proof header")

	// ErrInvalidProof indicates the proof format is invalid
	ErrInvalidProof = errors.New("invalid proof format")

	// ErrTokenNotFound indicates the token doesn't exist in the store
	ErrTokenNotFound = errors.New("token not found")

	// ErrTokenExpired indicates the token has expired
	ErrTokenExpired = errors.New("token expired")

	// ErrTokenNotYetValid indicates the token's NotBefore time hasn't arrived
	ErrTokenNotYetValid = errors.New("token not yet valid")

	// ErrTokenRevoked indicates the token has been revoked
	ErrTokenRevoked = errors.New("token revoked")

	// ErrClockSkew indicates the request timestamp is outside acceptable bounds
	ErrClockSkew = errors.New("clock skew detected")

	// ErrReplayDetected indicates the same nonce was used twice
	ErrReplayDetected = errors.New("replay attack detected")

	// ErrInvalidSignature indicates cryptographic verification failed
	ErrInvalidSignature = errors.New("invalid signature")

	// ErrKeyNotFound indicates the master key couldn't be retrieved
	ErrKeyNotFound = errors.New("master key not found")

	// ErrInternalError indicates an internal server error
	ErrInternalError = errors.New("internal server error")

	// ErrPurposeMismatch indicates the token's purpose doesn't match requirements
	ErrPurposeMismatch = errors.New("token purpose mismatch")

	// ErrRequestTooLarge indicates the request exceeds maximum allowed size
	ErrRequestTooLarge = errors.New("request too large")
)

Standard authentication errors

Functions

func SignetMiddleware

func SignetMiddleware(opts ...Option) (func(http.Handler) http.Handler, error)

SignetMiddleware creates HTTP middleware that enforces Signet two-step verification. The middleware validates requests by:

  1. Verifying the master key signed the ephemeral key
  2. Verifying the ephemeral key signed the request
  3. Preventing replay attacks via nonce tracking
  4. Enforcing time-based validity windows

Returns an error if the middleware configuration is invalid. This allows graceful error handling instead of panicking.

Example usage:

middleware, err := SignetMiddleware(
    WithMasterKey(masterPub),
    WithClockSkew(30*time.Second),
)
if err != nil {
    return fmt.Errorf("failed to create middleware: %w", err)
}
handler := middleware(myHandler)

Types

type AuthContext

type AuthContext struct {
	TokenID            string
	Token              *signet.Token
	Purpose            string
	IssuerID           string
	MasterKeyHash      []byte
	EphemeralPublicKey crypto.PublicKey
	VerifiedAt         time.Time
}

AuthContext contains authentication information added to verified requests

func GetAuthContext

func GetAuthContext(r *http.Request) (*AuthContext, bool)

GetAuthContext retrieves the authentication context from a request

type Config

type Config struct {
	// Core configuration
	ClockSkew         time.Duration
	TokenStore        TokenStore
	NonceStore        NonceStore
	KeyProvider       KeyProvider
	ErrorHandler      ErrorHandler
	RequestBuilder    RequestBuilder
	RevocationChecker revocation.Checker

	// Observability
	Logger   Logger
	Metrics  Metrics
	Observer ObserverHook // Context-based monitoring hook

	// Security settings
	MaxRequestSize int64 // Maximum request body size (0 = use default 1MB)

	// Advanced options
	SkipPaths        []string
	RequiredPurposes []string
}

Config holds middleware configuration

type ErrorHandler

type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)

ErrorHandler handles authentication errors with custom responses.

var DefaultErrorHandler ErrorHandler = defaultErrorHandler

DefaultErrorHandler is the default error handler for testing/direct use

type KeyProvider

type KeyProvider interface {
	// GetMasterKey retrieves the master public key for an issuer.
	// Returns ErrKeyNotFound if the key doesn't exist.
	GetMasterKey(ctx context.Context, issuerID string) (crypto.PublicKey, error)

	// RefreshKeys updates the key cache (optional).
	RefreshKeys(ctx context.Context) error
}

KeyProvider defines the interface for retrieving master public keys. This allows for dynamic key management and rotation.

type Logger

type Logger interface {
	Debug(msg string, args ...interface{})
	Info(msg string, args ...interface{})
	Warn(msg string, args ...interface{})
	Error(msg string, args ...interface{})
}

Logger defines the logging interface for the middleware.

type MemoryNonceStore

type MemoryNonceStore struct {
	// contains filtered or unexported fields
}

MemoryNonceStore implements NonceStore using in-memory storage. Suitable for single-instance deployments and testing.

func NewMemoryNonceStore

func NewMemoryNonceStore() *MemoryNonceStore

NewMemoryNonceStore creates a new in-memory nonce store.

func (*MemoryNonceStore) CheckAndStore

func (s *MemoryNonceStore) CheckAndStore(ctx context.Context, nonceKey string, expiry int64) error

CheckAndStore atomically checks and stores a nonce

func (*MemoryNonceStore) Cleanup

func (s *MemoryNonceStore) Cleanup(ctx context.Context) error

Cleanup removes expired nonces

func (*MemoryNonceStore) Close

func (s *MemoryNonceStore) Close()

Close stops the cleanup goroutine

type MemoryTokenStore

type MemoryTokenStore struct {
	// contains filtered or unexported fields
}

MemoryTokenStore implements TokenStore using in-memory storage. Suitable for single-instance deployments and testing.

func NewMemoryTokenStore

func NewMemoryTokenStore() *MemoryTokenStore

NewMemoryTokenStore creates a new in-memory token store.

func (*MemoryTokenStore) Cleanup

func (s *MemoryTokenStore) Cleanup(ctx context.Context) error

Cleanup removes expired tokens

func (*MemoryTokenStore) Close

func (s *MemoryTokenStore) Close()

Close stops the cleanup goroutine

func (*MemoryTokenStore) Delete

func (s *MemoryTokenStore) Delete(ctx context.Context, tokenID string) error

Delete removes a token record

func (*MemoryTokenStore) Get

func (s *MemoryTokenStore) Get(ctx context.Context, tokenID string) (*TokenRecord, error)

Get retrieves a token record by ID

func (*MemoryTokenStore) Store

func (s *MemoryTokenStore) Store(ctx context.Context, record *TokenRecord) (string, error)

Store saves a token record and returns its ID

type Metrics

type Metrics interface {
	// RecordAuthResult records the result of an authentication attempt.
	RecordAuthResult(result string, duration time.Duration)

	// RecordTokenUsage records token usage statistics.
	RecordTokenUsage(tokenID string, purpose string)
}

Metrics defines the metrics collection interface.

type MultiKeyProvider

type MultiKeyProvider struct {
	// contains filtered or unexported fields
}

MultiKeyProvider implements KeyProvider with support for multiple issuers

func NewMultiKeyProvider

func NewMultiKeyProvider() *MultiKeyProvider

NewMultiKeyProvider creates a key provider supporting multiple issuers

func (*MultiKeyProvider) AddKey

func (p *MultiKeyProvider) AddKey(issuerID string, key crypto.PublicKey)

AddKey registers a master public key for an issuer

func (*MultiKeyProvider) GetMasterKey

func (p *MultiKeyProvider) GetMasterKey(ctx context.Context, issuerID string) (crypto.PublicKey, error)

GetMasterKey retrieves the master public key for an issuer

func (*MultiKeyProvider) RefreshKeys

func (p *MultiKeyProvider) RefreshKeys(ctx context.Context) error

RefreshKeys is a no-op for the in-memory provider

type NetworkSettings

type NetworkSettings struct {
	// ChunkedTransferTimeout limits the total time for reading chunked request bodies.
	// This prevents DoS attacks via slow-drip chunked transfers that bypass
	// Content-Length checks. Set to 0 to disable timeout.
	//
	// Background (Issue #28 enhancement):
	//   - Current DoS fix only validates Content-Length header
	//   - Chunked transfers don't have Content-Length
	//   - Attacker can send infinite chunks slowly to exhaust connections
	//   - This timeout closes the connection if chunks take too long
	//
	// Default: 30 seconds
	// Environment variable: SIGNET_CHUNKED_TIMEOUT
	//
	// Future Implementation (tracked in issue #23):
	//   - Add io.ReadCloser wrapper with deadline
	//   - Monitor total read time, not individual chunk time
	//   - Emit metrics on timeout events
	ChunkedTransferTimeout time.Duration `yaml:"chunked_timeout" toml:"chunked_timeout" env:"SIGNET_CHUNKED_TIMEOUT" default:"30s"`

	// ReadHeaderTimeout limits time to read request headers.
	// Default: 10 seconds
	// Environment variable: SIGNET_READ_HEADER_TIMEOUT
	ReadHeaderTimeout time.Duration `yaml:"read_header_timeout" toml:"read_header_timeout" env:"SIGNET_READ_HEADER_TIMEOUT" default:"10s"`

	// WriteTimeout limits time to write response.
	// Default: 30 seconds
	// Environment variable: SIGNET_WRITE_TIMEOUT
	WriteTimeout time.Duration `yaml:"write_timeout" toml:"write_timeout" env:"SIGNET_WRITE_TIMEOUT" default:"30s"`

	// IdleTimeout limits time for keep-alive connections.
	// Default: 90 seconds
	// Environment variable: SIGNET_IDLE_TIMEOUT
	IdleTimeout time.Duration `yaml:"idle_timeout" toml:"idle_timeout" env:"SIGNET_IDLE_TIMEOUT" default:"90s"`
}

NetworkSettings controls network-level behavior.

type NoOpMetrics

type NoOpMetrics = noOpMetrics

NoOpMetrics is an exported no-op metrics implementation for testing

type NonceStore

type NonceStore interface {
	// CheckAndStore atomically checks if a nonce exists and stores it if not.
	// Returns ErrReplayDetected if the nonce was already used.
	// The expiry parameter hints when the nonce can be safely removed.
	CheckAndStore(ctx context.Context, nonceKey string, expiry int64) error

	// Cleanup removes expired nonces (optional, for maintenance).
	Cleanup(ctx context.Context) error
}

NonceStore defines the interface for replay prevention. Each nonce should only be used once within a token's lifetime.

type ObservabilitySettings

type ObservabilitySettings struct {
	// MetricsEnabled determines whether to emit Prometheus/StatsD metrics.
	// Default: true
	// Environment variable: SIGNET_METRICS_ENABLED
	MetricsEnabled bool `yaml:"metrics_enabled" toml:"metrics_enabled" env:"SIGNET_METRICS_ENABLED" default:"true"`

	// LogLevel controls logging verbosity.
	// Options: "debug", "info", "warn", "error"
	// Default: "info"
	// Environment variable: SIGNET_LOG_LEVEL
	LogLevel string `yaml:"log_level" toml:"log_level" env:"SIGNET_LOG_LEVEL" default:"info"`

	// TracingEnabled determines whether to emit distributed tracing spans.
	// Default: false
	// Environment variable: SIGNET_TRACING_ENABLED
	TracingEnabled bool `yaml:"tracing_enabled" toml:"tracing_enabled" env:"SIGNET_TRACING_ENABLED" default:"false"`

	// TracingProvider specifies the tracing backend.
	// Options: "opentelemetry", "jaeger", "zipkin"
	// Default: "opentelemetry"
	// Environment variable: SIGNET_TRACING_PROVIDER
	TracingProvider string `yaml:"tracing_provider" toml:"tracing_provider" env:"SIGNET_TRACING_PROVIDER" default:"opentelemetry"`
}

ObservabilitySettings controls monitoring and logging.

type ObserverHook

type ObserverHook interface {
	// OnAuthStart is called before authentication begins.
	// Returns a new context (possibly with trace IDs, span IDs, etc.)
	OnAuthStart(ctx context.Context, r *http.Request) context.Context

	// OnAuthSuccess is called after successful authentication.
	// Can emit metrics, log events, close spans, etc.
	OnAuthSuccess(ctx context.Context, authCtx *AuthContext)

	// OnAuthFailure is called after authentication failure.
	// Can emit metrics, log errors, close spans, etc.
	OnAuthFailure(ctx context.Context, err error, stage string)
}

ObserverHook defines the interface for observing authentication events. This enables integration with monitoring systems (OpenTelemetry, Prometheus, etc.) via context propagation.

Observer hooks are called at key points in the authentication flow:

  1. OnAuthStart - Before authentication begins
  2. OnAuthSuccess - After successful verification
  3. OnAuthFailure - After verification failure

Context-Based Monitoring Pattern:

  • Observers can attach metadata to context (trace IDs, span IDs)
  • Downstream services can read this metadata for distributed tracing
  • No tight coupling to specific monitoring tools

Example: OpenTelemetry Integration

type OTelObserver struct{ tracer trace.Tracer }

func (o *OTelObserver) OnAuthStart(ctx context.Context, r *http.Request) context.Context {
    ctx, span := o.tracer.Start(ctx, "signet.authenticate")
    return ctx
}

func (o *OTelObserver) OnAuthSuccess(ctx context.Context, authCtx *AuthContext) {
    span := trace.SpanFromContext(ctx)
    span.SetAttributes(attribute.String("token_id", authCtx.TokenID))
    span.End()
}

type Option

type Option func(*Config)

Option configures the Signet middleware.

func WithClockSkew

func WithClockSkew(duration time.Duration) Option

WithClockSkew sets the maximum allowed time difference between client and server. Default is 30 seconds.

func WithErrorHandler

func WithErrorHandler(handler ErrorHandler) Option

WithErrorHandler sets a custom error response handler.

func WithJSONErrors

func WithJSONErrors() Option

WithJSONErrors configures JSON error responses.

func WithKeyProvider

func WithKeyProvider(provider KeyProvider) Option

WithKeyProvider sets a custom key provider for retrieving master keys. This enables dynamic key management and rotation.

func WithLogger

func WithLogger(logger Logger) Option

WithLogger sets a custom logger for the middleware.

func WithMasterKey

func WithMasterKey(key crypto.PublicKey) Option

WithMasterKey sets a static master public key for verification. This is a convenience method for simple deployments. Accepts any crypto.PublicKey (Ed25519, ML-DSA, etc.).

func WithMaxRequestSize

func WithMaxRequestSize(size int64) Option

WithMaxRequestSize sets the maximum allowed request body size in bytes. This protects against DoS attacks via oversized requests. Default: 1MB (1048576 bytes) if not configured.

Example:

middleware := SignetMiddleware(
    WithMaxRequestSize(5 * 1024 * 1024), // 5MB limit
)

func WithMetrics

func WithMetrics(metrics Metrics) Option

WithMetrics enables metrics collection.

func WithNonceStore

func WithNonceStore(store NonceStore) Option

WithNonceStore sets a custom nonce storage backend for replay prevention. Default is in-memory storage.

func WithObserver

func WithObserver(observer ObserverHook) Option

WithObserver configures a custom observer hook for monitoring. Observer hooks enable integration with distributed tracing systems (OpenTelemetry, Jaeger) and custom monitoring solutions via context propagation.

Example: OpenTelemetry Integration

type OTelObserver struct{ tracer trace.Tracer }

func (o *OTelObserver) OnAuthStart(ctx context.Context, r *http.Request) context.Context {
    ctx, span := o.tracer.Start(ctx, "signet.authenticate")
    return ctx
}

middleware := SignetMiddleware(
    WithObserver(&OTelObserver{tracer: tracer}),
)

func WithRequestBuilder

func WithRequestBuilder(builder RequestBuilder) Option

WithRequestBuilder sets a custom canonical request builder.

func WithRequiredPurposes

func WithRequiredPurposes(purposes ...string) Option

WithRequiredPurposes enforces that tokens must have one of the specified purposes. This provides additional access control beyond cryptographic verification.

func WithRevocationChecker

func WithRevocationChecker(checker revocation.Checker) Option

WithRevocationChecker sets a custom revocation checker for the middleware.

func WithSkipPaths

func WithSkipPaths(paths ...string) Option

WithSkipPaths configures paths that bypass authentication. Useful for health checks and public endpoints.

func WithTokenStore

func WithTokenStore(store TokenStore) Option

WithTokenStore sets a custom token storage backend. Default is in-memory storage.

type RequestBuilder

type RequestBuilder interface {
	// Build creates the canonical request bytes that were signed.
	Build(r *http.Request, proof *header.SignetProof) ([]byte, error)
}

RequestBuilder constructs the canonical request representation for signing. Different applications may need different canonicalization strategies.

var DefaultRequestBuilder RequestBuilder = defaultRequestBuilder

DefaultRequestBuilder is the default request builder for testing/direct use

type SecuritySettings

type SecuritySettings struct {
	// MaxRequestSize limits request body size to prevent DoS attacks.
	// Default: 1MB (1048576 bytes)
	// Environment variable: SIGNET_MAX_REQUEST_SIZE
	MaxRequestSize int64 `yaml:"max_request_size" toml:"max_request_size" env:"SIGNET_MAX_REQUEST_SIZE" default:"1048576"`

	// ClockSkew is the maximum allowed time difference between client and server.
	// Default: 30 seconds
	// Environment variable: SIGNET_CLOCK_SKEW
	ClockSkew time.Duration `yaml:"clock_skew" toml:"clock_skew" env:"SIGNET_CLOCK_SKEW" default:"30s"`

	// MaxTokensPerUser limits tokens per user to prevent resource exhaustion.
	// Default: 100
	// Environment variable: SIGNET_MAX_TOKENS_PER_USER
	MaxTokensPerUser int `yaml:"max_tokens_per_user" toml:"max_tokens_per_user" env:"SIGNET_MAX_TOKENS_PER_USER" default:"100"`
}

SecuritySettings controls security-related middleware behavior.

type Settings

type Settings struct {
	// Security settings
	Security SecuritySettings

	// Storage settings
	Storage StorageSettings

	// Observability settings
	Observability ObservabilitySettings

	// Network settings
	Network NetworkSettings
}

Settings represents runtime configuration for Signet middleware. This struct is designed to be loaded from YAML/TOML configuration files and environment variables. See issue #23 for full configuration system design.

Future Configuration System Design:

  • Load from YAML/TOML files (e.g., /etc/signet/config.yaml)
  • Override with environment variables (e.g., SIGNET_MAX_REQUEST_SIZE)
  • Validate on startup with clear error messages
  • Support hot reload for safe settings (log levels, timeouts)

Example YAML configuration:

security:
  max_request_size: 1048576  # 1MB
  clock_skew: 30s
  chunked_timeout: 30s
storage:
  token_store: redis
  redis_url: redis://localhost:6379
observability:
  metrics_enabled: true
  log_level: info
  tracing_enabled: true

func DefaultSettings

func DefaultSettings() *Settings

DefaultSettings returns a Settings instance with sensible defaults. This is the baseline configuration that can be overridden via files/env vars.

func (*Settings) ApplyToConfig

func (s *Settings) ApplyToConfig(config *Config) error

ApplyToConfig applies Settings to the middleware Config struct. This bridges the gap between file-based Settings and runtime Config.

Future Implementation (issue #23):

  • Map Settings fields to Config fields
  • Instantiate storage backends based on Settings
  • Configure logger based on LogLevel
  • Set up metrics/tracing exporters

Example usage:

settings := LoadSettings("config.yaml")
config := &Config{...}
settings.ApplyToConfig(config)

func (*Settings) Validate

func (s *Settings) Validate() error

Validate checks if the settings are valid. Returns an error if any setting is invalid or incompatible.

Future Implementation (issue #23):

  • Validate MaxRequestSize > 0 and < 100MB
  • Validate ClockSkew >= 0 and <= 5 minutes
  • Validate storage backends are available
  • Check Redis/Postgres URLs are parseable
  • Validate log levels are recognized
  • Ensure timeout values are reasonable

type StorageSettings

type StorageSettings struct {
	// TokenStoreType specifies the token storage backend.
	// Options: "memory", "redis", "postgres", "dynamodb"
	// Default: "memory"
	// Environment variable: SIGNET_TOKEN_STORE
	TokenStoreType string `yaml:"token_store" toml:"token_store" env:"SIGNET_TOKEN_STORE" default:"memory"`

	// NonceStoreType specifies the nonce storage backend.
	// Options: "memory", "redis"
	// Default: "memory"
	// Environment variable: SIGNET_NONCE_STORE
	NonceStoreType string `yaml:"nonce_store" toml:"nonce_store" env:"SIGNET_NONCE_STORE" default:"memory"`

	// CleanupInterval determines how often expired tokens/nonces are removed.
	// Default: 5 minutes
	// Environment variable: SIGNET_CLEANUP_INTERVAL
	CleanupInterval time.Duration `yaml:"cleanup_interval" toml:"cleanup_interval" env:"SIGNET_CLEANUP_INTERVAL" default:"5m"`

	// RedisURL is the connection string for Redis storage backends.
	// Example: "redis://localhost:6379/0"
	// Environment variable: SIGNET_REDIS_URL
	RedisURL string `yaml:"redis_url" toml:"redis_url" env:"SIGNET_REDIS_URL"`

	// PostgresURL is the connection string for PostgreSQL storage.
	// Example: "postgres://user:pass@localhost/signet?sslmode=require"
	// Environment variable: SIGNET_POSTGRES_URL
	PostgresURL string `yaml:"postgres_url" toml:"postgres_url" env:"SIGNET_POSTGRES_URL"`
}

StorageSettings controls token and nonce storage backends.

type TokenRecord

type TokenRecord struct {
	Token              *signet.Token
	MasterPublicKey    crypto.PublicKey
	EphemeralPublicKey crypto.PublicKey
	BindingSignature   []byte
	IssuedAt           time.Time
	Purpose            string
	Metadata           map[string]string // Optional metadata for extensions
}

TokenRecord represents a stored token with its cryptographic context. This contains all information needed for two-step verification.

type TokenStore

type TokenStore interface {
	// Get retrieves a token record by its ID.
	// The tokenID parameter must be a 32-character hex string from hex.EncodeToString(JTI).
	// Returns ErrTokenNotFound if the token doesn't exist.
	Get(ctx context.Context, tokenID string) (*TokenRecord, error)

	// Store saves a token record and returns its ID.
	// The returned tokenID will be a 32-character hex string from hex.EncodeToString(token.JTI).
	Store(ctx context.Context, record *TokenRecord) (string, error)

	// Delete removes a token record (optional, for revocation).
	Delete(ctx context.Context, tokenID string) error

	// Cleanup removes expired tokens (optional, for maintenance).
	// Implementations may handle this automatically.
	Cleanup(ctx context.Context) error
}

TokenStore defines the interface for token storage and retrieval. Implementations can use in-memory storage, Redis, databases, or any other backend.

Token ID Format:

  • Token IDs are derived from the token's JTI (CBOR field 4)
  • Format: hex.EncodeToString(JTI) = 32 hex characters (from 16 bytes)
  • Example: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"

Security Considerations:

  • MUST use full 16-byte JTI to prevent collisions (birthday paradox)
  • Truncating to 8 bytes hits 50% collision probability at ~4 billion tokens
  • Full JTI provides 2^64 uniqueness (computationally infeasible to collide)

Migration Notes:

  • Previous versions used 8-byte truncation (16 hex chars)
  • Alpha software uses big bang migration (all old tokens invalidated)
  • Production systems should implement dual lookup during transition

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL