Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hs2019 with PSS support #12

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ Signing requests is done by constructing a new `Signer`. The key id, key,
algorithm, and what headers to sign are required.

For example to construct a `Signer` with key id `"foo"`, using an RSA private
key, for the rsa-sha256 algorithm, with the default header set, you can do:
key, for the hs2019 algorithm and RSA key (PSS), with the default header set, you can do:

```
var key *rsa.PrivateKey = ...
signer := httpsig.NewSigner("foo", key, httpsig.RSASHA256, nil)
signer := httpsig.NewSigner("foo", key, httpsig.HS1029_PSS, nil)
```

There are helper functions for specific algorithms that are less verbose and
Expand All @@ -31,7 +31,7 @@ because the type required for the algorithm is known).

```
var key *rsa.PrivateKey = ...
signer := httpsig.NewRSASHA256Signer("foo", key, nil)
signer := httpsig.NewHS2019PSSSigner("foo", key, nil, rsa.PSSSaltLengthEqualsHash)
```

By default, if no headers are passed to `NewSigner` (or the helpers), the
Expand All @@ -49,7 +49,7 @@ fmt.Println("AUTHORIZATION:", req.Header.Get('Authorization'))

...

AUTHORIZATION: Signature: keyId="foo",algorithm="sha-256",signature="..."
AUTHORIZATION: Signature: keyId="foo",algorithm="hs2019",signature="..."

```

Expand Down Expand Up @@ -98,6 +98,12 @@ var hmac_key []byte = ...
keystore.SetKey("foo", hmac_key)
```

In order to support hs2019 the keystore also includes a store for the key
algorithm. Key algorithms can be added using the SetKeyAlgorithm method:
```
keystore.SetKeyAlgorithm("foo", NewHS2019_PSS(rsa.PSSSaltLengthEqualHash))
```

## Handler

A convenience function is provided that wraps an `http.Handler` and verifies
Expand All @@ -118,6 +124,9 @@ If signature validation fails, a `401` is returned along with a

## Supported algorithms

- hs2019 (using PSS)

### Deprecated algorithms
- rsa-sha1 (using PKCS1v15)
- rsa-sha256 (using PKCS1v15)
- hmac-sha256
Expand Down
3 changes: 2 additions & 1 deletion handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package httpsig

import (
"crypto/rsa"
"net/http"
"net/http/httptest"
"testing"
Expand Down Expand Up @@ -136,7 +137,7 @@ func TestHandlerAcceptsSignedRequest(t *testing.T) {
req, err := http.NewRequest("GET", server.URL, nil)
test.AssertNoError(err)

s := NewRSASHA256Signer("Test", test.PrivateKey, v.RequiredHeaders())
s := NewHS2019PSSSigner("Test", test.PrivateKey, v.RequiredHeaders(), rsa.PSSSaltLengthAuto)
test.AssertNoError(s.Sign(req))

resp, err := http.DefaultClient.Do(req)
Expand Down
50 changes: 50 additions & 0 deletions hs2019.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package httpsig

import (
"crypto"
"crypto/rsa"
)

type hs2019_pss struct {
saltLength int
hash crypto.Hash
}

func NewHS2019_PSS(saltLenght int) *hs2019_pss {
return &hs2019_pss{
saltLength: saltLenght,
hash: crypto.SHA512,
}
}

func (hs2019_pss) Name() string {
return "hs2019"
}

func (a hs2019_pss) Sign(key interface{}, data []byte) ([]byte, error) {
k := toRSAPrivateKey(key)
if k == nil {
return nil, unsupportedAlgorithm(a)
}

h := a.hash.New()
if _, err := h.Write(data); err != nil {
return nil, err
}
opt := &rsa.PSSOptions{SaltLength: a.saltLength}
return rsa.SignPSS(Rand, k, a.hash, h.Sum(nil), opt)
}

func (a hs2019_pss) Verify(key interface{}, data, sig []byte) error {
k := toRSAPublicKey(key)
if k == nil {
return unsupportedAlgorithm(a)
}

h := a.hash.New()
if _, err := h.Write(data); err != nil {
return err
}
opt := &rsa.PSSOptions{SaltLength: a.saltLength}
return rsa.VerifyPSS(k, a.hash, h.Sum(nil), sig, opt)
}
1 change: 1 addition & 0 deletions httpsig.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Algorithm interface {
// Other types will treated as if no key was returned.
type KeyGetter interface {
GetKey(id string) interface{}
GetKeyAlgorithm(id string) Algorithm
}

// KeyGetterFunc is a convenience type for implementing a KeyGetter with a
Expand Down
34 changes: 31 additions & 3 deletions httpsig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
func TestDate(t *testing.T) {
test := NewTest(t)

signer := NewRSASHA256Signer("Test", test.PrivateKey, []string{"date"})
signer := NewHS2019PSSSigner("Test", test.PrivateKey, []string{"date"}, rsa.PSSSaltLengthAuto)
verifier := NewVerifier(test)

req := test.NewRequest()
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestRequestTargetAndHost(t *testing.T) {
test := NewTest(t)

headers := []string{"(request-target)", "host", "date"}
signer := NewRSASHA256Signer("Test", test.PrivateKey, headers)
signer := NewHS2019PSSSigner("Test", test.PrivateKey, headers, rsa.PSSSaltLengthAuto)
verifier := NewVerifier(test)

req := test.NewRequest()
Expand Down Expand Up @@ -116,6 +116,31 @@ func TestRequestTargetAndHost(t *testing.T) {
req.Host = orig_host
}

func TestAlgorithmMismatch(t *testing.T) {
test := NewTest(t)

signer := NewRSASHA256Signer("Test", test.PrivateKey, nil)
verifier := NewVerifier(test)

req := test.NewRequest()
test.AssertNoError(signer.Sign(req))

err := verifier.Verify(req)
test.AssertAnyError(err)
test.AssertStringEqual(err.Error(),"algorithm header mismatch. Signature header value: rsa-sha256, derived value: hs2019")
}

func TestDeprecatedAlgorithm(t *testing.T) {
test := NewTest(t)

signer := NewRSASHA256Signer("Test_SHA256", test.PrivateKey, nil)
verifier := NewVerifier(test)

req := test.NewRequest()
test.AssertNoError(signer.Sign(req))
test.AssertNoError(verifier.Verify(req))
}

/////////////////////////////////////////////////////////////////////////////
// Helpers
/////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -152,6 +177,9 @@ func NewTest(tb testing.TB) *Test {

keystore := NewMemoryKeyStore()
keystore.SetKey("Test", key)
keystore.SetKeyAlgorithm("Test", NewHS2019_PSS(rsa.PSSSaltLengthAuto))

keystore.SetKey("Test_SHA256", key)

return &Test{
tb: tb,
Expand All @@ -167,7 +195,7 @@ func (t *Test) NewRequest() *http.Request {
req.Header.Set("Date", "Thu, 05 Jan 2014 21:31:40 GMT")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Digest",
"SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=")
"SHA-512=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=")
return req
}

Expand Down
10 changes: 10 additions & 0 deletions keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ package httpsig

type MemoryKeyStore struct {
keys map[string]interface{}
keyAlgorithms map[string]Algorithm
}

func NewMemoryKeyStore() *MemoryKeyStore {
return &MemoryKeyStore{
keys: make(map[string]interface{}),
keyAlgorithms: make(map[string]Algorithm),
}
}

Expand All @@ -31,3 +33,11 @@ func (m *MemoryKeyStore) GetKey(id string) interface{} {
func (m *MemoryKeyStore) SetKey(id string, key interface{}) {
m.keys[id] = key
}

func (m *MemoryKeyStore) SetKeyAlgorithm(id string, algorithm Algorithm) {
m.keyAlgorithms[id] = algorithm
}

func (m *MemoryKeyStore) GetKeyAlgorithm(id string) Algorithm {
return m.keyAlgorithms[id]
}
11 changes: 11 additions & 0 deletions sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func NewSigner(id string, key interface{}, algo Algorithm, headers []string) (
algo: algo,
}

if !strings.Contains(algo.Name(), "hs2019") {
fmt.Printf("Algorithm %s is deprecated, please update to 'hs2019'", algo.Name())
}

// copy the headers slice, lowercasing as necessary
if len(headers) == 0 {
headers = []string{"(request-target)", "date"}
Expand Down Expand Up @@ -73,6 +77,13 @@ func NewHMACSHA256Signer(id string, key []byte, headers []string) (
return NewSigner(id, key, HMACSHA256, headers)
}

// NewHS2019PSSSigner constructs a signer with the specified key id, hmac key,
// and headers to sign.
func NewHS2019PSSSigner(id string, key *rsa.PrivateKey, headers []string, saltLength int) (
signer *Signer) {
return NewSigner(id, key, NewHS2019_PSS(saltLength), headers)
}

// Sign signs an http request and adds the signature to the authorization header
func (r *Signer) Sign(req *http.Request) error {
params, err := signRequest(r.id, r.key, r.algo, r.headers, req)
Expand Down
20 changes: 16 additions & 4 deletions verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@ func (v *Verifier) Verify(req *http.Request) error {
if params.KeyId == "" {
return fmt.Errorf("keyId is required")
}
if params.Algorithm == "" {
return fmt.Errorf("algorithm is required")
}
if len(params.Signature) == 0 {
return fmt.Errorf("signature is required")
}
Expand All @@ -89,6 +86,18 @@ header_check:
if key == nil {
return fmt.Errorf("no key with id %q", params.KeyId)
}
keyAlgorithm := v.key_getter.GetKeyAlgorithm(params.KeyId)
if params.Algorithm == "hs2019" && keyAlgorithm == nil {
return fmt.Errorf("no key algorithm with id %q", params.KeyId)
}

if params.Algorithm != "hs2019" {
fmt.Printf("algorithm %s is deprecated, please update to 'hs2019'", params.Algorithm)
}

if keyAlgorithm != nil && params.Algorithm != keyAlgorithm.Name() {
return fmt.Errorf("algorithm header mismatch. Signature header value: %s, derived value: %s", params.Algorithm, keyAlgorithm.Name())
}

switch params.Algorithm {
case "rsa-sha1":
Expand All @@ -112,6 +121,8 @@ header_check:
params.Algorithm, params.KeyId)
}
return HMACVerify(hmac_key, crypto.SHA256, sig_data, params.Signature)
case "hs2019":
return keyAlgorithm.Verify(key, sig_data, params.Signature)
default:
return fmt.Errorf("unsupported algorithm %q", params.Algorithm)
}
Expand Down Expand Up @@ -181,8 +192,9 @@ func getParams(req *http.Request, header, prefix string) *Params {
// parseAlgorithm parses recognized algorithm values
func parseAlgorithm(s string) (algorithm string, ok bool) {
s = strings.TrimSpace(s)

switch s {
case "rsa-sha1", "rsa-sha256", "hmac-sha256":
case "rsa-sha1", "rsa-sha256", "hmac-sha256", "hs2019":
return s, true
}
return "", false
Expand Down