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

Implement rsa-sha1/rsa-sha256/ecdsa-sha256 algorithms #12

Open
wants to merge 2 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
135 changes: 129 additions & 6 deletions algorithm.go
Original file line number Diff line number Diff line change
@@ -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)}
Copy link
Author

@ejholmes ejholmes Apr 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably validate that the key is using a P256 curve. If someone generates a P224 curve, the hash would be truncated when siging:

Sign signs a hash (which should be the result of hashing a larger message) using the private key, priv. If the hash is longer than the bit-length of the private key's curve order, the hash will be truncated to that length. It returns the signature as a pair of integers. The security of the private key depends on the entropy of rand.

https://golang.org/pkg/crypto/ecdsa/#PrivateKey.Sign

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically "ecdsa-sha256" only prescribes that the digest function uses SHA-256. The EC key can use any curve or number of bits. That's something the consumer must be able to infer from the key id.


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) {
Expand All @@ -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)
}
}
66 changes: 66 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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,
Expand Down Expand Up @@ -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.
}
}
40 changes: 29 additions & 11 deletions signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
package httpsignatures

import (
"crypto/hmac"
"crypto/subtle"
"crypto/ecdsa"
"crypto/rsa"
"encoding/base64"
"errors"
"fmt"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading