package signature_test import ( "errors" "testing" "time" "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 := &signature.Request{ SourceHost: testHost, SourcePath: "/a:b", 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.SourcePath = "/a" replayed.SourceQuery = "b:" t.Logf("signature for path %q query %q: %s", signed.SourcePath, signed.SourceQuery, signed.Signature) t.Logf("signature for path %q query %q: %s", replayed.SourcePath, replayed.SourceQuery, 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.SourcePath, replayed.SourceQuery, signed.SourcePath, 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 := &signature.Request{ SourceHost: testHost, SourcePath: tt.path, SourceQuery: tt.query, 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) } }) } }