package signature_test import ( "errors" "testing" "time" "sneak.berlin/go/pixa/internal/imagerequest" "sneak.berlin/go/pixa/internal/signature" ) // TestSigner_Verify_ColonCannotMovePathQueryBoundary checks that a URL // signed for path "/a:b" with no query does not verify for path "/a" with // query "b:", a different resource on the same host. func TestSigner_Verify_ColonCannotMovePathQueryBoundary(t *testing.T) { t.Parallel() signer := signature.New(goldenSigningKey) signed := &imagerequest.Request{ Source: imagerequest.Source{ Host: testHost, Path: "/a:b", }, Transform: imagerequest.Transform{ Size: imagerequest.Size{Width: 800, Height: 600}, Format: testFormatWebP, Quality: 85, FitMode: testFitCover, }, Expires: time.Now().Add(1 * time.Hour), } signed.Signature = signer.Sign(signed) replayed := *signed replayed.Source.Path = "/a" replayed.Source.Query = "b:" t.Logf("signature for path %q query %q: %s", signed.Source.Path, signed.Source.Query, signed.Signature) t.Logf("signature for path %q query %q: %s", replayed.Source.Path, replayed.Source.Query, signer.Sign(&replayed)) err := signer.Verify(&replayed) if !errors.Is(err, signature.ErrInvalid) { t.Errorf("Verify() of path %q query %q with the signature for "+ "path %q = %v, want %v", replayed.Source.Path, replayed.Source.Query, signed.Source.Path, err, signature.ErrInvalid) } } // TestSigner_EscapedGoldenVectors pins the signature of requests whose path // or query holds ":" or "%", which are signed with "%" written as "%25" and // ":" as "%3A". The expected values were computed once, outside pixa, from // the signed data in each comment. func TestSigner_EscapedGoldenVectors(t *testing.T) { t.Parallel() signer := signature.New(goldenSigningKey) vectors := []struct { name string path string query string wantSignature string }{ { name: "colon in path", path: "/photos/a:b.jpg", // Signed data: // "cdn.example.com:/photos/a%3Ab.jpg::800:600:webp:1704067200:85:cover" wantSignature: "WnEpZ7bQp1Zvw8WDatlj2vu-4wYKWE3BFBizV8m__6k=", }, { name: "colon in query", path: testPath, query: "t=1:2", // Signed data: // "cdn.example.com:/photos/cat.jpg:t=1%3A2:800:600:webp:1704067200:85:cover" wantSignature: "85kRoN8hhr4cCUkEUjNvUsvVYKjYYfWdeffx5TjJjqA=", }, { name: "percent in path and query", path: "/photos/a%20b.jpg", query: "q=50%", // Signed data: // "cdn.example.com:/photos/a%2520b.jpg:q=50%25:800:600:webp:1704067200:85:cover" wantSignature: "MjDiiRdgXMrsjqPMsENtD4aG1hp1e-a6zy67vKTX4CQ=", }, } for _, tt := range vectors { t.Run(tt.name, func(t *testing.T) { t.Parallel() req := &imagerequest.Request{ Source: imagerequest.Source{ Host: testHost, Path: tt.path, Query: tt.query, }, Transform: imagerequest.Transform{ Size: imagerequest.Size{Width: 800, Height: 600}, Format: testFormatWebP, Quality: 85, FitMode: testFitCover, }, Expires: time.Unix(goldenExpiresUnix, 0), } gotSignature := signer.Sign(req) t.Logf("Sign() of path %q query %q = %s", tt.path, tt.query, gotSignature) if gotSignature != tt.wantSignature { t.Errorf("Sign() = %q, want %q", gotSignature, tt.wantSignature) } }) } }