Skip to content
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package httpsig

import (
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"fmt"
Expand Down Expand Up @@ -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:
Expand Down
92 changes: 92 additions & 0 deletions ecdsa.go
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 8 additions & 8 deletions handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
)

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

v := NewVerifier(test)

Expand All @@ -44,7 +44,7 @@ func TestHandlerNoRealm(t *testing.T) {
}

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

v := NewVerifier(test)

Expand All @@ -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"})
Expand All @@ -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)
Expand All @@ -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"})
Expand All @@ -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"
Expand All @@ -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"})
Expand All @@ -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)
Expand Down
76 changes: 67 additions & 9 deletions httpsig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package httpsig

import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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")
}
Expand All @@ -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"}`))
Expand All @@ -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())}...)
}
Expand Down
8 changes: 8 additions & 0 deletions sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package httpsig

import (
"crypto/ecdsa"
"crypto/rsa"
"encoding/base64"
"fmt"
Expand Down Expand Up @@ -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) (
Expand Down
9 changes: 8 additions & 1 deletion verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down