Adding upstream version 1.2.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
ed523a11b4
commit
d4d4f346b4
13 changed files with 3867 additions and 0 deletions
3
.travis.yml
Normal file
3
.travis.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.15
|
29
LICENSE
Normal file
29
LICENSE
Normal 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
115
README.md
Normal 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
550
algorithms.go
Normal 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
1099
algorithms_test.go
Normal file
File diff suppressed because it is too large
Load diff
120
digest.go
Normal file
120
digest.go
Normal 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
163
digest_test.go
Normal 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
7
go.mod
Normal 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
6
go.sum
Normal 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
356
httpsig.go
Normal 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
901
httpsig_test.go
Normal 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
334
signing.go
Normal 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
184
verifying.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue