diff --git a/algorithm.go b/algorithm.go index 1a9b260..2cb7c77 100644 --- a/algorithm.go +++ b/algorithm.go @@ -1,22 +1,37 @@ package httpsignatures import ( - "crypto/sha1" - "crypto/sha256" + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" "errors" - "hash" + "math/big" ) var ( - AlgorithmHmacSha256 = &Algorithm{"hmac-sha256", sha256.New} - AlgorithmHmacSha1 = &Algorithm{"hmac-sha1", sha1.New} + AlgorithmHmacSha256 = &Algorithm{"hmac-sha256", hmacSign(crypto.SHA256), hmacVerify(crypto.SHA256)} + AlgorithmHmacSha1 = &Algorithm{"hmac-sha1", hmacSign(crypto.SHA1), hmacVerify(crypto.SHA1)} + AlgorithmRsaSha256 = &Algorithm{"rsa-sha256", rsaSign(crypto.SHA256), rsaVerify(crypto.SHA256)} + AlgorithmRsaSha1 = &Algorithm{"rsa-sha1", rsaSign(crypto.SHA1), rsaVerify(crypto.SHA1)} + AlgorithmEcdsaSha256 = &Algorithm{"ecdsa-sha256", ecdsaSign(crypto.SHA256), ecdsaVerify(crypto.SHA256)} ErrorUnknownAlgorithm = errors.New("Unknown Algorithm") ) +// signFn signs message m using key k. +type signFn func(k interface{}, m []byte) ([]byte, error) + +// verifyFn verifies that signature s, for message m was signed by key k. +type verifyFn func(k interface{}, m []byte, s []byte) bool + type Algorithm struct { name string - hash func() hash.Hash + + sign signFn + verify verifyFn } func algorithmFromString(name string) (*Algorithm, error) { @@ -25,7 +40,115 @@ func algorithmFromString(name string) (*Algorithm, error) { return AlgorithmHmacSha1, nil case AlgorithmHmacSha256.name: return AlgorithmHmacSha256, nil + case AlgorithmRsaSha1.name: + return AlgorithmRsaSha1, nil + case AlgorithmRsaSha256.name: + return AlgorithmRsaSha256, nil + case AlgorithmEcdsaSha256.name: + return AlgorithmEcdsaSha256, nil } return nil, ErrorUnknownAlgorithm } + +// hmacSign returns a function that will HMAC sign some message using the given +// hash function. +func hmacSign(h crypto.Hash) signFn { + return func(k interface{}, m []byte) ([]byte, error) { + hash := hmac.New(h.New, []byte(k.(string))) + hash.Write(m) + return hash.Sum(nil), nil + } +} + +// hmacVerify returns a function that will verify that the signature signed with +// the given hashfn matches the calculated signature. +func hmacVerify(h crypto.Hash) verifyFn { + sign := hmacSign(h) + return func(k interface{}, m []byte, s []byte) bool { + calculatedSignature, err := sign(k, m) + if err != nil { + return false + } + + return hmac.Equal(calculatedSignature, s) + } +} + +// rsaSign returns a function that will sign a message with an RSA private key, +// using the given hash function. +func rsaSign(h crypto.Hash) signFn { + return func(k interface{}, m []byte) ([]byte, error) { + hash := h.New() + hash.Write(m) + hashed := hash.Sum(nil) + return rsa.SignPKCS1v15(rand.Reader, k.(*rsa.PrivateKey), h, hashed[:]) + } +} + +// rsaVerify returns a function that will verify that a message was signed with +// an RSA private key. +func rsaVerify(h crypto.Hash) verifyFn { + return func(k interface{}, m []byte, s []byte) bool { + hash := h.New() + hash.Write(m) + hashed := hash.Sum(nil) + return rsa.VerifyPKCS1v15(k.(*rsa.PublicKey), h, hashed[:], s) == nil + } +} + +// ecdsaSign returns a function that will sign a message with an RSA private key, +// using the given hash function. +// +// http://self-issued.info/docs/draft-ietf-jose-json-web-algorithms-00.html#DefiningECDSA +func ecdsaSign(h crypto.Hash) signFn { + return func(k interface{}, m []byte) ([]byte, error) { + privateKey := k.(*ecdsa.PrivateKey) + + hash := h.New() + hash.Write(m) + hashed := hash.Sum(nil) + + r, s, err := ecdsa.Sign(rand.Reader, privateKey, hashed[:]) + if err != nil { + return nil, err + } + + rBytes := pad(r.Bytes(), 32) + sBytes := pad(s.Bytes(), 32) + + sig := append(rBytes, sBytes...) + + return sig, nil + } +} + +// pad left pads the byte array with 0's until b is of length l. +func pad(b []byte, l int) []byte { + r := l - len(b) + p := bytes.Repeat([]byte{0}, r) + return append(p, b...) +} + +// ecdsaVerify returns a function that will verify that a message was signed with +// an ECDSA private key. +func ecdsaVerify(h crypto.Hash) verifyFn { + return func(k interface{}, m []byte, sig []byte) bool { + if len(sig) != 64 { + return false + } + + hash := h.New() + hash.Write(m) + hashed := hash.Sum(nil) + + r := new(big.Int) + s := new(big.Int) + l := len(sig) / 2 + + r.SetBytes(sig[0:l]) + s.SetBytes(sig[l:]) + + return ecdsa.Verify(k.(*ecdsa.PublicKey), hashed[:], r, s) + } +} diff --git a/examples_test.go b/examples_test.go index 37459d7..1b5f7a4 100644 --- a/examples_test.go +++ b/examples_test.go @@ -1,11 +1,38 @@ package httpsignatures_test import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" "net/http" "github.com/99designs/httpsignatures-go" ) +const ( + ExamplePrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF +NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F +UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB +AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA +QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK +kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg +f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u +412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc +mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 +kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA +gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW +G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI +7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== +-----END RSA PRIVATE KEY-----` + ExamplePublicyKey = `-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 +6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 +Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw +oYi+1hqp1fIekaxsyQIDAQAB +-----END PUBLIC KEY-----` +) + func Example_signing() { r, _ := http.NewRequest("GET", "http://example.com/some-api", nil) @@ -17,6 +44,20 @@ func Example_signing() { http.DefaultClient.Do(r) } +func Example_signingRSA() { + block, _ := pem.Decode([]byte(ExamplePrivateKey)) + privateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + + r, _ := http.NewRequest("GET", "http://example.com/some-api", nil) + + // Sign using the 'Signature' header + httpsignatures.DefaultRsaSha256Signer.SignRequestRSA("KeyId", privateKey, r) + // OR Sign using the 'Authorization' header + httpsignatures.DefaultRsaSha256Signer.AuthRequestRSA("KeyId", privateKey, r) + + http.DefaultClient.Do(r) +} + func Example_customSigning() { signer := httpsignatures.NewSigner( httpsignatures.AlgorithmHmacSha256, @@ -51,3 +92,28 @@ func Example_verification() { // request was signed correctly. } } + +func Example_verificationRSA() { + _ = func(w http.ResponseWriter, r *http.Request) { + sig, err := httpsignatures.FromRequest(r) + if err != nil { + // Probably a malformed header + http.Error(w, "Bad Request", http.StatusBadRequest) + panic(err) + } + + // if you have headers that must be signed check + // that they are in sig.Headers + + var pemPublicKeyBytes []byte // = lookup using sig.KeyID + block, _ := pem.Decode(pemPublicKeyBytes) + publicKey, _ := x509.ParsePKIXPublicKey(block.Bytes) + + if !sig.IsValidRSA(publicKey.(*rsa.PublicKey), r) { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + // request was signed correctly. + } +} diff --git a/signature.go b/signature.go index 3a27cd9..6654daa 100644 --- a/signature.go +++ b/signature.go @@ -3,8 +3,8 @@ package httpsignatures import ( - "crypto/hmac" - "crypto/subtle" + "crypto/ecdsa" + "crypto/rsa" "encoding/base64" "errors" "fmt" @@ -107,21 +107,22 @@ func (s Signature) String() string { return str } -func (s Signature) calculateSignature(key string, r *http.Request) (string, error) { - hash := hmac.New(s.Algorithm.hash, []byte(key)) - +func (s Signature) calculateSignature(key interface{}, r *http.Request) (string, error) { signingString, err := s.Headers.signingString(r) if err != nil { return "", err } - hash.Write([]byte(signingString)) + b, err := s.Algorithm.sign(key, []byte(signingString)) + if err != nil { + return "", err + } - return base64.StdEncoding.EncodeToString(hash.Sum(nil)), nil + return base64.StdEncoding.EncodeToString(b), nil } // Sign this signature using the given key -func (s *Signature) sign(key string, r *http.Request) error { +func (s *Signature) sign(key interface{}, r *http.Request) error { sig, err := s.calculateSignature(key, r) if err != nil { return err @@ -132,17 +133,34 @@ func (s *Signature) sign(key string, r *http.Request) error { } // IsValid validates this signature for the given key -func (s Signature) IsValid(key string, r *http.Request) bool { +func (s Signature) IsValid(key interface{}, r *http.Request) bool { if !s.Headers.hasDate() { return false } - sig, err := s.calculateSignature(key, r) + signingString, err := s.Headers.signingString(r) + if err != nil { + return false + } + + signature, err := base64.StdEncoding.DecodeString(s.Signature) if err != nil { return false } - return subtle.ConstantTimeCompare([]byte(s.Signature), []byte(sig)) == 1 + return s.Algorithm.verify(key, []byte(signingString), signature) +} + +// IsValidRSA validates that the request was signed by an RSA private key, using +// the public key for verification. +func (s Signature) IsValidRSA(key *rsa.PublicKey, r *http.Request) bool { + return s.IsValid(key, r) +} + +// IsValidECDSA validates that the request was signed by an ECDSA private key, +// using the public key for verification. +func (s Signature) IsValidECDSA(key *ecdsa.PublicKey, r *http.Request) bool { + return s.IsValid(key, r) } type HeaderList []string diff --git a/signature_test.go b/signature_test.go index d813fa8..b2ca147 100644 --- a/signature_test.go +++ b/signature_test.go @@ -1,6 +1,11 @@ package httpsignatures import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" "net/http" "testing" @@ -8,18 +13,45 @@ import ( ) const ( - TEST_SIGNATURE = `keyId="Test",algorithm="hmac-sha256",signature="JldXnt8W9t643M2Sce10gqCh/+E7QIYLiI+bSjnFBGCti7s+mPPvOjVb72sbd1FjeOUwPTDpKbrQQORrm+xBYfAwCxF3LBSSzORvyJ5nRFCFxfJ3nlQD6Kdxhw8wrVZX5nSem4A/W3C8qH5uhFTRwF4ruRjh+ENHWuovPgO/HGQ="` - TEST_HASH = `JldXnt8W9t643M2Sce10gqCh/+E7QIYLiI+bSjnFBGCti7s+mPPvOjVb72sbd1FjeOUwPTDpKbrQQORrm+xBYfAwCxF3LBSSzORvyJ5nRFCFxfJ3nlQD6Kdxhw8wrVZX5nSem4A/W3C8qH5uhFTRwF4ruRjh+ENHWuovPgO/HGQ=` - TEST_KEY = "SomethingRandom" - TEST_DATE = "Thu, 05 Jan 2012 21:31:40 GMT" - TEST_KEY_ID = "Test" + // HMAC + TEST_SIGNATURE_HMAC_SHA256 = `keyId="Test",algorithm="hmac-sha256",signature="JldXnt8W9t643M2Sce10gqCh/+E7QIYLiI+bSjnFBGCti7s+mPPvOjVb72sbd1FjeOUwPTDpKbrQQORrm+xBYfAwCxF3LBSSzORvyJ5nRFCFxfJ3nlQD6Kdxhw8wrVZX5nSem4A/W3C8qH5uhFTRwF4ruRjh+ENHWuovPgO/HGQ="` + TEST_HASH_HMAC_SHA256 = `JldXnt8W9t643M2Sce10gqCh/+E7QIYLiI+bSjnFBGCti7s+mPPvOjVb72sbd1FjeOUwPTDpKbrQQORrm+xBYfAwCxF3LBSSzORvyJ5nRFCFxfJ3nlQD6Kdxhw8wrVZX5nSem4A/W3C8qH5uhFTRwF4ruRjh+ENHWuovPgO/HGQ=` + TEST_KEY_HMAC = "SomethingRandom" + + // RSA + TEST_SIGNATURE_RSA_SHA256 = `keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w="` + TEST_HASH_RSA_SHA256 = `jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=` + TEST_PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF +NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F +UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB +AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA +QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK +kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg +f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u +412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc +mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 +kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA +gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW +G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI +7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== +-----END RSA PRIVATE KEY-----` + TEST_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 +6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 +Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw +oYi+1hqp1fIekaxsyQIDAQAB +-----END PUBLIC KEY-----` + + TEST_DATE = "Thu, 05 Jan 2012 21:31:40 GMT" + TEST_KEY_ID = "Test" ) func TestCreateSignatureFromAuthorizationHeader(t *testing.T) { r := http.Request{ Header: http.Header{ "Date": []string{TEST_DATE}, - headerAuthorization: []string{authScheme + TEST_SIGNATURE}, + headerAuthorization: []string{authScheme + TEST_SIGNATURE_HMAC_SHA256}, }, } @@ -28,16 +60,16 @@ func TestCreateSignatureFromAuthorizationHeader(t *testing.T) { assert.Equal(t, "Test", s.KeyID) assert.Equal(t, AlgorithmHmacSha256, s.Algorithm) - assert.Equal(t, TEST_HASH, s.Signature) + assert.Equal(t, TEST_HASH_HMAC_SHA256, s.Signature) - assert.Equal(t, s.String(), TEST_SIGNATURE) + assert.Equal(t, s.String(), TEST_SIGNATURE_HMAC_SHA256) } func TestCreateSignatureFromSignatureHeaderHeader(t *testing.T) { r := http.Request{ Header: http.Header{ "Date": []string{TEST_DATE}, - headerSignature: []string{TEST_SIGNATURE}, + headerSignature: []string{TEST_SIGNATURE_HMAC_SHA256}, }, } @@ -46,9 +78,9 @@ func TestCreateSignatureFromSignatureHeaderHeader(t *testing.T) { assert.Equal(t, "Test", s.KeyID) assert.Equal(t, AlgorithmHmacSha256, s.Algorithm) - assert.Equal(t, TEST_HASH, s.Signature) + assert.Equal(t, TEST_HASH_HMAC_SHA256, s.Signature) - assert.Equal(t, s.String(), TEST_SIGNATURE) + assert.Equal(t, s.String(), TEST_SIGNATURE_HMAC_SHA256) } func TestCreateSignatureWithNoSignature(t *testing.T) { @@ -87,19 +119,59 @@ func TestCreateWithInvalidKey(t *testing.T) { assert.Nil(t, s) } -func TestValidRequestIsValid(t *testing.T) { +func TestValidRequestIsValid_HmacSha256(t *testing.T) { r := &http.Request{ Header: http.Header{ "Date": []string{TEST_DATE}, }, } - err := DefaultSha256Signer.SignRequest(TEST_KEY_ID, TEST_KEY, r) + err := DefaultSha256Signer.SignRequest(TEST_KEY_ID, TEST_KEY_HMAC, r) assert.Nil(t, err) sig, err := FromRequest(r) assert.Nil(t, err) - assert.True(t, sig.IsValid(TEST_KEY, r)) + assert.True(t, sig.IsValid(TEST_KEY_HMAC, r)) +} + +func TestValidRequestIsValid_RsaSha256(t *testing.T) { + r := &http.Request{ + Header: http.Header{ + "Date": []string{TEST_DATE}, + }, + } + + block, _ := pem.Decode([]byte(TEST_PRIVATE_KEY)) + privateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + + err := DefaultRsaSha256Signer.SignRequestRSA(TEST_KEY_ID, privateKey, r) + assert.Nil(t, err) + + sig, err := FromRequest(r) + assert.Nil(t, err) + + assert.True(t, sig.IsValidRSA(&privateKey.PublicKey, r)) +} + +func TestValidRequestIsValid_EcdsaSha256(t *testing.T) { + r := &http.Request{ + Header: http.Header{ + "Date": []string{TEST_DATE}, + }, + } + + privateKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + err = DefaultEcdsaSha256Signer.SignRequestECDSA(TEST_KEY_ID, privateKey, r) + assert.Nil(t, err) + + sig, err := FromRequest(r) + assert.Nil(t, err) + + assert.True(t, sig.IsValidECDSA(&privateKey.PublicKey, r)) } func TestNotValidIfRequestHeadersChange(t *testing.T) { @@ -108,14 +180,14 @@ func TestNotValidIfRequestHeadersChange(t *testing.T) { "Date": []string{TEST_DATE}, }, } - err := DefaultSha256Signer.SignRequest(TEST_KEY_ID, TEST_KEY, r) + err := DefaultSha256Signer.SignRequest(TEST_KEY_ID, TEST_KEY_HMAC, r) assert.Nil(t, err) r.Header.Set("Date", "Thu, 05 Jan 2012 21:31:41 GMT") sig, err := FromRequest(r) assert.Nil(t, err) - assert.False(t, sig.IsValid(TEST_KEY, r)) + assert.False(t, sig.IsValid(TEST_KEY_HMAC, r)) } func TestNotValidIfRequestIsMissingDate(t *testing.T) { @@ -127,11 +199,11 @@ func TestNotValidIfRequestIsMissingDate(t *testing.T) { s := Signer{AlgorithmHmacSha1, HeaderList{RequestTarget}} - err := s.SignRequest(TEST_KEY_ID, TEST_KEY, r) + err := s.SignRequest(TEST_KEY_ID, TEST_KEY_HMAC, r) assert.Nil(t, err) sig, err := FromRequest(r) assert.Nil(t, err) - assert.False(t, sig.IsValid(TEST_KEY, r)) + assert.False(t, sig.IsValid(TEST_KEY_HMAC, r)) } diff --git a/signer.go b/signer.go index dce5f51..73b0f2c 100644 --- a/signer.go +++ b/signer.go @@ -1,6 +1,8 @@ package httpsignatures import ( + "crypto/ecdsa" + "crypto/rsa" "net/http" "strings" "time" @@ -20,6 +22,19 @@ var ( // DefaultSha256Signer will sign requests with the url and date using the SHA256 algorithm. // Users are encouraged to create their own signer with the headers they require. DefaultSha256Signer = NewSigner(AlgorithmHmacSha256, RequestTarget, "date") + + // DefaultSha1Signer will sign requests with the url and date using the SHA1 algorithm. + // Users are encouraged to create their own signer with the headers they require. + DefaultRsaSha1Signer = NewSigner(AlgorithmRsaSha1, RequestTarget, "date") + + // DefaultRsaSha256Signer will sign requests with the url and date using the SHA256 algorithm. + // Users are encouraged to create their own signer with the headers they require. + DefaultRsaSha256Signer = NewSigner(AlgorithmRsaSha256, RequestTarget, "date") + + // DefaultEcdsaSha256Signer will sign requests with the url and date + // using the SHA256 algorithm. Users are encouraged to create their own + // signer with the headers they require. + DefaultEcdsaSha256Signer = NewSigner(AlgorithmEcdsaSha256, RequestTarget, "date") ) func NewSigner(algorithm *Algorithm, headers ...string) *Signer { @@ -36,8 +51,8 @@ func NewSigner(algorithm *Algorithm, headers ...string) *Signer { } // SignRequest adds a http signature to the Signature: HTTP Header -func (s Signer) SignRequest(id, key string, r *http.Request) error { - sig, err := s.buildSignature(id, key, r) +func (s Signer) SignRequest(keyId string, key interface{}, r *http.Request) error { + sig, err := s.buildSignature(keyId, key, r) if err != nil { return err } @@ -47,9 +62,19 @@ func (s Signer) SignRequest(id, key string, r *http.Request) error { return nil } +// SignRequestRSA signs a request with an RSA private key. +func (s Signer) SignRequestRSA(keyId string, key *rsa.PrivateKey, r *http.Request) error { + return s.SignRequest(keyId, key, r) +} + +// SignRequestECDSA signs a request with an ECDSA private key. +func (s Signer) SignRequestECDSA(keyId string, key *ecdsa.PrivateKey, r *http.Request) error { + return s.SignRequest(keyId, key, r) +} + // AuthRequest adds a http signature to the Authorization: HTTP Header -func (s Signer) AuthRequest(id, key string, r *http.Request) error { - sig, err := s.buildSignature(id, key, r) +func (s Signer) AuthRequest(keyId string, key interface{}, r *http.Request) error { + sig, err := s.buildSignature(keyId, key, r) if err != nil { return err } @@ -59,13 +84,20 @@ func (s Signer) AuthRequest(id, key string, r *http.Request) error { return nil } -func (s Signer) buildSignature(id, key string, r *http.Request) (*Signature, error) { +// AuthRequestRSA adds an http signature to the Authorization: HTTP Header using +// an RSA private key to generate the signature.This method should only be +// called when the underlying Algorithm is an RSA backed implementation. +func (s Signer) AuthRequestRSA(keyId string, key *rsa.PrivateKey, r *http.Request) error { + return s.AuthRequest(keyId, key, r) +} + +func (s Signer) buildSignature(keyId string, key interface{}, r *http.Request) (*Signature, error) { if r.Header.Get("date") == "" { r.Header.Set("date", time.Now().Format(time.RFC1123)) } sig := &Signature{ - KeyID: id, + KeyID: keyId, Algorithm: s.algorithm, Headers: s.headers, } diff --git a/signer_test.go b/signer_test.go index b891928..a51c211 100644 --- a/signer_test.go +++ b/signer_test.go @@ -1,20 +1,23 @@ package httpsignatures import ( + "crypto/x509" + "encoding/pem" "net/http" + "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestSignSha1(t *testing.T) { +func TestSignHmacSha1(t *testing.T) { r := &http.Request{ Header: http.Header{ "Date": []string{"Thu, 05 Jan 2012 21:31:40 GMT"}, }, } - err := DefaultSha1Signer.SignRequest(TEST_KEY_ID, TEST_KEY, r) + err := DefaultSha1Signer.SignRequest(TEST_KEY_ID, TEST_KEY_HMAC, r) assert.Nil(t, err) s, err := FromRequest(r) @@ -30,14 +33,14 @@ func TestSignSha1(t *testing.T) { ) } -func TestSignSha256(t *testing.T) { +func TestSignHmacSha256(t *testing.T) { r := &http.Request{ Header: http.Header{ "Date": []string{"Thu, 05 Jan 2012 21:31:40 GMT"}, }, } - err := DefaultSha256Signer.SignRequest(TEST_KEY_ID, TEST_KEY, r) + err := DefaultSha256Signer.SignRequest(TEST_KEY_ID, TEST_KEY_HMAC, r) assert.Nil(t, err) s, err := FromRequest(r) @@ -53,10 +56,104 @@ func TestSignSha256(t *testing.T) { ) } +func TestSignRsaSha1(t *testing.T) { + r := &http.Request{ + Header: http.Header{ + "Date": []string{"Thu, 05 Jan 2012 21:31:40 GMT"}, + }, + } + + block, _ := pem.Decode([]byte(TEST_PRIVATE_KEY)) + privateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + + err := DefaultRsaSha1Signer.SignRequestRSA(TEST_KEY_ID, privateKey, r) + assert.Nil(t, err) + + s, err := FromRequest(r) + assert.Nil(t, err) + + assert.Equal(t, TEST_KEY_ID, s.KeyID) + + assert.Equal(t, + "KcypPq/UJBlvY9WR/zb6pGS2vkhlzKX1OUjtImOG8d4CynptmMxXWuzi3LeJW8jOnEmjC00Ga2tOruaSDo8MuDlXEy7JrYIqqD39XYKt5pFQ7dScpZARIrQ4H0n8bn4uIQFLMxkNt2aeuDogyUPcRMxBr6mVe0OHw8MY1y5xdpQ=", + s.Signature, + ) +} + +func TestSignRsaSha256(t *testing.T) { + r := &http.Request{ + Header: http.Header{ + "Date": []string{"Thu, 05 Jan 2012 21:31:40 GMT"}, + }, + } + + block, _ := pem.Decode([]byte(TEST_PRIVATE_KEY)) + privateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + + err := DefaultRsaSha256Signer.SignRequestRSA(TEST_KEY_ID, privateKey, r) + assert.Nil(t, err) + + s, err := FromRequest(r) + assert.Nil(t, err) + + assert.Equal(t, TEST_KEY_ID, s.KeyID) + + assert.Equal(t, + "TQZq1wGaOdAT3kiSOUq29jh6UG0DgZH2TW6aHYVNrHwKiACi1b9U58la/0SeDqEt6mKe836tHVKXouzNM5LaRiXWW13lZstdg/rXYxZ6N46jZKwVKRXcw9sc6/nZfjnDsxWs6/Zi4Si8hdEZx4CczUjPWBGDi+EaY+PPyZWSibs=", + s.Signature, + ) +} + +// Tests conformance with the test cases provided in the RFC document. +func TestSignRsaSha256_RFC(t *testing.T) { + block, _ := pem.Decode([]byte(TEST_PRIVATE_KEY)) + privateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + + tests := []struct { + signer *Signer + signature string + }{ + // https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C.1 + { + NewSigner(AlgorithmRsaSha256, "date"), + "jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=", + }, + + // https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C.2 + { + NewSigner(AlgorithmRsaSha256, RequestTarget, "host", "date"), + "HUxc9BS3P/kPhSmJo+0pQ4IsCo007vkv6bUm4Qehrx+B1Eo4Mq5/6KylET72ZpMUS80XvjlOPjKzxfeTQj4DiKbAzwJAb4HX3qX6obQTa00/qPDXlMepD2JtTw33yNnm/0xV7fQuvILN/ys+378Ysi082+4xBQFwvhNvSoVsGv4=", + }, + + // https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C.3 + { + NewSigner(AlgorithmRsaSha256, RequestTarget, "host", "date", "content-type", "digest", "content-length"), + "Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0=", + }, + } + + for _, tt := range tests { + r := newRFCRequest() + + err := tt.signer.SignRequestRSA(TEST_KEY_ID, privateKey, r) + assert.Nil(t, err) + + s, err := FromRequest(r) + assert.Nil(t, err) + + assert.Equal(t, TEST_KEY_ID, s.KeyID) + + assert.Equal(t, + tt.signature, + s.Signature, + ) + } +} + func TestSignWithMissingDateHeader(t *testing.T) { r := &http.Request{Header: http.Header{}} - err := DefaultSha1Signer.AuthRequest(TEST_KEY_ID, TEST_KEY, r) + err := DefaultSha1Signer.AuthRequest(TEST_KEY_ID, TEST_KEY_HMAC, r) assert.Nil(t, err) assert.NotEqual(t, "", r.Header.Get("date")) @@ -71,6 +168,21 @@ func TestSignWithMissingHeader(t *testing.T) { s := NewSigner(AlgorithmHmacSha1, "foo") - err := s.SignRequest(TEST_KEY_ID, TEST_KEY, r) + err := s.SignRequest(TEST_KEY_ID, TEST_KEY_HMAC, r) assert.Equal(t, "Missing required header 'foo'", err.Error()) } + +// newRFCRequest generates an http.Request that matches the test request in the +// RFC https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C +func newRFCRequest() *http.Request { + r, _ := http.NewRequest("POST", "/foo?param=value&pet=dog", strings.NewReader(`{"hello": "world"}`)) + r.Header.Set("Host", "example.com") + // The date in the RFC is wrong (2014 instead of 2012). + // + // See https://goo.gl/QrvrTE + r.Header.Set("Date", "Thu, 05 Jan 2014 21:31:40 GMT") + r.Header.Set("Content-Type", "application/json") + r.Header.Set("Digest", "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=") + r.Header.Set("Content-Length", "18") + return r +}