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