[feature] Try HTTP signature validation with and without query params for incoming requests (#2591)
* [feature] Verify signatures both with + without query params * Bump to tagged version
This commit is contained in:
parent
c675d47a8c
commit
b614d33c40
|
@ -263,7 +263,6 @@ The following open source libraries, frameworks, and tools are used by GoToSocia
|
|||
- [gin-contrib/gzip](https://github.com/gin-contrib/gzip); Gin gzip middleware. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
- [gin-contrib/sessions](https://github.com/gin-contrib/sessions); Gin sessions middleware. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
- [gin-gonic/gin](https://github.com/gin-gonic/gin); speedy router engine. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
- [go-fed/httpsig](https://github.com/go-fed/httpsig); secure HTTP signature library. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
|
||||
- [google/uuid](https://github.com/google/uuid); UUID generation. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
|
||||
- [google/wuffs](https://github.com/google/wuffs); png-stripping code. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html).
|
||||
- Go-Playground:
|
||||
|
@ -306,6 +305,7 @@ The following open source libraries, frameworks, and tools are used by GoToSocia
|
|||
- superseriousbusiness:
|
||||
- [superseriousbusiness/activity](https://github.com/superseriousbusiness/activity) forked from [go-fed/activity](https://github.com/go-fed/activity); Golang ActivityPub/ActivityStreams library. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
|
||||
- [superseriousbusiness/exif-terminator](https://codeberg.org/superseriousbusiness/exif-terminator); EXIF data removal. [GNU AGPL v3 LICENSE](https://spdx.org/licenses/AGPL-3.0-or-later.html).
|
||||
- [superseriousbusiness/httpsig](https://github.com/superseriousbusiness/httpsig) forked from [go-fed/httpsig](https://github.com/go-fed/httpsig); secure HTTP signature library. [BSD-3-Clause License](https://spdx.org/licenses/BSD-3-Clause.html).
|
||||
- [superseriousbusiness/oauth2](https://github.com/superseriousbusiness/oauth2) forked from [go-oauth2/oauth2](https://github.com/go-oauth2/oauth2); OAuth server framework and token handling. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
- [tdewolff/minify](https://github.com/tdewolff/minify); HTML minification for Markdown-submitted posts. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
- [uber-go/automaxprocs](https://github.com/uber-go/automaxprocs); GOMAXPROCS automation. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
|
|
|
@ -10,7 +10,7 @@ GoToSocial will also sign all outgoing `GET` and `POST` requests that it makes t
|
|||
|
||||
This behavior is the equivalent of Mastodon's [AUTHORIZED_FETCH / "secure mode"](https://docs.joinmastodon.org/admin/config/#authorized_fetch).
|
||||
|
||||
GoToSocial uses the [go-fed/httpsig](https://github.com/go-fed/httpsig) library for signing outgoing requests, and for parsing and validating the signatures of incoming requests. This library strictly follows the [Cavage http signature RFC](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12), which is the same RFC used by other implementations like Mastodon, Pixelfed, Akkoma/Pleroma, etc. (This RFC has since been superceded by the [httpbis http signature RFC](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures), but this is not yet widely implemented.)
|
||||
GoToSocial uses the [superseriousbusiness/httpsig](https://github.com/superseriousbusiness/httpsign) library (forked from go-fed) for signing outgoing requests, and for parsing and validating the signatures of incoming requests. This library strictly follows the [Cavage http signature RFC](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12), which is the same RFC used by other implementations like Mastodon, Pixelfed, Akkoma/Pleroma, etc. (This RFC has since been superceded by the [httpbis http signature RFC](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures), but this is not yet widely implemented.)
|
||||
|
||||
### Incoming Requests
|
||||
|
||||
|
|
3
go.mod
3
go.mod
|
@ -31,7 +31,6 @@ require (
|
|||
github.com/gin-contrib/gzip v0.0.6
|
||||
github.com/gin-contrib/sessions v0.0.5
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-fed/httpsig v1.1.0
|
||||
github.com/go-playground/form/v4 v4.2.1
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/gorilla/feeds v1.1.2
|
||||
|
@ -48,6 +47,7 @@ require (
|
|||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/superseriousbusiness/activity v1.4.0-gts
|
||||
github.com/superseriousbusiness/httpsig v1.2.0-SSB
|
||||
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8
|
||||
github.com/tdewolff/minify/v2 v2.20.14
|
||||
github.com/technologize/otel-go-contrib v1.1.0
|
||||
|
@ -106,6 +106,7 @@ require (
|
|||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-errors/errors v1.4.1 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -489,6 +489,8 @@ github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430
|
|||
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe/go.mod h1:gH4P6gN1V+wmIw5o97KGaa1RgXB/tVpC2UNzijhg3E4=
|
||||
github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB h1:8psprYSK1KdOSH7yQ4PbJq0YYaGQY+gzdW/B0ExDb/8=
|
||||
github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB/go.mod h1:ymKGfy9kg4dIdraeZRAdobMS/flzLk3VcRPLpEWOAXg=
|
||||
github.com/superseriousbusiness/httpsig v1.2.0-SSB h1:BinBGKbf2LSuVT5+MuH0XynHN9f0XVshx2CTDtkaWj0=
|
||||
github.com/superseriousbusiness/httpsig v1.2.0-SSB/go.mod h1:+rxfATjFaDoDIVaJOTSP0gj6UrbicaYPEptvCLC9F28=
|
||||
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 h1:nTIhuP157oOFcscuoK1kCme1xTeGIzztSw70lX9NrDQ=
|
||||
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo=
|
||||
github.com/tdewolff/minify/v2 v2.20.14 h1:sktSuVixRwk0ryQjqvKBu/uYS+MWmkwEFMEWtFZ+TdE=
|
||||
|
|
|
@ -28,7 +28,6 @@ import (
|
|||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-kv"
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
|
@ -37,6 +36,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/httpsig"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -509,25 +509,63 @@ var signingAlgorithms = []httpsig.Algorithm{
|
|||
httpsig.ED25519, // Try ED25519 as a long shot.
|
||||
}
|
||||
|
||||
// verifyAuth verifies auth using generated verifier, according to pubkey and our supported signing algorithms.
|
||||
func verifyAuth(l *log.Entry, verifier httpsig.Verifier, pubKey *rsa.PublicKey) bool {
|
||||
// Cheeky type to wrap a signing option with a
|
||||
// description of that option for logging purposes.
|
||||
type signingOption struct {
|
||||
desc string // Description of this options set.
|
||||
sigOpt httpsig.SignatureOption // The options themselves.
|
||||
}
|
||||
|
||||
var signingOptions = []signingOption{
|
||||
{
|
||||
// Prefer include query params.
|
||||
desc: "include query params",
|
||||
sigOpt: httpsig.SignatureOption{
|
||||
ExcludeQueryStringFromPathPseudoHeader: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Fall back to exclude query params.
|
||||
desc: "exclude query params",
|
||||
sigOpt: httpsig.SignatureOption{
|
||||
ExcludeQueryStringFromPathPseudoHeader: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// verifyAuth verifies auth using generated verifier,
|
||||
// according to pubkey, our supported signing algorithms,
|
||||
// and signature options. The loops in the function are
|
||||
// arranged in such a way that the most common combos are
|
||||
// tried first, so that we can hopefully succeed quickly
|
||||
// without wasting too many CPU cycles.
|
||||
func verifyAuth(
|
||||
l *log.Entry,
|
||||
verifier httpsig.VerifierWithOptions,
|
||||
pubKey *rsa.PublicKey,
|
||||
) bool {
|
||||
if pubKey == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Loop through all supported algorithms.
|
||||
// Loop through supported algorithms.
|
||||
for _, algo := range signingAlgorithms {
|
||||
|
||||
// Verify according to pubkey and algo.
|
||||
err := verifier.Verify(pubKey, algo)
|
||||
// Loop through signing options.
|
||||
for _, opt := range signingOptions {
|
||||
|
||||
// Try to verify according to this pubkey,
|
||||
// algo, and signing options combination.
|
||||
err := verifier.VerifyWithOptions(pubKey, algo, opt.sigOpt)
|
||||
if err != nil {
|
||||
l.Tracef("authentication NOT PASSED with %s: %v", algo, err)
|
||||
l.Tracef("authentication NOT PASSED with %s (%s): %v", algo, opt.desc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Tracef("authenticated PASSED with %s", algo)
|
||||
l.Tracef("authenticated PASSED with %s (%s)", algo, opt.desc)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -27,12 +27,12 @@ import (
|
|||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
"github.com/superseriousbusiness/httpsig"
|
||||
)
|
||||
|
||||
type FederatingProtocolTestSuite struct {
|
||||
|
|
|
@ -21,8 +21,8 @@ import (
|
|||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/httpsig"
|
||||
)
|
||||
|
||||
// package private context key type.
|
||||
|
@ -129,8 +129,8 @@ func SetOtherIRIs(ctx context.Context, iris []*url.URL) context.Context {
|
|||
|
||||
// HTTPSignatureVerifier returns an http signature verifier for the current ActivityPub
|
||||
// request chain. This verifier can be called to authenticate the current request.
|
||||
func HTTPSignatureVerifier(ctx context.Context) httpsig.Verifier {
|
||||
verifier, _ := ctx.Value(httpSigVerifierKey).(httpsig.Verifier)
|
||||
func HTTPSignatureVerifier(ctx context.Context) httpsig.VerifierWithOptions {
|
||||
verifier, _ := ctx.Value(httpSigVerifierKey).(httpsig.VerifierWithOptions)
|
||||
return verifier
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/superseriousbusiness/httpsig"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/superseriousbusiness/httpsig"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -27,10 +27,10 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/httpclient"
|
||||
"github.com/superseriousbusiness/httpsig"
|
||||
)
|
||||
|
||||
// Transport implements the pub.Transport interface with some additional functionality for fetching remote media.
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2018, go-fed
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,101 @@
|
|||
# httpsig
|
||||
|
||||
**THIS IS A FORK OF https://github.com/go-fed/httpsig, WHICH WAS NO LONGER MAINTAINED. THANK YOU TO [cjslep](https://github.com/cjslep) FOR ALL YOUR HARD WORK!**
|
||||
|
||||
> HTTP Signatures made simple
|
||||
|
||||
`go get github.com/superseriousbusiness/httpsig`
|
||||
|
||||
Implementation of [HTTP Signatures](https://tools.ietf.org/html/draft-cavage-http-signatures).
|
||||
|
||||
Supports many different combinations of MAC, HMAC signing of hash, or RSA
|
||||
signing of hash schemes. Its goals are:
|
||||
|
||||
* Have a very simple interface for signing and validating
|
||||
* Support a variety of signing algorithms and combinations
|
||||
* Support setting either headers (`Authorization` or `Signature`)
|
||||
* Remaining flexible with headers included in the signing string
|
||||
* Support both HTTP requests and responses
|
||||
* Explicitly not support known-cryptographically weak algorithms
|
||||
* Support automatic signing and validating Digest headers
|
||||
|
||||
## How to use
|
||||
|
||||
`import "github.com/superseriousbusiness/httpsig"`
|
||||
|
||||
### Signing
|
||||
|
||||
Signing a request or response requires creating a new `Signer` and using it:
|
||||
|
||||
```go
|
||||
func sign(privateKey crypto.PrivateKey, pubKeyId string, r *http.Request) error {
|
||||
prefs := []httpsig.Algorithm{httpsig.RSA_SHA512, httpsig.RSA_SHA256}
|
||||
digestAlgorithm := DigestSha256
|
||||
// The "Date" and "Digest" headers must already be set on r, as well as r.URL.
|
||||
headersToSign := []string{httpsig.RequestTarget, "date", "digest"}
|
||||
signer, chosenAlgo, err := httpsig.NewSigner(prefs, digestAlgorithm, headersToSign, httpsig.Signature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// To sign the digest, we need to give the signer a copy of the body...
|
||||
// ...but it is optional, no digest will be signed if given "nil"
|
||||
body := ...
|
||||
// If r were a http.ResponseWriter, call SignResponse instead.
|
||||
return signer.SignRequest(privateKey, pubKeyId, r, body)
|
||||
}
|
||||
```
|
||||
|
||||
`Signer`s are not safe for concurrent use by goroutines, so be sure to guard
|
||||
access:
|
||||
|
||||
```go
|
||||
type server struct {
|
||||
signer httpsig.Signer
|
||||
mu *sync.Mutex
|
||||
}
|
||||
|
||||
func (s *server) handlerFunc(w http.ResponseWriter, r *http.Request) {
|
||||
privateKey := ...
|
||||
pubKeyId := ...
|
||||
// Set headers and such on w
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// To sign the digest, we need to give the signer a copy of the response body...
|
||||
// ...but it is optional, no digest will be signed if given "nil"
|
||||
body := ...
|
||||
err := s.signer.SignResponse(privateKey, pubKeyId, w, body)
|
||||
if err != nil {
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The `pubKeyId` will be used at verification time.
|
||||
|
||||
### Verifying
|
||||
|
||||
Verifying requires an application to use the `pubKeyId` to both retrieve the key
|
||||
needed for verification as well as determine the algorithm to use. Use a
|
||||
`Verifier`:
|
||||
|
||||
```go
|
||||
func verify(r *http.Request) error {
|
||||
verifier, err := httpsig.NewVerifier(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pubKeyId := verifier.KeyId()
|
||||
var algo httpsig.Algorithm = ...
|
||||
var pubKey crypto.PublicKey = ...
|
||||
// The verifier will verify the Digest in addition to the HTTP signature
|
||||
return verifier.Verify(pubKey, algo)
|
||||
}
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
[License-Image]: https://img.shields.io/github/license/go-fed/httpsig?color=blue
|
||||
[License-Url]: https://opensource.org/licenses/BSD-3-Clause
|
|
@ -0,0 +1,532 @@
|
|||
package httpsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/hmac"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle" // Use should trigger great care
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/blake2b"
|
||||
"golang.org/x/crypto/blake2s"
|
||||
"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"
|
||||
md5String = "md5"
|
||||
sha1String = "sha1"
|
||||
sha224String = "sha224"
|
||||
sha256String = "sha256"
|
||||
sha384String = "sha384"
|
||||
sha512String = "sha512"
|
||||
md5sha1String = "md5sha1"
|
||||
ripemd160String = "ripemd160"
|
||||
sha3_224String = "sha3-224"
|
||||
sha3_256String = "sha3-256"
|
||||
sha3_384String = "sha3-384"
|
||||
sha3_512String = "sha3-512"
|
||||
sha512_224String = "sha512-224"
|
||||
sha512_256String = "sha512-256"
|
||||
blake2s_256String = "blake2s-256"
|
||||
blake2b_256String = "blake2b-256"
|
||||
blake2b_384String = "blake2b-384"
|
||||
blake2b_512String = "blake2b-512"
|
||||
)
|
||||
|
||||
var blake2Algorithms = map[crypto.Hash]bool{
|
||||
crypto.BLAKE2s_256: true,
|
||||
crypto.BLAKE2b_256: true,
|
||||
crypto.BLAKE2b_384: true,
|
||||
crypto.BLAKE2b_512: true,
|
||||
}
|
||||
|
||||
var hashToDef = map[crypto.Hash]struct {
|
||||
name string
|
||||
new func(key []byte) (hash.Hash, error) // Only MACers will accept a key
|
||||
}{
|
||||
// Which standard names these?
|
||||
// The spec lists the following as a canonical reference, which is dead:
|
||||
// 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 }},
|
||||
// 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 }},
|
||||
crypto.SHA512: {sha512String, func(key []byte) (hash.Hash, error) { return sha512.New(), nil }},
|
||||
crypto.MD5SHA1: {md5sha1String, func(key []byte) (hash.Hash, error) { return nil, nil }},
|
||||
crypto.RIPEMD160: {ripemd160String, func(key []byte) (hash.Hash, error) { return ripemd160.New(), nil }},
|
||||
crypto.SHA3_224: {sha3_224String, func(key []byte) (hash.Hash, error) { return sha3.New224(), nil }},
|
||||
crypto.SHA3_256: {sha3_256String, func(key []byte) (hash.Hash, error) { return sha3.New256(), nil }},
|
||||
crypto.SHA3_384: {sha3_384String, func(key []byte) (hash.Hash, error) { return sha3.New384(), nil }},
|
||||
crypto.SHA3_512: {sha3_512String, func(key []byte) (hash.Hash, error) { return sha3.New512(), nil }},
|
||||
crypto.SHA512_224: {sha512_224String, func(key []byte) (hash.Hash, error) { return sha512.New512_224(), nil }},
|
||||
crypto.SHA512_256: {sha512_256String, func(key []byte) (hash.Hash, error) { return sha512.New512_256(), nil }},
|
||||
crypto.BLAKE2s_256: {blake2s_256String, func(key []byte) (hash.Hash, error) { return blake2s.New256(key) }},
|
||||
crypto.BLAKE2b_256: {blake2b_256String, func(key []byte) (hash.Hash, error) { return blake2b.New256(key) }},
|
||||
crypto.BLAKE2b_384: {blake2b_384String, func(key []byte) (hash.Hash, error) { return blake2b.New384(key) }},
|
||||
crypto.BLAKE2b_512: {blake2b_512String, func(key []byte) (hash.Hash, error) { return blake2b.New512(key) }},
|
||||
}
|
||||
|
||||
var stringToHash map[string]crypto.Hash
|
||||
|
||||
const (
|
||||
defaultAlgorithm = RSA_SHA256
|
||||
defaultAlgorithmHashing = sha256String
|
||||
)
|
||||
|
||||
func init() {
|
||||
stringToHash = make(map[string]crypto.Hash, len(hashToDef))
|
||||
for k, v := range hashToDef {
|
||||
stringToHash[v.name] = k
|
||||
}
|
||||
// This should guarantee that at runtime the defaultAlgorithm will not
|
||||
// result in errors when fetching a macer or signer (see algorithms.go)
|
||||
if ok, err := isAvailable(string(defaultAlgorithmHashing)); err != nil {
|
||||
panic(err)
|
||||
} else if !ok {
|
||||
panic(fmt.Sprintf("the default httpsig algorithm is unavailable: %q", defaultAlgorithm))
|
||||
}
|
||||
}
|
||||
|
||||
func isForbiddenHash(h crypto.Hash) bool {
|
||||
switch h {
|
||||
// Not actually cryptographically secure
|
||||
case crypto.MD4:
|
||||
fallthrough
|
||||
case crypto.MD5:
|
||||
fallthrough
|
||||
case crypto.MD5SHA1: // shorthand for crypto/tls, not actually implemented
|
||||
return true
|
||||
}
|
||||
// Still cryptographically secure
|
||||
return false
|
||||
}
|
||||
|
||||
// signer is an internally public type.
|
||||
type signer interface {
|
||||
Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error)
|
||||
Verify(pub crypto.PublicKey, toHash, signature []byte) error
|
||||
String() string
|
||||
}
|
||||
|
||||
// macer is an internally public type.
|
||||
type macer interface {
|
||||
Sign(sig, key []byte) ([]byte, error)
|
||||
Equal(sig, actualMAC, key []byte) (bool, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
var _ macer = &hmacAlgorithm{}
|
||||
|
||||
type hmacAlgorithm struct {
|
||||
fn func(key []byte) (hash.Hash, error)
|
||||
kind crypto.Hash
|
||||
}
|
||||
|
||||
func (h *hmacAlgorithm) Sign(sig, key []byte) ([]byte, error) {
|
||||
hs, err := h.fn(key)
|
||||
if err = setSig(hs, sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hs.Sum(nil), nil
|
||||
}
|
||||
|
||||
func (h *hmacAlgorithm) Equal(sig, actualMAC, key []byte) (bool, error) {
|
||||
hs, err := h.fn(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer hs.Reset()
|
||||
err = setSig(hs, sig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
expected := hs.Sum(nil)
|
||||
return hmac.Equal(actualMAC, expected), nil
|
||||
}
|
||||
|
||||
func (h *hmacAlgorithm) String() string {
|
||||
return fmt.Sprintf("%s-%s", hmacPrefix, hashToDef[h.kind].name)
|
||||
}
|
||||
|
||||
var _ signer = &rsaAlgorithm{}
|
||||
|
||||
type rsaAlgorithm struct {
|
||||
hash.Hash
|
||||
kind crypto.Hash
|
||||
sshSigner ssh.Signer
|
||||
}
|
||||
|
||||
func (r *rsaAlgorithm) setSig(b []byte) error {
|
||||
n, err := r.Write(b)
|
||||
if err != nil {
|
||||
r.Reset()
|
||||
return err
|
||||
} else if n != len(b) {
|
||||
r.Reset()
|
||||
return fmt.Errorf("could only write %d of %d bytes of signature to hash", n, len(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
rsaK, ok := p.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("crypto.PrivateKey is not *rsa.PrivateKey")
|
||||
}
|
||||
return rsa.SignPKCS1v15(rand, rsaK, r.kind, r.Sum(nil))
|
||||
}
|
||||
|
||||
func (r *rsaAlgorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) error {
|
||||
defer r.Reset()
|
||||
rsaK, ok := pub.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return errors.New("crypto.PublicKey is not *rsa.PublicKey")
|
||||
}
|
||||
if err := r.setSig(toHash); err != nil {
|
||||
return err
|
||||
}
|
||||
return rsa.VerifyPKCS1v15(rsaK, r.kind, r.Sum(nil), signature)
|
||||
}
|
||||
|
||||
func (r *rsaAlgorithm) String() string {
|
||||
return fmt.Sprintf("%s-%s", rsaPrefix, hashToDef[r.kind].name)
|
||||
}
|
||||
|
||||
var _ signer = &ed25519Algorithm{}
|
||||
|
||||
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")
|
||||
}
|
||||
return ed25519.Sign(ed25519K, sig), nil
|
||||
}
|
||||
|
||||
func (r *ed25519Algorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) error {
|
||||
ed25519K, ok := pub.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return errors.New("crypto.PublicKey is not ed25519.PublicKey")
|
||||
}
|
||||
|
||||
if ed25519.Verify(ed25519K, toHash, signature) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("ed25519 verify failed")
|
||||
}
|
||||
|
||||
func (r *ed25519Algorithm) String() string {
|
||||
return fmt.Sprintf("%s", ed25519Prefix)
|
||||
}
|
||||
|
||||
var _ signer = &ecdsaAlgorithm{}
|
||||
|
||||
type ecdsaAlgorithm struct {
|
||||
hash.Hash
|
||||
kind crypto.Hash
|
||||
}
|
||||
|
||||
func (r *ecdsaAlgorithm) setSig(b []byte) error {
|
||||
n, err := r.Write(b)
|
||||
if err != nil {
|
||||
r.Reset()
|
||||
return err
|
||||
} else if n != len(b) {
|
||||
r.Reset()
|
||||
return fmt.Errorf("could only write %d of %d bytes of signature to hash", n, len(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ECDSASignature struct {
|
||||
R, S *big.Int
|
||||
}
|
||||
|
||||
func (r *ecdsaAlgorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) {
|
||||
defer r.Reset()
|
||||
if err := r.setSig(sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ecdsaK, ok := p.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("crypto.PrivateKey is not *ecdsa.PrivateKey")
|
||||
}
|
||||
R, S, err := ecdsa.Sign(rand, ecdsaK, r.Sum(nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature := ECDSASignature{R: R, S: S}
|
||||
bytes, err := asn1.Marshal(signature)
|
||||
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
func (r *ecdsaAlgorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) error {
|
||||
defer r.Reset()
|
||||
ecdsaK, ok := pub.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return errors.New("crypto.PublicKey is not *ecdsa.PublicKey")
|
||||
}
|
||||
if err := r.setSig(toHash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sig := new(ECDSASignature)
|
||||
_, err := asn1.Unmarshal(signature, sig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ecdsa.Verify(ecdsaK, r.Sum(nil), sig.R, sig.S) {
|
||||
return nil
|
||||
} else {
|
||||
return errors.New("Invalid signature")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ecdsaAlgorithm) String() string {
|
||||
return fmt.Sprintf("%s-%s", ecdsaPrefix, hashToDef[r.kind].name)
|
||||
}
|
||||
|
||||
var _ macer = &blakeMacAlgorithm{}
|
||||
|
||||
type blakeMacAlgorithm struct {
|
||||
fn func(key []byte) (hash.Hash, error)
|
||||
kind crypto.Hash
|
||||
}
|
||||
|
||||
func (r *blakeMacAlgorithm) Sign(sig, key []byte) ([]byte, error) {
|
||||
hs, err := r.fn(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = setSig(hs, sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hs.Sum(nil), nil
|
||||
}
|
||||
|
||||
func (r *blakeMacAlgorithm) Equal(sig, actualMAC, key []byte) (bool, error) {
|
||||
hs, err := r.fn(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer hs.Reset()
|
||||
err = setSig(hs, sig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
expected := hs.Sum(nil)
|
||||
return subtle.ConstantTimeCompare(actualMAC, expected) == 1, nil
|
||||
}
|
||||
|
||||
func (r *blakeMacAlgorithm) String() string {
|
||||
return fmt.Sprintf("%s", hashToDef[r.kind].name)
|
||||
}
|
||||
|
||||
func setSig(a hash.Hash, b []byte) error {
|
||||
n, err := a.Write(b)
|
||||
if err != nil {
|
||||
a.Reset()
|
||||
return err
|
||||
} else if n != len(b) {
|
||||
a.Reset()
|
||||
return fmt.Errorf("could only write %d of %d bytes of signature to hash", n, len(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSupportedHttpSigAlgorithm returns true if the string is supported by this
|
||||
// library, is not a hash known to be weak, and is supported by the hardware.
|
||||
func IsSupportedHttpSigAlgorithm(algo string) bool {
|
||||
a, err := isAvailable(algo)
|
||||
return a && err == nil
|
||||
}
|
||||
|
||||
// isAvailable is an internally public function
|
||||
func isAvailable(algo string) (bool, error) {
|
||||
c, ok := stringToHash[algo]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("no match for %q", algo)
|
||||
}
|
||||
if isForbiddenHash(c) {
|
||||
return false, fmt.Errorf("forbidden hash type in %q", algo)
|
||||
}
|
||||
return c.Available(), nil
|
||||
}
|
||||
|
||||
func newAlgorithmConstructor(algo string) (fn func(k []byte) (hash.Hash, error), c crypto.Hash, e error) {
|
||||
ok := false
|
||||
c, ok = stringToHash[algo]
|
||||
if !ok {
|
||||
e = fmt.Errorf("no match for %q", algo)
|
||||
return
|
||||
}
|
||||
if isForbiddenHash(c) {
|
||||
e = fmt.Errorf("forbidden hash type in %q", algo)
|
||||
return
|
||||
}
|
||||
algoDef, ok := hashToDef[c]
|
||||
if !ok {
|
||||
e = fmt.Errorf("have crypto.Hash %v but no definition", c)
|
||||
return
|
||||
}
|
||||
fn = func(key []byte) (hash.Hash, error) {
|
||||
h, err := algoDef.new(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newAlgorithm(algo string, key []byte) (hash.Hash, crypto.Hash, error) {
|
||||
fn, c, err := newAlgorithmConstructor(algo)
|
||||
if err != nil {
|
||||
return nil, c, err
|
||||
}
|
||||
h, err := fn(key)
|
||||
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)
|
||||
isEcdsa := false
|
||||
isEd25519 := false
|
||||
var algo string = ""
|
||||
if strings.HasPrefix(s, ecdsaPrefix) {
|
||||
algo = strings.TrimPrefix(s, ecdsaPrefix+"-")
|
||||
isEcdsa = true
|
||||
} else if strings.HasPrefix(s, rsaPrefix) {
|
||||
algo = strings.TrimPrefix(s, rsaPrefix+"-")
|
||||
} else if strings.HasPrefix(s, ed25519Prefix) {
|
||||
isEd25519 = true
|
||||
algo = "sha512"
|
||||
} else {
|
||||
return nil, fmt.Errorf("no signer matching %q", s)
|
||||
}
|
||||
hash, cHash, err := newAlgorithm(algo, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isEd25519 {
|
||||
return &ed25519Algorithm{}, nil
|
||||
}
|
||||
if isEcdsa {
|
||||
return &ecdsaAlgorithm{
|
||||
Hash: hash,
|
||||
kind: cHash,
|
||||
}, nil
|
||||
}
|
||||
return &rsaAlgorithm{
|
||||
Hash: hash,
|
||||
kind: cHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// macerFromString is an internally public method constructor
|
||||
func macerFromString(s string) (macer, error) {
|
||||
s = strings.ToLower(s)
|
||||
if strings.HasPrefix(s, hmacPrefix) {
|
||||
algo := strings.TrimPrefix(s, hmacPrefix+"-")
|
||||
hashFn, cHash, err := newAlgorithmConstructor(algo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Ensure below does not panic
|
||||
_, err = hashFn(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hmacAlgorithm{
|
||||
fn: func(key []byte) (hash.Hash, error) {
|
||||
return hmac.New(func() hash.Hash {
|
||||
h, e := hashFn(nil)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
return h
|
||||
}, key), nil
|
||||
},
|
||||
kind: cHash,
|
||||
}, nil
|
||||
} else if bl, ok := stringToHash[s]; ok && blake2Algorithms[bl] {
|
||||
hashFn, cHash, err := newAlgorithmConstructor(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &blakeMacAlgorithm{
|
||||
fn: hashFn,
|
||||
kind: cHash,
|
||||
}, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("no MACer matching %q", s)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package httpsig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"hash"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DigestAlgorithm string
|
||||
|
||||
const (
|
||||
DigestSha256 DigestAlgorithm = "SHA-256"
|
||||
DigestSha512 = "SHA-512"
|
||||
)
|
||||
|
||||
var digestToDef = map[DigestAlgorithm]crypto.Hash{
|
||||
DigestSha256: crypto.SHA256,
|
||||
DigestSha512: crypto.SHA512,
|
||||
}
|
||||
|
||||
// IsSupportedDigestAlgorithm returns true if hte string is supported by this
|
||||
// library, is not a hash known to be weak, and is supported by the hardware.
|
||||
func IsSupportedDigestAlgorithm(algo string) bool {
|
||||
uc := DigestAlgorithm(strings.ToUpper(algo))
|
||||
c, ok := digestToDef[uc]
|
||||
return ok && c.Available()
|
||||
}
|
||||
|
||||
func getHash(alg DigestAlgorithm) (h hash.Hash, toUse DigestAlgorithm, err error) {
|
||||
upper := DigestAlgorithm(strings.ToUpper(string(alg)))
|
||||
c, ok := digestToDef[upper]
|
||||
if !ok {
|
||||
err = fmt.Errorf("unknown or unsupported Digest algorithm: %s", alg)
|
||||
} else if !c.Available() {
|
||||
err = fmt.Errorf("unavailable Digest algorithm: %s", alg)
|
||||
} else {
|
||||
h = c.New()
|
||||
toUse = upper
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
digestHeader = "Digest"
|
||||
digestDelim = "="
|
||||
)
|
||||
|
||||
func addDigest(r *http.Request, algo DigestAlgorithm, b []byte) (err error) {
|
||||
_, ok := r.Header[digestHeader]
|
||||
if ok {
|
||||
err = fmt.Errorf("cannot add Digest: Digest is already set")
|
||||
return
|
||||
}
|
||||
var h hash.Hash
|
||||
var a DigestAlgorithm
|
||||
h, a, err = getHash(algo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h.Write(b)
|
||||
sum := h.Sum(nil)
|
||||
r.Header.Add(digestHeader,
|
||||
fmt.Sprintf("%s%s%s",
|
||||
a,
|
||||
digestDelim,
|
||||
base64.StdEncoding.EncodeToString(sum[:])))
|
||||
return
|
||||
}
|
||||
|
||||
func addDigestResponse(r http.ResponseWriter, algo DigestAlgorithm, b []byte) (err error) {
|
||||
_, ok := r.Header()[digestHeader]
|
||||
if ok {
|
||||
err = fmt.Errorf("cannot add Digest: Digest is already set")
|
||||
return
|
||||
}
|
||||
var h hash.Hash
|
||||
var a DigestAlgorithm
|
||||
h, a, err = getHash(algo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h.Write(b)
|
||||
sum := h.Sum(nil)
|
||||
r.Header().Add(digestHeader,
|
||||
fmt.Sprintf("%s%s%s",
|
||||
a,
|
||||
digestDelim,
|
||||
base64.StdEncoding.EncodeToString(sum[:])))
|
||||
return
|
||||
}
|
||||
|
||||
func verifyDigest(r *http.Request, body *bytes.Buffer) (err error) {
|
||||
d := r.Header.Get(digestHeader)
|
||||
if len(d) == 0 {
|
||||
err = fmt.Errorf("cannot verify Digest: request has no Digest header")
|
||||
return
|
||||
}
|
||||
elem := strings.SplitN(d, digestDelim, 2)
|
||||
if len(elem) != 2 {
|
||||
err = fmt.Errorf("cannot verify Digest: malformed Digest: %s", d)
|
||||
return
|
||||
}
|
||||
var h hash.Hash
|
||||
h, _, err = getHash(DigestAlgorithm(elem[0]))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h.Write(body.Bytes())
|
||||
sum := h.Sum(nil)
|
||||
encSum := base64.StdEncoding.EncodeToString(sum[:])
|
||||
if encSum != elem[1] {
|
||||
err = fmt.Errorf("cannot verify Digest: header Digest does not match the digest of the request body")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,413 @@
|
|||
// Implements HTTP request and response signing and verification. Supports the
|
||||
// major MAC and asymmetric key signature algorithms. It has several safety
|
||||
// restrictions: One, none of the widely known non-cryptographically safe
|
||||
// algorithms are permitted; Two, the RSA SHA256 algorithms must be available in
|
||||
// the binary (and it should, barring export restrictions); Finally, the library
|
||||
// assumes either the 'Authorizationn' or 'Signature' headers are to be set (but
|
||||
// not both).
|
||||
package httpsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// Algorithm specifies a cryptography secure algorithm for signing HTTP requests
|
||||
// and responses.
|
||||
type Algorithm string
|
||||
|
||||
const (
|
||||
// MAC-based algoirthms.
|
||||
HMAC_SHA224 Algorithm = hmacPrefix + "-" + sha224String
|
||||
HMAC_SHA256 Algorithm = hmacPrefix + "-" + sha256String
|
||||
HMAC_SHA384 Algorithm = hmacPrefix + "-" + sha384String
|
||||
HMAC_SHA512 Algorithm = hmacPrefix + "-" + sha512String
|
||||
HMAC_RIPEMD160 Algorithm = hmacPrefix + "-" + ripemd160String
|
||||
HMAC_SHA3_224 Algorithm = hmacPrefix + "-" + sha3_224String
|
||||
HMAC_SHA3_256 Algorithm = hmacPrefix + "-" + sha3_256String
|
||||
HMAC_SHA3_384 Algorithm = hmacPrefix + "-" + sha3_384String
|
||||
HMAC_SHA3_512 Algorithm = hmacPrefix + "-" + sha3_512String
|
||||
HMAC_SHA512_224 Algorithm = hmacPrefix + "-" + sha512_224String
|
||||
HMAC_SHA512_256 Algorithm = hmacPrefix + "-" + sha512_256String
|
||||
HMAC_BLAKE2S_256 Algorithm = hmacPrefix + "-" + blake2s_256String
|
||||
HMAC_BLAKE2B_256 Algorithm = hmacPrefix + "-" + blake2b_256String
|
||||
HMAC_BLAKE2B_384 Algorithm = hmacPrefix + "-" + blake2b_384String
|
||||
HMAC_BLAKE2B_512 Algorithm = hmacPrefix + "-" + blake2b_512String
|
||||
BLAKE2S_256 Algorithm = blake2s_256String
|
||||
BLAKE2B_256 Algorithm = blake2b_256String
|
||||
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
|
||||
RSA_SHA384 Algorithm = rsaPrefix + "-" + sha384String
|
||||
RSA_SHA512 Algorithm = rsaPrefix + "-" + sha512String
|
||||
RSA_RIPEMD160 Algorithm = rsaPrefix + "-" + ripemd160String
|
||||
// ECDSA algorithms
|
||||
ECDSA_SHA224 Algorithm = ecdsaPrefix + "-" + sha224String
|
||||
ECDSA_SHA256 Algorithm = ecdsaPrefix + "-" + sha256String
|
||||
ECDSA_SHA384 Algorithm = ecdsaPrefix + "-" + sha384String
|
||||
ECDSA_SHA512 Algorithm = ecdsaPrefix + "-" + sha512String
|
||||
ECDSA_RIPEMD160 Algorithm = ecdsaPrefix + "-" + ripemd160String
|
||||
// ED25519 algorithms
|
||||
// can only be SHA512
|
||||
ED25519 Algorithm = ed25519Prefix
|
||||
|
||||
// Just because you can glue things together, doesn't mean they will
|
||||
// work. The following options are not supported.
|
||||
rsa_SHA3_224 Algorithm = rsaPrefix + "-" + sha3_224String
|
||||
rsa_SHA3_256 Algorithm = rsaPrefix + "-" + sha3_256String
|
||||
rsa_SHA3_384 Algorithm = rsaPrefix + "-" + sha3_384String
|
||||
rsa_SHA3_512 Algorithm = rsaPrefix + "-" + sha3_512String
|
||||
rsa_SHA512_224 Algorithm = rsaPrefix + "-" + sha512_224String
|
||||
rsa_SHA512_256 Algorithm = rsaPrefix + "-" + sha512_256String
|
||||
rsa_BLAKE2S_256 Algorithm = rsaPrefix + "-" + blake2s_256String
|
||||
rsa_BLAKE2B_256 Algorithm = rsaPrefix + "-" + blake2b_256String
|
||||
rsa_BLAKE2B_384 Algorithm = rsaPrefix + "-" + blake2b_384String
|
||||
rsa_BLAKE2B_512 Algorithm = rsaPrefix + "-" + blake2b_512String
|
||||
)
|
||||
|
||||
// HTTP Signatures can be applied to different HTTP headers, depending on the
|
||||
// expected application behavior.
|
||||
type SignatureScheme string
|
||||
|
||||
const (
|
||||
// Signature will place the HTTP Signature into the 'Signature' HTTP
|
||||
// header.
|
||||
Signature SignatureScheme = "Signature"
|
||||
// Authorization will place the HTTP Signature into the 'Authorization'
|
||||
// HTTP header.
|
||||
Authorization SignatureScheme = "Authorization"
|
||||
)
|
||||
|
||||
const (
|
||||
// The HTTP Signatures specification uses the "Signature" auth-scheme
|
||||
// for the Authorization header. This is coincidentally named, but not
|
||||
// semantically the same, as the "Signature" HTTP header value.
|
||||
signatureAuthScheme = "Signature"
|
||||
)
|
||||
|
||||
// There are subtle differences to the values in the header. The Authorization
|
||||
// header has an 'auth-scheme' value that must be prefixed to the rest of the
|
||||
// key and values.
|
||||
func (s SignatureScheme) authScheme() string {
|
||||
switch s {
|
||||
case Authorization:
|
||||
return signatureAuthScheme
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type SignatureOption struct {
|
||||
// ExcludeQueryStringFromPathPseudoHeader omits the query parameters from the
|
||||
// `:path` pseudo-header in the HTTP signature.
|
||||
//
|
||||
// The query string is optional in the `:path` pseudo-header.
|
||||
// https://www.rfc-editor.org/rfc/rfc9113#section-8.3.1-2.4.1
|
||||
ExcludeQueryStringFromPathPseudoHeader bool
|
||||
}
|
||||
|
||||
// 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 Signer interface {
|
||||
// SignRequest signs the request using a private key. The public key id
|
||||
// is used by the HTTP server to identify which key to use to verify the
|
||||
// signature.
|
||||
//
|
||||
// If the Signer was created using a MAC based algorithm, then the key
|
||||
// is expected to be of type []byte. If the Signer was created using an
|
||||
// RSA based algorithm, then the private key is expected to be of type
|
||||
// *rsa.PrivateKey.
|
||||
//
|
||||
// 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(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error
|
||||
// SignResponse signs the response using a private key. The public key
|
||||
// id is used by the HTTP client to identify which key to use to verify
|
||||
// the signature.
|
||||
//
|
||||
// If the Signer was created using a MAC based algorithm, then the key
|
||||
// is expected to be of type []byte. If the Signer was created using an
|
||||
// RSA based algorithm, then the private key is expected to be of type
|
||||
// *rsa.PrivateKey.
|
||||
//
|
||||
// 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(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error
|
||||
}
|
||||
|
||||
type SignerWithOptions interface {
|
||||
Signer
|
||||
|
||||
// SignRequestWithOptions signs the request using a private key. The public key id
|
||||
// is used by the HTTP server to identify which key to use to verify the
|
||||
// signature.
|
||||
//
|
||||
// If the Signer was created using a MAC based algorithm, then the key
|
||||
// is expected to be of type []byte. If the Signer was created using an
|
||||
// RSA based algorithm, then the private key is expected to be of type
|
||||
// *rsa.PrivateKey.
|
||||
//
|
||||
// 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.
|
||||
SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte, opts SignatureOption) error
|
||||
// SignResponseWithOptions signs the response using a private key. The public key
|
||||
// id is used by the HTTP client to identify which key to use to verify
|
||||
// the signature.
|
||||
//
|
||||
// If the Signer was created using a MAC based algorithm, then the key
|
||||
// is expected to be of type []byte. If the Signer was created using an
|
||||
// RSA based algorithm, then the private key is expected to be of type
|
||||
// *rsa.PrivateKey.
|
||||
//
|
||||
// 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.
|
||||
SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte, opts SignatureOption) error
|
||||
}
|
||||
|
||||
// NewSigner creates a new Signer with the provided algorithm preferences to
|
||||
// make HTTP signatures. Only the first available algorithm will be used, which
|
||||
// is returned by this function along with the Signer. If none of the preferred
|
||||
// algorithms were available, then the default algorithm is used. 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.
|
||||
//
|
||||
// An error is returned if an unknown or a known cryptographically insecure
|
||||
// Algorithm is provided.
|
||||
func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SignerWithOptions, Algorithm, error) {
|
||||
for _, pref := range prefs {
|
||||
s, err := newSigner(pref, dAlgo, headers, scheme, expiresIn)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return s, pref, err
|
||||
}
|
||||
s, err := newSigner(defaultAlgorithm, dAlgo, headers, scheme, expiresIn)
|
||||
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
|
||||
// that define the signature.
|
||||
//
|
||||
// Verifiers are not safe to use between multiple goroutines.
|
||||
//
|
||||
// Note that verification ignores the deprecated 'algorithm' parameter.
|
||||
type Verifier interface {
|
||||
// KeyId gets the public key id that the signature is signed with.
|
||||
//
|
||||
// Note that the application is expected to determine the algorithm
|
||||
// used based on metadata or out-of-band information for this key id.
|
||||
KeyId() string
|
||||
// Verify accepts the public key specified by KeyId and returns an
|
||||
// error if verification fails or if the signature is malformed. The
|
||||
// algorithm must be the one used to create the signature in order to
|
||||
// pass verification. The algorithm is determined based on metadata or
|
||||
// out-of-band information for the key id.
|
||||
//
|
||||
// If the signature was created using a MAC based algorithm, then the
|
||||
// key is expected to be of type []byte. If the signature was created
|
||||
// using an RSA based algorithm, then the public key is expected to be
|
||||
// of type *rsa.PublicKey.
|
||||
Verify(pKey crypto.PublicKey, algo Algorithm) error
|
||||
}
|
||||
|
||||
type VerifierWithOptions interface {
|
||||
Verifier
|
||||
|
||||
VerifyWithOptions(pKey crypto.PublicKey, algo Algorithm, opts SignatureOption) error
|
||||
}
|
||||
|
||||
const (
|
||||
// host is treated specially because golang may not include it in the
|
||||
// request header map on the server side of a request.
|
||||
hostHeader = "Host"
|
||||
)
|
||||
|
||||
// NewVerifier verifies the given request. It returns an error if the HTTP
|
||||
// Signature parameters are not present in any headers, are present in more than
|
||||
// one header, are malformed, or are missing required parameters. It ignores
|
||||
// unknown HTTP Signature parameters.
|
||||
func NewVerifier(r *http.Request) (VerifierWithOptions, error) {
|
||||
h := r.Header
|
||||
if _, hasHostHeader := h[hostHeader]; len(r.Host) > 0 && !hasHostHeader {
|
||||
h[hostHeader] = []string{r.Host}
|
||||
}
|
||||
return newVerifier(h, func(h http.Header, toInclude []string, created int64, expires int64, opts SignatureOption) (string, error) {
|
||||
return signatureString(h, toInclude, addRequestTarget(r, opts), created, expires)
|
||||
})
|
||||
}
|
||||
|
||||
// NewResponseVerifier verifies the given response. It returns errors under the
|
||||
// same conditions as NewVerifier.
|
||||
func NewResponseVerifier(r *http.Response) (Verifier, error) {
|
||||
return newVerifier(r.Header, func(h http.Header, toInclude []string, created int64, expires int64, _ SignatureOption) (string, error) {
|
||||
return signatureString(h, toInclude, requestTargetNotPermitted, created, expires)
|
||||
})
|
||||
}
|
||||
|
||||
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) (SignerWithOptions, error) {
|
||||
|
||||
var expires, created int64 = 0, 0
|
||||
if expiresIn != 0 {
|
||||
created = time.Now().Unix()
|
||||
expires = created + expiresIn
|
||||
}
|
||||
|
||||
s, err := signerFromString(string(algo))
|
||||
if err == nil {
|
||||
a := &asymmSigner{
|
||||
s: s,
|
||||
dAlgo: dAlgo,
|
||||
headers: headers,
|
||||
targetHeader: scheme,
|
||||
prefix: scheme.authScheme(),
|
||||
created: created,
|
||||
expires: expires,
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
m, err := macerFromString(string(algo))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no crypto implementation available for %q: %s", algo, err)
|
||||
}
|
||||
c := &macSigner{
|
||||
m: m,
|
||||
dAlgo: dAlgo,
|
||||
headers: headers,
|
||||
targetHeader: scheme,
|
||||
prefix: scheme.authScheme(),
|
||||
created: created,
|
||||
expires: expires,
|
||||
}
|
||||
return c, nil
|
||||
}
|
|
@ -0,0 +1,350 @@
|
|||
package httpsig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// Signature Parameters
|
||||
keyIdParameter = "keyId"
|
||||
algorithmParameter = "algorithm"
|
||||
headersParameter = "headers"
|
||||
signatureParameter = "signature"
|
||||
prefixSeparater = " "
|
||||
parameterKVSeparater = "="
|
||||
parameterValueDelimiter = "\""
|
||||
parameterSeparater = ","
|
||||
headerParameterValueDelim = " "
|
||||
// RequestTarget specifies to include the http request method and
|
||||
// entire URI in the signature. Pass it as a header to NewSigner.
|
||||
RequestTarget = "(request-target)"
|
||||
createdKey = "created"
|
||||
expiresKey = "expires"
|
||||
dateHeader = "date"
|
||||
|
||||
// Signature String Construction
|
||||
headerFieldDelimiter = ": "
|
||||
headersDelimiter = "\n"
|
||||
headerValueDelimiter = ", "
|
||||
requestTargetSeparator = " "
|
||||
)
|
||||
|
||||
var defaultHeaders = []string{dateHeader}
|
||||
|
||||
var _ SignerWithOptions = &macSigner{}
|
||||
|
||||
type macSigner struct {
|
||||
m macer
|
||||
makeDigest bool
|
||||
dAlgo DigestAlgorithm
|
||||
headers []string
|
||||
targetHeader SignatureScheme
|
||||
prefix string
|
||||
created int64
|
||||
expires int64
|
||||
}
|
||||
|
||||
func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error {
|
||||
return m.SignRequestWithOptions(pKey, pubKeyId, r, body, SignatureOption{})
|
||||
}
|
||||
|
||||
func (m *macSigner) SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte, opts SignatureOption) error {
|
||||
if body != nil {
|
||||
err := addDigest(r, m.dAlgo, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s, err := m.signatureString(r, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc, err := m.signSignature(pKey, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setSignatureHeader(r.Header, string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers, m.created, m.expires)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *macSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error {
|
||||
return m.SignResponseWithOptions(pKey, pubKeyId, r, body, SignatureOption{})
|
||||
}
|
||||
|
||||
func (m *macSigner) SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte, _ SignatureOption) error {
|
||||
if body != nil {
|
||||
err := addDigestResponse(r, m.dAlgo, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s, err := m.signatureStringResponse(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc, err := m.signSignature(pKey, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setSignatureHeader(r.Header(), string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers, m.created, m.expires)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *macSigner) signSignature(pKey crypto.PrivateKey, s string) (string, error) {
|
||||
pKeyBytes, ok := pKey.([]byte)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("private key for MAC signing must be of type []byte")
|
||||
}
|
||||
sig, err := m.m.Sign([]byte(s), pKeyBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
enc := base64.StdEncoding.EncodeToString(sig)
|
||||
return enc, nil
|
||||
}
|
||||
|
||||
func (m *macSigner) signatureString(r *http.Request, opts SignatureOption) (string, error) {
|
||||
return signatureString(r.Header, m.headers, addRequestTarget(r, opts), m.created, m.expires)
|
||||
}
|
||||
|
||||
func (m *macSigner) signatureStringResponse(r http.ResponseWriter) (string, error) {
|
||||
return signatureString(r.Header(), m.headers, requestTargetNotPermitted, m.created, m.expires)
|
||||
}
|
||||
|
||||
var _ SignerWithOptions = &asymmSigner{}
|
||||
|
||||
type asymmSigner struct {
|
||||
s signer
|
||||
makeDigest bool
|
||||
dAlgo DigestAlgorithm
|
||||
headers []string
|
||||
targetHeader SignatureScheme
|
||||
prefix string
|
||||
created int64
|
||||
expires int64
|
||||
}
|
||||
|
||||
func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error {
|
||||
return a.SignRequestWithOptions(pKey, pubKeyId, r, body, SignatureOption{})
|
||||
}
|
||||
|
||||
func (a *asymmSigner) SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte, opts SignatureOption) error {
|
||||
if body != nil {
|
||||
err := addDigest(r, a.dAlgo, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s, err := a.signatureString(r, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc, err := a.signSignature(pKey, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setSignatureHeader(r.Header, string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers, a.created, a.expires)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *asymmSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error {
|
||||
return a.SignResponseWithOptions(pKey, pubKeyId, r, body, SignatureOption{})
|
||||
}
|
||||
|
||||
func (a *asymmSigner) SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte, _ SignatureOption) error {
|
||||
if body != nil {
|
||||
err := addDigestResponse(r, a.dAlgo, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s, err := a.signatureStringResponse(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc, err := a.signSignature(pKey, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setSignatureHeader(r.Header(), string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers, a.created, a.expires)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *asymmSigner) signSignature(pKey crypto.PrivateKey, s string) (string, error) {
|
||||
sig, err := a.s.Sign(rand.Reader, pKey, []byte(s))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
enc := base64.StdEncoding.EncodeToString(sig)
|
||||
return enc, nil
|
||||
}
|
||||
|
||||
func (a *asymmSigner) signatureString(r *http.Request, opts SignatureOption) (string, error) {
|
||||
return signatureString(r.Header, a.headers, addRequestTarget(r, opts), a.created, a.expires)
|
||||
}
|
||||
|
||||
func (a *asymmSigner) signatureStringResponse(r http.ResponseWriter) (string, error) {
|
||||
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
|
||||
}
|
||||
var b bytes.Buffer
|
||||
// KeyId
|
||||
b.WriteString(prefix)
|
||||
if len(prefix) > 0 {
|
||||
b.WriteString(prefixSeparater)
|
||||
}
|
||||
b.WriteString(keyIdParameter)
|
||||
b.WriteString(parameterKVSeparater)
|
||||
b.WriteString(parameterValueDelimiter)
|
||||
b.WriteString(pubKeyId)
|
||||
b.WriteString(parameterValueDelimiter)
|
||||
b.WriteString(parameterSeparater)
|
||||
// Algorithm
|
||||
b.WriteString(algorithmParameter)
|
||||
b.WriteString(parameterKVSeparater)
|
||||
b.WriteString(parameterValueDelimiter)
|
||||
b.WriteString("hs2019") //real algorithm is hidden, see newest version of spec draft
|
||||
b.WriteString(parameterValueDelimiter)
|
||||
b.WriteString(parameterSeparater)
|
||||
|
||||
hasCreated := false
|
||||
hasExpires := false
|
||||
for _, h := range headers {
|
||||
val := strings.ToLower(h)
|
||||
if val == "("+createdKey+")" {
|
||||
hasCreated = true
|
||||
} else if val == "("+expiresKey+")" {
|
||||
hasExpires = true
|
||||
}
|
||||
}
|
||||
|
||||
// Created
|
||||
if hasCreated == true {
|
||||
b.WriteString(createdKey)
|
||||
b.WriteString(parameterKVSeparater)
|
||||
b.WriteString(strconv.FormatInt(created, 10))
|
||||
b.WriteString(parameterSeparater)
|
||||
}
|
||||
|
||||
// Expires
|
||||
if hasExpires == true {
|
||||
b.WriteString(expiresKey)
|
||||
b.WriteString(parameterKVSeparater)
|
||||
b.WriteString(strconv.FormatInt(expires, 10))
|
||||
b.WriteString(parameterSeparater)
|
||||
}
|
||||
|
||||
// Headers
|
||||
b.WriteString(headersParameter)
|
||||
b.WriteString(parameterKVSeparater)
|
||||
b.WriteString(parameterValueDelimiter)
|
||||
for i, h := range headers {
|
||||
b.WriteString(strings.ToLower(h))
|
||||
if i != len(headers)-1 {
|
||||
b.WriteString(headerParameterValueDelim)
|
||||
}
|
||||
}
|
||||
b.WriteString(parameterValueDelimiter)
|
||||
b.WriteString(parameterSeparater)
|
||||
// Signature
|
||||
b.WriteString(signatureParameter)
|
||||
b.WriteString(parameterKVSeparater)
|
||||
b.WriteString(parameterValueDelimiter)
|
||||
b.WriteString(enc)
|
||||
b.WriteString(parameterValueDelimiter)
|
||||
h.Add(targetHeader, b.String())
|
||||
}
|
||||
|
||||
func requestTargetNotPermitted(b *bytes.Buffer) error {
|
||||
return fmt.Errorf("cannot sign with %q on anything other than an http request", RequestTarget)
|
||||
}
|
||||
|
||||
func addRequestTarget(r *http.Request, opts SignatureOption) func(b *bytes.Buffer) error {
|
||||
return func(b *bytes.Buffer) error {
|
||||
b.WriteString(RequestTarget)
|
||||
b.WriteString(headerFieldDelimiter)
|
||||
b.WriteString(strings.ToLower(r.Method))
|
||||
b.WriteString(requestTargetSeparator)
|
||||
b.WriteString(r.URL.Path)
|
||||
|
||||
if !opts.ExcludeQueryStringFromPathPseudoHeader && r.URL.RawQuery != "" {
|
||||
b.WriteString("?")
|
||||
b.WriteString(r.URL.RawQuery)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func signatureString(values http.Header, include []string, requestTargetFn func(b *bytes.Buffer) error, created int64, expires int64) (string, error) {
|
||||
if len(include) == 0 {
|
||||
include = defaultHeaders
|
||||
}
|
||||
var b bytes.Buffer
|
||||
for n, i := range include {
|
||||
i := strings.ToLower(i)
|
||||
if i == RequestTarget {
|
||||
err := requestTargetFn(&b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else if i == "("+expiresKey+")" {
|
||||
if expires == 0 {
|
||||
return "", fmt.Errorf("missing expires value")
|
||||
}
|
||||
b.WriteString(i)
|
||||
b.WriteString(headerFieldDelimiter)
|
||||
b.WriteString(strconv.FormatInt(expires, 10))
|
||||
} else if i == "("+createdKey+")" {
|
||||
if created == 0 {
|
||||
return "", fmt.Errorf("missing created value")
|
||||
}
|
||||
b.WriteString(i)
|
||||
b.WriteString(headerFieldDelimiter)
|
||||
b.WriteString(strconv.FormatInt(created, 10))
|
||||
} else {
|
||||
hv, ok := values[textproto.CanonicalMIMEHeaderKey(i)]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("missing header %q", i)
|
||||
}
|
||||
b.WriteString(i)
|
||||
b.WriteString(headerFieldDelimiter)
|
||||
for i, v := range hv {
|
||||
b.WriteString(strings.TrimSpace(v))
|
||||
if i < len(hv)-1 {
|
||||
b.WriteString(headerValueDelimiter)
|
||||
}
|
||||
}
|
||||
}
|
||||
if n < len(include)-1 {
|
||||
b.WriteString(headersDelimiter)
|
||||
}
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package httpsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ VerifierWithOptions = &verifier{}
|
||||
|
||||
type verifier struct {
|
||||
header http.Header
|
||||
kId string
|
||||
signature string
|
||||
created int64
|
||||
expires int64
|
||||
headers []string
|
||||
sigStringFn func(http.Header, []string, int64, int64, SignatureOption) (string, error)
|
||||
}
|
||||
|
||||
func newVerifier(h http.Header, sigStringFn func(http.Header, []string, int64, int64, SignatureOption) (string, error)) (*verifier, error) {
|
||||
scheme, s, err := getSignatureScheme(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kId, sig, headers, created, expires, err := getSignatureComponents(scheme, s)
|
||||
if created != 0 {
|
||||
//check if created is not in the future, we assume a maximum clock offset of 10 seconds
|
||||
now := time.Now().Unix()
|
||||
if created-now > 10 {
|
||||
return nil, errors.New("created is in the future")
|
||||
}
|
||||
}
|
||||
if expires != 0 {
|
||||
//check if expires is in the past, we assume a maximum clock offset of 10 seconds
|
||||
now := time.Now().Unix()
|
||||
if now-expires > 10 {
|
||||
return nil, errors.New("signature expired")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &verifier{
|
||||
header: h,
|
||||
kId: kId,
|
||||
signature: sig,
|
||||
created: created,
|
||||
expires: expires,
|
||||
headers: headers,
|
||||
sigStringFn: sigStringFn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *verifier) KeyId() string {
|
||||
return v.kId
|
||||
}
|
||||
|
||||
func (v *verifier) Verify(pKey crypto.PublicKey, algo Algorithm) error {
|
||||
return v.VerifyWithOptions(pKey, algo, SignatureOption{})
|
||||
}
|
||||
|
||||
func (v *verifier) VerifyWithOptions(pKey crypto.PublicKey, algo Algorithm, opts SignatureOption) error {
|
||||
s, err := signerFromString(string(algo))
|
||||
if err == nil {
|
||||
return v.asymmVerify(s, pKey, opts)
|
||||
}
|
||||
m, err := macerFromString(string(algo))
|
||||
if err == nil {
|
||||
return v.macVerify(m, pKey, opts)
|
||||
}
|
||||
return fmt.Errorf("no crypto implementation available for %q: %s", algo, err)
|
||||
}
|
||||
|
||||
func (v *verifier) macVerify(m macer, pKey crypto.PublicKey, opts SignatureOption) error {
|
||||
key, ok := pKey.([]byte)
|
||||
if !ok {
|
||||
return fmt.Errorf("public key for MAC verifying must be of type []byte")
|
||||
}
|
||||
signature, err := v.sigStringFn(v.header, v.headers, v.created, v.expires, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actualMAC, err := base64.StdEncoding.DecodeString(v.signature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err = m.Equal([]byte(signature), actualMAC, key)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
return fmt.Errorf("invalid http signature")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *verifier) asymmVerify(s signer, pKey crypto.PublicKey, opts SignatureOption) error {
|
||||
toHash, err := v.sigStringFn(v.header, v.headers, v.created, v.expires, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signature, err := base64.StdEncoding.DecodeString(v.signature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.Verify(pKey, []byte(toHash), signature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSignatureScheme(h http.Header) (scheme SignatureScheme, val string, err error) {
|
||||
s := h.Get(string(Signature))
|
||||
sigHasAll := strings.Contains(s, keyIdParameter) ||
|
||||
strings.Contains(s, headersParameter) ||
|
||||
strings.Contains(s, signatureParameter)
|
||||
a := h.Get(string(Authorization))
|
||||
authHasAll := strings.Contains(a, keyIdParameter) ||
|
||||
strings.Contains(a, headersParameter) ||
|
||||
strings.Contains(a, signatureParameter)
|
||||
if sigHasAll && authHasAll {
|
||||
err = fmt.Errorf("both %q and %q have signature parameters", Signature, Authorization)
|
||||
return
|
||||
} else if !sigHasAll && !authHasAll {
|
||||
err = fmt.Errorf("neither %q nor %q have signature parameters", Signature, Authorization)
|
||||
return
|
||||
} else if sigHasAll {
|
||||
val = s
|
||||
scheme = Signature
|
||||
return
|
||||
} else { // authHasAll
|
||||
val = a
|
||||
scheme = Authorization
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getSignatureComponents(scheme SignatureScheme, s string) (kId, sig string, headers []string, created int64, expires int64, err error) {
|
||||
if as := scheme.authScheme(); len(as) > 0 {
|
||||
s = strings.TrimPrefix(s, as+prefixSeparater)
|
||||
}
|
||||
params := strings.Split(s, parameterSeparater)
|
||||
for _, p := range params {
|
||||
kv := strings.SplitN(p, parameterKVSeparater, 2)
|
||||
if len(kv) != 2 {
|
||||
err = fmt.Errorf("malformed http signature parameter: %v", kv)
|
||||
return
|
||||
}
|
||||
k := kv[0]
|
||||
v := strings.Trim(kv[1], parameterValueDelimiter)
|
||||
switch k {
|
||||
case keyIdParameter:
|
||||
kId = v
|
||||
case createdKey:
|
||||
created, err = strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case expiresKey:
|
||||
expires, err = strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case algorithmParameter:
|
||||
// Deprecated, ignore
|
||||
case headersParameter:
|
||||
headers = strings.Split(v, headerParameterValueDelim)
|
||||
case signatureParameter:
|
||||
sig = v
|
||||
default:
|
||||
// Ignore unrecognized parameters
|
||||
}
|
||||
}
|
||||
if len(kId) == 0 {
|
||||
err = fmt.Errorf("missing %q parameter in http signature", keyIdParameter)
|
||||
} else if len(sig) == 0 {
|
||||
err = fmt.Errorf("missing %q parameter in http signature", signatureParameter)
|
||||
} else if len(headers) == 0 { // Optional
|
||||
headers = defaultHeaders
|
||||
}
|
||||
return
|
||||
}
|
|
@ -676,6 +676,9 @@ github.com/superseriousbusiness/go-jpeg-image-structure/v2
|
|||
# github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB
|
||||
## explicit; go 1.12
|
||||
github.com/superseriousbusiness/go-png-image-structure/v2
|
||||
# github.com/superseriousbusiness/httpsig v1.2.0-SSB
|
||||
## explicit; go 1.21
|
||||
github.com/superseriousbusiness/httpsig
|
||||
# github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8
|
||||
## explicit; go 1.13
|
||||
github.com/superseriousbusiness/oauth2/v4
|
||||
|
|
Loading…
Reference in New Issue