From 63235c7f1c0cd35a64dc1066c47c99daa924c2e3 Mon Sep 17 00:00:00 2001 From: Bryan Angelo Date: Thu, 24 Oct 2019 11:27:31 -0700 Subject: [PATCH] Add ecdsa-sha256 support --- README.md | 1 + common.go | 21 +++++++++++ ecdsa.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++ handler_test.go | 16 ++++----- httpsig_test.go | 76 +++++++++++++++++++++++++++++++++++----- sign.go | 8 +++++ verify.go | 9 ++++- 7 files changed, 205 insertions(+), 18 deletions(-) create mode 100644 ecdsa.go diff --git a/README.md b/README.md index f58524b..921a19b 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ If signature validation fails, a `401` is returned along with a - rsa-sha1 (using PKCS1v15) - rsa-sha256 (using PKCS1v15) +- ecdsa-sha256 - hmac-sha256 ### License diff --git a/common.go b/common.go index 43d73ea..a70f20c 100644 --- a/common.go +++ b/common.go @@ -15,6 +15,7 @@ package httpsig import ( + "crypto/ecdsa" "crypto/rand" "crypto/rsa" "fmt" @@ -95,6 +96,26 @@ func toRSAPublicKey(key interface{}) *rsa.PublicKey { } } +func toECDSAPrivateKey(key interface{}) *ecdsa.PrivateKey { + switch k := key.(type) { + case *ecdsa.PrivateKey: + return k + default: + return nil + } +} + +func toECDSAPublicKey(key interface{}) *ecdsa.PublicKey { + switch k := key.(type) { + case *ecdsa.PublicKey: + return k + case *ecdsa.PrivateKey: + return &k.PublicKey + default: + return nil + } +} + func toHMACKey(key interface{}) []byte { switch k := key.(type) { case []byte: diff --git a/ecdsa.go b/ecdsa.go new file mode 100644 index 0000000..7842ac5 --- /dev/null +++ b/ecdsa.go @@ -0,0 +1,92 @@ +// Copyright (C) 2017 Space Monkey, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpsig + +import ( + "crypto" + "crypto/ecdsa" + "encoding/asn1" + "fmt" + "math/big" +) + +type ecdsa_signature struct { + R *big.Int + S *big.Int +} + +// ECDSASHA256 implements ECDSA PKCS1v15 signatures over a SHA256 digest +var ECDSASHA256 Algorithm = ecdsa_sha256{} + +type ecdsa_sha256 struct{} + +func (ecdsa_sha256) Name() string { + return "ecdsa-sha256" +} + +func (a ecdsa_sha256) Sign(key interface{}, data []byte) ([]byte, error) { + k := toECDSAPrivateKey(key) + if k == nil { + return nil, unsupportedAlgorithm(a) + } + return ECDSASign(k, crypto.SHA256, data) +} + +func (a ecdsa_sha256) Verify(key interface{}, data, sig []byte) error { + k := toECDSAPublicKey(key) + if k == nil { + return unsupportedAlgorithm(a) + } + return ECDSAVerify(k, crypto.SHA256, data, sig) +} + +// ECDSASign signs a digest of the data hashed using the provided hash +func ECDSASign(key *ecdsa.PrivateKey, hash crypto.Hash, data []byte) ( + signature []byte, err error) { + + var sig ecdsa_signature + + h := hash.New() + if _, err := h.Write(data); err != nil { + return nil, err + } + + sig.R, sig.S, err = ecdsa.Sign(Rand, key, h.Sum(nil)) + if err != nil { + return nil, err + } + + return asn1.Marshal(sig) +} + +// ECDSAVerify verifies a signed digest of the data hashed using the provided hash +func ECDSAVerify(key *ecdsa.PublicKey, hash crypto.Hash, data, sig []byte) ( + err error) { + + var signature ecdsa_signature + + if _, err := asn1.Unmarshal(sig, &signature); err != nil { + return err + } + + h := hash.New() + if _, err := h.Write(data); err != nil { + return err + } + if !ecdsa.Verify(key, h.Sum(nil), signature.R, signature.S) { + return fmt.Errorf("ecdsa: invalid signature") + } + return nil +} diff --git a/handler_test.go b/handler_test.go index 991b8b8..fcfd95e 100644 --- a/handler_test.go +++ b/handler_test.go @@ -21,7 +21,7 @@ import ( ) func TestHandlerNoRealm(t *testing.T) { - test := NewTest(t) + test := NewRSATest(t) v := NewVerifier(test) @@ -44,7 +44,7 @@ func TestHandlerNoRealm(t *testing.T) { } func TestHandlerWithRealm(t *testing.T) { - test := NewTest(t) + test := NewRSATest(t) v := NewVerifier(test) @@ -67,7 +67,7 @@ func TestHandlerWithRealm(t *testing.T) { } func TestHandlerRejectsRequestWithoutRequiredHeadersInSignature(t *testing.T) { - test := NewTest(t) + test := NewRSATest(t) v := NewVerifier(test) v.SetRequiredHeaders([]string{"(request-target)", "date"}) @@ -80,7 +80,7 @@ func TestHandlerRejectsRequestWithoutRequiredHeadersInSignature(t *testing.T) { req, err := http.NewRequest("GET", server.URL, nil) test.AssertNoError(err) - s := NewRSASHA256Signer("Test", test.PrivateKey, []string{"date"}) + s := NewRSASHA256Signer("Test", test.RSAPrivateKey(), []string{"date"}) test.AssertNoError(s.Sign(req)) resp, err := http.DefaultClient.Do(req) @@ -94,7 +94,7 @@ func TestHandlerRejectsRequestWithoutRequiredHeadersInSignature(t *testing.T) { } func TestHandlerRejectsModifiedRequest(t *testing.T) { - test := NewTest(t) + test := NewRSATest(t) v := NewVerifier(test) v.SetRequiredHeaders([]string{"(request-target)", "date"}) @@ -107,7 +107,7 @@ func TestHandlerRejectsModifiedRequest(t *testing.T) { req, err := http.NewRequest("GET", server.URL, nil) test.AssertNoError(err) - s := NewRSASHA256Signer("Test", test.PrivateKey, v.RequiredHeaders()) + s := NewRSASHA256Signer("Test", test.RSAPrivateKey(), v.RequiredHeaders()) test.AssertNoError(s.Sign(req)) req.URL.Path = "/foo" @@ -123,7 +123,7 @@ func TestHandlerRejectsModifiedRequest(t *testing.T) { } func TestHandlerAcceptsSignedRequest(t *testing.T) { - test := NewTest(t) + test := NewRSATest(t) v := NewVerifier(test) v.SetRequiredHeaders([]string{"(request-target)", "date"}) @@ -136,7 +136,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 := NewRSASHA256Signer("Test", test.RSAPrivateKey(), v.RequiredHeaders()) test.AssertNoError(s.Sign(req)) resp, err := http.DefaultClient.Do(req) diff --git a/httpsig_test.go b/httpsig_test.go index 7f7809c..6003351 100644 --- a/httpsig_test.go +++ b/httpsig_test.go @@ -19,6 +19,7 @@ package httpsig import ( + "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/pem" @@ -31,7 +32,7 @@ import ( ) var ( - privKey = `-----BEGIN RSA PRIVATE KEY----- + privRSAKey = `-----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB @@ -46,12 +47,25 @@ gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI 7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== -----END RSA PRIVATE KEY-----` + + privECDSAKey = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIG1OavhFPnayZivES62otFT6/xTFBHqDS5ViNCO8XvV2oAoGCCqGSM49 +AwEHoUQDQgAE0gTn0ky0WDyet0XfkeXahZ7FlwTeknfJrxZAZ+vIQmTZSTScJ7qD +/Dp4y6Dyr3gcfvG4cVpR2h6XTBGBm2Fjng== +-----END EC PRIVATE KEY-----` ) func TestDate(t *testing.T) { - test := NewTest(t) + test := NewRSATest(t) + signer := NewRSASHA256Signer("Test", test.RSAPrivateKey(), []string{"date"}) + testDate(t, test, signer) + + test = NewECDSATest(t) + signer = NewECDSASHA256Signer("Test", test.ECDSAPrivateKey(), []string{"date"}) + testDate(t, test, signer) +} - signer := NewRSASHA256Signer("Test", test.PrivateKey, []string{"date"}) +func testDate(t *testing.T, test *Test, signer *Signer) { verifier := NewVerifier(test) req := test.NewRequest() @@ -77,10 +91,18 @@ func TestDate(t *testing.T) { } func TestRequestTargetAndHost(t *testing.T) { - test := NewTest(t) - headers := []string{"(request-target)", "host", "date"} - signer := NewRSASHA256Signer("Test", test.PrivateKey, headers) + + test := NewRSATest(t) + signer := NewRSASHA256Signer("Test", test.RSAPrivateKey(), headers) + testTargetAndHost(t, test, signer, headers) + + test = NewECDSATest(t) + signer = NewECDSASHA256Signer("Test", test.ECDSAPrivateKey(), headers) + testTargetAndHost(t, test, signer, headers) +} + +func testTargetAndHost(t *testing.T, test *Test, signer *Signer, headers []string) { verifier := NewVerifier(test) req := test.NewRequest() @@ -137,11 +159,11 @@ func trimHeader(header http.Header, keepers ...string) http.Header { type Test struct { tb testing.TB KeyGetter - PrivateKey *rsa.PrivateKey + PrivateKey interface{} } -func NewTest(tb testing.TB) *Test { - block, _ := pem.Decode([]byte(privKey)) +func NewRSATest(tb testing.TB) *Test { + block, _ := pem.Decode([]byte(privRSAKey)) if block == nil { tb.Fatalf("test setup failure: malformed PEM on private key") } @@ -160,6 +182,26 @@ func NewTest(tb testing.TB) *Test { } } +func NewECDSATest(tb testing.TB) *Test { + block, _ := pem.Decode([]byte(privECDSAKey)) + if block == nil { + tb.Fatalf("test setup failure: malformed PEM on private key") + } + key, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + tb.Fatal(err) + } + + keystore := NewMemoryKeyStore() + keystore.SetKey("Test", key) + + return &Test{ + tb: tb, + KeyGetter: keystore, + PrivateKey: key, + } +} + func (t *Test) NewRequest() *http.Request { req, err := http.NewRequest("POST", "http://example.com/foo", strings.NewReader(`{"hello": "world"}`)) @@ -171,6 +213,22 @@ func (t *Test) NewRequest() *http.Request { return req } +func (t *Test) RSAPrivateKey() *rsa.PrivateKey { + key, ok := t.PrivateKey.(*rsa.PrivateKey) + if !ok { + return nil + } + return key +} + +func (t *Test) ECDSAPrivateKey() *ecdsa.PrivateKey { + key, ok := t.PrivateKey.(*ecdsa.PrivateKey) + if !ok { + return nil + } + return key +} + func (t *Test) Fatal(msg interface{}) { t.tb.Fatalf("\nFATAL:\n%v\nSTACK:\n%s", []interface{}{msg, string(debug.Stack())}...) } diff --git a/sign.go b/sign.go index 745ee69..6181ea9 100644 --- a/sign.go +++ b/sign.go @@ -15,6 +15,7 @@ package httpsig import ( + "crypto/ecdsa" "crypto/rsa" "encoding/base64" "fmt" @@ -66,6 +67,13 @@ func NewRSASHA256Signer(id string, key *rsa.PrivateKey, headers []string) ( return NewSigner(id, key, RSASHA256, headers) } +// NewECDSASHA256Signer contructs a signer with the specified key id, ecdsa private +// key and headers to sign. +func NewECDSASHA256Signer(id string, key *ecdsa.PrivateKey, headers []string) ( + signer *Signer) { + return NewSigner(id, key, ECDSASHA256, headers) +} + // NewHMACSHA256Signer contructs a signer with the specified key id, hmac key, // and headers to sign. func NewHMACSHA256Signer(id string, key []byte, headers []string) ( diff --git a/verify.go b/verify.go index d9df7ab..bc4445f 100644 --- a/verify.go +++ b/verify.go @@ -105,6 +105,13 @@ header_check: params.Algorithm, params.KeyId) } return RSAVerify(rsa_pubkey, crypto.SHA256, sig_data, params.Signature) + case "ecdsa-sha256": + ecdsa_pubkey := toECDSAPublicKey(key) + if ecdsa_pubkey == nil { + return fmt.Errorf("algorithm %q is not supported by key %q", + params.Algorithm, params.KeyId) + } + return ECDSAVerify(ecdsa_pubkey, crypto.SHA256, sig_data, params.Signature) case "hmac-sha256": hmac_key := toHMACKey(key) if hmac_key == nil { @@ -182,7 +189,7 @@ func getParams(req *http.Request, header, prefix string) *Params { 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", "ecdsa-sha256", "hmac-sha256": return s, true } return "", false