check / check (push) Waiting to run
A URL signed for path /a:b with no query must not verify for path /a with query b:, and the two must not share a cache key. New known-answer vectors pin the signature of a path with ":", a query with ":", and a "%" in path and query, with "%" written as "%25" and ":" as "%3A" in the signed data. These fail until the fix for #247 lands. Model: opus-5-5
114 lines
3.1 KiB
Go
114 lines
3.1 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|