Take the signature, the expiry and the URL scheme off the image request (closes #314)
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
This commit is contained in:
2026-10-26 12:18:23 +00:00
parent 63690a9ec9
commit 8a03d0dbe7
46 changed files with 753 additions and 908 deletions
+20 -15
View File
@@ -34,14 +34,14 @@ func New(secretKey string) *Signer {
}
}
// Sign generates an HMAC-SHA256 signature for the given request.
// The signature covers: host + path + query + width + height + format +
// expiration + quality + fit. The quality and fit mode are the effective
// values the request resolves to: callers pass the defaults when the request
// omits those parameters, so an omitted quality or fit mode signs
// Sign generates an HMAC-SHA256 signature for the given request, expiring at
// expires. The signature covers: host + path + query + width + height +
// format + expiration + quality + fit. The quality and fit mode are the
// effective values the request resolves to: callers pass the defaults when
// the request omits those parameters, so an omitted quality or fit mode signs
// identically to that same value stated explicitly.
func (s *Signer) Sign(req *imagerequest.Request) string {
data := s.buildSignatureData(req)
func (s *Signer) Sign(req *imagerequest.Request, expires time.Time) string {
data := s.buildSignatureData(req, expires)
mac := hmac.New(sha256.New, s.secretKey)
mac.Write([]byte(data))
sig := mac.Sum(nil)
@@ -49,28 +49,31 @@ func (s *Signer) Sign(req *imagerequest.Request) string {
return base64.URLEncoding.EncodeToString(sig)
}
// Verify checks if the signature on the request is valid and not expired.
// Verify checks that sig is the signature of the request expiring at expires,
// and that expires has not passed.
// Signatures are exact-match only: every component of the signed data
// (host, path, query, dimensions, format, expiration, quality, fit) must
// match exactly.
// No suffix matching, wildcard matching, or partial matching is supported.
// A signature for "cdn.example.com" will NOT verify for "example.com" or
// "other.cdn.example.com", and vice versa.
func (s *Signer) Verify(req *imagerequest.Request) error {
func (s *Signer) Verify(
req *imagerequest.Request, sig string, expires time.Time,
) error {
// Check expiration first
if req.Expires.IsZero() {
if expires.IsZero() {
return ErrMissingExpiration
}
if time.Now().After(req.Expires) {
if time.Now().After(expires) {
return ErrExpired
}
// Compute expected signature
expected := s.Sign(req)
expected := s.Sign(req, expires)
// Constant-time comparison to prevent timing attacks
if !hmac.Equal([]byte(req.Signature), []byte(expected)) {
if !hmac.Equal([]byte(sig), []byte(expected)) {
return ErrInvalid
}
@@ -85,7 +88,9 @@ func (s *Signer) Verify(req *imagerequest.Request) error {
// expansion is performed. Quality and fit are the effective transform
// values, so replaying a signed URL with a different quality or fit mode
// fails verification.
func (s *Signer) buildSignatureData(req *imagerequest.Request) string {
func (s *Signer) buildSignatureData(
req *imagerequest.Request, expires time.Time,
) string {
return fmt.Sprintf("%s:%s:%s:%d:%d:%s:%d:%d:%s",
req.Source.Host,
escapePercentAndColon(req.Source.Path),
@@ -93,7 +98,7 @@ func (s *Signer) buildSignatureData(req *imagerequest.Request) string {
req.Transform.Size.Width,
req.Transform.Size.Height,
req.Transform.Format,
req.Expires.Unix(),
expires.Unix(),
req.Transform.Quality,
req.Transform.FitMode,
)