check / check (push) Canceled after 0s
The image request now holds only its source and its transform, exactly what the variant key covers. The /v1/image/ handler checks the allowlist and verifies the signature itself with Signer.Verify(req, sig, expires). Both image handlers keep the URL's expiry as a local value for Cache-Control. The image service holds no signer, allowlist or AllowHTTP, and ValidateRequest is gone. The fetcher takes the source and builds its URL with Source.URL from its own AllowHTTP, so no URL string is built and parsed again. chooseAutoFormat returns the format it chooses instead of writing into the request. The signed URL format and every route's answers are unchanged. Model: opus-5-5
120 lines
3.3 KiB
Go
120 lines
3.3 KiB
Go
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)
|
|
sig := signer.Sign(signed, expires)
|
|
|
|
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, sig)
|
|
t.Logf("signature for path %q query %q: %s",
|
|
replayed.Source.Path, replayed.Source.Query, signer.Sign(&replayed, expires))
|
|
|
|
err := signer.Verify(&replayed, sig, expires)
|
|
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,
|
|
},
|
|
}
|
|
|
|
gotSignature := signer.Sign(req, time.Unix(goldenExpiresUnix, 0))
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|