diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2c0c604 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: + - 1.15 diff --git a/README.md b/README.md index 3b83854..3d4cb06 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # httpsig +> HTTP Signatures made simple + +[![Build Status][Build-Status-Image]][Build-Status-Url] [![Go Reference][Go-Reference-Image]][Go-Reference-Url] +[![Go Report Card][Go-Report-Card-Image]][Go-Report-Card-Url] [![License][License-Image]][License-Url] +[![Chat][Chat-Image]][Chat-Url] [![OpenCollective][OpenCollective-Image]][OpenCollective-Url] + `go get github.com/go-fed/httpsig` Implementation of [HTTP Signatures](https://tools.ietf.org/html/draft-cavage-http-signatures). @@ -92,3 +98,16 @@ func verify(r *http.Request) error { `Verifier`s are not safe for concurrent use by goroutines, but since they are constructed on a per-request or per-response basis it should not be a common restriction. + +[Build-Status-Image]: https://travis-ci.org/go-fed/httpsig.svg?branch=master +[Build-Status-Url]: https://travis-ci.org/go-fed/httpsig +[Go-Reference-Image]: https://pkg.go.dev/badge/github.com/go-fed/httpsig +[Go-Reference-Url]: https://pkg.go.dev/github.com/go-fed/httpsig +[Go-Report-Card-Image]: https://goreportcard.com/badge/github.com/go-fed/httpsig +[Go-Report-Card-Url]: https://goreportcard.com/report/github.com/go-fed/httpsig +[License-Image]: https://img.shields.io/github/license/go-fed/httpsig?color=blue +[License-Url]: https://opensource.org/licenses/BSD-3-Clause +[Chat-Image]: https://img.shields.io/matrix/go-fed:feneas.org?server_fqdn=matrix.org +[Chat-Url]: https://matrix.to/#/!BLOSvIyKTDLIVjRKSc:feneas.org?via=feneas.org&via=matrix.org +[OpenCollective-Image]: https://img.shields.io/opencollective/backers/go-fed-activitypub-labs +[OpenCollective-Url]: https://opencollective.com/go-fed-activitypub-labs diff --git a/algorithms.go b/algorithms.go index dfde090..9595941 100644 --- a/algorithms.go +++ b/algorithms.go @@ -2,25 +2,35 @@ 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" - "hash" - "io" - "strings" + "golang.org/x/crypto/ssh" ) const ( hmacPrefix = "hmac" rsaPrefix = "rsa" + sshPrefix = "ssh" + ecdsaPrefix = "ecdsa" + ed25519Prefix = "ed25519" md4String = "md4" md5String = "md5" sha1String = "sha1" @@ -58,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 }}, @@ -107,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 } @@ -167,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 { @@ -183,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 } @@ -210,6 +229,115 @@ 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 { @@ -312,17 +440,51 @@ func newAlgorithm(algo string, key []byte) (hash.Hash, crypto.Hash, error) { return h, c, err } +func signerFromSSHSigner(sshSigner ssh.Signer, s string) (signer, error) { + switch { + case strings.HasPrefix(s, rsaPrefix): + return &rsaAlgorithm{ + sshSigner: sshSigner, + }, nil + case strings.HasPrefix(s, ed25519Prefix): + return &ed25519Algorithm{ + sshSigner: sshSigner, + }, nil + default: + return nil, fmt.Errorf("no signer matching %q", s) + } +} + // signerFromString is an internally public method constructor func signerFromString(s string) (signer, error) { s = strings.ToLower(s) - if !strings.HasPrefix(s, rsaPrefix) { + 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) } - algo := strings.TrimPrefix(s, rsaPrefix+"-") 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, diff --git a/algorithms_test.go b/algorithms_test.go index 84167c0..5c3a3a3 100644 --- a/algorithms_test.go +++ b/algorithms_test.go @@ -44,10 +44,11 @@ func TestIsAvailable(t *testing.T) { expectError: true, }, { + // TODO: Disable once https://github.com/golang/go/issues/37278 is fixed name: sha1String, algo: sha1String, - expected: false, - expectError: true, + expected: true, + expectError: false, }, { name: sha224String, diff --git a/digest.go b/digest.go index b5c6242..bf9e3a9 100644 --- a/digest.go +++ b/digest.go @@ -61,7 +61,8 @@ func addDigest(r *http.Request, algo DigestAlgorithm, b []byte) (err error) { if err != nil { return } - sum := h.Sum(b) + h.Write(b) + sum := h.Sum(nil) r.Header.Add(digestHeader, fmt.Sprintf("%s%s%s", a, @@ -82,7 +83,8 @@ func addDigestResponse(r http.ResponseWriter, algo DigestAlgorithm, b []byte) (e if err != nil { return } - sum := h.Sum(b) + h.Write(b) + sum := h.Sum(nil) r.Header().Add(digestHeader, fmt.Sprintf("%s%s%s", a, @@ -107,7 +109,8 @@ func verifyDigest(r *http.Request, body *bytes.Buffer) (err error) { if err != nil { return } - sum := h.Sum(body.Bytes()) + 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") diff --git a/digest_test.go b/digest_test.go index 0d5d2fb..4ee4fa0 100644 --- a/digest_test.go +++ b/digest_test.go @@ -23,7 +23,7 @@ func TestAddDigest(t *testing.T) { }, algo: "SHA-256", body: []byte("johnny grab your gun"), - expectedDigest: "SHA-256=am9obm55IGdyYWIgeW91ciBndW7jsMRCmPwcFJr79MiZb7kkJ65B5GSbk0yklZkbeFK4VQ==", + expectedDigest: "SHA-256=RYiuVuVdRpU+BWcNUUg3sf0EbJjQ9LDj9tUqR546hhk=", }, { name: "adds sha512 digest", @@ -33,7 +33,7 @@ func TestAddDigest(t *testing.T) { }, algo: "SHA-512", body: []byte("yours is the drill that will pierce the heavens"), - expectedDigest: "SHA-512=eW91cnMgaXMgdGhlIGRyaWxsIHRoYXQgd2lsbCBwaWVyY2UgdGhlIGhlYXZlbnPPg+E1fu+4vfFUKFDWbYAH1iDkBQtXFdyD9Kkh02zpzkfQ0TxdhfKw/4MY0od+7C9juTG9R0F6gaU4Mnr5J9o+", + expectedDigest: "SHA-512=bM0eBRnZkuiOTsejYNb/UpvFozde+Do1ZqlXfRTS39aGmoEzoXBpjmIIuznPslc3kaprUtI/VXH8/5HsD+thGg==", }, { name: "digest already set", @@ -90,7 +90,7 @@ func TestVerifyDigest(t *testing.T) { name: "verify sha256", r: func() *http.Request { r, _ := http.NewRequest("POST", "example.com", nil) - r.Header.Set("Digest", "SHA-256=am9obm55IGdyYWIgeW91ciBndW7jsMRCmPwcFJr79MiZb7kkJ65B5GSbk0yklZkbeFK4VQ==") + r.Header.Set("Digest", "SHA-256=RYiuVuVdRpU+BWcNUUg3sf0EbJjQ9LDj9tUqR546hhk=") return r }, body: []byte("johnny grab your gun"), @@ -99,7 +99,7 @@ func TestVerifyDigest(t *testing.T) { name: "verify sha512", r: func() *http.Request { r, _ := http.NewRequest("POST", "example.com", nil) - r.Header.Set("Digest", "SHA-512=eW91cnMgaXMgdGhlIGRyaWxsIHRoYXQgd2lsbCBwaWVyY2UgdGhlIGhlYXZlbnPPg+E1fu+4vfFUKFDWbYAH1iDkBQtXFdyD9Kkh02zpzkfQ0TxdhfKw/4MY0od+7C9juTG9R0F6gaU4Mnr5J9o+") + r.Header.Set("Digest", "SHA-512=bM0eBRnZkuiOTsejYNb/UpvFozde+Do1ZqlXfRTS39aGmoEzoXBpjmIIuznPslc3kaprUtI/VXH8/5HsD+thGg==") return r }, body: []byte("yours is the drill that will pierce the heavens"), diff --git a/go.mod b/go.mod index 2dbb7f7..a407692 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,5 @@ module github.com/go-fed/httpsig -require ( - golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 - golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43 -) +require golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + +go 1.13 diff --git a/go.modverify b/go.modverify deleted file mode 100644 index c13a726..0000000 --- a/go.modverify +++ /dev/null @@ -1,2 +0,0 @@ -golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo= -golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43 h1:PvnWIWTbA7gsEBkKjt0HV9hckYfcqYv8s/ju7ArZ0do= diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8b7d318 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +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= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/httpsig.go b/httpsig.go index de9868e..17310c8 100644 --- a/httpsig.go +++ b/httpsig.go @@ -11,6 +11,10 @@ import ( "crypto" "fmt" "net/http" + "strings" + "time" + + "golang.org/x/crypto/ssh" ) // Algorithm specifies a cryptography secure algorithm for signing HTTP requests @@ -39,12 +43,23 @@ 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 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 @@ -147,18 +162,84 @@ type Signer interface { // // 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) (Signer, Algorithm, error) { +func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, Algorithm, error) { for _, pref := range prefs { - s, err := newSigner(pref, dAlgo, headers, scheme) + s, err := newSigner(pref, dAlgo, headers, scheme, expiresIn) if err != nil { continue } return s, pref, err } - s, err := newSigner(defaultAlgorithm, dAlgo, headers, scheme) + 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 @@ -186,25 +267,70 @@ type Verifier interface { Verify(pKey crypto.PublicKey, algo Algorithm) 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) (Verifier, error) { - return newVerifier(r.Header, func(h http.Header, toInclude []string) (string, error) { - return signatureString(h, toInclude, addRequestTarget(r)) + 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) (string, error) { + return signatureString(h, toInclude, addRequestTarget(r), 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) (string, error) { - return signatureString(h, toInclude, requestTargetNotPermitted) + return newVerifier(r.Header, func(h http.Header, toInclude []string, created int64, expires int64) (string, error) { + return signatureString(h, toInclude, requestTargetNotPermitted, created, expires) }) } -func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme) (Signer, error) { +func newSSHSigner(sshSigner ssh.Signer, algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SSHSigner, error) { + var expires, created int64 = 0, 0 + + if expiresIn != 0 { + created = time.Now().Unix() + expires = created + expiresIn + } + + s, err := signerFromSSHSigner(sshSigner, string(algo)) + if err != nil { + return nil, fmt.Errorf("no crypto implementation available for ssh algo %q: %s", algo, err) + } + + a := &asymmSSHSigner{ + asymmSigner: &asymmSigner{ + s: s, + dAlgo: dAlgo, + headers: headers, + targetHeader: scheme, + prefix: scheme.authScheme(), + created: created, + expires: expires, + }, + } + + return a, nil +} + +func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, error) { + + var expires, created int64 = 0, 0 + if expiresIn != 0 { + created = time.Now().Unix() + expires = created + expiresIn + } + s, err := signerFromString(string(algo)) if err == nil { a := &asymmSigner{ @@ -213,12 +339,14 @@ func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme S 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", algo) + return nil, fmt.Errorf("no crypto implementation available for %q: %s", algo, err) } c := &macSigner{ m: m, @@ -226,6 +354,8 @@ func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme S headers: headers, targetHeader: scheme, prefix: scheme.authScheme(), + created: created, + expires: expires, } return c, nil } diff --git a/httpsig_test.go b/httpsig_test.go index f4fe39e..dca48e7 100644 --- a/httpsig_test.go +++ b/httpsig_test.go @@ -7,6 +7,7 @@ import ( "crypto/rsa" "crypto/sha256" "crypto/x509" + "encoding/asn1" "encoding/base64" "encoding/pem" "fmt" @@ -16,6 +17,8 @@ import ( "strconv" "strings" "testing" + + "golang.org/x/crypto/ed25519" ) const ( @@ -36,18 +39,36 @@ type httpsigTest struct { privKey crypto.PrivateKey pubKey crypto.PublicKey pubKeyId string + expectedSignatureAlgorithm string expectedAlgorithm Algorithm expectErrorSigningResponse bool expectRequestPath bool expectedDigest string } +type ed25519PrivKey struct { + Version int + ObjectIdentifier struct { + ObjectIdentifier asn1.ObjectIdentifier + } + PrivateKey []byte +} + +type ed25519PubKey struct { + OBjectIdentifier struct { + ObjectIdentifier asn1.ObjectIdentifier + } + PublicKey asn1.BitString +} + var ( privKey *rsa.PrivateKey macKey []byte tests []httpsigTest testSpecRSAPrivateKey *rsa.PrivateKey testSpecRSAPublicKey *rsa.PublicKey + testEd25519PrivateKey ed25519.PrivateKey + testEd25519PublicKey ed25519.PublicKey ) func init() { @@ -56,6 +77,10 @@ func init() { if err != nil { panic(err) } + pubEd25519Key, privEd25519Key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + panic(err) + } macKey = make([]byte, 128) err = readFullFromCrypto(macKey) if err != nil { @@ -63,105 +88,152 @@ func init() { } tests = []httpsigTest{ { - name: "rsa signature", - prefs: []Algorithm{RSA_SHA512}, - digestAlg: DigestSha256, - headers: []string{"Date", "Digest"}, - scheme: Signature, - privKey: privKey, - pubKey: privKey.Public(), - pubKeyId: "pubKeyId", - expectedAlgorithm: RSA_SHA512, + name: "rsa signature", + prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + expectedSignatureAlgorithm: "hs2019", }, { - name: "digest on rsa signature", - prefs: []Algorithm{RSA_SHA512}, - digestAlg: DigestSha256, - headers: []string{"Date", "Digest"}, - body: []byte("Last night as I lay dreaming This strangest kind of feeling Revealed its secret meaning And now I know..."), - scheme: Signature, - privKey: privKey, - pubKey: privKey.Public(), - pubKeyId: "pubKeyId", - expectedAlgorithm: RSA_SHA512, - expectedDigest: "SHA-256=TGFzdCBuaWdodCBhcyBJIGxheSBkcmVhbWluZyBUaGlzIHN0cmFuZ2VzdCBraW5kIG9mIGZlZWxpbmcgUmV2ZWFsZWQgaXRzIHNlY3JldCBtZWFuaW5nIEFuZCBub3cgSSBrbm93Li4u47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + name: "ed25519 signature", + prefs: []Algorithm{ED25519}, + digestAlg: DigestSha512, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privEd25519Key, + pubKey: pubEd25519Key, + pubKeyId: "pubKeyId", + expectedAlgorithm: ED25519, + expectedSignatureAlgorithm: "hs2019", }, { - name: "hmac signature", - prefs: []Algorithm{HMAC_SHA256}, - digestAlg: DigestSha256, - headers: []string{"Date", "Digest"}, - scheme: Signature, - privKey: macKey, - pubKey: macKey, - pubKeyId: "pubKeyId", - expectedAlgorithm: HMAC_SHA256, + name: "digest on rsa signature", + prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + body: []byte("Last night as I lay dreaming This strangest kind of feeling Revealed its secret meaning And now I know..."), + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + expectedSignatureAlgorithm: "hs2019", + expectedDigest: "SHA-256=07PJQngqg8+BlomdI6zM7ieOxhINWI+iivJxBDSm3Dg=", }, { - name: "digest on hmac signature", - prefs: []Algorithm{HMAC_SHA256}, - digestAlg: DigestSha256, - headers: []string{"Date", "Digest"}, - body: []byte("I've never ever been to paradise I've never ever seen no angel's eyes You'll never ever let this magic die No matter where you are, you are my lucky star."), - scheme: Signature, - privKey: macKey, - pubKey: macKey, - pubKeyId: "pubKeyId", - expectedAlgorithm: HMAC_SHA256, - expectedDigest: "SHA-256=SSd2ZSBuZXZlciBldmVyIGJlZW4gdG8gcGFyYWRpc2UgSSd2ZSBuZXZlciBldmVyIHNlZW4gbm8gYW5nZWwncyBleWVzIFlvdSdsbCBuZXZlciBldmVyIGxldCB0aGlzIG1hZ2ljIGRpZSBObyBtYXR0ZXIgd2hlcmUgeW91IGFyZSwgeW91IGFyZSBteSBsdWNreSBzdGFyLuOwxEKY/BwUmvv0yJlvuSQnrkHkZJuTTKSVmRt4UrhV", + name: "digest on ed25519 signature", + prefs: []Algorithm{ED25519}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + body: []byte("Last night as I lay dreaming This strangest kind of feeling Revealed its secret meaning And now I know..."), + scheme: Signature, + privKey: privEd25519Key, + pubKey: pubEd25519Key, + pubKeyId: "pubKeyId", + expectedAlgorithm: ED25519, + expectedSignatureAlgorithm: "hs2019", + expectedDigest: "SHA-256=07PJQngqg8+BlomdI6zM7ieOxhINWI+iivJxBDSm3Dg=", }, { - name: "rsa authorization", - prefs: []Algorithm{RSA_SHA512}, - digestAlg: DigestSha256, - headers: []string{"Date", "Digest"}, - scheme: Authorization, - privKey: privKey, - pubKey: privKey.Public(), - pubKeyId: "pubKeyId", - expectedAlgorithm: RSA_SHA512, + name: "hmac signature", + prefs: []Algorithm{HMAC_SHA256}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: macKey, + pubKey: macKey, + pubKeyId: "pubKeyId", + expectedAlgorithm: HMAC_SHA256, + expectedSignatureAlgorithm: "hs2019", }, { - name: "hmac authorization", - prefs: []Algorithm{HMAC_SHA256}, - digestAlg: DigestSha256, - headers: []string{"Date", "Digest"}, - scheme: Authorization, - privKey: macKey, - pubKey: macKey, - pubKeyId: "pubKeyId", - expectedAlgorithm: HMAC_SHA256, + name: "digest on hmac signature", + prefs: []Algorithm{HMAC_SHA256}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + body: []byte("I've never ever been to paradise I've never ever seen no angel's eyes You'll never ever let this magic die No matter where you are, you are my lucky star."), + scheme: Signature, + privKey: macKey, + pubKey: macKey, + pubKeyId: "pubKeyId", + expectedAlgorithm: HMAC_SHA256, + expectedSignatureAlgorithm: "hs2019", + expectedDigest: "SHA-256=d0JoDjbDZRZF7/gUdgrazZCdKCJ9z9uUcMd6n1YKWRU=", + }, + { + name: "rsa authorization", + prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Authorization, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + expectedSignatureAlgorithm: "hs2019", }, { - name: "default algo", - digestAlg: DigestSha256, - headers: []string{"Date", "Digest"}, - scheme: Signature, - privKey: privKey, - pubKey: privKey.Public(), - pubKeyId: "pubKeyId", - expectedAlgorithm: RSA_SHA256, + name: "ed25519 authorization", + prefs: []Algorithm{ED25519}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Authorization, + privKey: privEd25519Key, + pubKey: pubEd25519Key, + pubKeyId: "pubKeyId", + expectedAlgorithm: ED25519, + expectedSignatureAlgorithm: "hs2019", }, { - name: "default headers", - prefs: []Algorithm{RSA_SHA512}, - digestAlg: DigestSha256, - scheme: Signature, - privKey: privKey, - pubKey: privKey.Public(), - pubKeyId: "pubKeyId", - expectedAlgorithm: RSA_SHA512, + name: "hmac authorization", + prefs: []Algorithm{HMAC_SHA256}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Authorization, + privKey: macKey, + pubKey: macKey, + pubKeyId: "pubKeyId", + expectedAlgorithm: HMAC_SHA256, + expectedSignatureAlgorithm: "hs2019", }, { - name: "different pub key id", - prefs: []Algorithm{RSA_SHA512}, - digestAlg: DigestSha256, - headers: []string{"Date", "Digest"}, - scheme: Signature, - privKey: privKey, - pubKey: privKey.Public(), - pubKeyId: "i write code that sucks", - expectedAlgorithm: RSA_SHA512, + name: "default algo", + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA256, + expectedSignatureAlgorithm: "hs2019", + }, + { + name: "default headers", + prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + expectedSignatureAlgorithm: "hs2019", + }, + { + name: "different pub key id", + prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "i write code that sucks", + expectedAlgorithm: RSA_SHA512, + expectedSignatureAlgorithm: "hs2019", }, { name: "with request target", @@ -173,6 +245,7 @@ func init() { pubKey: privKey.Public(), pubKeyId: "pubKeyId", expectedAlgorithm: RSA_SHA512, + expectedSignatureAlgorithm: "hs2019", expectErrorSigningResponse: true, expectRequestPath: true, }, @@ -187,6 +260,16 @@ func init() { if err != nil { panic(err) } + + testEd25519PrivateKey, err = loadEd25519PrivateKey([]byte(testEd25519PrivateKeyPEM)) + if err != nil { + panic(err) + } + + testEd25519PublicKey, err = loadEd25519PublicKey([]byte(testEd25519PublicKeyPEM)) + if err != nil { + panic(err) + } } func toSignatureParameter(k, v string) string { @@ -205,7 +288,7 @@ func toHeaderSignatureParameters(k string, vals []string) string { func TestSignerRequest(t *testing.T) { testFn := func(t *testing.T, test httpsigTest) { - s, a, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) + s, a, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme, 0) if err != nil { t.Fatalf("%s", err) } @@ -234,7 +317,7 @@ func TestSignerRequest(t *testing.T) { } if p := toSignatureParameter(keyIdParameter, test.pubKeyId); !strings.Contains(vals[0], p) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) - } else if p := toSignatureParameter(algorithmParameter, string(test.expectedAlgorithm)); !strings.Contains(vals[0], p) { + } else if p := toSignatureParameter(algorithmParameter, string(test.expectedSignatureAlgorithm)); !strings.Contains(vals[0], p) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) } else if p := toHeaderSignatureParameters(headersParameter, test.headers); !strings.Contains(vals[0], p) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) @@ -259,7 +342,7 @@ func TestSignerRequest(t *testing.T) { func TestSignerResponse(t *testing.T) { testFn := func(t *testing.T, test httpsigTest) { - s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) + s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme, 0) // Test response signing resp := httptest.NewRecorder() resp.HeaderMap.Set("Date", testDate) @@ -284,7 +367,7 @@ func TestSignerResponse(t *testing.T) { } if p := toSignatureParameter(keyIdParameter, test.pubKeyId); !strings.Contains(vals[0], p) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) - } else if p := toSignatureParameter(algorithmParameter, string(test.expectedAlgorithm)); !strings.Contains(vals[0], p) { + } else if p := toSignatureParameter(algorithmParameter, string(test.expectedSignatureAlgorithm)); !strings.Contains(vals[0], p) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) } else if p := toHeaderSignatureParameters(headersParameter, test.headers); !strings.Contains(vals[0], p) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) @@ -309,30 +392,32 @@ func TestSignerResponse(t *testing.T) { func TestNewSignerRequestMissingHeaders(t *testing.T) { failingTests := []struct { - name string - prefs []Algorithm - digestAlg DigestAlgorithm - headers []string - scheme SignatureScheme - privKey crypto.PrivateKey - pubKeyId string - expectedAlgorithm Algorithm + name string + prefs []Algorithm + digestAlg DigestAlgorithm + headers []string + scheme SignatureScheme + privKey crypto.PrivateKey + pubKeyId string + expectedAlgorithm Algorithm + expectedSignatureAlgorithm string }{ { - name: "wants digest", - prefs: []Algorithm{RSA_SHA512}, - digestAlg: DigestSha256, - headers: []string{"Date", "Digest"}, - scheme: Signature, - privKey: privKey, - pubKeyId: "pubKeyId", - expectedAlgorithm: RSA_SHA512, + name: "wants digest", + prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privKey, + pubKeyId: "pubKeyId", + expectedSignatureAlgorithm: "hs2019", + expectedAlgorithm: RSA_SHA512, }, } for _, test := range failingTests { t.Run(test.name, func(t *testing.T) { test := test - s, a, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) + s, a, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme, 0) if err != nil { t.Fatalf("%s", err) } @@ -363,22 +448,24 @@ func TestNewSignerResponseMissingHeaders(t *testing.T) { pubKeyId string expectedAlgorithm Algorithm expectErrorSigningResponse bool + expectedSignatureAlgorithm string }{ { - name: "want digest", - prefs: []Algorithm{RSA_SHA512}, - digestAlg: DigestSha256, - headers: []string{"Date", "Digest"}, - scheme: Signature, - privKey: privKey, - pubKeyId: "pubKeyId", - expectedAlgorithm: RSA_SHA512, + name: "want digest", + prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privKey, + pubKeyId: "pubKeyId", + expectedSignatureAlgorithm: "hs2019", + expectedAlgorithm: RSA_SHA512, }, } for _, test := range failingTests { t.Run(test.name, func(t *testing.T) { test := test - s, a, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) + s, a, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme, 0) if err != nil { t.Fatalf("%s", err) } @@ -409,7 +496,7 @@ func TestNewVerifier(t *testing.T) { if test.body == nil { req.Header.Set("Digest", testDigest) } - s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) + s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme, 0) if err != nil { t.Fatalf("%s", err) } @@ -446,7 +533,7 @@ func TestNewResponseVerifier(t *testing.T) { if test.body == nil { resp.HeaderMap.Set("Digest", testDigest) } - s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) + s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme, 0) if err != nil { t.Fatalf("%s", err) } @@ -490,17 +577,17 @@ func Test_Signing_HTTP_Messages_AppendixC(t *testing.T) { // NOTE: In verification, if there are no headers listed, the // default headers (date) are indeed used as required by the // specification. - expectedSignature: `Authorization: Signature keyId="Test",algorithm="rsa-sha256",headers="date",signature="SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM="`, + expectedSignature: `Authorization: Signature keyId="Test",algorithm="hs2019",headers="date",signature="SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM="`, }, { name: "C.2. Basic Test", headers: []string{"(request-target)", "host", "date"}, - expectedSignature: `Authorization: Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="`, + expectedSignature: `Authorization: Signature keyId="Test",algorithm="hs2019",headers="(request-target) host date",signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="`, }, { name: "C.3. All Headers Test", headers: []string{"(request-target)", "host", "date", "content-type", "digest", "content-length"}, - expectedSignature: `Authorization: Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="`, + expectedSignature: `Authorization: Signature keyId="Test",algorithm="hs2019",headers="(request-target) host date content-type digest content-length",signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="`, }, } @@ -518,7 +605,7 @@ func Test_Signing_HTTP_Messages_AppendixC(t *testing.T) { r.Header["Content-Type"] = []string{"application/json"} setDigest(r) - s, _, err := NewSigner([]Algorithm{RSA_SHA256}, DigestSha256, test.headers, Authorization) + s, _, err := NewSigner([]Algorithm{RSA_SHA256}, DigestSha256, test.headers, Authorization, 0) if err != nil { t.Fatalf("error creating signer: %s", err) } @@ -536,6 +623,69 @@ func Test_Signing_HTTP_Messages_AppendixC(t *testing.T) { } } +func TestSigningEd25519(t *testing.T) { + specTests := []struct { + name string + headers []string + expectedSignature string + }{ + { + name: "Default Test", + headers: []string{}, + // NOTE: In the Appendix C tests, the following is NOT included: + // `headers="date"` + // But httpsig will ALWAYS explicitly list the headers used in its + // signature. Hence, I have introduced it here. + // + // NOTE: In verification, if there are no headers listed, the + // default headers (date) are indeed used as required by the + // specification. + expectedSignature: `Authorization: Signature keyId="Test",algorithm="hs2019",headers="date",signature="6G9bNnUfph4pnl3j8l4UTcSPJVg6r4tM73eWFAn+w4IdIi8yzzZs65QlgM31lAuVCRKlqMzME9VGgMt16nU1AQ=="`, + }, + { + name: "Basic Test", + headers: []string{"(request-target)", "host", "date"}, + expectedSignature: `Authorization: Signature keyId="Test",algorithm="hs2019",headers="(request-target) host date",signature="upsoNpw5oJTD3lTIQHEnDGWTaKmlT7o2c9Lz3kqy2UTwOEpEop3Sd7F/K2bYD2lQ4AH1HRyvC4/9AcKgNBg1AA=="`, + }, + { + name: "All Headers Test", + headers: []string{"(request-target)", "host", "date", "content-type", "digest", "content-length"}, + expectedSignature: `Authorization: Signature keyId="Test",algorithm="hs2019",headers="(request-target) host date content-type digest content-length",signature="UkxhZl0W5/xcuCIP5xOPv4V6rX0TmaV2lmrYYGWauKhdFHihpW80tCqTNFDhyD+nYeGNCRSFRHmDS0bGm0PVAg=="`, + }, + } + + for _, test := range specTests { + t.Run(test.name, func(t *testing.T) { + test := test + r, err := http.NewRequest("POST", "http://example.com/foo?param=value&pet=dog", bytes.NewBuffer([]byte(testSpecBody))) + if err != nil { + t.Fatalf("error creating request: %s", err) + } + + r.Header["Date"] = []string{testSpecDate} + r.Header["Host"] = []string{r.URL.Host} + r.Header["Content-Length"] = []string{strconv.Itoa(len(testSpecBody))} + r.Header["Content-Type"] = []string{"application/json"} + setDigest(r) + + s, _, err := NewSigner([]Algorithm{ED25519}, DigestSha256, test.headers, Authorization, 0) + if err != nil { + t.Fatalf("error creating signer: %s", err) + } + + if err := s.SignRequest(testEd25519PrivateKey, "Test", r, nil); err != nil { + t.Fatalf("error signing request: %s", err) + } + + expectedAuth := test.expectedSignature + gotAuth := fmt.Sprintf("Authorization: %s", r.Header["Authorization"][0]) + if gotAuth != expectedAuth { + t.Errorf("Signature string mismatch\nGot: %s\nWant: %s", gotAuth, expectedAuth) + } + }) + } +} + // Test_Verifying_HTTP_Messages_AppendixC implement tests from Appendix C // in the http signatures specification: // https://tools.ietf.org/html/draft-cavage-http-signatures-10#appendix-C @@ -592,6 +742,59 @@ func Test_Verifying_HTTP_Messages_AppendixC(t *testing.T) { } } +func TestVerifyingEd25519(t *testing.T) { + specTests := []struct { + name string + headers []string + signature string + }{ + { + name: "Default Test", + headers: []string{}, + signature: `Signature keyId="Test",algorithm="hs2019",headers="date",signature="6G9bNnUfph4pnl3j8l4UTcSPJVg6r4tM73eWFAn+w4IdIi8yzzZs65QlgM31lAuVCRKlqMzME9VGgMt16nU1AQ=="`, + }, + { + name: "Basic Test", + headers: []string{"(request-target)", "host", "date"}, + signature: `Signature keyId="Test",algorithm="hs2019",headers="(request-target) host date",signature="upsoNpw5oJTD3lTIQHEnDGWTaKmlT7o2c9Lz3kqy2UTwOEpEop3Sd7F/K2bYD2lQ4AH1HRyvC4/9AcKgNBg1AA=="`, + }, + { + name: "All Headers Test", + headers: []string{"(request-target)", "host", "date", "content-type", "digest", "content-length"}, + signature: `Signature keyId="Test",algorithm="hs2019",headers="(request-target) host date content-type digest content-length",signature="UkxhZl0W5/xcuCIP5xOPv4V6rX0TmaV2lmrYYGWauKhdFHihpW80tCqTNFDhyD+nYeGNCRSFRHmDS0bGm0PVAg=="`, + }, + } + + for _, test := range specTests { + t.Run(test.name, func(t *testing.T) { + test := test + r, err := http.NewRequest("POST", "http://example.com/foo?param=value&pet=dog", bytes.NewBuffer([]byte(testSpecBody))) + if err != nil { + t.Fatalf("error creating request: %s", err) + } + + r.Header["Date"] = []string{testSpecDate} + r.Header["Host"] = []string{r.URL.Host} + r.Header["Content-Length"] = []string{strconv.Itoa(len(testSpecBody))} + r.Header["Content-Type"] = []string{"application/json"} + setDigest(r) + r.Header["Authorization"] = []string{test.signature} + + v, err := NewVerifier(r) + if err != nil { + t.Fatalf("error creating verifier: %s", err) + } + + if "Test" != v.KeyId() { + t.Errorf("KeyId mismatch\nGot: %s\nWant: Test", v.KeyId()) + } + if err := v.Verify(testEd25519PublicKey, ED25519); err != nil { + t.Errorf("Verification failure: %s", err) + } + }) + } +} + func loadPrivateKey(keyData []byte) (*rsa.PrivateKey, error) { pem, _ := pem.Decode(keyData) if pem.Type != "RSA PRIVATE KEY" { @@ -601,6 +804,19 @@ func loadPrivateKey(keyData []byte) (*rsa.PrivateKey, error) { return x509.ParsePKCS1PrivateKey(pem.Bytes) } +// taken from https://blainsmith.com/articles/signing-jwts-with-gos-crypto-ed25519/ +func loadEd25519PrivateKey(keyData []byte) (ed25519.PrivateKey, error) { + var block *pem.Block + block, _ = pem.Decode(keyData) + + var asn1PrivKey ed25519PrivKey + asn1.Unmarshal(block.Bytes, &asn1PrivKey) + + // [2:] is skipping the byte for TAG and the byte for LEN + // see also https://tools.ietf.org/html/draft-ietf-curdle-pkix-10#section-10.3 + return ed25519.NewKeyFromSeed(asn1PrivKey.PrivateKey[2:]), nil +} + func loadPublicKey(keyData []byte) (*rsa.PublicKey, error) { pem, _ := pem.Decode(keyData) if pem.Type != "PUBLIC KEY" { @@ -615,6 +831,17 @@ func loadPublicKey(keyData []byte) (*rsa.PublicKey, error) { return key.(*rsa.PublicKey), nil } +// taken from https://blainsmith.com/articles/signing-jwts-with-gos-crypto-ed25519/ +func loadEd25519PublicKey(keyData []byte) (ed25519.PublicKey, error) { + var block *pem.Block + block, _ = pem.Decode(keyData) + + var asn1PubKey ed25519PubKey + asn1.Unmarshal(block.Bytes, &asn1PubKey) + + return ed25519.PublicKey(asn1PubKey.PublicKey.Bytes), nil +} + func setDigest(r *http.Request) ([]byte, error) { var bodyBytes []byte if _, ok := r.Header["Digest"]; !ok { @@ -664,3 +891,11 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw oYi+1hqp1fIekaxsyQIDAQAB -----END PUBLIC KEY-----` + +const testEd25519PrivateKeyPEM = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIAP+PK4NtdzCe04sbtwBvf9IShlky298SMMBqkCCToHn +-----END PRIVATE KEY-----` + +const testEd25519PublicKeyPEM = `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAhyP+7zpNCsr7/ipGJjK0zVszTEQ5tooyX3VLAnBSc1c= +-----END PUBLIC KEY-----` diff --git a/signing.go b/signing.go index f3c9421..e18db41 100644 --- a/signing.go +++ b/signing.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "net/textproto" + "strconv" "strings" ) @@ -25,6 +26,8 @@ const ( // 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 @@ -45,6 +48,8 @@ type macSigner struct { 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 { @@ -62,7 +67,7 @@ func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http if err != nil { return err } - setSignatureHeader(r.Header, string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers) + setSignatureHeader(r.Header, string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers, m.created, m.expires) return nil } @@ -81,7 +86,7 @@ func (m *macSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http if err != nil { return err } - setSignatureHeader(r.Header(), string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers) + setSignatureHeader(r.Header(), string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers, m.created, m.expires) return nil } @@ -99,11 +104,11 @@ func (m *macSigner) signSignature(pKey crypto.PrivateKey, s string) (string, err } func (m *macSigner) signatureString(r *http.Request) (string, error) { - return signatureString(r.Header, m.headers, addRequestTarget(r)) + return signatureString(r.Header, m.headers, addRequestTarget(r), m.created, m.expires) } func (m *macSigner) signatureStringResponse(r http.ResponseWriter) (string, error) { - return signatureString(r.Header(), m.headers, requestTargetNotPermitted) + return signatureString(r.Header(), m.headers, requestTargetNotPermitted, m.created, m.expires) } var _ Signer = &asymmSigner{} @@ -115,6 +120,8 @@ type asymmSigner struct { 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 { @@ -132,7 +139,7 @@ func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *ht if err != nil { return err } - setSignatureHeader(r.Header, string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers) + setSignatureHeader(r.Header, string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers, a.created, a.expires) return nil } @@ -151,7 +158,7 @@ func (a *asymmSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r ht if err != nil { return err } - setSignatureHeader(r.Header(), string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers) + setSignatureHeader(r.Header(), string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers, a.created, a.expires) return nil } @@ -165,14 +172,28 @@ func (a *asymmSigner) signSignature(pKey crypto.PrivateKey, s string) (string, e } func (a *asymmSigner) signatureString(r *http.Request) (string, error) { - return signatureString(r.Header, a.headers, addRequestTarget(r)) + return signatureString(r.Header, a.headers, addRequestTarget(r), a.created, a.expires) } func (a *asymmSigner) signatureStringResponse(r http.ResponseWriter) (string, error) { - return signatureString(r.Header(), a.headers, requestTargetNotPermitted) + return signatureString(r.Header(), a.headers, requestTargetNotPermitted, a.created, a.expires) } -func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyId, algo, enc string, headers []string) { +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 } @@ -188,14 +209,41 @@ func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyId, algo, enc b.WriteString(pubKeyId) b.WriteString(parameterValueDelimiter) b.WriteString(parameterSeparater) - // Algorithm (deprecated) - // TODO: Remove this. + // Algorithm b.WriteString(algorithmParameter) b.WriteString(parameterKVSeparater) b.WriteString(parameterValueDelimiter) - b.WriteString(algo) + 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) @@ -238,7 +286,7 @@ func addRequestTarget(r *http.Request) func(b *bytes.Buffer) error { } } -func signatureString(values http.Header, include []string, requestTargetFn func(b *bytes.Buffer) error) (string, error) { +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 } @@ -250,6 +298,20 @@ func signatureString(values http.Header, include []string, requestTargetFn func( 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 { diff --git a/verifying.go b/verifying.go index a70d4f7..e39b9dc 100644 --- a/verifying.go +++ b/verifying.go @@ -3,9 +3,12 @@ package httpsig import ( "crypto" "encoding/base64" + "errors" "fmt" "net/http" + "strconv" "strings" + "time" ) var _ Verifier = &verifier{} @@ -14,16 +17,32 @@ type verifier struct { header http.Header kId string signature string + created int64 + expires int64 headers []string - sigStringFn func(http.Header, []string) (string, error) + sigStringFn func(http.Header, []string, int64, int64) (string, error) } -func newVerifier(h http.Header, sigStringFn func(http.Header, []string) (string, error)) (*verifier, error) { +func newVerifier(h http.Header, sigStringFn func(http.Header, []string, int64, int64) (string, error)) (*verifier, error) { scheme, s, err := getSignatureScheme(h) if err != nil { return nil, err } - kId, sig, headers, err := getSignatureComponents(scheme, s) + 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 } @@ -31,6 +50,8 @@ func newVerifier(h http.Header, sigStringFn func(http.Header, []string) (string, header: h, kId: kId, signature: sig, + created: created, + expires: expires, headers: headers, sigStringFn: sigStringFn, }, nil @@ -49,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 { @@ -57,7 +78,7 @@ func (v *verifier) macVerify(m macer, pKey crypto.PublicKey) error { if !ok { return fmt.Errorf("public key for MAC verifying must be of type []byte") } - signature, err := v.sigStringFn(v.header, v.headers) + signature, err := v.sigStringFn(v.header, v.headers, v.created, v.expires) if err != nil { return err } @@ -75,7 +96,7 @@ func (v *verifier) macVerify(m macer, pKey crypto.PublicKey) error { } func (v *verifier) asymmVerify(s signer, pKey crypto.PublicKey) error { - toHash, err := v.sigStringFn(v.header, v.headers) + toHash, err := v.sigStringFn(v.header, v.headers, v.created, v.expires) if err != nil { return err } @@ -116,7 +137,7 @@ func getSignatureScheme(h http.Header) (scheme SignatureScheme, val string, err } } -func getSignatureComponents(scheme SignatureScheme, s string) (kId, sig string, headers []string, err error) { +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) } @@ -132,6 +153,16 @@ func getSignatureComponents(scheme SignatureScheme, s string) (kId, sig string, 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: