diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2c0c604 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: + - 1.15 diff --git a/README.md b/README.md index 3b83854..3d4cb06 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # httpsig +> HTTP Signatures made simple + +[![Build Status][Build-Status-Image]][Build-Status-Url] [![Go Reference][Go-Reference-Image]][Go-Reference-Url] +[![Go Report Card][Go-Report-Card-Image]][Go-Report-Card-Url] [![License][License-Image]][License-Url] +[![Chat][Chat-Image]][Chat-Url] [![OpenCollective][OpenCollective-Image]][OpenCollective-Url] + `go get github.com/go-fed/httpsig` Implementation of [HTTP Signatures](https://tools.ietf.org/html/draft-cavage-http-signatures). @@ -92,3 +98,16 @@ func verify(r *http.Request) error { `Verifier`s are not safe for concurrent use by goroutines, but since they are constructed on a per-request or per-response basis it should not be a common restriction. + +[Build-Status-Image]: https://travis-ci.org/go-fed/httpsig.svg?branch=master +[Build-Status-Url]: https://travis-ci.org/go-fed/httpsig +[Go-Reference-Image]: https://pkg.go.dev/badge/github.com/go-fed/httpsig +[Go-Reference-Url]: https://pkg.go.dev/github.com/go-fed/httpsig +[Go-Report-Card-Image]: https://goreportcard.com/badge/github.com/go-fed/httpsig +[Go-Report-Card-Url]: https://goreportcard.com/report/github.com/go-fed/httpsig +[License-Image]: https://img.shields.io/github/license/go-fed/httpsig?color=blue +[License-Url]: https://opensource.org/licenses/BSD-3-Clause +[Chat-Image]: https://img.shields.io/matrix/go-fed:feneas.org?server_fqdn=matrix.org +[Chat-Url]: https://matrix.to/#/!BLOSvIyKTDLIVjRKSc:feneas.org?via=feneas.org&via=matrix.org +[OpenCollective-Image]: https://img.shields.io/opencollective/backers/go-fed-activitypub-labs +[OpenCollective-Url]: https://opencollective.com/go-fed-activitypub-labs diff --git a/algorithms.go b/algorithms.go index a6300a6..9595941 100644 --- a/algorithms.go +++ b/algorithms.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/hmac" "crypto/rsa" + "crypto/sha1" "crypto/sha256" "crypto/sha512" "crypto/subtle" // Use should trigger great care @@ -21,11 +22,13 @@ import ( "golang.org/x/crypto/ed25519" "golang.org/x/crypto/ripemd160" "golang.org/x/crypto/sha3" + "golang.org/x/crypto/ssh" ) const ( hmacPrefix = "hmac" rsaPrefix = "rsa" + sshPrefix = "ssh" ecdsaPrefix = "ecdsa" ed25519Prefix = "ed25519" md4String = "md4" @@ -65,9 +68,10 @@ var hashToDef = map[crypto.Hash]struct { // http://www.iana.org/assignments/signature-algorithms // // Note that the forbidden hashes have an invalid 'new' function. - crypto.MD4: {md4String, func(key []byte) (hash.Hash, error) { return nil, nil }}, - crypto.MD5: {md5String, func(key []byte) (hash.Hash, error) { return nil, nil }}, - crypto.SHA1: {sha1String, func(key []byte) (hash.Hash, error) { return nil, nil }}, + crypto.MD4: {md4String, func(key []byte) (hash.Hash, error) { return nil, nil }}, + crypto.MD5: {md5String, func(key []byte) (hash.Hash, error) { return nil, nil }}, + // Temporarily enable SHA1 because of issue https://github.com/golang/go/issues/37278 + crypto.SHA1: {sha1String, func(key []byte) (hash.Hash, error) { return sha1.New(), nil }}, crypto.SHA224: {sha224String, func(key []byte) (hash.Hash, error) { return sha256.New224(), nil }}, crypto.SHA256: {sha256String, func(key []byte) (hash.Hash, error) { return sha256.New(), nil }}, crypto.SHA384: {sha384String, func(key []byte) (hash.Hash, error) { return sha512.New384(), nil }}, @@ -114,8 +118,6 @@ func isForbiddenHash(h crypto.Hash) bool { fallthrough case crypto.MD5: fallthrough - case crypto.SHA1: - fallthrough case crypto.MD5SHA1: // shorthand for crypto/tls, not actually implemented return true } @@ -174,7 +176,8 @@ var _ signer = &rsaAlgorithm{} type rsaAlgorithm struct { hash.Hash - kind crypto.Hash + kind crypto.Hash + sshSigner ssh.Signer } func (r *rsaAlgorithm) setSig(b []byte) error { @@ -190,7 +193,16 @@ func (r *rsaAlgorithm) setSig(b []byte) error { } func (r *rsaAlgorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) { + if r.sshSigner != nil { + sshsig, err := r.sshSigner.Sign(rand, sig) + if err != nil { + return nil, err + } + + return sshsig.Blob, nil + } defer r.Reset() + if err := r.setSig(sig); err != nil { return nil, err } @@ -219,9 +231,19 @@ func (r *rsaAlgorithm) String() string { var _ signer = &ed25519Algorithm{} -type ed25519Algorithm struct{} +type ed25519Algorithm struct { + sshSigner ssh.Signer +} func (r *ed25519Algorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) { + if r.sshSigner != nil { + sshsig, err := r.sshSigner.Sign(rand, sig) + if err != nil { + return nil, err + } + + return sshsig.Blob, nil + } ed25519K, ok := p.(ed25519.PrivateKey) if !ok { return nil, errors.New("crypto.PrivateKey is not ed25519.PrivateKey") @@ -418,6 +440,21 @@ func newAlgorithm(algo string, key []byte) (hash.Hash, crypto.Hash, error) { return h, c, err } +func signerFromSSHSigner(sshSigner ssh.Signer, s string) (signer, error) { + switch { + case strings.HasPrefix(s, rsaPrefix): + return &rsaAlgorithm{ + sshSigner: sshSigner, + }, nil + case strings.HasPrefix(s, ed25519Prefix): + return &ed25519Algorithm{ + sshSigner: sshSigner, + }, nil + default: + return nil, fmt.Errorf("no signer matching %q", s) + } +} + // signerFromString is an internally public method constructor func signerFromString(s string) (signer, error) { s = strings.ToLower(s) diff --git a/algorithms_test.go b/algorithms_test.go index 84167c0..5c3a3a3 100644 --- a/algorithms_test.go +++ b/algorithms_test.go @@ -44,10 +44,11 @@ func TestIsAvailable(t *testing.T) { expectError: true, }, { + // TODO: Disable once https://github.com/golang/go/issues/37278 is fixed name: sha1String, algo: sha1String, - expected: false, - expectError: true, + expected: true, + expectError: false, }, { name: sha224String, diff --git a/go.mod b/go.mod index 57d79d7..a407692 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/go-fed/httpsig -require golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 +require golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 go 1.13 diff --git a/go.sum b/go.sum index 5ed72aa..8b7d318 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= -golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= diff --git a/httpsig.go b/httpsig.go index 70c6c69..17310c8 100644 --- a/httpsig.go +++ b/httpsig.go @@ -11,7 +11,10 @@ import ( "crypto" "fmt" "net/http" + "strings" "time" + + "golang.org/x/crypto/ssh" ) // Algorithm specifies a cryptography secure algorithm for signing HTTP requests @@ -40,6 +43,7 @@ const ( BLAKE2B_384 Algorithm = blake2b_384String BLAKE2B_512 Algorithm = blake2b_512String // RSA-based algorithms. + RSA_SHA1 Algorithm = rsaPrefix + "-" + sha1String RSA_SHA224 Algorithm = rsaPrefix + "-" + sha224String // RSA_SHA256 is the default algorithm. RSA_SHA256 Algorithm = rsaPrefix + "-" + sha256String @@ -170,6 +174,72 @@ func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, schem return s, defaultAlgorithm, err } +// Signers will sign HTTP requests or responses based on the algorithms and +// headers selected at creation time. +// +// Signers are not safe to use between multiple goroutines. +// +// Note that signatures do set the deprecated 'algorithm' parameter for +// backwards compatibility. +type SSHSigner interface { + // SignRequest signs the request using ssh.Signer. + // The public key id is used by the HTTP server to identify which key to use + // to verify the signature. + // + // A Digest (RFC 3230) will be added to the request. The body provided + // must match the body used in the request, and is allowed to be nil. + // The Digest ensures the request body is not tampered with in flight, + // and if the signer is created to also sign the "Digest" header, the + // HTTP Signature will then ensure both the Digest and body are not both + // modified to maliciously represent different content. + SignRequest(pubKeyId string, r *http.Request, body []byte) error + // SignResponse signs the response using ssh.Signer. The public key + // id is used by the HTTP client to identify which key to use to verify + // the signature. + // + // A Digest (RFC 3230) will be added to the response. The body provided + // must match the body written in the response, and is allowed to be + // nil. The Digest ensures the response body is not tampered with in + // flight, and if the signer is created to also sign the "Digest" + // header, the HTTP Signature will then ensure both the Digest and body + // are not both modified to maliciously represent different content. + SignResponse(pubKeyId string, r http.ResponseWriter, body []byte) error +} + +// NewwSSHSigner creates a new Signer using the specified ssh.Signer +// At the moment only ed25519 ssh keys are supported. +// The headers specified will be included into the HTTP signatures. +// +// The Digest will also be calculated on a request's body using the provided +// digest algorithm, if "Digest" is one of the headers listed. +// +// The provided scheme determines which header is populated with the HTTP +// Signature. +func NewSSHSigner(s ssh.Signer, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SSHSigner, Algorithm, error) { + sshAlgo := getSSHAlgorithm(s.PublicKey().Type()) + if sshAlgo == "" { + return nil, "", fmt.Errorf("key type: %s not supported yet.", s.PublicKey().Type()) + } + + signer, err := newSSHSigner(s, sshAlgo, dAlgo, headers, scheme, expiresIn) + if err != nil { + return nil, "", err + } + + return signer, sshAlgo, nil +} + +func getSSHAlgorithm(pkType string) Algorithm { + switch { + case strings.HasPrefix(pkType, sshPrefix+"-"+ed25519Prefix): + return ED25519 + case strings.HasPrefix(pkType, sshPrefix+"-"+rsaPrefix): + return RSA_SHA1 + } + + return "" +} + // Verifier verifies HTTP Signatures. // // It will determine which of the supported headers has the parameters @@ -225,6 +295,34 @@ func NewResponseVerifier(r *http.Response) (Verifier, error) { }) } +func newSSHSigner(sshSigner ssh.Signer, algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SSHSigner, error) { + var expires, created int64 = 0, 0 + + if expiresIn != 0 { + created = time.Now().Unix() + expires = created + expiresIn + } + + s, err := signerFromSSHSigner(sshSigner, string(algo)) + if err != nil { + return nil, fmt.Errorf("no crypto implementation available for ssh algo %q: %s", algo, err) + } + + a := &asymmSSHSigner{ + asymmSigner: &asymmSigner{ + s: s, + dAlgo: dAlgo, + headers: headers, + targetHeader: scheme, + prefix: scheme.authScheme(), + created: created, + expires: expires, + }, + } + + return a, nil +} + func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, error) { var expires, created int64 = 0, 0 @@ -248,7 +346,7 @@ func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme S } m, err := macerFromString(string(algo)) if err != nil { - return nil, fmt.Errorf("no crypto implementation available for %q", algo) + return nil, fmt.Errorf("no crypto implementation available for %q: %s", algo, err) } c := &macSigner{ m: m, diff --git a/signing.go b/signing.go index ea419aa..e18db41 100644 --- a/signing.go +++ b/signing.go @@ -179,6 +179,20 @@ func (a *asymmSigner) signatureStringResponse(r http.ResponseWriter) (string, er return signatureString(r.Header(), a.headers, requestTargetNotPermitted, a.created, a.expires) } +var _ SSHSigner = &asymmSSHSigner{} + +type asymmSSHSigner struct { + *asymmSigner +} + +func (a *asymmSSHSigner) SignRequest(pubKeyId string, r *http.Request, body []byte) error { + return a.asymmSigner.SignRequest(nil, pubKeyId, r, body) +} + +func (a *asymmSSHSigner) SignResponse(pubKeyId string, r http.ResponseWriter, body []byte) error { + return a.asymmSigner.SignResponse(nil, pubKeyId, r, body) +} + func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyId, algo, enc string, headers []string, created int64, expires int64) { if len(headers) == 0 { headers = defaultHeaders diff --git a/verifying.go b/verifying.go index 6d8df02..e39b9dc 100644 --- a/verifying.go +++ b/verifying.go @@ -70,7 +70,7 @@ func (v *verifier) Verify(pKey crypto.PublicKey, algo Algorithm) error { if err == nil { return v.macVerify(m, pKey) } - return fmt.Errorf("no crypto implementation available for %q", algo) + return fmt.Errorf("no crypto implementation available for %q: %s", algo, err) } func (v *verifier) macVerify(m macer, pKey crypto.PublicKey) error {