0
Fork 0
mirror of https://github.com/willnorris/imageproxy.git synced 2024-12-16 21:56:43 -05:00

allow request signatures to cover options

URL-only signatures are still accepted, though no longer recommended.

Fixes #145
This commit is contained in:
Will Norris 2019-03-27 20:57:15 +00:00
parent ae2a31cc01
commit 38d3bcc7fe
3 changed files with 93 additions and 12 deletions

View file

@ -1,6 +1,71 @@
# How to generate signed requests
## Go
Signing requests allows an imageproxy instance to proxy images from arbitrary
remote hosts, but without opening the service up for potential abuse. When
appropriately configured, the imageproxy instance will only serve requests that
are for allowed hosts, or which have a valid signature.
Signatures can be calculated in two ways:
1. they can be calculated solely on the remote image URL, in which case any
transformations of the image can be requested without changes to the
signature value. This used to be the only way to sign requests, but is no
longer recommended since it still leaves the imageproxy instance open to
potential abuse.
2. they can be calculated based on the combination of the remote image URL and
the requested transformation options.
In both cases, the signature is calculated using HMAC-SHA256 and a secret key
which is provided to imageproxy on startup. The message to be signed is the
remote URL, with the transformation options optionally set as the URL fragment,
[as documented below](#Signing-options). The signature is url-safe base64
encoded, and [provided as an option][s-option] in the imageproxy request.
imageproxy will accept signatures for URLs with or without options
transparently. It's up to the publisher of the signed URLs to decide which
method they use to generate the URL.
[s-option]: https://godoc.org/willnorris.com/go/imageproxy#hdr-Signature
## Signing options
Transformation options for a proxied URL are [specified as a comma separated
string][ParseOptions] of individual options, which can be supplied in any
order. When calculating a signature, options should be put in their canonical
form, sorted in lexigraphical order (omitting the signature option itself), and
appended to the remote URL as the URL fragment.
Currently, only [size option][] has a canonical form, which is
`{width}x{height}` with the number `0` used when no value is specified. For
example, a request that does not request any size option would still have a
canonical size value of `0x0`, indicating that no size transformation is being
performed. If only a height of 500px is requested, the canonical form would be
`0x500`.
For example, requesting the remote URL of `http://example.com/image.jpg`,
resized to 100 pixels square, rotated 90 degrees, and converted to 75% quality
might produce an imageproxy URL similar to:
http://localhost:8080/100,r90,q75/http://example.com/image.jpg
When calculating a signature for this request including transformation options,
the signed value would be:
http://example.com/image.jpg#100x100,q75,r90
The `100` size option was put in its canonical form of `100x100`, and the
options are sorted, moving `q75` before `r90`.
[ParseOptions]: https://godoc.org/willnorris.com/go/imageproxy#ParseOptions
[size option]: https://godoc.org/willnorris.com/go/imageproxy#hdr-Size_and_Cropping
## Language Examples
Here are examples of calculating signatures in a variety of languages. These
demonstrate the HMAC-SHA256 bits, but not the option canonicalization.
### Go
main.go:
```go
@ -27,14 +92,14 @@ $ go run main.go "test" "https://www.google.fr/images/srpr/logo11w.png"
result: RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
```
## OpenSSL
### OpenSSL
```shell
$ echo -n "https://www.google.fr/images/srpr/logo11w.png" | openssl dgst -sha256 -hmac "test" -binary|base64| tr '/+' '_-'
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
```
## Java
### Java
```java
import org.apache.commons.codec.binary.Base64;
@ -63,7 +128,7 @@ $ java -cp commons-codec-1.10.jar:. EncodeUrl test https://www.google.fr/images/
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA
```
## Ruby
### Ruby
```ruby
require 'openssl'
@ -79,7 +144,7 @@ puts Base64.urlsafe_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'),
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
```
## Python
### Python
```python
import hmac
@ -91,7 +156,7 @@ data = 'https://octodex.github.com/images/codercat.jpg'
print base64.urlsafe_b64encode(hmac.new(key, msg=data, digestmod=hashlib.sha256).digest())
```
## JavaScript
### JavaScript
```javascript
import crypto from 'crypto';
@ -102,7 +167,7 @@ let data = 'https://octodex.github.com/images/codercat.jpg';
console.log(URLSafeBase64.encode(crypto.createHmac('sha256', key).update(data).digest()));
```
## PHP
### PHP
````php
<?php
@ -114,4 +179,4 @@ echo strtr(base64_encode(hash_hmac('sha256', $data, $key, 1)), '/+' , '_-');
````shell
$ php ex.php
RYifAJRfbhsitJeOrDNxWURCCkPsVR4ihCPXNv-ePbA=
````
````

View file

@ -146,15 +146,15 @@ func (p *Proxy) serveImage(w http.ResponseWriter, r *http.Request) {
return
}
// assign static settings from proxy to req.Options
req.Options.ScaleUp = p.ScaleUp
if err := p.allowed(req); err != nil {
log.Printf("%s: %v", err, req)
http.Error(w, msgNotAllowed, http.StatusForbidden)
return
}
// assign static settings from proxy to req.Options
req.Options.ScaleUp = p.ScaleUp
actualReq, _ := http.NewRequest("GET", req.String(), nil)
if p.UserAgent != "" {
actualReq.Header.Set("User-Agent", p.UserAgent)
@ -322,10 +322,22 @@ func validSignature(key []byte, r *Request) bool {
return false
}
// check signature with URL only
mac := hmac.New(sha256.New, key)
mac.Write([]byte(r.URL.String()))
want := mac.Sum(nil)
if hmac.Equal(got, want) {
return true
}
// check signature with URL and options
u, opt := *r.URL, r.Options // make copies
opt.Signature = ""
u.Fragment = opt.String()
mac = hmac.New(sha256.New, key)
mac.Write([]byte(u.String()))
want = mac.Sum(nil)
return hmac.Equal(got, want)
}

View file

@ -228,6 +228,10 @@ func TestValidSignature(t *testing.T) {
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, true},
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ"}, true},
{"http://test/image", emptyOptions, false},
// url-only signature with options
{"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ", Rotate: 90}, true},
// signature calculated from url plus options
{"http://test/image", Options{Signature: "ZGTzEm32o4iZ7qcChls3EVYaWyrDd9u0etySo0-WkF8=", Rotate: 90}, true},
}
for _, tt := range tests {
@ -237,7 +241,7 @@ func TestValidSignature(t *testing.T) {
}
req := &Request{u, tt.options, &http.Request{}}
if got, want := validSignature(key, req), tt.valid; got != want {
t.Errorf("validSignature(%v, %q) returned %v, want %v", key, u, got, want)
t.Errorf("validSignature(%v, %v) returned %v, want %v", key, req, got, want)
}
}
}