Pin the new signed format in the golden vectors (closes #60)
check / check (push) Successful in 3m6s

The three known-answer vectors in golden_test.go now sign the effective
quality and fit (85 and cover when the URL has neither), matching the
new signed data format. The extra non-default vector (quality 40, fit
contain) moves in from golden_qualityfit_test.go, which is deleted, so
there is one known-answer test file. The fit-mode fixtures join the
shared test constants.

README: the exact-match list names quality and fit, the field list says
where each value comes from, and the worked example shows a
non-default case.

Model: opus-5-5
This commit is contained in:
2026-09-28 09:14:50 +00:00
parent c9b282f2cf
commit fb9fbf4625
6 changed files with 55 additions and 146 deletions
+10 -11
View File
@@ -79,9 +79,9 @@ hosts require an HMAC-SHA256 signature.
Signatures use HMAC-SHA256 and include an expiration timestamp to
prevent replay attacks. Signatures are **exact match only**: every
component (host, path, query, dimensions, format, expiration) must
match exactly what was signed. No suffix matching, wildcard matching,
or partial matching is supported.
component (host, path, query, dimensions, format, expiration, quality,
fit) must match exactly what was signed. No suffix matching, wildcard
matching, or partial matching is supported.
**Signed data format** (colon-separated):
@@ -98,14 +98,10 @@ Where:
- `height` — requested height in pixels, `0` for original
- `format` — output format (jpeg, png, webp, avif, gif, orig)
- `expiration` — Unix timestamp when signature expires
- `quality` — output quality 1-100; sign `85` (the default) when the URL
omits the `q` parameter
- `fit` — fit mode (cover, contain, fill, inside, outside); sign `cover`
(the default) when the URL omits the `fit` parameter
The `q` and `fit` query parameters are covered by the signature. A URL
signed for one quality or fit value will not verify when replayed with a
different value; the effective (post-default) value is what is signed.
- `quality` — the URL's `q` query parameter (1-100), or `85` when the URL
has no `q`
- `fit` — the URL's `fit` query parameter (cover, contain, fill, inside,
outside), or `cover` when the URL has no `fit`
**Example:** resize `https://cdn.example.com/photos/cat.jpg` to 800x600
WebP with expiration 1704067200, default quality and fit:
@@ -117,6 +113,9 @@ WebP with expiration 1704067200, default quality and fit:
4. URL:
`/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200`
For the same image at quality 40 with fit `contain`, the input ends in
`:40:contain` and the URL also carries `q=40&fit=contain`.
**Allowlist patterns:**
- **Exact match**: `cdn.example.com` — matches only that host
+7
View File
@@ -30,6 +30,13 @@ exhaustion
# Completed Steps
- 2026-09-28 quality and fit in the URL signature (closes #60): the signed
data is now `host:path:query:width:height:format:expiration:quality:fit`,
using `85` and `cover` when the URL has no `q` or `fit`, so one signed
URL can no longer be replayed across other quality and fit values to
create unauthorized cache entries and transcodes; the known-answer
vectors in `internal/signature/golden_test.go` and the README signature
specification describe the new format.
- 2026-09-21 trusted-proxy client IP resolution (closes #94): a
`trusted_proxies` config key taking a list of CIDRs, parsed by the same
`net/netip` list parser as `blocked_networks` (an invalid entry aborts
@@ -1,122 +0,0 @@
package signature_test
import (
"testing"
"time"
"sneak.berlin/go/pixa/internal/signature"
)
// Fixed inputs for the quality/fit golden vectors. They are independent of
// the constants in golden_test.go so this file pins the current signed
// format on its own.
const (
qfSigningKey = "golden-test-key"
qfExpiresUnix int64 = 1704067200 // 2024-01-01T00:00:00Z
qfFitCover = "cover"
qfFitContain = "contain"
)
type qualityFitGoldenVector struct {
name string
req signature.Request
// wantSignature is the exact base64url (RFC 4648 URL-safe, padded)
// HMAC-SHA256 signature for the request with Expires set to
// qfExpiresUnix, under the signed format
// "host:path:query:width:height:format:expiration:quality:fit".
wantSignature string
}
// qualityFitGoldenVectors returns the known-answer vectors that pin quality
// and fit as signed components. The three default-value vectors use the
// effective quality (85) and fit ("cover") the handler applies when a URL
// omits q and fit, so they are the signatures real signed URLs must carry.
func qualityFitGoldenVectors() []qualityFitGoldenVector {
return []qualityFitGoldenVector{
{
name: "resized, default quality and fit",
req: signature.Request{
SourceHost: testHost,
SourcePath: testPath,
Width: 800,
Height: 600,
Format: testFormatWebP,
Quality: 85,
FitMode: qfFitCover,
},
// "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:85:cover"
wantSignature: "kdqeGoW2SX7qnaYtoB970wEnLydn0UnIgQYQLfAnjXQ=",
},
{
name: "resized with query, default quality and fit",
req: signature.Request{
SourceHost: testHost,
SourcePath: testPath,
SourceQuery: "token=abc&v=2",
Width: 800,
Height: 600,
Format: testFormatWebP,
Quality: 85,
FitMode: qfFitCover,
},
// "cdn.example.com:/photos/cat.jpg:token=abc&v=2:800:600:webp:1704067200:85:cover"
wantSignature: "pKgVBOTd_Q_EikI7MNQLC9Q8Hurdxzyv3EIYvVhqc2I=",
},
{
name: "original size, default quality and fit",
req: signature.Request{
SourceHost: testHost,
SourcePath: testPath,
Width: 0,
Height: 0,
Format: testFormatPNG,
Quality: 85,
FitMode: qfFitCover,
},
// "cdn.example.com:/photos/cat.jpg::0:0:png:1704067200:85:cover"
wantSignature: "6_rZ0yyVbGZRs8kG7n7HLgLi5Jt8vjiWQljIEL1jbIs=",
},
{
name: "non-default quality and fit",
req: signature.Request{
SourceHost: testHost,
SourcePath: testPath,
Width: 800,
Height: 600,
Format: testFormatWebP,
Quality: 40,
FitMode: qfFitContain,
},
// "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:40:contain"
wantSignature: "pGaXpPUbI3A7nMx-4T9bfq9bYWBNL0kY4bxlcv3g1F8=",
},
}
}
// TestSigner_GoldenVectors_QualityFit pins the exact HMAC-SHA256 signature
// output for requests that carry quality and fit as signed components. If
// these assertions fail, the signed byte format
// ("host:path:query:width:height:format:expiration:quality:fit") or the
// base64url encoding has changed, breaking every signature already issued.
// Update these constants only as part of a deliberate, documented signature
// format migration.
func TestSigner_GoldenVectors_QualityFit(t *testing.T) {
t.Parallel()
signer := signature.New(qfSigningKey)
for _, tt := range qualityFitGoldenVectors() {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
signReq := tt.req
signReq.Expires = time.Unix(qfExpiresUnix, 0)
gotSignature := signer.Sign(&signReq)
if gotSignature != tt.wantSignature {
t.Errorf("Sign() = %q, want %q (signed byte format changed?)",
gotSignature, tt.wantSignature)
}
})
}
}
+34 -11
View File
@@ -41,9 +41,12 @@ func goldenVectors() []goldenVector {
Width: 800,
Height: 600,
Format: testFormatWebP,
Quality: 85,
FitMode: testFitCover,
},
// Signed data: "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200"
wantSignature: "x5PfPp8QSDo0cJT96od-AEgrQyOVLfqifH5sst61_-w=",
// Signed data:
// "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:85:cover"
wantSignature: "kdqeGoW2SX7qnaYtoB970wEnLydn0UnIgQYQLfAnjXQ=",
wantSignedPath: testSignedPath,
},
{
@@ -55,10 +58,12 @@ func goldenVectors() []goldenVector {
Width: 800,
Height: 600,
Format: testFormatWebP,
Quality: 85,
FitMode: testFitCover,
},
// Signed data:
// "cdn.example.com:/photos/cat.jpg:token=abc&v=2:800:600:webp:1704067200"
wantSignature: "394_Vf9TdQFkpQ3XKFDQSyxgqKq8N7mApf2S4QaHqyo=",
// "cdn.example.com:/photos/cat.jpg:token=abc&v=2:800:600:webp:1704067200:85:cover"
wantSignature: "pKgVBOTd_Q_EikI7MNQLC9Q8Hurdxzyv3EIYvVhqc2I=",
wantSignedPath: "/v1/image/cdn.example.com/photos/cat.jpg" +
"%3Ftoken=abc&v=2/800x600.webp",
},
@@ -71,11 +76,31 @@ func goldenVectors() []goldenVector {
Width: 0,
Height: 0,
Format: testFormatPNG,
Quality: 85,
FitMode: testFitCover,
},
// Signed data: "cdn.example.com:/photos/cat.jpg::0:0:png:1704067200"
wantSignature: "7Be7oteeQwvnSPU4bchyQ4ZGYGsAGBKpeEtuQ02ox60=",
// Signed data:
// "cdn.example.com:/photos/cat.jpg::0:0:png:1704067200:85:cover"
wantSignature: "6_rZ0yyVbGZRs8kG7n7HLgLi5Jt8vjiWQljIEL1jbIs=",
wantSignedPath: "/v1/image/cdn.example.com/photos/cat.jpg/orig.png",
},
{
name: "non-default quality and fit",
req: signature.Request{
SourceHost: testHost,
SourcePath: testPath,
SourceQuery: "",
Width: 800,
Height: 600,
Format: testFormatWebP,
Quality: 40,
FitMode: testFitContain,
},
// Signed data:
// "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:40:contain"
wantSignature: "pGaXpPUbI3A7nMx-4T9bfq9bYWBNL0kY4bxlcv3g1F8=",
wantSignedPath: testSignedPath,
},
}
}
@@ -84,11 +109,9 @@ func goldenVectors() []goldenVector {
// hardcoded signing key.
//
// 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.
// ("host:path:query:width:height:format:expiration:quality:fit"), the
// base64url encoding, or the signed URL layout has changed. Update these
// constants only when that change is intended.
func TestSigner_GoldenVectors(t *testing.T) {
t.Parallel()
+2 -2
View File
@@ -19,7 +19,7 @@ func signedQualityFitRequest(signer *signature.Signer) *signature.Request {
Height: 600,
Format: testFormatWebP,
Quality: 85,
FitMode: "cover",
FitMode: testFitCover,
Expires: time.Now().Add(1 * time.Hour),
}
req.Signature = signer.Sign(req)
@@ -48,7 +48,7 @@ func TestSigner_Verify_QualityAndFitAreSigned(t *testing.T) {
},
{
name: "replayed with different fit mode",
tamper: func(r *signature.Request) { r.FitMode = "contain" },
tamper: func(r *signature.Request) { r.FitMode = testFitContain },
},
}
+2
View File
@@ -15,6 +15,8 @@ const (
testPath = "/photos/cat.jpg"
testFormatWebP = "webp"
testFormatPNG = "png"
testFitCover = "cover"
testFitContain = "contain"
testSignedPath = "/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp"
testSig = "abc123"
)