1
0
Fork 0

Adding upstream version 1.2.3.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-18 13:02:54 +02:00
parent ed523a11b4
commit d4d4f346b4
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
13 changed files with 3867 additions and 0 deletions

3
.travis.yml Normal file
View file

@ -0,0 +1,3 @@
language: go
go:
- 1.15

29
LICENSE Normal file
View file

@ -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.

115
README.md Normal file
View file

@ -0,0 +1,115 @@
# httpsig
Forked from <https://github.com/go-fed/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/42wim/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/42wim/httpsig"`
### Signing
Signing a request or response requires creating a new `Signer` and using it:
```
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:
```
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`:
```
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.
[Build-Status-Image]: https://travis-ci.org/42wim/httpsig.svg?branch=master
[Build-Status-Url]: https://travis-ci.org/42wim/httpsig
[Go-Reference-Image]: https://pkg.go.dev/badge/github.com/42wim/httpsig
[Go-Reference-Url]: https://pkg.go.dev/github.com/42wim/httpsig
[Go-Report-Card-Image]: https://goreportcard.com/badge/github.com/42wim/httpsig
[Go-Report-Card-Url]: https://goreportcard.com/report/github.com/42wim/httpsig
[License-Image]: https://img.shields.io/github/license/42wim/httpsig?color=blue
[License-Url]: https://opensource.org/licenses/BSD-3-Clause
[Chat-Image]: https://img.shields.io/matrix/42wim: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/42wim-activitypub-labs
[OpenCollective-Url]: https://opencollective.com/42wim-activitypub-labs

550
algorithms.go Normal file
View file

@ -0,0 +1,550 @@
package httpsig
import (
"crypto"
"crypto/ecdsa"
"crypto/hmac"
"crypto/rsa"
"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/sha3"
"golang.org/x/crypto/ssh"
)
const (
hmacPrefix = "hmac"
rsaPrefix = "rsa"
sshPrefix = "ssh"
ecdsaPrefix = "ecdsa"
ed25519Prefix = "ed25519"
sha224String = "sha224"
sha256String = "sha256"
sha384String = "sha384"
sha512String = "sha512"
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.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.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 {
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 != nil {
return nil, err
}
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
}
// Code from https://github.com/cloudtools/ssh-cert-authority/pull/49/files
// This interface provides a way to reach the exported, but not accessible SignWithOpts() method
// in x/crypto/ssh/agent. Access to this is needed to sign with more secure signing algorithms
type agentKeyringSigner interface {
SignWithOpts(rand io.Reader, data []byte, opts crypto.SignerOpts) (*ssh.Signature, error)
}
// A struct to wrap an SSH Signer with one that will switch to SHA256 Signatures.
// Replaces the call to Sign() with a call to SignWithOpts using HashFunc() algorithm.
type Sha256Signer struct {
ssh.Signer
}
func (s Sha256Signer) HashFunc() crypto.Hash {
return crypto.SHA256
}
func (s Sha256Signer) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
if aks, ok := s.Signer.(agentKeyringSigner); !ok {
return nil, fmt.Errorf("ssh: can't wrap a non ssh agentKeyringSigner")
} else {
return aks.SignWithOpts(rand, data, s)
}
}
func (r *rsaAlgorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) {
if r.sshSigner != nil {
var (
sshsig *ssh.Signature
err error
)
// are we using an SSH Agent
if _, ok := r.sshSigner.(agentKeyringSigner); ok {
signer := Sha256Signer{r.sshSigner}
sshsig, err = signer.Sign(rand, sig)
if err != nil {
return nil, err
}
} else {
sshsig, err = r.sshSigner.(ssh.AlgorithmSigner).SignWithAlgorithm(rand, sig, ssh.SigAlgoRSASHA2256)
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)
}
}

1099
algorithms_test.go Normal file

File diff suppressed because it is too large Load diff

120
digest.go Normal file
View file

@ -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
}

163
digest_test.go Normal file
View file

@ -0,0 +1,163 @@
package httpsig
import (
"bytes"
"net/http"
"testing"
)
func TestAddDigest(t *testing.T) {
tests := []struct {
name string
r func() *http.Request
algo DigestAlgorithm
body []byte
expectedDigest string
expectError bool
}{
{
name: "adds sha256 digest",
r: func() *http.Request {
r, _ := http.NewRequest("POST", "example.com", nil)
return r
},
algo: "SHA-256",
body: []byte("johnny grab your gun"),
expectedDigest: "SHA-256=RYiuVuVdRpU+BWcNUUg3sf0EbJjQ9LDj9tUqR546hhk=",
},
{
name: "adds sha512 digest",
r: func() *http.Request {
r, _ := http.NewRequest("POST", "example.com", nil)
return r
},
algo: "SHA-512",
body: []byte("yours is the drill that will pierce the heavens"),
expectedDigest: "SHA-512=bM0eBRnZkuiOTsejYNb/UpvFozde+Do1ZqlXfRTS39aGmoEzoXBpjmIIuznPslc3kaprUtI/VXH8/5HsD+thGg==",
},
{
name: "digest already set",
r: func() *http.Request {
r, _ := http.NewRequest("POST", "example.com", nil)
r.Header.Set("Digest", "oops")
return r
},
algo: "SHA-512",
body: []byte("did bob ewell fall on his knife"),
expectError: true,
},
{
name: "unknown/unsupported digest algorithm",
r: func() *http.Request {
r, _ := http.NewRequest("POST", "example.com", nil)
return r
},
algo: "MD5",
body: []byte("two times Cuchulainn almost drowned"),
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test := test
req := test.r()
err := addDigest(req, test.algo, test.body)
gotErr := err != nil
if gotErr != test.expectError {
if test.expectError {
t.Fatalf("expected error, got: %s", err)
} else {
t.Fatalf("expected no error, got: %s", err)
}
} else if !gotErr {
d := req.Header.Get("Digest")
if d != test.expectedDigest {
t.Fatalf("unexpected digest: want %s, got %s", test.expectedDigest, d)
}
}
})
}
}
func TestVerifyDigest(t *testing.T) {
tests := []struct {
name string
r func() *http.Request
body []byte
expectError bool
}{
{
name: "verify sha256",
r: func() *http.Request {
r, _ := http.NewRequest("POST", "example.com", nil)
r.Header.Set("Digest", "SHA-256=RYiuVuVdRpU+BWcNUUg3sf0EbJjQ9LDj9tUqR546hhk=")
return r
},
body: []byte("johnny grab your gun"),
},
{
name: "verify sha512",
r: func() *http.Request {
r, _ := http.NewRequest("POST", "example.com", nil)
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"),
},
{
name: "no digest header",
r: func() *http.Request {
r, _ := http.NewRequest("POST", "example.com", nil)
return r
},
body: []byte("Yuji's gender is blue"),
expectError: true,
},
{
name: "malformed digest",
r: func() *http.Request {
r, _ := http.NewRequest("POST", "example.com", nil)
r.Header.Set("Digest", "SHA-256am9obm55IGdyYWIgeW91ciBndW7jsMRCmPwcFJr79MiZb7kkJ65B5GSbk0yklZkbeFK4VQ==")
return r
},
body: []byte("Tochee and Ozzie BFFs forever"),
expectError: true,
},
{
name: "unsupported/unknown algo",
r: func() *http.Request {
r, _ := http.NewRequest("POST", "example.com", nil)
r.Header.Set("Digest", "MD5=poo")
return r
},
body: []byte("what is a man? a miserable pile of secrets"),
expectError: true,
},
{
name: "bad digest",
r: func() *http.Request {
r, _ := http.NewRequest("POST", "example.com", nil)
r.Header.Set("Digest", "SHA-256=bm9obm55IGdyYWIgeW91ciBndW7jsMRCmPwcFJr79MiZb7kkJ65B5GSbk0yklZkbeFK4VQ==")
return r
},
body: []byte("johnny grab your gun"),
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test := test
req := test.r()
buf := bytes.NewBuffer(test.body)
err := verifyDigest(req, buf)
gotErr := err != nil
if gotErr != test.expectError {
if test.expectError {
t.Fatalf("expected error, got: %s", err)
} else {
t.Fatalf("expected no error, got: %s", err)
}
}
})
}
}

7
go.mod Normal file
View file

@ -0,0 +1,7 @@
module github.com/42wim/httpsig
require golang.org/x/crypto v0.37.0
require golang.org/x/sys v0.32.0 // indirect
go 1.23.0

6
go.sum Normal file
View file

@ -0,0 +1,6 @@
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=

356
httpsig.go Normal file
View file

@ -0,0 +1,356 @@
// 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_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_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
// ECDSA algorithms
ECDSA_SHA224 Algorithm = ecdsaPrefix + "-" + sha224String
ECDSA_SHA256 Algorithm = ecdsaPrefix + "-" + sha256String
ECDSA_SHA384 Algorithm = ecdsaPrefix + "-" + sha384String
ECDSA_SHA512 Algorithm = ecdsaPrefix + "-" + sha512String
// 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 ""
}
}
// 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
}
// 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) (Signer, 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_SHA256
}
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
}
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) {
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, created int64, expires int64) (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) (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{
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
}

901
httpsig_test.go Normal file
View file

@ -0,0 +1,901 @@
package httpsig
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"golang.org/x/crypto/ed25519"
)
const (
testUrl = "foo.net/bar/baz?q=test&r=ok"
testUrlPath = "bar/baz"
testDate = "Tue, 07 Jun 2014 20:51:35 GMT"
testDigest = "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="
testMethod = "GET"
)
type httpsigTest struct {
name string
prefs []Algorithm
digestAlg DigestAlgorithm
headers []string
body []byte
scheme SignatureScheme
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() {
var err error
privKey, err = rsa.GenerateKey(rand.Reader, 2048)
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 {
panic(err)
}
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,
expectedSignatureAlgorithm: "hs2019",
},
{
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: "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 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: "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: "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: "ed25519 authorization",
prefs: []Algorithm{ED25519},
digestAlg: DigestSha256,
headers: []string{"Date", "Digest"},
scheme: Authorization,
privKey: privEd25519Key,
pubKey: pubEd25519Key,
pubKeyId: "pubKeyId",
expectedAlgorithm: ED25519,
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,
expectedSignatureAlgorithm: "hs2019",
},
{
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",
prefs: []Algorithm{RSA_SHA512},
digestAlg: DigestSha256,
headers: []string{"Date", "Digest", RequestTarget},
scheme: Signature,
privKey: privKey,
pubKey: privKey.Public(),
pubKeyId: "pubKeyId",
expectedAlgorithm: RSA_SHA512,
expectedSignatureAlgorithm: "hs2019",
expectErrorSigningResponse: true,
expectRequestPath: true,
},
}
testSpecRSAPrivateKey, err = loadPrivateKey([]byte(testSpecPrivateKeyPEM))
if err != nil {
panic(err)
}
testSpecRSAPublicKey, err = loadPublicKey([]byte(testSpecPublicKeyPEM))
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 {
return fmt.Sprintf("%s%s%s%s%s", k, parameterKVSeparater, parameterValueDelimiter, v, parameterValueDelimiter)
}
func toHeaderSignatureParameters(k string, vals []string) string {
if len(vals) == 0 {
vals = defaultHeaders
}
v := strings.Join(vals, headerParameterValueDelim)
k = strings.ToLower(k)
v = strings.ToLower(v)
return fmt.Sprintf("%s%s%s%s%s", k, parameterKVSeparater, parameterValueDelimiter, v, parameterValueDelimiter)
}
func TestSignerRequest(t *testing.T) {
testFn := func(t *testing.T, test httpsigTest) {
s, a, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme, 0)
if err != nil {
t.Fatalf("%s", err)
}
if a != test.expectedAlgorithm {
t.Fatalf("got %s, want %s", a, test.expectedAlgorithm)
}
// Test request signing
req, err := http.NewRequest(testMethod, testUrl, nil)
if err != nil {
t.Fatalf("%s", err)
}
req.Header.Set("Date", testDate)
if test.body == nil {
req.Header.Set("Digest", testDigest)
}
err = s.SignRequest(test.privKey, test.pubKeyId, req, test.body)
if err != nil {
t.Fatalf("%s", err)
}
vals, ok := req.Header[string(test.scheme)]
if !ok {
t.Fatalf("not in header %s", test.scheme)
}
if len(vals) != 1 {
t.Fatalf("too many in header %s: %d", test.scheme, len(vals))
}
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.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)
} else if !strings.Contains(vals[0], signatureParameter) {
t.Fatalf("%s\ndoes not contain\n%s", vals[0], signatureParameter)
} else if test.body != nil && req.Header.Get("Digest") != test.expectedDigest {
t.Fatalf("%s\ndoes not match\n%s", req.Header.Get("Digest"), test.expectedDigest)
}
// For schemes with an authScheme, enforce its is present and at the beginning
if len(test.scheme.authScheme()) > 0 {
if !strings.HasPrefix(vals[0], test.scheme.authScheme()) {
t.Fatalf("%s\ndoes not start with\n%s", vals[0], test.scheme.authScheme())
}
}
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testFn(t, test)
})
}
}
func TestSignerResponse(t *testing.T) {
testFn := func(t *testing.T, test httpsigTest) {
s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme, 0)
// Test response signing
resp := httptest.NewRecorder()
resp.HeaderMap.Set("Date", testDate)
if test.body == nil {
resp.HeaderMap.Set("Digest", testDigest)
}
err = s.SignResponse(test.privKey, test.pubKeyId, resp, test.body)
if test.expectErrorSigningResponse {
if err != nil {
// Skip rest of testing
return
} else {
t.Fatalf("expected error, got nil")
}
}
vals, ok := resp.HeaderMap[string(test.scheme)]
if !ok {
t.Fatalf("not in header %s", test.scheme)
}
if len(vals) != 1 {
t.Fatalf("too many in header %s: %d", test.scheme, len(vals))
}
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.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)
} else if !strings.Contains(vals[0], signatureParameter) {
t.Fatalf("%s\ndoes not contain\n%s", vals[0], signatureParameter)
} else if test.body != nil && resp.Header().Get("Digest") != test.expectedDigest {
t.Fatalf("%s\ndoes not match\n%s", resp.Header().Get("Digest"), test.expectedDigest)
}
// For schemes with an authScheme, enforce its is present and at the beginning
if len(test.scheme.authScheme()) > 0 {
if !strings.HasPrefix(vals[0], test.scheme.authScheme()) {
t.Fatalf("%s\ndoes not start with\n%s", vals[0], test.scheme.authScheme())
}
}
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testFn(t, test)
})
}
}
func TestNewSignerRequestMissingHeaders(t *testing.T) {
failingTests := []struct {
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",
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, 0)
if err != nil {
t.Fatalf("%s", err)
}
if a != test.expectedAlgorithm {
t.Fatalf("got %s, want %s", a, test.expectedAlgorithm)
}
req, err := http.NewRequest(testMethod, testUrl, nil)
if err != nil {
t.Fatalf("%s", err)
}
req.Header.Set("Date", testDate)
err = s.SignRequest(test.privKey, test.pubKeyId, req, nil)
if err == nil {
t.Fatalf("expect error but got nil")
}
})
}
}
func TestNewSignerResponseMissingHeaders(t *testing.T) {
failingTests := []struct {
name string
prefs []Algorithm
digestAlg DigestAlgorithm
headers []string
scheme SignatureScheme
privKey crypto.PrivateKey
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",
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, 0)
if err != nil {
t.Fatalf("%s", err)
}
if a != test.expectedAlgorithm {
t.Fatalf("got %s, want %s", a, test.expectedAlgorithm)
}
resp := httptest.NewRecorder()
resp.HeaderMap.Set("Date", testDate)
resp.HeaderMap.Set("Digest", testDigest)
err = s.SignResponse(test.privKey, test.pubKeyId, resp, nil)
if err != nil {
t.Fatalf("expected error, got nil")
}
})
}
}
func TestNewVerifier(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test := test
// Prepare
req, err := http.NewRequest(testMethod, testUrl, nil)
if err != nil {
t.Fatalf("%s", err)
}
req.Header.Set("Date", testDate)
if test.body == nil {
req.Header.Set("Digest", testDigest)
}
s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme, 0)
if err != nil {
t.Fatalf("%s", err)
}
err = s.SignRequest(test.privKey, test.pubKeyId, req, test.body)
if err != nil {
t.Fatalf("%s", err)
}
// Test verification
v, err := NewVerifier(req)
if err != nil {
t.Fatalf("%s", err)
}
if v.KeyId() != test.pubKeyId {
t.Fatalf("got %s, want %s", v.KeyId(), test.pubKeyId)
}
err = v.Verify(test.pubKey, test.expectedAlgorithm)
if err != nil {
t.Fatalf("%s", err)
}
})
}
}
func TestNewResponseVerifier(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test := test
if test.expectErrorSigningResponse {
return
}
// Prepare
resp := httptest.NewRecorder()
resp.HeaderMap.Set("Date", testDate)
if test.body == nil {
resp.HeaderMap.Set("Digest", testDigest)
}
s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme, 0)
if err != nil {
t.Fatalf("%s", err)
}
err = s.SignResponse(test.privKey, test.pubKeyId, resp, test.body)
if err != nil {
t.Fatalf("%s", err)
}
// Test verification
v, err := NewResponseVerifier(resp.Result())
if err != nil {
t.Fatalf("%s", err)
}
if v.KeyId() != test.pubKeyId {
t.Fatalf("got %s, want %s", v.KeyId(), test.pubKeyId)
}
err = v.Verify(test.pubKey, test.expectedAlgorithm)
if err != nil {
t.Fatalf("%s", err)
}
})
}
}
// Test_Signing_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
func Test_Signing_HTTP_Messages_AppendixC(t *testing.T) {
specTests := []struct {
name string
headers []string
expectedSignature string
}{
{
name: "C.1. 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="SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM="`,
},
{
name: "C.2. Basic Test",
headers: []string{"(request-target)", "host", "date"},
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="hs2019",headers="(request-target) host date content-type digest content-length",signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="`,
},
}
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{RSA_SHA256}, DigestSha256, test.headers, Authorization, 0)
if err != nil {
t.Fatalf("error creating signer: %s", err)
}
if err := s.SignRequest(testSpecRSAPrivateKey, "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)
}
})
}
}
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
func Test_Verifying_HTTP_Messages_AppendixC(t *testing.T) {
specTests := []struct {
name string
headers []string
signature string
}{
{
name: "C.1. Default Test",
headers: []string{},
signature: `Signature keyId="Test",algorithm="rsa-sha256",signature="SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM="`,
},
{
name: "C.2. Basic Test",
headers: []string{"(request-target)", "host", "date"},
signature: `Signature keyId="Test",algorithm="rsa-sha256",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"},
signature: `Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="`,
},
}
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(testSpecRSAPublicKey, RSA_SHA256); err != nil {
t.Errorf("Verification failure: %s", err)
}
})
}
}
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" {
return nil, fmt.Errorf("RSA private key is of the wrong type: %s", pem.Type)
}
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" {
return nil, fmt.Errorf("public key is of the wrong type: %s", pem.Type)
}
key, err := x509.ParsePKIXPublicKey(pem.Bytes)
if err != nil {
return nil, err
}
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 {
body := ""
if r.Body != nil {
var err error
bodyBytes, err = ioutil.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("error reading body. %v", err)
}
// And now set a new body, which will simulate the same data we read:
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
body = string(bodyBytes)
}
d := sha256.Sum256([]byte(body))
r.Header["Digest"] = []string{fmt.Sprintf("SHA-256=%s", base64.StdEncoding.EncodeToString(d[:]))}
}
return bodyBytes, nil
}
const testSpecBody = `{"hello": "world"}`
const testSpecDate = `Sun, 05 Jan 2014 21:31:40 GMT`
const testSpecPrivateKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF
NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F
UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB
AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA
QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK
kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg
f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u
412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc
mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7
kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA
gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW
G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==
-----END RSA PRIVATE KEY-----`
const testSpecPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
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-----`

334
signing.go Normal file
View file

@ -0,0 +1,334 @@
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 _ Signer = &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 {
if body != nil {
err := addDigest(r, m.dAlgo, body)
if err != nil {
return err
}
}
s, err := m.signatureString(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) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) 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) (string, error) {
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, m.created, m.expires)
}
var _ Signer = &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 {
if body != nil {
err := addDigest(r, a.dAlgo, body)
if err != nil {
return err
}
}
s, err := a.signatureString(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) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) 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) (string, error) {
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, 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) 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 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
}

184
verifying.go Normal file
View file

@ -0,0 +1,184 @@
package httpsig
import (
"crypto"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
var _ Verifier = &verifier{}
type verifier struct {
header http.Header
kId string
signature string
created int64
expires int64
headers []string
sigStringFn func(http.Header, []string, int64, int64) (string, 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, 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 {
s, err := signerFromString(string(algo))
if err == nil {
return v.asymmVerify(s, pKey)
}
m, err := macerFromString(string(algo))
if err == nil {
return v.macVerify(m, pKey)
}
return fmt.Errorf("no crypto implementation available for %q: %s", algo, err)
}
func (v *verifier) macVerify(m macer, pKey crypto.PublicKey) 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)
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) error {
toHash, err := v.sigStringFn(v.header, v.headers, v.created, v.expires)
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
}