From 03551c9ef7fbe53560a9dc7f77d95ff5de2a20bf Mon Sep 17 00:00:00 2001 From: Wim Date: Sun, 14 Jun 2020 19:42:29 +0200 Subject: [PATCH 1/9] Add initial support for using ssh keys for signing At the moment only ed25519 keys are supported. The NewSSHSigner method takes a ssh.Signer which will be used when actually signing the request/responses. --- algorithms.go | 22 +++++++++++- go.mod | 2 +- go.sum | 4 +-- httpsig.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++ signing.go | 14 ++++++++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/algorithms.go b/algorithms.go index a6300a6..84742c6 100644 --- a/algorithms.go +++ b/algorithms.go @@ -21,6 +21,7 @@ import ( "golang.org/x/crypto/ed25519" "golang.org/x/crypto/ripemd160" "golang.org/x/crypto/sha3" + "golang.org/x/crypto/ssh" ) const ( @@ -219,9 +220,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 +429,15 @@ func newAlgorithm(algo string, key []byte) (hash.Hash, crypto.Hash, error) { return h, c, err } +func signerFromSSHSigner(sshSigner ssh.Signer, s string) (signer, error) { + if !strings.HasPrefix(s, ed25519Prefix) { + return nil, fmt.Errorf("no signer matching %q", s) + } + return &ed25519Algorithm{ + sshSigner: sshSigner, + }, nil +} + // signerFromString is an internally public method constructor func signerFromString(s string) (signer, error) { s = strings.ToLower(s) 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..9beb251 100644 --- a/httpsig.go +++ b/httpsig.go @@ -12,6 +12,8 @@ import ( "fmt" "net/http" "time" + + "golang.org/x/crypto/ssh" ) // Algorithm specifies a cryptography secure algorithm for signing HTTP requests @@ -170,6 +172,70 @@ 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 pkType { + case "ssh-ed25519": + return ED25519 + } + + return "" +} + // Verifier verifies HTTP Signatures. // // It will determine which of the supported headers has the parameters @@ -225,6 +291,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", algo) + } + + 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 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 From 17ff967c7fece0cd8a967dace6b4b2e9334c3d0a Mon Sep 17 00:00:00 2001 From: Wim Date: Thu, 2 Jul 2020 01:03:30 +0200 Subject: [PATCH 2/9] Add RSA support to ssh key signing --- algorithms.go | 37 +++++++++++++++++++++++++++---------- httpsig.go | 5 ++++- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/algorithms.go b/algorithms.go index 84742c6..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 @@ -27,6 +28,7 @@ import ( const ( hmacPrefix = "hmac" rsaPrefix = "rsa" + sshPrefix = "ssh" ecdsaPrefix = "ecdsa" ed25519Prefix = "ed25519" md4String = "md4" @@ -66,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 }}, @@ -115,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 } @@ -175,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 { @@ -191,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 } @@ -430,12 +441,18 @@ func newAlgorithm(algo string, key []byte) (hash.Hash, crypto.Hash, error) { } func signerFromSSHSigner(sshSigner ssh.Signer, s string) (signer, error) { - if !strings.HasPrefix(s, ed25519Prefix) { + 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) } - return &ed25519Algorithm{ - sshSigner: sshSigner, - }, nil } // signerFromString is an internally public method constructor diff --git a/httpsig.go b/httpsig.go index 9beb251..ed55219 100644 --- a/httpsig.go +++ b/httpsig.go @@ -42,6 +42,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 @@ -229,8 +230,10 @@ func NewSSHSigner(s ssh.Signer, dAlgo DigestAlgorithm, headers []string, scheme func getSSHAlgorithm(pkType string) Algorithm { switch pkType { - case "ssh-ed25519": + case sshPrefix + "-" + ed25519Prefix: return ED25519 + case sshPrefix + "-" + rsaPrefix: + return RSA_SHA1 } return "" From 38d400649cdff74a95b96474a78ec2a37c9ec0f2 Mon Sep 17 00:00:00 2001 From: Wim Date: Fri, 10 Jul 2020 23:17:17 +0200 Subject: [PATCH 3/9] Match on prefix for certificates This also gives support for signing with ssh certificates - ssh-rsa-cert-v01@openssh.com - ssh-ed25519-cert-v01@openssh.com --- httpsig.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/httpsig.go b/httpsig.go index ed55219..1a6b954 100644 --- a/httpsig.go +++ b/httpsig.go @@ -11,6 +11,7 @@ import ( "crypto" "fmt" "net/http" + "strings" "time" "golang.org/x/crypto/ssh" @@ -229,10 +230,10 @@ func NewSSHSigner(s ssh.Signer, dAlgo DigestAlgorithm, headers []string, scheme } func getSSHAlgorithm(pkType string) Algorithm { - switch pkType { - case sshPrefix + "-" + ed25519Prefix: + switch { + case strings.HasPrefix(pkType, sshPrefix+"-"+ed25519Prefix): return ED25519 - case sshPrefix + "-" + rsaPrefix: + case strings.HasPrefix(pkType, sshPrefix+"-"+rsaPrefix): return RSA_SHA1 } From 724019526f23bded3dfea74eba7dfd32ca249e2e Mon Sep 17 00:00:00 2001 From: Cory Slep Date: Mon, 21 Dec 2020 22:19:56 +0100 Subject: [PATCH 4/9] Propagate error when crypto algorithm is not supported --- httpsig.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpsig.go b/httpsig.go index 1a6b954..17310c8 100644 --- a/httpsig.go +++ b/httpsig.go @@ -305,7 +305,7 @@ func newSSHSigner(sshSigner ssh.Signer, algo Algorithm, dAlgo DigestAlgorithm, h s, err := signerFromSSHSigner(sshSigner, string(algo)) if err != nil { - return nil, fmt.Errorf("no crypto implementation available for ssh algo %q", algo) + return nil, fmt.Errorf("no crypto implementation available for ssh algo %q: %s", algo, err) } a := &asymmSSHSigner{ @@ -346,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, From aaea07ed54fedfe6054f9f7092663d2e14903144 Mon Sep 17 00:00:00 2001 From: Cory Slep Date: Mon, 21 Dec 2020 22:20:30 +0100 Subject: [PATCH 5/9] Have test temporarily pass due to SHA1 inclusion --- algorithms_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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, From dda7895774cb54739197e31b0225b939ab824108 Mon Sep 17 00:00:00 2001 From: Cory Slep Date: Mon, 21 Dec 2020 22:26:26 +0100 Subject: [PATCH 6/9] Propagate error message for verifier when crypto algo not supported --- verifying.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 { From 2a46c50a663bb2b817c8da2012021d0665480d03 Mon Sep 17 00:00:00 2001 From: Cory Slep Date: Tue, 22 Dec 2020 21:58:35 +0100 Subject: [PATCH 7/9] Add travis.yml --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .travis.yml 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 From 83c3d4b10e6d370eb5f833c63afd57ed607d3442 Mon Sep 17 00:00:00 2001 From: Cory Slep Date: Tue, 22 Dec 2020 22:00:09 +0100 Subject: [PATCH 8/9] Add badges to the README --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 3b83854..e40f3d4 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] +[![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,14 @@ 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 +[OpenCollective-Image]: https://img.shields.io/opencollective/backers/go-fed-activitypub-labs +[OpenCollective-Url]: https://opencollective.com/go-fed-activitypub-labs From 55836744818e5aee1c28fa283aec277dd7ab5d59 Mon Sep 17 00:00:00 2001 From: Cory Slep Date: Wed, 23 Dec 2020 12:23:13 +0100 Subject: [PATCH 9/9] Add badge for Matrix chat room --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e40f3d4..3d4cb06 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![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] -[![OpenCollective][OpenCollective-Image]][OpenCollective-Url] +[![Chat][Chat-Image]][Chat-Url] [![OpenCollective][OpenCollective-Image]][OpenCollective-Url] `go get github.com/go-fed/httpsig` @@ -107,5 +107,7 @@ restriction. [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