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) } }) } }