Compare commits
2
Commits
6526b717dd
...
43b9f1cb59
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43b9f1cb59 | ||
|
|
d69019b9d1 |
@@ -84,7 +84,7 @@ func (s *Handlers) initImageService() error {
|
||||
Cache: cache,
|
||||
FetcherConfig: fetcherCfg,
|
||||
SigningKey: s.config.SigningKey,
|
||||
Whitelist: s.config.WhitelistHosts,
|
||||
Allowlist: s.config.WhitelistHosts,
|
||||
Logger: s.log,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -57,7 +57,7 @@ func setupTestHandler(t *testing.T) *testFixtures {
|
||||
Cache: cache,
|
||||
Fetcher: newMockFetcher(mockFS),
|
||||
SigningKey: "test-signing-key-must-be-32-chars",
|
||||
Whitelist: []string{goodHost},
|
||||
Allowlist: []string{goodHost},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
|
||||
@@ -75,7 +75,7 @@ type ImageRequest struct {
|
||||
Quality int
|
||||
// FitMode is how to fit the image into requested dimensions
|
||||
FitMode FitMode
|
||||
// Signature is the HMAC signature for non-whitelisted hosts
|
||||
// Signature is the HMAC signature for non-allowlisted hosts
|
||||
Signature string
|
||||
// Expires is the signature expiration timestamp
|
||||
Expires time.Time
|
||||
@@ -163,10 +163,10 @@ type SignatureValidator interface {
|
||||
Generate(req *ImageRequest) string
|
||||
}
|
||||
|
||||
// Whitelist checks if a URL is whitelisted (no signature required)
|
||||
type Whitelist interface {
|
||||
// IsWhitelisted returns true if the URL doesn't require a signature
|
||||
IsWhitelisted(u *url.URL) bool
|
||||
// Allowlist checks if a URL is allowlisted (no signature required)
|
||||
type Allowlist interface {
|
||||
// IsAllowlisted returns true if the URL doesn't require a signature
|
||||
IsAllowlisted(u *url.URL) bool
|
||||
}
|
||||
|
||||
// Storage handles persistent storage of cached content
|
||||
|
||||
@@ -40,8 +40,8 @@ type ServiceConfig struct {
|
||||
Fetcher httpfetcher.Fetcher
|
||||
// SigningKey is the HMAC signing key (empty disables signing)
|
||||
SigningKey string
|
||||
// Whitelist is the list of hosts that don't require signatures
|
||||
Whitelist []string
|
||||
// Allowlist is the list of hosts that don't require signatures
|
||||
Allowlist []string
|
||||
// Logger for logging
|
||||
Logger *slog.Logger
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func NewService(cfg *ServiceConfig) (*Service, error) {
|
||||
fetcher: fetcher,
|
||||
processor: imageprocessor.New(imageprocessor.Params{MaxInputBytes: maxResponseSize}),
|
||||
signer: signer,
|
||||
allowlist: allowlist.New(cfg.Whitelist),
|
||||
allowlist: allowlist.New(cfg.Allowlist),
|
||||
log: log,
|
||||
allowHTTP: allowHTTP,
|
||||
maxResponseSize: maxResponseSize,
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"sneak.berlin/go/pixa/internal/signature"
|
||||
)
|
||||
|
||||
func TestService_Get_WhitelistedHost(t *testing.T) {
|
||||
func TestService_Get_AllowlistedHost(t *testing.T) {
|
||||
svc, fixtures := SetupTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestService_Get_WhitelistedHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Get_NonWhitelistedHost_NoSignature(t *testing.T) {
|
||||
func TestService_Get_NonAllowlistedHost_NoSignature(t *testing.T) {
|
||||
svc, fixtures := SetupTestService(t, WithSigningKey("test-key"))
|
||||
|
||||
req := &ImageRequest{
|
||||
@@ -56,14 +56,14 @@ func TestService_Get_NonWhitelistedHost_NoSignature(t *testing.T) {
|
||||
FitMode: FitCover,
|
||||
}
|
||||
|
||||
// Should fail validation - not whitelisted and no signature
|
||||
// Should fail validation - not allowlisted and no signature
|
||||
err := svc.ValidateRequest(req)
|
||||
if err == nil {
|
||||
t.Error("ValidateRequest() expected error for non-whitelisted host without signature")
|
||||
t.Error("ValidateRequest() expected error for non-allowlisted host without signature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Get_NonWhitelistedHost_ValidSignature(t *testing.T) {
|
||||
func TestService_Get_NonAllowlistedHost_ValidSignature(t *testing.T) {
|
||||
signingKey := "test-signing-key-12345"
|
||||
svc, fixtures := SetupTestService(t, WithSigningKey(signingKey))
|
||||
ctx := context.Background()
|
||||
@@ -105,7 +105,7 @@ func TestService_Get_NonWhitelistedHost_ValidSignature(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Get_NonWhitelistedHost_ExpiredSignature(t *testing.T) {
|
||||
func TestService_Get_NonAllowlistedHost_ExpiredSignature(t *testing.T) {
|
||||
signingKey := "test-signing-key-12345"
|
||||
svc, fixtures := SetupTestService(t, WithSigningKey(signingKey))
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestService_Get_NonWhitelistedHost_ExpiredSignature(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Get_NonWhitelistedHost_InvalidSignature(t *testing.T) {
|
||||
func TestService_Get_NonAllowlistedHost_InvalidSignature(t *testing.T) {
|
||||
signingKey := "test-signing-key-12345"
|
||||
svc, fixtures := SetupTestService(t, WithSigningKey(signingKey))
|
||||
|
||||
@@ -162,7 +162,7 @@ func TestService_ValidateRequest_SignatureExactHostMatch(t *testing.T) {
|
||||
signingKey := "test-signing-key-must-be-32-chars"
|
||||
svc, _ := SetupTestService(t,
|
||||
WithSigningKey(signingKey),
|
||||
WithNoWhitelist(),
|
||||
WithNoAllowlist(),
|
||||
)
|
||||
|
||||
signer := signature.New(signingKey)
|
||||
@@ -438,8 +438,8 @@ func TestService_Get_DifferentSizes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestService_ValidateRequest_NoSigningKey(t *testing.T) {
|
||||
// Service with no signing key - all non-whitelisted requests should fail
|
||||
svc, fixtures := SetupTestService(t, WithNoWhitelist())
|
||||
// Service with no signing key - all non-allowlisted requests should fail
|
||||
svc, fixtures := SetupTestService(t, WithNoAllowlist())
|
||||
|
||||
req := &ImageRequest{
|
||||
SourceHost: fixtures.OtherHost,
|
||||
@@ -452,7 +452,7 @@ func TestService_ValidateRequest_NoSigningKey(t *testing.T) {
|
||||
|
||||
err := svc.ValidateRequest(req)
|
||||
if err == nil {
|
||||
t.Error("ValidateRequest() expected error when no signing key and host not whitelisted")
|
||||
t.Error("ValidateRequest() expected error when no signing key and host not allowlisted")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
// TestFixtures contains paths to test files in the mock filesystem.
|
||||
type TestFixtures struct {
|
||||
// Valid image files
|
||||
GoodHostJPEG string // whitelisted host, valid JPEG
|
||||
GoodHostPNG string // whitelisted host, valid PNG
|
||||
GoodHostGIF string // whitelisted host, valid GIF
|
||||
OtherHostJPEG string // non-whitelisted host, valid JPEG
|
||||
OtherHostPNG string // non-whitelisted host, valid PNG
|
||||
GoodHostJPEG string // allowlisted host, valid JPEG
|
||||
GoodHostPNG string // allowlisted host, valid PNG
|
||||
GoodHostGIF string // allowlisted host, valid GIF
|
||||
OtherHostJPEG string // non-allowlisted host, valid JPEG
|
||||
OtherHostPNG string // non-allowlisted host, valid PNG
|
||||
|
||||
// Invalid/edge case files
|
||||
InvalidFile string // file with wrong magic bytes
|
||||
@@ -33,8 +33,8 @@ type TestFixtures struct {
|
||||
TextFile string // text file masquerading as image
|
||||
|
||||
// Hostnames
|
||||
GoodHost string // whitelisted hostname
|
||||
OtherHost string // non-whitelisted hostname
|
||||
GoodHost string // allowlisted hostname
|
||||
OtherHost string // non-allowlisted hostname
|
||||
}
|
||||
|
||||
// DefaultFixtures returns the standard test fixture paths.
|
||||
@@ -148,7 +148,7 @@ func SetupTestService(t *testing.T, opts ...TestServiceOption) (*Service, *TestF
|
||||
mockFS, fixtures := NewTestFS(t)
|
||||
|
||||
cfg := &testServiceConfig{
|
||||
whitelist: []string{fixtures.GoodHost},
|
||||
allowlist: []string{fixtures.GoodHost},
|
||||
signingKey: "test-signing-key-must-be-32-chars",
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ func SetupTestService(t *testing.T, opts ...TestServiceOption) (*Service, *TestF
|
||||
Cache: cache,
|
||||
Fetcher: httpfetcher.NewMock(mockFS),
|
||||
SigningKey: cfg.signingKey,
|
||||
Whitelist: cfg.whitelist,
|
||||
Allowlist: cfg.allowlist,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
@@ -203,17 +203,17 @@ func setupServiceTestDB(t *testing.T) *sql.DB {
|
||||
}
|
||||
|
||||
type testServiceConfig struct {
|
||||
whitelist []string
|
||||
allowlist []string
|
||||
signingKey string
|
||||
}
|
||||
|
||||
// TestServiceOption configures the test service.
|
||||
type TestServiceOption func(*testServiceConfig)
|
||||
|
||||
// WithWhitelist sets the whitelist for the test service.
|
||||
func WithWhitelist(hosts ...string) TestServiceOption {
|
||||
// WithAllowlist sets the allowlist for the test service.
|
||||
func WithAllowlist(hosts ...string) TestServiceOption {
|
||||
return func(c *testServiceConfig) {
|
||||
c.whitelist = hosts
|
||||
c.allowlist = hosts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,9 +224,9 @@ func WithSigningKey(key string) TestServiceOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoWhitelist removes all whitelisted hosts.
|
||||
func WithNoWhitelist() TestServiceOption {
|
||||
// WithNoAllowlist removes all allowlisted hosts.
|
||||
func WithNoAllowlist() TestServiceOption {
|
||||
return func(c *testServiceConfig) {
|
||||
c.whitelist = nil
|
||||
c.allowlist = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// goldenExpiresUnix is the fixed expiration timestamp used by all golden
|
||||
// vectors: 2024-01-01T00:00:00Z.
|
||||
const goldenExpiresUnix int64 = 1704067200
|
||||
|
||||
// goldenSigningKey is the fixed signing key used by all golden vectors.
|
||||
const goldenSigningKey = "golden-test-key"
|
||||
|
||||
// TestSigner_GoldenVectors pins the exact HMAC-SHA256 signature output and
|
||||
// the exact generated signed URL path for fully-specified requests with a
|
||||
// hardcoded signing key. The expected values were computed once and are
|
||||
// hardcoded here as known answers.
|
||||
//
|
||||
// If any of these assertions fail, the signed byte format
|
||||
// ("host:path:query:width:height:format:expiration"), the base64url
|
||||
// encoding, or the signed URL layout has changed. Such a change breaks
|
||||
// every signature already issued to clients, so it must be made
|
||||
// deliberately: update these constants only as part of an intentional,
|
||||
// documented signature format migration.
|
||||
func TestSigner_GoldenVectors(t *testing.T) {
|
||||
signer := New(goldenSigningKey)
|
||||
|
||||
vectors := []struct {
|
||||
name string
|
||||
req Request
|
||||
// wantSignature is the exact base64url (RFC 4648 URL-safe,
|
||||
// padded) HMAC-SHA256 signature for the request with Expires
|
||||
// set to goldenExpiresUnix.
|
||||
wantSignature string
|
||||
// wantSignedPath is the exact path returned by
|
||||
// GenerateSignedURL for the request. The signature and
|
||||
// expiration are returned separately by GenerateSignedURL and
|
||||
// are not embedded in the path.
|
||||
wantSignedPath string
|
||||
}{
|
||||
{
|
||||
name: "resized without query",
|
||||
req: Request{
|
||||
SourceHost: "cdn.example.com",
|
||||
SourcePath: "/photos/cat.jpg",
|
||||
SourceQuery: "",
|
||||
Width: 800,
|
||||
Height: 600,
|
||||
Format: "webp",
|
||||
},
|
||||
// Signed data: "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200"
|
||||
wantSignature: "x5PfPp8QSDo0cJT96od-AEgrQyOVLfqifH5sst61_-w=",
|
||||
wantSignedPath: "/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp",
|
||||
},
|
||||
{
|
||||
name: "resized with query string",
|
||||
req: Request{
|
||||
SourceHost: "cdn.example.com",
|
||||
SourcePath: "/photos/cat.jpg",
|
||||
SourceQuery: "token=abc&v=2",
|
||||
Width: 800,
|
||||
Height: 600,
|
||||
Format: "webp",
|
||||
},
|
||||
// Signed data: "cdn.example.com:/photos/cat.jpg:token=abc&v=2:800:600:webp:1704067200"
|
||||
wantSignature: "394_Vf9TdQFkpQ3XKFDQSyxgqKq8N7mApf2S4QaHqyo=",
|
||||
wantSignedPath: "/v1/image/cdn.example.com/photos/cat.jpg%3Ftoken=abc&v=2/800x600.webp",
|
||||
},
|
||||
{
|
||||
name: "original size without query",
|
||||
req: Request{
|
||||
SourceHost: "cdn.example.com",
|
||||
SourcePath: "/photos/cat.jpg",
|
||||
SourceQuery: "",
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Format: "png",
|
||||
},
|
||||
// Signed data: "cdn.example.com:/photos/cat.jpg::0:0:png:1704067200"
|
||||
wantSignature: "7Be7oteeQwvnSPU4bchyQ4ZGYGsAGBKpeEtuQ02ox60=",
|
||||
wantSignedPath: "/v1/image/cdn.example.com/photos/cat.jpg/orig.png",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range vectors {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
signReq := tt.req
|
||||
signReq.Expires = time.Unix(goldenExpiresUnix, 0)
|
||||
|
||||
gotSignature := signer.Sign(&signReq)
|
||||
if gotSignature != tt.wantSignature {
|
||||
t.Errorf("Sign() = %q, want %q (signed byte format changed?)",
|
||||
gotSignature, tt.wantSignature)
|
||||
}
|
||||
|
||||
urlReq := tt.req
|
||||
gotPath, _, _ := signer.GenerateSignedURL(&urlReq, time.Hour)
|
||||
if gotPath != tt.wantSignedPath {
|
||||
t.Errorf("GenerateSignedURL() path = %q, want %q (signed URL layout changed?)",
|
||||
gotPath, tt.wantSignedPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user