refactor: move the transport onto the official Gitea SDK
The transport was hand-rolled net/http against the REST API. The payload shapes were ours, in internal/wire, which meant every field Gitea learned was a field somebody here had to notice; and "does this instance have issue dependencies?" had to be guessed from a status code, because a 404 from a missing route and a 404 from a missing issue look alike. The SDK settles both. The shapes are maintained by the people who maintain the server, and the client negotiates the server's version when it is built, so the dependency endpoint is now gated on `>= 1.20.0` — verified against the release where the route appears, not assumed. Below the gate nothing is requested at all. internal/wire keeps what the SDK has no answer for: addressing. The SDK takes an owner, a name and an int64 and never parses, while `42`, `#42`, `owner/repo#42` and an issue URL are four spellings of one address, all four are what somebody has in hand, and Key is what the ledger is keyed by. The payload structs go. Four things that had to survive the move, and did: - request bodies still land in .kettle/payload/, now via an http.RoundTripper on the client the SDK is given — which is better than before, because it files every request rather than the ones a call site remembered to name; - errors still carry the HTTP status AND the response body, and a decode failure on a 2xx is deliberately not an APIError, so the dependency probe cannot read a bad decode as "feature missing"; - the number -> slug ledger is untouched, entries still outlive the files they name; - Client.For(repo) still re-points at another repository for one call. What it cost, written down in AGENTS.md where it happened. internal/mapping's layering test was a fact about the import graph — nothing in its closure could open a socket — and the SDK ships its types and its client in one package, so the test now asserts what is still true: the bridge performs no I/O, checked on direct imports plus a grep for time.Now. A run makes one extra request before it does anything. Gitea's issue edit endpoint carries no labels, so a push whose labels changed needs a second call; push makes it and says so. go.mod requires go 1.26, which the SDK sets and which is now the floor for building this binary. internal/issue and internal/project are byte-identical. The domain did not notice, which is the whole argument for the layering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+29
@@ -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.
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
# httpsig
|
||||
|
||||
`go get github.com/go-fed/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/go-fed/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.
|
||||
+532
@@ -0,0 +1,532 @@
|
||||
package httpsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/hmac"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle" // Use should trigger great care
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/blake2b"
|
||||
"golang.org/x/crypto/blake2s"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"golang.org/x/crypto/ripemd160"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
hmacPrefix = "hmac"
|
||||
rsaPrefix = "rsa"
|
||||
sshPrefix = "ssh"
|
||||
ecdsaPrefix = "ecdsa"
|
||||
ed25519Prefix = "ed25519"
|
||||
md4String = "md4"
|
||||
md5String = "md5"
|
||||
sha1String = "sha1"
|
||||
sha224String = "sha224"
|
||||
sha256String = "sha256"
|
||||
sha384String = "sha384"
|
||||
sha512String = "sha512"
|
||||
md5sha1String = "md5sha1"
|
||||
ripemd160String = "ripemd160"
|
||||
sha3_224String = "sha3-224"
|
||||
sha3_256String = "sha3-256"
|
||||
sha3_384String = "sha3-384"
|
||||
sha3_512String = "sha3-512"
|
||||
sha512_224String = "sha512-224"
|
||||
sha512_256String = "sha512-256"
|
||||
blake2s_256String = "blake2s-256"
|
||||
blake2b_256String = "blake2b-256"
|
||||
blake2b_384String = "blake2b-384"
|
||||
blake2b_512String = "blake2b-512"
|
||||
)
|
||||
|
||||
var blake2Algorithms = map[crypto.Hash]bool{
|
||||
crypto.BLAKE2s_256: true,
|
||||
crypto.BLAKE2b_256: true,
|
||||
crypto.BLAKE2b_384: true,
|
||||
crypto.BLAKE2b_512: true,
|
||||
}
|
||||
|
||||
var hashToDef = map[crypto.Hash]struct {
|
||||
name string
|
||||
new func(key []byte) (hash.Hash, error) // Only MACers will accept a key
|
||||
}{
|
||||
// Which standard names these?
|
||||
// The spec lists the following as a canonical reference, which is dead:
|
||||
// http://www.iana.org/assignments/signature-algorithms
|
||||
//
|
||||
// Note that the forbidden hashes have an invalid 'new' function.
|
||||
crypto.MD4: {md4String, func(key []byte) (hash.Hash, error) { return nil, nil }},
|
||||
crypto.MD5: {md5String, func(key []byte) (hash.Hash, error) { return nil, nil }},
|
||||
// Temporarily enable SHA1 because of issue https://github.com/golang/go/issues/37278
|
||||
crypto.SHA1: {sha1String, func(key []byte) (hash.Hash, error) { return sha1.New(), nil }},
|
||||
crypto.SHA224: {sha224String, func(key []byte) (hash.Hash, error) { return sha256.New224(), nil }},
|
||||
crypto.SHA256: {sha256String, func(key []byte) (hash.Hash, error) { return sha256.New(), nil }},
|
||||
crypto.SHA384: {sha384String, func(key []byte) (hash.Hash, error) { return sha512.New384(), nil }},
|
||||
crypto.SHA512: {sha512String, func(key []byte) (hash.Hash, error) { return sha512.New(), nil }},
|
||||
crypto.MD5SHA1: {md5sha1String, func(key []byte) (hash.Hash, error) { return nil, nil }},
|
||||
crypto.RIPEMD160: {ripemd160String, func(key []byte) (hash.Hash, error) { return ripemd160.New(), nil }},
|
||||
crypto.SHA3_224: {sha3_224String, func(key []byte) (hash.Hash, error) { return sha3.New224(), nil }},
|
||||
crypto.SHA3_256: {sha3_256String, func(key []byte) (hash.Hash, error) { return sha3.New256(), nil }},
|
||||
crypto.SHA3_384: {sha3_384String, func(key []byte) (hash.Hash, error) { return sha3.New384(), nil }},
|
||||
crypto.SHA3_512: {sha3_512String, func(key []byte) (hash.Hash, error) { return sha3.New512(), nil }},
|
||||
crypto.SHA512_224: {sha512_224String, func(key []byte) (hash.Hash, error) { return sha512.New512_224(), nil }},
|
||||
crypto.SHA512_256: {sha512_256String, func(key []byte) (hash.Hash, error) { return sha512.New512_256(), nil }},
|
||||
crypto.BLAKE2s_256: {blake2s_256String, func(key []byte) (hash.Hash, error) { return blake2s.New256(key) }},
|
||||
crypto.BLAKE2b_256: {blake2b_256String, func(key []byte) (hash.Hash, error) { return blake2b.New256(key) }},
|
||||
crypto.BLAKE2b_384: {blake2b_384String, func(key []byte) (hash.Hash, error) { return blake2b.New384(key) }},
|
||||
crypto.BLAKE2b_512: {blake2b_512String, func(key []byte) (hash.Hash, error) { return blake2b.New512(key) }},
|
||||
}
|
||||
|
||||
var stringToHash map[string]crypto.Hash
|
||||
|
||||
const (
|
||||
defaultAlgorithm = RSA_SHA256
|
||||
defaultAlgorithmHashing = sha256String
|
||||
)
|
||||
|
||||
func init() {
|
||||
stringToHash = make(map[string]crypto.Hash, len(hashToDef))
|
||||
for k, v := range hashToDef {
|
||||
stringToHash[v.name] = k
|
||||
}
|
||||
// This should guarantee that at runtime the defaultAlgorithm will not
|
||||
// result in errors when fetching a macer or signer (see algorithms.go)
|
||||
if ok, err := isAvailable(string(defaultAlgorithmHashing)); err != nil {
|
||||
panic(err)
|
||||
} else if !ok {
|
||||
panic(fmt.Sprintf("the default httpsig algorithm is unavailable: %q", defaultAlgorithm))
|
||||
}
|
||||
}
|
||||
|
||||
func isForbiddenHash(h crypto.Hash) bool {
|
||||
switch h {
|
||||
// Not actually cryptographically secure
|
||||
case crypto.MD4:
|
||||
fallthrough
|
||||
case crypto.MD5:
|
||||
fallthrough
|
||||
case crypto.MD5SHA1: // shorthand for crypto/tls, not actually implemented
|
||||
return true
|
||||
}
|
||||
// Still cryptographically secure
|
||||
return false
|
||||
}
|
||||
|
||||
// signer is an internally public type.
|
||||
type signer interface {
|
||||
Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error)
|
||||
Verify(pub crypto.PublicKey, toHash, signature []byte) error
|
||||
String() string
|
||||
}
|
||||
|
||||
// macer is an internally public type.
|
||||
type macer interface {
|
||||
Sign(sig, key []byte) ([]byte, error)
|
||||
Equal(sig, actualMAC, key []byte) (bool, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
var _ macer = &hmacAlgorithm{}
|
||||
|
||||
type hmacAlgorithm struct {
|
||||
fn func(key []byte) (hash.Hash, error)
|
||||
kind crypto.Hash
|
||||
}
|
||||
|
||||
func (h *hmacAlgorithm) Sign(sig, key []byte) ([]byte, error) {
|
||||
hs, err := h.fn(key)
|
||||
if err = setSig(hs, sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hs.Sum(nil), nil
|
||||
}
|
||||
|
||||
func (h *hmacAlgorithm) Equal(sig, actualMAC, key []byte) (bool, error) {
|
||||
hs, err := h.fn(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer hs.Reset()
|
||||
err = setSig(hs, sig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
expected := hs.Sum(nil)
|
||||
return hmac.Equal(actualMAC, expected), nil
|
||||
}
|
||||
|
||||
func (h *hmacAlgorithm) String() string {
|
||||
return fmt.Sprintf("%s-%s", hmacPrefix, hashToDef[h.kind].name)
|
||||
}
|
||||
|
||||
var _ signer = &rsaAlgorithm{}
|
||||
|
||||
type rsaAlgorithm struct {
|
||||
hash.Hash
|
||||
kind crypto.Hash
|
||||
sshSigner ssh.Signer
|
||||
}
|
||||
|
||||
func (r *rsaAlgorithm) setSig(b []byte) error {
|
||||
n, err := r.Write(b)
|
||||
if err != nil {
|
||||
r.Reset()
|
||||
return err
|
||||
} else if n != len(b) {
|
||||
r.Reset()
|
||||
return fmt.Errorf("could only write %d of %d bytes of signature to hash", n, len(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rsaAlgorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) {
|
||||
if r.sshSigner != nil {
|
||||
sshsig, err := r.sshSigner.Sign(rand, sig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sshsig.Blob, nil
|
||||
}
|
||||
defer r.Reset()
|
||||
|
||||
if err := r.setSig(sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsaK, ok := p.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("crypto.PrivateKey is not *rsa.PrivateKey")
|
||||
}
|
||||
return rsa.SignPKCS1v15(rand, rsaK, r.kind, r.Sum(nil))
|
||||
}
|
||||
|
||||
func (r *rsaAlgorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) error {
|
||||
defer r.Reset()
|
||||
rsaK, ok := pub.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return errors.New("crypto.PublicKey is not *rsa.PublicKey")
|
||||
}
|
||||
if err := r.setSig(toHash); err != nil {
|
||||
return err
|
||||
}
|
||||
return rsa.VerifyPKCS1v15(rsaK, r.kind, r.Sum(nil), signature)
|
||||
}
|
||||
|
||||
func (r *rsaAlgorithm) String() string {
|
||||
return fmt.Sprintf("%s-%s", rsaPrefix, hashToDef[r.kind].name)
|
||||
}
|
||||
|
||||
var _ signer = &ed25519Algorithm{}
|
||||
|
||||
type ed25519Algorithm struct {
|
||||
sshSigner ssh.Signer
|
||||
}
|
||||
|
||||
func (r *ed25519Algorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) {
|
||||
if r.sshSigner != nil {
|
||||
sshsig, err := r.sshSigner.Sign(rand, sig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sshsig.Blob, nil
|
||||
}
|
||||
ed25519K, ok := p.(ed25519.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("crypto.PrivateKey is not ed25519.PrivateKey")
|
||||
}
|
||||
return ed25519.Sign(ed25519K, sig), nil
|
||||
}
|
||||
|
||||
func (r *ed25519Algorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) error {
|
||||
ed25519K, ok := pub.(ed25519.PublicKey)
|
||||
if !ok {
|
||||
return errors.New("crypto.PublicKey is not ed25519.PublicKey")
|
||||
}
|
||||
|
||||
if ed25519.Verify(ed25519K, toHash, signature) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("ed25519 verify failed")
|
||||
}
|
||||
|
||||
func (r *ed25519Algorithm) String() string {
|
||||
return fmt.Sprintf("%s", ed25519Prefix)
|
||||
}
|
||||
|
||||
var _ signer = &ecdsaAlgorithm{}
|
||||
|
||||
type ecdsaAlgorithm struct {
|
||||
hash.Hash
|
||||
kind crypto.Hash
|
||||
}
|
||||
|
||||
func (r *ecdsaAlgorithm) setSig(b []byte) error {
|
||||
n, err := r.Write(b)
|
||||
if err != nil {
|
||||
r.Reset()
|
||||
return err
|
||||
} else if n != len(b) {
|
||||
r.Reset()
|
||||
return fmt.Errorf("could only write %d of %d bytes of signature to hash", n, len(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ECDSASignature struct {
|
||||
R, S *big.Int
|
||||
}
|
||||
|
||||
func (r *ecdsaAlgorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) {
|
||||
defer r.Reset()
|
||||
if err := r.setSig(sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ecdsaK, ok := p.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("crypto.PrivateKey is not *ecdsa.PrivateKey")
|
||||
}
|
||||
R, S, err := ecdsa.Sign(rand, ecdsaK, r.Sum(nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature := ECDSASignature{R: R, S: S}
|
||||
bytes, err := asn1.Marshal(signature)
|
||||
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
func (r *ecdsaAlgorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) error {
|
||||
defer r.Reset()
|
||||
ecdsaK, ok := pub.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return errors.New("crypto.PublicKey is not *ecdsa.PublicKey")
|
||||
}
|
||||
if err := r.setSig(toHash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sig := new(ECDSASignature)
|
||||
_, err := asn1.Unmarshal(signature, sig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ecdsa.Verify(ecdsaK, r.Sum(nil), sig.R, sig.S) {
|
||||
return nil
|
||||
} else {
|
||||
return errors.New("Invalid signature")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ecdsaAlgorithm) String() string {
|
||||
return fmt.Sprintf("%s-%s", ecdsaPrefix, hashToDef[r.kind].name)
|
||||
}
|
||||
|
||||
var _ macer = &blakeMacAlgorithm{}
|
||||
|
||||
type blakeMacAlgorithm struct {
|
||||
fn func(key []byte) (hash.Hash, error)
|
||||
kind crypto.Hash
|
||||
}
|
||||
|
||||
func (r *blakeMacAlgorithm) Sign(sig, key []byte) ([]byte, error) {
|
||||
hs, err := r.fn(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = setSig(hs, sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hs.Sum(nil), nil
|
||||
}
|
||||
|
||||
func (r *blakeMacAlgorithm) Equal(sig, actualMAC, key []byte) (bool, error) {
|
||||
hs, err := r.fn(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer hs.Reset()
|
||||
err = setSig(hs, sig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
expected := hs.Sum(nil)
|
||||
return subtle.ConstantTimeCompare(actualMAC, expected) == 1, nil
|
||||
}
|
||||
|
||||
func (r *blakeMacAlgorithm) String() string {
|
||||
return fmt.Sprintf("%s", hashToDef[r.kind].name)
|
||||
}
|
||||
|
||||
func setSig(a hash.Hash, b []byte) error {
|
||||
n, err := a.Write(b)
|
||||
if err != nil {
|
||||
a.Reset()
|
||||
return err
|
||||
} else if n != len(b) {
|
||||
a.Reset()
|
||||
return fmt.Errorf("could only write %d of %d bytes of signature to hash", n, len(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsSupportedHttpSigAlgorithm returns true if the string is supported by this
|
||||
// library, is not a hash known to be weak, and is supported by the hardware.
|
||||
func IsSupportedHttpSigAlgorithm(algo string) bool {
|
||||
a, err := isAvailable(algo)
|
||||
return a && err == nil
|
||||
}
|
||||
|
||||
// isAvailable is an internally public function
|
||||
func isAvailable(algo string) (bool, error) {
|
||||
c, ok := stringToHash[algo]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("no match for %q", algo)
|
||||
}
|
||||
if isForbiddenHash(c) {
|
||||
return false, fmt.Errorf("forbidden hash type in %q", algo)
|
||||
}
|
||||
return c.Available(), nil
|
||||
}
|
||||
|
||||
func newAlgorithmConstructor(algo string) (fn func(k []byte) (hash.Hash, error), c crypto.Hash, e error) {
|
||||
ok := false
|
||||
c, ok = stringToHash[algo]
|
||||
if !ok {
|
||||
e = fmt.Errorf("no match for %q", algo)
|
||||
return
|
||||
}
|
||||
if isForbiddenHash(c) {
|
||||
e = fmt.Errorf("forbidden hash type in %q", algo)
|
||||
return
|
||||
}
|
||||
algoDef, ok := hashToDef[c]
|
||||
if !ok {
|
||||
e = fmt.Errorf("have crypto.Hash %v but no definition", c)
|
||||
return
|
||||
}
|
||||
fn = func(key []byte) (hash.Hash, error) {
|
||||
h, err := algoDef.new(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newAlgorithm(algo string, key []byte) (hash.Hash, crypto.Hash, error) {
|
||||
fn, c, err := newAlgorithmConstructor(algo)
|
||||
if err != nil {
|
||||
return nil, c, err
|
||||
}
|
||||
h, err := fn(key)
|
||||
return h, c, err
|
||||
}
|
||||
|
||||
func signerFromSSHSigner(sshSigner ssh.Signer, s string) (signer, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(s, rsaPrefix):
|
||||
return &rsaAlgorithm{
|
||||
sshSigner: sshSigner,
|
||||
}, nil
|
||||
case strings.HasPrefix(s, ed25519Prefix):
|
||||
return &ed25519Algorithm{
|
||||
sshSigner: sshSigner,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("no signer matching %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
// signerFromString is an internally public method constructor
|
||||
func signerFromString(s string) (signer, error) {
|
||||
s = strings.ToLower(s)
|
||||
isEcdsa := false
|
||||
isEd25519 := false
|
||||
var algo string = ""
|
||||
if strings.HasPrefix(s, ecdsaPrefix) {
|
||||
algo = strings.TrimPrefix(s, ecdsaPrefix+"-")
|
||||
isEcdsa = true
|
||||
} else if strings.HasPrefix(s, rsaPrefix) {
|
||||
algo = strings.TrimPrefix(s, rsaPrefix+"-")
|
||||
} else if strings.HasPrefix(s, ed25519Prefix) {
|
||||
isEd25519 = true
|
||||
algo = "sha512"
|
||||
} else {
|
||||
return nil, fmt.Errorf("no signer matching %q", s)
|
||||
}
|
||||
hash, cHash, err := newAlgorithm(algo, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isEd25519 {
|
||||
return &ed25519Algorithm{}, nil
|
||||
}
|
||||
if isEcdsa {
|
||||
return &ecdsaAlgorithm{
|
||||
Hash: hash,
|
||||
kind: cHash,
|
||||
}, nil
|
||||
}
|
||||
return &rsaAlgorithm{
|
||||
Hash: hash,
|
||||
kind: cHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// macerFromString is an internally public method constructor
|
||||
func macerFromString(s string) (macer, error) {
|
||||
s = strings.ToLower(s)
|
||||
if strings.HasPrefix(s, hmacPrefix) {
|
||||
algo := strings.TrimPrefix(s, hmacPrefix+"-")
|
||||
hashFn, cHash, err := newAlgorithmConstructor(algo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Ensure below does not panic
|
||||
_, err = hashFn(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hmacAlgorithm{
|
||||
fn: func(key []byte) (hash.Hash, error) {
|
||||
return hmac.New(func() hash.Hash {
|
||||
h, e := hashFn(nil)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
return h
|
||||
}, key), nil
|
||||
},
|
||||
kind: cHash,
|
||||
}, nil
|
||||
} else if bl, ok := stringToHash[s]; ok && blake2Algorithms[bl] {
|
||||
hashFn, cHash, err := newAlgorithmConstructor(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &blakeMacAlgorithm{
|
||||
fn: hashFn,
|
||||
kind: cHash,
|
||||
}, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("no MACer matching %q", s)
|
||||
}
|
||||
}
|
||||
+120
@@ -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
|
||||
}
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
// Implements HTTP request and response signing and verification. Supports the
|
||||
// major MAC and asymmetric key signature algorithms. It has several safety
|
||||
// restrictions: One, none of the widely known non-cryptographically safe
|
||||
// algorithms are permitted; Two, the RSA SHA256 algorithms must be available in
|
||||
// the binary (and it should, barring export restrictions); Finally, the library
|
||||
// assumes either the 'Authorizationn' or 'Signature' headers are to be set (but
|
||||
// not both).
|
||||
package httpsig
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// Algorithm specifies a cryptography secure algorithm for signing HTTP requests
|
||||
// and responses.
|
||||
type Algorithm string
|
||||
|
||||
const (
|
||||
// MAC-based algoirthms.
|
||||
HMAC_SHA224 Algorithm = hmacPrefix + "-" + sha224String
|
||||
HMAC_SHA256 Algorithm = hmacPrefix + "-" + sha256String
|
||||
HMAC_SHA384 Algorithm = hmacPrefix + "-" + sha384String
|
||||
HMAC_SHA512 Algorithm = hmacPrefix + "-" + sha512String
|
||||
HMAC_RIPEMD160 Algorithm = hmacPrefix + "-" + ripemd160String
|
||||
HMAC_SHA3_224 Algorithm = hmacPrefix + "-" + sha3_224String
|
||||
HMAC_SHA3_256 Algorithm = hmacPrefix + "-" + sha3_256String
|
||||
HMAC_SHA3_384 Algorithm = hmacPrefix + "-" + sha3_384String
|
||||
HMAC_SHA3_512 Algorithm = hmacPrefix + "-" + sha3_512String
|
||||
HMAC_SHA512_224 Algorithm = hmacPrefix + "-" + sha512_224String
|
||||
HMAC_SHA512_256 Algorithm = hmacPrefix + "-" + sha512_256String
|
||||
HMAC_BLAKE2S_256 Algorithm = hmacPrefix + "-" + blake2s_256String
|
||||
HMAC_BLAKE2B_256 Algorithm = hmacPrefix + "-" + blake2b_256String
|
||||
HMAC_BLAKE2B_384 Algorithm = hmacPrefix + "-" + blake2b_384String
|
||||
HMAC_BLAKE2B_512 Algorithm = hmacPrefix + "-" + blake2b_512String
|
||||
BLAKE2S_256 Algorithm = blake2s_256String
|
||||
BLAKE2B_256 Algorithm = blake2b_256String
|
||||
BLAKE2B_384 Algorithm = blake2b_384String
|
||||
BLAKE2B_512 Algorithm = blake2b_512String
|
||||
// RSA-based algorithms.
|
||||
RSA_SHA1 Algorithm = rsaPrefix + "-" + sha1String
|
||||
RSA_SHA224 Algorithm = rsaPrefix + "-" + sha224String
|
||||
// RSA_SHA256 is the default algorithm.
|
||||
RSA_SHA256 Algorithm = rsaPrefix + "-" + sha256String
|
||||
RSA_SHA384 Algorithm = rsaPrefix + "-" + sha384String
|
||||
RSA_SHA512 Algorithm = rsaPrefix + "-" + sha512String
|
||||
RSA_RIPEMD160 Algorithm = rsaPrefix + "-" + ripemd160String
|
||||
// ECDSA algorithms
|
||||
ECDSA_SHA224 Algorithm = ecdsaPrefix + "-" + sha224String
|
||||
ECDSA_SHA256 Algorithm = ecdsaPrefix + "-" + sha256String
|
||||
ECDSA_SHA384 Algorithm = ecdsaPrefix + "-" + sha384String
|
||||
ECDSA_SHA512 Algorithm = ecdsaPrefix + "-" + sha512String
|
||||
ECDSA_RIPEMD160 Algorithm = ecdsaPrefix + "-" + ripemd160String
|
||||
// ED25519 algorithms
|
||||
// can only be SHA512
|
||||
ED25519 Algorithm = ed25519Prefix
|
||||
|
||||
// Just because you can glue things together, doesn't mean they will
|
||||
// work. The following options are not supported.
|
||||
rsa_SHA3_224 Algorithm = rsaPrefix + "-" + sha3_224String
|
||||
rsa_SHA3_256 Algorithm = rsaPrefix + "-" + sha3_256String
|
||||
rsa_SHA3_384 Algorithm = rsaPrefix + "-" + sha3_384String
|
||||
rsa_SHA3_512 Algorithm = rsaPrefix + "-" + sha3_512String
|
||||
rsa_SHA512_224 Algorithm = rsaPrefix + "-" + sha512_224String
|
||||
rsa_SHA512_256 Algorithm = rsaPrefix + "-" + sha512_256String
|
||||
rsa_BLAKE2S_256 Algorithm = rsaPrefix + "-" + blake2s_256String
|
||||
rsa_BLAKE2B_256 Algorithm = rsaPrefix + "-" + blake2b_256String
|
||||
rsa_BLAKE2B_384 Algorithm = rsaPrefix + "-" + blake2b_384String
|
||||
rsa_BLAKE2B_512 Algorithm = rsaPrefix + "-" + blake2b_512String
|
||||
)
|
||||
|
||||
// HTTP Signatures can be applied to different HTTP headers, depending on the
|
||||
// expected application behavior.
|
||||
type SignatureScheme string
|
||||
|
||||
const (
|
||||
// Signature will place the HTTP Signature into the 'Signature' HTTP
|
||||
// header.
|
||||
Signature SignatureScheme = "Signature"
|
||||
// Authorization will place the HTTP Signature into the 'Authorization'
|
||||
// HTTP header.
|
||||
Authorization SignatureScheme = "Authorization"
|
||||
)
|
||||
|
||||
const (
|
||||
// The HTTP Signatures specification uses the "Signature" auth-scheme
|
||||
// for the Authorization header. This is coincidentally named, but not
|
||||
// semantically the same, as the "Signature" HTTP header value.
|
||||
signatureAuthScheme = "Signature"
|
||||
)
|
||||
|
||||
// There are subtle differences to the values in the header. The Authorization
|
||||
// header has an 'auth-scheme' value that must be prefixed to the rest of the
|
||||
// key and values.
|
||||
func (s SignatureScheme) authScheme() string {
|
||||
switch s {
|
||||
case Authorization:
|
||||
return signatureAuthScheme
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// 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_SHA1
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Verifier verifies HTTP Signatures.
|
||||
//
|
||||
// It will determine which of the supported headers has the parameters
|
||||
// that define the signature.
|
||||
//
|
||||
// Verifiers are not safe to use between multiple goroutines.
|
||||
//
|
||||
// Note that verification ignores the deprecated 'algorithm' parameter.
|
||||
type Verifier interface {
|
||||
// KeyId gets the public key id that the signature is signed with.
|
||||
//
|
||||
// Note that the application is expected to determine the algorithm
|
||||
// used based on metadata or out-of-band information for this key id.
|
||||
KeyId() string
|
||||
// Verify accepts the public key specified by KeyId and returns an
|
||||
// error if verification fails or if the signature is malformed. The
|
||||
// algorithm must be the one used to create the signature in order to
|
||||
// pass verification. The algorithm is determined based on metadata or
|
||||
// out-of-band information for the key id.
|
||||
//
|
||||
// If the signature was created using a MAC based algorithm, then the
|
||||
// key is expected to be of type []byte. If the signature was created
|
||||
// using an RSA based algorithm, then the public key is expected to be
|
||||
// of type *rsa.PublicKey.
|
||||
Verify(pKey crypto.PublicKey, algo Algorithm) error
|
||||
}
|
||||
|
||||
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", algo)
|
||||
}
|
||||
|
||||
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", algo)
|
||||
}
|
||||
c := &macSigner{
|
||||
m: m,
|
||||
dAlgo: dAlgo,
|
||||
headers: headers,
|
||||
targetHeader: scheme,
|
||||
prefix: scheme.authScheme(),
|
||||
created: created,
|
||||
expires: expires,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
+334
@@ -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
@@ -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", algo)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user