diff --git a/README.md b/README.md index f58524b..7bf748d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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="..." ``` @@ -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 @@ -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 diff --git a/handler_test.go b/handler_test.go index 991b8b8..b27c1ea 100644 --- a/handler_test.go +++ b/handler_test.go @@ -15,6 +15,7 @@ package httpsig import ( + "crypto/rsa" "net/http" "net/http/httptest" "testing" @@ -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) diff --git a/hs2019.go b/hs2019.go new file mode 100644 index 0000000..2f6ff9a --- /dev/null +++ b/hs2019.go @@ -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) +} diff --git a/httpsig.go b/httpsig.go index 36d982f..60fe8af 100644 --- a/httpsig.go +++ b/httpsig.go @@ -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 diff --git a/httpsig_test.go b/httpsig_test.go index 7f7809c..8c2a558 100644 --- a/httpsig_test.go +++ b/httpsig_test.go @@ -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() @@ -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() @@ -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 ///////////////////////////////////////////////////////////////////////////// @@ -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, @@ -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 } diff --git a/keystore.go b/keystore.go index efead79..406326c 100644 --- a/keystore.go +++ b/keystore.go @@ -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), } } @@ -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] +} diff --git a/sign.go b/sign.go index 745ee69..9d050f1 100644 --- a/sign.go +++ b/sign.go @@ -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"} @@ -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) diff --git a/verify.go b/verify.go index d9df7ab..392d842 100644 --- a/verify.go +++ b/verify.go @@ -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") } @@ -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": @@ -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) } @@ -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