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:
naudachu
2026-08-11 19:29:38 +05:00
parent 9480e48312
commit 1239fdee70
292 changed files with 51205 additions and 864 deletions
+3
View File
@@ -0,0 +1,3 @@
language: go
go:
- 1.15
+29
View File
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2018, go-fed
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+115
View File
@@ -0,0 +1,115 @@
# httpsig
Forked from <https://github.com/go-fed/httpsig>
> HTTP Signatures made simple
[![Build Status][Build-Status-Image]][Build-Status-Url] [![Go Reference][Go-Reference-Image]][Go-Reference-Url]
[![Go Report Card][Go-Report-Card-Image]][Go-Report-Card-Url] [![License][License-Image]][License-Url]
[![Chat][Chat-Image]][Chat-Url] [![OpenCollective][OpenCollective-Image]][OpenCollective-Url]
`go get github.com/42wim/httpsig`
Implementation of [HTTP Signatures](https://tools.ietf.org/html/draft-cavage-http-signatures).
Supports many different combinations of MAC, HMAC signing of hash, or RSA
signing of hash schemes. Its goals are:
* Have a very simple interface for signing and validating
* Support a variety of signing algorithms and combinations
* Support setting either headers (`Authorization` or `Signature`)
* Remaining flexible with headers included in the signing string
* Support both HTTP requests and responses
* Explicitly not support known-cryptographically weak algorithms
* Support automatic signing and validating Digest headers
## How to use
`import "github.com/42wim/httpsig"`
### Signing
Signing a request or response requires creating a new `Signer` and using it:
```
func sign(privateKey crypto.PrivateKey, pubKeyId string, r *http.Request) error {
prefs := []httpsig.Algorithm{httpsig.RSA_SHA512, httpsig.RSA_SHA256}
digestAlgorithm := DigestSha256
// The "Date" and "Digest" headers must already be set on r, as well as r.URL.
headersToSign := []string{httpsig.RequestTarget, "date", "digest"}
signer, chosenAlgo, err := httpsig.NewSigner(prefs, digestAlgorithm, headersToSign, httpsig.Signature)
if err != nil {
return err
}
// To sign the digest, we need to give the signer a copy of the body...
// ...but it is optional, no digest will be signed if given "nil"
body := ...
// If r were a http.ResponseWriter, call SignResponse instead.
return signer.SignRequest(privateKey, pubKeyId, r, body)
}
```
`Signer`s are not safe for concurrent use by goroutines, so be sure to guard
access:
```
type server struct {
signer httpsig.Signer
mu *sync.Mutex
}
func (s *server) handlerFunc(w http.ResponseWriter, r *http.Request) {
privateKey := ...
pubKeyId := ...
// Set headers and such on w
s.mu.Lock()
defer s.mu.Unlock()
// To sign the digest, we need to give the signer a copy of the response body...
// ...but it is optional, no digest will be signed if given "nil"
body := ...
err := s.signer.SignResponse(privateKey, pubKeyId, w, body)
if err != nil {
...
}
...
}
```
The `pubKeyId` will be used at verification time.
### Verifying
Verifying requires an application to use the `pubKeyId` to both retrieve the key
needed for verification as well as determine the algorithm to use. Use a
`Verifier`:
```
func verify(r *http.Request) error {
verifier, err := httpsig.NewVerifier(r)
if err != nil {
return err
}
pubKeyId := verifier.KeyId()
var algo httpsig.Algorithm = ...
var pubKey crypto.PublicKey = ...
// The verifier will verify the Digest in addition to the HTTP signature
return verifier.Verify(pubKey, algo)
}
```
`Verifier`s are not safe for concurrent use by goroutines, but since they are
constructed on a per-request or per-response basis it should not be a common
restriction.
[Build-Status-Image]: https://travis-ci.org/42wim/httpsig.svg?branch=master
[Build-Status-Url]: https://travis-ci.org/42wim/httpsig
[Go-Reference-Image]: https://pkg.go.dev/badge/github.com/42wim/httpsig
[Go-Reference-Url]: https://pkg.go.dev/github.com/42wim/httpsig
[Go-Report-Card-Image]: https://goreportcard.com/badge/github.com/42wim/httpsig
[Go-Report-Card-Url]: https://goreportcard.com/report/github.com/42wim/httpsig
[License-Image]: https://img.shields.io/github/license/42wim/httpsig?color=blue
[License-Url]: https://opensource.org/licenses/BSD-3-Clause
[Chat-Image]: https://img.shields.io/matrix/42wim:feneas.org?server_fqdn=matrix.org
[Chat-Url]: https://matrix.to/#/!BLOSvIyKTDLIVjRKSc:feneas.org?via=feneas.org&via=matrix.org
[OpenCollective-Image]: https://img.shields.io/opencollective/backers/42wim-activitypub-labs
[OpenCollective-Url]: https://opencollective.com/42wim-activitypub-labs
+550
View File
@@ -0,0 +1,550 @@
package httpsig
import (
"crypto"
"crypto/ecdsa"
"crypto/hmac"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"crypto/subtle" // Use should trigger great care
"encoding/asn1"
"errors"
"fmt"
"hash"
"io"
"math/big"
"strings"
"golang.org/x/crypto/blake2b"
"golang.org/x/crypto/blake2s"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/sha3"
"golang.org/x/crypto/ssh"
)
const (
hmacPrefix = "hmac"
rsaPrefix = "rsa"
sshPrefix = "ssh"
ecdsaPrefix = "ecdsa"
ed25519Prefix = "ed25519"
sha224String = "sha224"
sha256String = "sha256"
sha384String = "sha384"
sha512String = "sha512"
sha3_224String = "sha3-224"
sha3_256String = "sha3-256"
sha3_384String = "sha3-384"
sha3_512String = "sha3-512"
sha512_224String = "sha512-224"
sha512_256String = "sha512-256"
blake2s_256String = "blake2s-256"
blake2b_256String = "blake2b-256"
blake2b_384String = "blake2b-384"
blake2b_512String = "blake2b-512"
)
var blake2Algorithms = map[crypto.Hash]bool{
crypto.BLAKE2s_256: true,
crypto.BLAKE2b_256: true,
crypto.BLAKE2b_384: true,
crypto.BLAKE2b_512: true,
}
var hashToDef = map[crypto.Hash]struct {
name string
new func(key []byte) (hash.Hash, error) // Only MACers will accept a key
}{
// Which standard names these?
// The spec lists the following as a canonical reference, which is dead:
// http://www.iana.org/assignments/signature-algorithms
//
// Note that the forbidden hashes have an invalid 'new' function.
crypto.SHA224: {sha224String, func(key []byte) (hash.Hash, error) { return sha256.New224(), nil }},
crypto.SHA256: {sha256String, func(key []byte) (hash.Hash, error) { return sha256.New(), nil }},
crypto.SHA384: {sha384String, func(key []byte) (hash.Hash, error) { return sha512.New384(), nil }},
crypto.SHA512: {sha512String, func(key []byte) (hash.Hash, error) { return sha512.New(), nil }},
crypto.SHA3_224: {sha3_224String, func(key []byte) (hash.Hash, error) { return sha3.New224(), nil }},
crypto.SHA3_256: {sha3_256String, func(key []byte) (hash.Hash, error) { return sha3.New256(), nil }},
crypto.SHA3_384: {sha3_384String, func(key []byte) (hash.Hash, error) { return sha3.New384(), nil }},
crypto.SHA3_512: {sha3_512String, func(key []byte) (hash.Hash, error) { return sha3.New512(), nil }},
crypto.SHA512_224: {sha512_224String, func(key []byte) (hash.Hash, error) { return sha512.New512_224(), nil }},
crypto.SHA512_256: {sha512_256String, func(key []byte) (hash.Hash, error) { return sha512.New512_256(), nil }},
crypto.BLAKE2s_256: {blake2s_256String, func(key []byte) (hash.Hash, error) { return blake2s.New256(key) }},
crypto.BLAKE2b_256: {blake2b_256String, func(key []byte) (hash.Hash, error) { return blake2b.New256(key) }},
crypto.BLAKE2b_384: {blake2b_384String, func(key []byte) (hash.Hash, error) { return blake2b.New384(key) }},
crypto.BLAKE2b_512: {blake2b_512String, func(key []byte) (hash.Hash, error) { return blake2b.New512(key) }},
}
var stringToHash map[string]crypto.Hash
const (
defaultAlgorithm = RSA_SHA256
defaultAlgorithmHashing = sha256String
)
func init() {
stringToHash = make(map[string]crypto.Hash, len(hashToDef))
for k, v := range hashToDef {
stringToHash[v.name] = k
}
// This should guarantee that at runtime the defaultAlgorithm will not
// result in errors when fetching a macer or signer (see algorithms.go)
if ok, err := isAvailable(string(defaultAlgorithmHashing)); err != nil {
panic(err)
} else if !ok {
panic(fmt.Sprintf("the default httpsig algorithm is unavailable: %q", defaultAlgorithm))
}
}
func isForbiddenHash(h crypto.Hash) bool {
return false
}
// signer is an internally public type.
type signer interface {
Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error)
Verify(pub crypto.PublicKey, toHash, signature []byte) error
String() string
}
// macer is an internally public type.
type macer interface {
Sign(sig, key []byte) ([]byte, error)
Equal(sig, actualMAC, key []byte) (bool, error)
String() string
}
var _ macer = &hmacAlgorithm{}
type hmacAlgorithm struct {
fn func(key []byte) (hash.Hash, error)
kind crypto.Hash
}
func (h *hmacAlgorithm) Sign(sig, key []byte) ([]byte, error) {
hs, err := h.fn(key)
if err != nil {
return nil, err
}
if err = setSig(hs, sig); err != nil {
return nil, err
}
return hs.Sum(nil), nil
}
func (h *hmacAlgorithm) Equal(sig, actualMAC, key []byte) (bool, error) {
hs, err := h.fn(key)
if err != nil {
return false, err
}
defer hs.Reset()
err = setSig(hs, sig)
if err != nil {
return false, err
}
expected := hs.Sum(nil)
return hmac.Equal(actualMAC, expected), nil
}
func (h *hmacAlgorithm) String() string {
return fmt.Sprintf("%s-%s", hmacPrefix, hashToDef[h.kind].name)
}
var _ signer = &rsaAlgorithm{}
type rsaAlgorithm struct {
hash.Hash
kind crypto.Hash
sshSigner ssh.Signer
}
func (r *rsaAlgorithm) setSig(b []byte) error {
n, err := r.Write(b)
if err != nil {
r.Reset()
return err
} else if n != len(b) {
r.Reset()
return fmt.Errorf("could only write %d of %d bytes of signature to hash", n, len(b))
}
return nil
}
// Code from https://github.com/cloudtools/ssh-cert-authority/pull/49/files
// This interface provides a way to reach the exported, but not accessible SignWithOpts() method
// in x/crypto/ssh/agent. Access to this is needed to sign with more secure signing algorithms
type agentKeyringSigner interface {
SignWithOpts(rand io.Reader, data []byte, opts crypto.SignerOpts) (*ssh.Signature, error)
}
// A struct to wrap an SSH Signer with one that will switch to SHA256 Signatures.
// Replaces the call to Sign() with a call to SignWithOpts using HashFunc() algorithm.
type Sha256Signer struct {
ssh.Signer
}
func (s Sha256Signer) HashFunc() crypto.Hash {
return crypto.SHA256
}
func (s Sha256Signer) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
if aks, ok := s.Signer.(agentKeyringSigner); !ok {
return nil, fmt.Errorf("ssh: can't wrap a non ssh agentKeyringSigner")
} else {
return aks.SignWithOpts(rand, data, s)
}
}
func (r *rsaAlgorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) {
if r.sshSigner != nil {
var (
sshsig *ssh.Signature
err error
)
// are we using an SSH Agent
if _, ok := r.sshSigner.(agentKeyringSigner); ok {
signer := Sha256Signer{r.sshSigner}
sshsig, err = signer.Sign(rand, sig)
if err != nil {
return nil, err
}
} else {
sshsig, err = r.sshSigner.(ssh.AlgorithmSigner).SignWithAlgorithm(rand, sig, ssh.SigAlgoRSASHA2256)
if err != nil {
return nil, err
}
}
return sshsig.Blob, nil
}
defer r.Reset()
if err := r.setSig(sig); err != nil {
return nil, err
}
rsaK, ok := p.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("crypto.PrivateKey is not *rsa.PrivateKey")
}
return rsa.SignPKCS1v15(rand, rsaK, r.kind, r.Sum(nil))
}
func (r *rsaAlgorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) error {
defer r.Reset()
rsaK, ok := pub.(*rsa.PublicKey)
if !ok {
return errors.New("crypto.PublicKey is not *rsa.PublicKey")
}
if err := r.setSig(toHash); err != nil {
return err
}
return rsa.VerifyPKCS1v15(rsaK, r.kind, r.Sum(nil), signature)
}
func (r *rsaAlgorithm) String() string {
return fmt.Sprintf("%s-%s", rsaPrefix, hashToDef[r.kind].name)
}
var _ signer = &ed25519Algorithm{}
type ed25519Algorithm struct {
sshSigner ssh.Signer
}
func (r *ed25519Algorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) {
if r.sshSigner != nil {
sshsig, err := r.sshSigner.Sign(rand, sig)
if err != nil {
return nil, err
}
return sshsig.Blob, nil
}
ed25519K, ok := p.(ed25519.PrivateKey)
if !ok {
return nil, errors.New("crypto.PrivateKey is not ed25519.PrivateKey")
}
return ed25519.Sign(ed25519K, sig), nil
}
func (r *ed25519Algorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) error {
ed25519K, ok := pub.(ed25519.PublicKey)
if !ok {
return errors.New("crypto.PublicKey is not ed25519.PublicKey")
}
if ed25519.Verify(ed25519K, toHash, signature) {
return nil
}
return errors.New("ed25519 verify failed")
}
func (r *ed25519Algorithm) String() string {
return fmt.Sprintf("%s", ed25519Prefix)
}
var _ signer = &ecdsaAlgorithm{}
type ecdsaAlgorithm struct {
hash.Hash
kind crypto.Hash
}
func (r *ecdsaAlgorithm) setSig(b []byte) error {
n, err := r.Write(b)
if err != nil {
r.Reset()
return err
} else if n != len(b) {
r.Reset()
return fmt.Errorf("could only write %d of %d bytes of signature to hash", n, len(b))
}
return nil
}
type ECDSASignature struct {
R, S *big.Int
}
func (r *ecdsaAlgorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) {
defer r.Reset()
if err := r.setSig(sig); err != nil {
return nil, err
}
ecdsaK, ok := p.(*ecdsa.PrivateKey)
if !ok {
return nil, errors.New("crypto.PrivateKey is not *ecdsa.PrivateKey")
}
R, S, err := ecdsa.Sign(rand, ecdsaK, r.Sum(nil))
if err != nil {
return nil, err
}
signature := ECDSASignature{R: R, S: S}
bytes, err := asn1.Marshal(signature)
return bytes, err
}
func (r *ecdsaAlgorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) error {
defer r.Reset()
ecdsaK, ok := pub.(*ecdsa.PublicKey)
if !ok {
return errors.New("crypto.PublicKey is not *ecdsa.PublicKey")
}
if err := r.setSig(toHash); err != nil {
return err
}
sig := new(ECDSASignature)
_, err := asn1.Unmarshal(signature, sig)
if err != nil {
return err
}
if ecdsa.Verify(ecdsaK, r.Sum(nil), sig.R, sig.S) {
return nil
} else {
return errors.New("Invalid signature")
}
}
func (r *ecdsaAlgorithm) String() string {
return fmt.Sprintf("%s-%s", ecdsaPrefix, hashToDef[r.kind].name)
}
var _ macer = &blakeMacAlgorithm{}
type blakeMacAlgorithm struct {
fn func(key []byte) (hash.Hash, error)
kind crypto.Hash
}
func (r *blakeMacAlgorithm) Sign(sig, key []byte) ([]byte, error) {
hs, err := r.fn(key)
if err != nil {
return nil, err
}
if err = setSig(hs, sig); err != nil {
return nil, err
}
return hs.Sum(nil), nil
}
func (r *blakeMacAlgorithm) Equal(sig, actualMAC, key []byte) (bool, error) {
hs, err := r.fn(key)
if err != nil {
return false, err
}
defer hs.Reset()
err = setSig(hs, sig)
if err != nil {
return false, err
}
expected := hs.Sum(nil)
return subtle.ConstantTimeCompare(actualMAC, expected) == 1, nil
}
func (r *blakeMacAlgorithm) String() string {
return fmt.Sprintf("%s", hashToDef[r.kind].name)
}
func setSig(a hash.Hash, b []byte) error {
n, err := a.Write(b)
if err != nil {
a.Reset()
return err
} else if n != len(b) {
a.Reset()
return fmt.Errorf("could only write %d of %d bytes of signature to hash", n, len(b))
}
return nil
}
// IsSupportedHttpSigAlgorithm returns true if the string is supported by this
// library, is not a hash known to be weak, and is supported by the hardware.
func IsSupportedHttpSigAlgorithm(algo string) bool {
a, err := isAvailable(algo)
return a && err == nil
}
// isAvailable is an internally public function
func isAvailable(algo string) (bool, error) {
c, ok := stringToHash[algo]
if !ok {
return false, fmt.Errorf("no match for %q", algo)
}
if isForbiddenHash(c) {
return false, fmt.Errorf("forbidden hash type in %q", algo)
}
return c.Available(), nil
}
func newAlgorithmConstructor(algo string) (fn func(k []byte) (hash.Hash, error), c crypto.Hash, e error) {
ok := false
c, ok = stringToHash[algo]
if !ok {
e = fmt.Errorf("no match for %q", algo)
return
}
if isForbiddenHash(c) {
e = fmt.Errorf("forbidden hash type in %q", algo)
return
}
algoDef, ok := hashToDef[c]
if !ok {
e = fmt.Errorf("have crypto.Hash %v but no definition", c)
return
}
fn = func(key []byte) (hash.Hash, error) {
h, err := algoDef.new(key)
if err != nil {
return nil, err
}
return h, nil
}
return
}
func newAlgorithm(algo string, key []byte) (hash.Hash, crypto.Hash, error) {
fn, c, err := newAlgorithmConstructor(algo)
if err != nil {
return nil, c, err
}
h, err := fn(key)
return h, c, err
}
func signerFromSSHSigner(sshSigner ssh.Signer, s string) (signer, error) {
switch {
case strings.HasPrefix(s, rsaPrefix):
return &rsaAlgorithm{
sshSigner: sshSigner,
}, nil
case strings.HasPrefix(s, ed25519Prefix):
return &ed25519Algorithm{
sshSigner: sshSigner,
}, nil
default:
return nil, fmt.Errorf("no signer matching %q", s)
}
}
// signerFromString is an internally public method constructor
func signerFromString(s string) (signer, error) {
s = strings.ToLower(s)
isEcdsa := false
isEd25519 := false
var algo string = ""
if strings.HasPrefix(s, ecdsaPrefix) {
algo = strings.TrimPrefix(s, ecdsaPrefix+"-")
isEcdsa = true
} else if strings.HasPrefix(s, rsaPrefix) {
algo = strings.TrimPrefix(s, rsaPrefix+"-")
} else if strings.HasPrefix(s, ed25519Prefix) {
isEd25519 = true
algo = "sha512"
} else {
return nil, fmt.Errorf("no signer matching %q", s)
}
hash, cHash, err := newAlgorithm(algo, nil)
if err != nil {
return nil, err
}
if isEd25519 {
return &ed25519Algorithm{}, nil
}
if isEcdsa {
return &ecdsaAlgorithm{
Hash: hash,
kind: cHash,
}, nil
}
return &rsaAlgorithm{
Hash: hash,
kind: cHash,
}, nil
}
// macerFromString is an internally public method constructor
func macerFromString(s string) (macer, error) {
s = strings.ToLower(s)
if strings.HasPrefix(s, hmacPrefix) {
algo := strings.TrimPrefix(s, hmacPrefix+"-")
hashFn, cHash, err := newAlgorithmConstructor(algo)
if err != nil {
return nil, err
}
// Ensure below does not panic
_, err = hashFn(nil)
if err != nil {
return nil, err
}
return &hmacAlgorithm{
fn: func(key []byte) (hash.Hash, error) {
return hmac.New(func() hash.Hash {
h, e := hashFn(nil)
if e != nil {
panic(e)
}
return h
}, key), nil
},
kind: cHash,
}, nil
} else if bl, ok := stringToHash[s]; ok && blake2Algorithms[bl] {
hashFn, cHash, err := newAlgorithmConstructor(s)
if err != nil {
return nil, err
}
return &blakeMacAlgorithm{
fn: hashFn,
kind: cHash,
}, nil
} else {
return nil, fmt.Errorf("no MACer matching %q", s)
}
}
+120
View File
@@ -0,0 +1,120 @@
package httpsig
import (
"bytes"
"crypto"
"encoding/base64"
"fmt"
"hash"
"net/http"
"strings"
)
type DigestAlgorithm string
const (
DigestSha256 DigestAlgorithm = "SHA-256"
DigestSha512 = "SHA-512"
)
var digestToDef = map[DigestAlgorithm]crypto.Hash{
DigestSha256: crypto.SHA256,
DigestSha512: crypto.SHA512,
}
// IsSupportedDigestAlgorithm returns true if hte string is supported by this
// library, is not a hash known to be weak, and is supported by the hardware.
func IsSupportedDigestAlgorithm(algo string) bool {
uc := DigestAlgorithm(strings.ToUpper(algo))
c, ok := digestToDef[uc]
return ok && c.Available()
}
func getHash(alg DigestAlgorithm) (h hash.Hash, toUse DigestAlgorithm, err error) {
upper := DigestAlgorithm(strings.ToUpper(string(alg)))
c, ok := digestToDef[upper]
if !ok {
err = fmt.Errorf("unknown or unsupported Digest algorithm: %s", alg)
} else if !c.Available() {
err = fmt.Errorf("unavailable Digest algorithm: %s", alg)
} else {
h = c.New()
toUse = upper
}
return
}
const (
digestHeader = "Digest"
digestDelim = "="
)
func addDigest(r *http.Request, algo DigestAlgorithm, b []byte) (err error) {
_, ok := r.Header[digestHeader]
if ok {
err = fmt.Errorf("cannot add Digest: Digest is already set")
return
}
var h hash.Hash
var a DigestAlgorithm
h, a, err = getHash(algo)
if err != nil {
return
}
h.Write(b)
sum := h.Sum(nil)
r.Header.Add(digestHeader,
fmt.Sprintf("%s%s%s",
a,
digestDelim,
base64.StdEncoding.EncodeToString(sum[:])))
return
}
func addDigestResponse(r http.ResponseWriter, algo DigestAlgorithm, b []byte) (err error) {
_, ok := r.Header()[digestHeader]
if ok {
err = fmt.Errorf("cannot add Digest: Digest is already set")
return
}
var h hash.Hash
var a DigestAlgorithm
h, a, err = getHash(algo)
if err != nil {
return
}
h.Write(b)
sum := h.Sum(nil)
r.Header().Add(digestHeader,
fmt.Sprintf("%s%s%s",
a,
digestDelim,
base64.StdEncoding.EncodeToString(sum[:])))
return
}
func verifyDigest(r *http.Request, body *bytes.Buffer) (err error) {
d := r.Header.Get(digestHeader)
if len(d) == 0 {
err = fmt.Errorf("cannot verify Digest: request has no Digest header")
return
}
elem := strings.SplitN(d, digestDelim, 2)
if len(elem) != 2 {
err = fmt.Errorf("cannot verify Digest: malformed Digest: %s", d)
return
}
var h hash.Hash
h, _, err = getHash(DigestAlgorithm(elem[0]))
if err != nil {
return
}
h.Write(body.Bytes())
sum := h.Sum(nil)
encSum := base64.StdEncoding.EncodeToString(sum[:])
if encSum != elem[1] {
err = fmt.Errorf("cannot verify Digest: header Digest does not match the digest of the request body")
return
}
return
}
+356
View File
@@ -0,0 +1,356 @@
// Implements HTTP request and response signing and verification. Supports the
// major MAC and asymmetric key signature algorithms. It has several safety
// restrictions: One, none of the widely known non-cryptographically safe
// algorithms are permitted; Two, the RSA SHA256 algorithms must be available in
// the binary (and it should, barring export restrictions); Finally, the library
// assumes either the 'Authorizationn' or 'Signature' headers are to be set (but
// not both).
package httpsig
import (
"crypto"
"fmt"
"net/http"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
// Algorithm specifies a cryptography secure algorithm for signing HTTP requests
// and responses.
type Algorithm string
const (
// MAC-based algoirthms.
HMAC_SHA224 Algorithm = hmacPrefix + "-" + sha224String
HMAC_SHA256 Algorithm = hmacPrefix + "-" + sha256String
HMAC_SHA384 Algorithm = hmacPrefix + "-" + sha384String
HMAC_SHA512 Algorithm = hmacPrefix + "-" + sha512String
HMAC_SHA3_224 Algorithm = hmacPrefix + "-" + sha3_224String
HMAC_SHA3_256 Algorithm = hmacPrefix + "-" + sha3_256String
HMAC_SHA3_384 Algorithm = hmacPrefix + "-" + sha3_384String
HMAC_SHA3_512 Algorithm = hmacPrefix + "-" + sha3_512String
HMAC_SHA512_224 Algorithm = hmacPrefix + "-" + sha512_224String
HMAC_SHA512_256 Algorithm = hmacPrefix + "-" + sha512_256String
HMAC_BLAKE2S_256 Algorithm = hmacPrefix + "-" + blake2s_256String
HMAC_BLAKE2B_256 Algorithm = hmacPrefix + "-" + blake2b_256String
HMAC_BLAKE2B_384 Algorithm = hmacPrefix + "-" + blake2b_384String
HMAC_BLAKE2B_512 Algorithm = hmacPrefix + "-" + blake2b_512String
BLAKE2S_256 Algorithm = blake2s_256String
BLAKE2B_256 Algorithm = blake2b_256String
BLAKE2B_384 Algorithm = blake2b_384String
BLAKE2B_512 Algorithm = blake2b_512String
// RSA-based algorithms.
RSA_SHA224 Algorithm = rsaPrefix + "-" + sha224String
// RSA_SHA256 is the default algorithm.
RSA_SHA256 Algorithm = rsaPrefix + "-" + sha256String
RSA_SHA384 Algorithm = rsaPrefix + "-" + sha384String
RSA_SHA512 Algorithm = rsaPrefix + "-" + sha512String
// ECDSA algorithms
ECDSA_SHA224 Algorithm = ecdsaPrefix + "-" + sha224String
ECDSA_SHA256 Algorithm = ecdsaPrefix + "-" + sha256String
ECDSA_SHA384 Algorithm = ecdsaPrefix + "-" + sha384String
ECDSA_SHA512 Algorithm = ecdsaPrefix + "-" + sha512String
// ED25519 algorithms
// can only be SHA512
ED25519 Algorithm = ed25519Prefix
// Just because you can glue things together, doesn't mean they will
// work. The following options are not supported.
rsa_SHA3_224 Algorithm = rsaPrefix + "-" + sha3_224String
rsa_SHA3_256 Algorithm = rsaPrefix + "-" + sha3_256String
rsa_SHA3_384 Algorithm = rsaPrefix + "-" + sha3_384String
rsa_SHA3_512 Algorithm = rsaPrefix + "-" + sha3_512String
rsa_SHA512_224 Algorithm = rsaPrefix + "-" + sha512_224String
rsa_SHA512_256 Algorithm = rsaPrefix + "-" + sha512_256String
rsa_BLAKE2S_256 Algorithm = rsaPrefix + "-" + blake2s_256String
rsa_BLAKE2B_256 Algorithm = rsaPrefix + "-" + blake2b_256String
rsa_BLAKE2B_384 Algorithm = rsaPrefix + "-" + blake2b_384String
rsa_BLAKE2B_512 Algorithm = rsaPrefix + "-" + blake2b_512String
)
// HTTP Signatures can be applied to different HTTP headers, depending on the
// expected application behavior.
type SignatureScheme string
const (
// Signature will place the HTTP Signature into the 'Signature' HTTP
// header.
Signature SignatureScheme = "Signature"
// Authorization will place the HTTP Signature into the 'Authorization'
// HTTP header.
Authorization SignatureScheme = "Authorization"
)
const (
// The HTTP Signatures specification uses the "Signature" auth-scheme
// for the Authorization header. This is coincidentally named, but not
// semantically the same, as the "Signature" HTTP header value.
signatureAuthScheme = "Signature"
)
// There are subtle differences to the values in the header. The Authorization
// header has an 'auth-scheme' value that must be prefixed to the rest of the
// key and values.
func (s SignatureScheme) authScheme() string {
switch s {
case Authorization:
return signatureAuthScheme
default:
return ""
}
}
// Signers will sign HTTP requests or responses based on the algorithms and
// headers selected at creation time.
//
// Signers are not safe to use between multiple goroutines.
//
// Note that signatures do set the deprecated 'algorithm' parameter for
// backwards compatibility.
type Signer interface {
// SignRequest signs the request using a private key. The public key id
// is used by the HTTP server to identify which key to use to verify the
// signature.
//
// If the Signer was created using a MAC based algorithm, then the key
// is expected to be of type []byte. If the Signer was created using an
// RSA based algorithm, then the private key is expected to be of type
// *rsa.PrivateKey.
//
// A Digest (RFC 3230) will be added to the request. The body provided
// must match the body used in the request, and is allowed to be nil.
// The Digest ensures the request body is not tampered with in flight,
// and if the signer is created to also sign the "Digest" header, the
// HTTP Signature will then ensure both the Digest and body are not both
// modified to maliciously represent different content.
SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error
// SignResponse signs the response using a private key. The public key
// id is used by the HTTP client to identify which key to use to verify
// the signature.
//
// If the Signer was created using a MAC based algorithm, then the key
// is expected to be of type []byte. If the Signer was created using an
// RSA based algorithm, then the private key is expected to be of type
// *rsa.PrivateKey.
//
// A Digest (RFC 3230) will be added to the response. The body provided
// must match the body written in the response, and is allowed to be
// nil. The Digest ensures the response body is not tampered with in
// flight, and if the signer is created to also sign the "Digest"
// header, the HTTP Signature will then ensure both the Digest and body
// are not both modified to maliciously represent different content.
SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error
}
// NewSigner creates a new Signer with the provided algorithm preferences to
// make HTTP signatures. Only the first available algorithm will be used, which
// is returned by this function along with the Signer. If none of the preferred
// algorithms were available, then the default algorithm is used. The headers
// specified will be included into the HTTP signatures.
//
// The Digest will also be calculated on a request's body using the provided
// digest algorithm, if "Digest" is one of the headers listed.
//
// The provided scheme determines which header is populated with the HTTP
// Signature.
//
// An error is returned if an unknown or a known cryptographically insecure
// Algorithm is provided.
func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, Algorithm, error) {
for _, pref := range prefs {
s, err := newSigner(pref, dAlgo, headers, scheme, expiresIn)
if err != nil {
continue
}
return s, pref, err
}
s, err := newSigner(defaultAlgorithm, dAlgo, headers, scheme, expiresIn)
return s, defaultAlgorithm, err
}
// Signers will sign HTTP requests or responses based on the algorithms and
// headers selected at creation time.
//
// Signers are not safe to use between multiple goroutines.
//
// Note that signatures do set the deprecated 'algorithm' parameter for
// backwards compatibility.
type SSHSigner interface {
// SignRequest signs the request using ssh.Signer.
// The public key id is used by the HTTP server to identify which key to use
// to verify the signature.
//
// A Digest (RFC 3230) will be added to the request. The body provided
// must match the body used in the request, and is allowed to be nil.
// The Digest ensures the request body is not tampered with in flight,
// and if the signer is created to also sign the "Digest" header, the
// HTTP Signature will then ensure both the Digest and body are not both
// modified to maliciously represent different content.
SignRequest(pubKeyId string, r *http.Request, body []byte) error
// SignResponse signs the response using ssh.Signer. The public key
// id is used by the HTTP client to identify which key to use to verify
// the signature.
//
// A Digest (RFC 3230) will be added to the response. The body provided
// must match the body written in the response, and is allowed to be
// nil. The Digest ensures the response body is not tampered with in
// flight, and if the signer is created to also sign the "Digest"
// header, the HTTP Signature will then ensure both the Digest and body
// are not both modified to maliciously represent different content.
SignResponse(pubKeyId string, r http.ResponseWriter, body []byte) error
}
// NewwSSHSigner creates a new Signer using the specified ssh.Signer
// At the moment only ed25519 ssh keys are supported.
// The headers specified will be included into the HTTP signatures.
//
// The Digest will also be calculated on a request's body using the provided
// digest algorithm, if "Digest" is one of the headers listed.
//
// The provided scheme determines which header is populated with the HTTP
// Signature.
func NewSSHSigner(s ssh.Signer, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SSHSigner, Algorithm, error) {
sshAlgo := getSSHAlgorithm(s.PublicKey().Type())
if sshAlgo == "" {
return nil, "", fmt.Errorf("key type: %s not supported yet.", s.PublicKey().Type())
}
signer, err := newSSHSigner(s, sshAlgo, dAlgo, headers, scheme, expiresIn)
if err != nil {
return nil, "", err
}
return signer, sshAlgo, nil
}
func getSSHAlgorithm(pkType string) Algorithm {
switch {
case strings.HasPrefix(pkType, sshPrefix+"-"+ed25519Prefix):
return ED25519
case strings.HasPrefix(pkType, sshPrefix+"-"+rsaPrefix):
return RSA_SHA256
}
return ""
}
// Verifier verifies HTTP Signatures.
//
// It will determine which of the supported headers has the parameters
// that define the signature.
//
// Verifiers are not safe to use between multiple goroutines.
//
// Note that verification ignores the deprecated 'algorithm' parameter.
type Verifier interface {
// KeyId gets the public key id that the signature is signed with.
//
// Note that the application is expected to determine the algorithm
// used based on metadata or out-of-band information for this key id.
KeyId() string
// Verify accepts the public key specified by KeyId and returns an
// error if verification fails or if the signature is malformed. The
// algorithm must be the one used to create the signature in order to
// pass verification. The algorithm is determined based on metadata or
// out-of-band information for the key id.
//
// If the signature was created using a MAC based algorithm, then the
// key is expected to be of type []byte. If the signature was created
// using an RSA based algorithm, then the public key is expected to be
// of type *rsa.PublicKey.
Verify(pKey crypto.PublicKey, algo Algorithm) error
}
const (
// host is treated specially because golang may not include it in the
// request header map on the server side of a request.
hostHeader = "Host"
)
// NewVerifier verifies the given request. It returns an error if the HTTP
// Signature parameters are not present in any headers, are present in more than
// one header, are malformed, or are missing required parameters. It ignores
// unknown HTTP Signature parameters.
func NewVerifier(r *http.Request) (Verifier, error) {
h := r.Header
if _, hasHostHeader := h[hostHeader]; len(r.Host) > 0 && !hasHostHeader {
h[hostHeader] = []string{r.Host}
}
return newVerifier(h, func(h http.Header, toInclude []string, created int64, expires int64) (string, error) {
return signatureString(h, toInclude, addRequestTarget(r), created, expires)
})
}
// NewResponseVerifier verifies the given response. It returns errors under the
// same conditions as NewVerifier.
func NewResponseVerifier(r *http.Response) (Verifier, error) {
return newVerifier(r.Header, func(h http.Header, toInclude []string, created int64, expires int64) (string, error) {
return signatureString(h, toInclude, requestTargetNotPermitted, created, expires)
})
}
func newSSHSigner(sshSigner ssh.Signer, algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SSHSigner, error) {
var expires, created int64 = 0, 0
if expiresIn != 0 {
created = time.Now().Unix()
expires = created + expiresIn
}
s, err := signerFromSSHSigner(sshSigner, string(algo))
if err != nil {
return nil, fmt.Errorf("no crypto implementation available for ssh algo %q: %s", algo, err)
}
a := &asymmSSHSigner{
asymmSigner: &asymmSigner{
s: s,
dAlgo: dAlgo,
headers: headers,
targetHeader: scheme,
prefix: scheme.authScheme(),
created: created,
expires: expires,
},
}
return a, nil
}
func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, error) {
var expires, created int64 = 0, 0
if expiresIn != 0 {
created = time.Now().Unix()
expires = created + expiresIn
}
s, err := signerFromString(string(algo))
if err == nil {
a := &asymmSigner{
s: s,
dAlgo: dAlgo,
headers: headers,
targetHeader: scheme,
prefix: scheme.authScheme(),
created: created,
expires: expires,
}
return a, nil
}
m, err := macerFromString(string(algo))
if err != nil {
return nil, fmt.Errorf("no crypto implementation available for %q: %s", algo, err)
}
c := &macSigner{
m: m,
dAlgo: dAlgo,
headers: headers,
targetHeader: scheme,
prefix: scheme.authScheme(),
created: created,
expires: expires,
}
return c, nil
}
+334
View File
@@ -0,0 +1,334 @@
package httpsig
import (
"bytes"
"crypto"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"net/textproto"
"strconv"
"strings"
)
const (
// Signature Parameters
keyIdParameter = "keyId"
algorithmParameter = "algorithm"
headersParameter = "headers"
signatureParameter = "signature"
prefixSeparater = " "
parameterKVSeparater = "="
parameterValueDelimiter = "\""
parameterSeparater = ","
headerParameterValueDelim = " "
// RequestTarget specifies to include the http request method and
// entire URI in the signature. Pass it as a header to NewSigner.
RequestTarget = "(request-target)"
createdKey = "created"
expiresKey = "expires"
dateHeader = "date"
// Signature String Construction
headerFieldDelimiter = ": "
headersDelimiter = "\n"
headerValueDelimiter = ", "
requestTargetSeparator = " "
)
var defaultHeaders = []string{dateHeader}
var _ Signer = &macSigner{}
type macSigner struct {
m macer
makeDigest bool
dAlgo DigestAlgorithm
headers []string
targetHeader SignatureScheme
prefix string
created int64
expires int64
}
func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error {
if body != nil {
err := addDigest(r, m.dAlgo, body)
if err != nil {
return err
}
}
s, err := m.signatureString(r)
if err != nil {
return err
}
enc, err := m.signSignature(pKey, s)
if err != nil {
return err
}
setSignatureHeader(r.Header, string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers, m.created, m.expires)
return nil
}
func (m *macSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error {
if body != nil {
err := addDigestResponse(r, m.dAlgo, body)
if err != nil {
return err
}
}
s, err := m.signatureStringResponse(r)
if err != nil {
return err
}
enc, err := m.signSignature(pKey, s)
if err != nil {
return err
}
setSignatureHeader(r.Header(), string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers, m.created, m.expires)
return nil
}
func (m *macSigner) signSignature(pKey crypto.PrivateKey, s string) (string, error) {
pKeyBytes, ok := pKey.([]byte)
if !ok {
return "", fmt.Errorf("private key for MAC signing must be of type []byte")
}
sig, err := m.m.Sign([]byte(s), pKeyBytes)
if err != nil {
return "", err
}
enc := base64.StdEncoding.EncodeToString(sig)
return enc, nil
}
func (m *macSigner) signatureString(r *http.Request) (string, error) {
return signatureString(r.Header, m.headers, addRequestTarget(r), m.created, m.expires)
}
func (m *macSigner) signatureStringResponse(r http.ResponseWriter) (string, error) {
return signatureString(r.Header(), m.headers, requestTargetNotPermitted, m.created, m.expires)
}
var _ Signer = &asymmSigner{}
type asymmSigner struct {
s signer
makeDigest bool
dAlgo DigestAlgorithm
headers []string
targetHeader SignatureScheme
prefix string
created int64
expires int64
}
func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error {
if body != nil {
err := addDigest(r, a.dAlgo, body)
if err != nil {
return err
}
}
s, err := a.signatureString(r)
if err != nil {
return err
}
enc, err := a.signSignature(pKey, s)
if err != nil {
return err
}
setSignatureHeader(r.Header, string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers, a.created, a.expires)
return nil
}
func (a *asymmSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error {
if body != nil {
err := addDigestResponse(r, a.dAlgo, body)
if err != nil {
return err
}
}
s, err := a.signatureStringResponse(r)
if err != nil {
return err
}
enc, err := a.signSignature(pKey, s)
if err != nil {
return err
}
setSignatureHeader(r.Header(), string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers, a.created, a.expires)
return nil
}
func (a *asymmSigner) signSignature(pKey crypto.PrivateKey, s string) (string, error) {
sig, err := a.s.Sign(rand.Reader, pKey, []byte(s))
if err != nil {
return "", err
}
enc := base64.StdEncoding.EncodeToString(sig)
return enc, nil
}
func (a *asymmSigner) signatureString(r *http.Request) (string, error) {
return signatureString(r.Header, a.headers, addRequestTarget(r), a.created, a.expires)
}
func (a *asymmSigner) signatureStringResponse(r http.ResponseWriter) (string, error) {
return signatureString(r.Header(), a.headers, requestTargetNotPermitted, a.created, a.expires)
}
var _ SSHSigner = &asymmSSHSigner{}
type asymmSSHSigner struct {
*asymmSigner
}
func (a *asymmSSHSigner) SignRequest(pubKeyId string, r *http.Request, body []byte) error {
return a.asymmSigner.SignRequest(nil, pubKeyId, r, body)
}
func (a *asymmSSHSigner) SignResponse(pubKeyId string, r http.ResponseWriter, body []byte) error {
return a.asymmSigner.SignResponse(nil, pubKeyId, r, body)
}
func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyId, algo, enc string, headers []string, created int64, expires int64) {
if len(headers) == 0 {
headers = defaultHeaders
}
var b bytes.Buffer
// KeyId
b.WriteString(prefix)
if len(prefix) > 0 {
b.WriteString(prefixSeparater)
}
b.WriteString(keyIdParameter)
b.WriteString(parameterKVSeparater)
b.WriteString(parameterValueDelimiter)
b.WriteString(pubKeyId)
b.WriteString(parameterValueDelimiter)
b.WriteString(parameterSeparater)
// Algorithm
b.WriteString(algorithmParameter)
b.WriteString(parameterKVSeparater)
b.WriteString(parameterValueDelimiter)
b.WriteString("hs2019") //real algorithm is hidden, see newest version of spec draft
b.WriteString(parameterValueDelimiter)
b.WriteString(parameterSeparater)
hasCreated := false
hasExpires := false
for _, h := range headers {
val := strings.ToLower(h)
if val == "("+createdKey+")" {
hasCreated = true
} else if val == "("+expiresKey+")" {
hasExpires = true
}
}
// Created
if hasCreated == true {
b.WriteString(createdKey)
b.WriteString(parameterKVSeparater)
b.WriteString(strconv.FormatInt(created, 10))
b.WriteString(parameterSeparater)
}
// Expires
if hasExpires == true {
b.WriteString(expiresKey)
b.WriteString(parameterKVSeparater)
b.WriteString(strconv.FormatInt(expires, 10))
b.WriteString(parameterSeparater)
}
// Headers
b.WriteString(headersParameter)
b.WriteString(parameterKVSeparater)
b.WriteString(parameterValueDelimiter)
for i, h := range headers {
b.WriteString(strings.ToLower(h))
if i != len(headers)-1 {
b.WriteString(headerParameterValueDelim)
}
}
b.WriteString(parameterValueDelimiter)
b.WriteString(parameterSeparater)
// Signature
b.WriteString(signatureParameter)
b.WriteString(parameterKVSeparater)
b.WriteString(parameterValueDelimiter)
b.WriteString(enc)
b.WriteString(parameterValueDelimiter)
h.Add(targetHeader, b.String())
}
func requestTargetNotPermitted(b *bytes.Buffer) error {
return fmt.Errorf("cannot sign with %q on anything other than an http request", RequestTarget)
}
func addRequestTarget(r *http.Request) func(b *bytes.Buffer) error {
return func(b *bytes.Buffer) error {
b.WriteString(RequestTarget)
b.WriteString(headerFieldDelimiter)
b.WriteString(strings.ToLower(r.Method))
b.WriteString(requestTargetSeparator)
b.WriteString(r.URL.Path)
if r.URL.RawQuery != "" {
b.WriteString("?")
b.WriteString(r.URL.RawQuery)
}
return nil
}
}
func signatureString(values http.Header, include []string, requestTargetFn func(b *bytes.Buffer) error, created int64, expires int64) (string, error) {
if len(include) == 0 {
include = defaultHeaders
}
var b bytes.Buffer
for n, i := range include {
i := strings.ToLower(i)
if i == RequestTarget {
err := requestTargetFn(&b)
if err != nil {
return "", err
}
} else if i == "("+expiresKey+")" {
if expires == 0 {
return "", fmt.Errorf("missing expires value")
}
b.WriteString(i)
b.WriteString(headerFieldDelimiter)
b.WriteString(strconv.FormatInt(expires, 10))
} else if i == "("+createdKey+")" {
if created == 0 {
return "", fmt.Errorf("missing created value")
}
b.WriteString(i)
b.WriteString(headerFieldDelimiter)
b.WriteString(strconv.FormatInt(created, 10))
} else {
hv, ok := values[textproto.CanonicalMIMEHeaderKey(i)]
if !ok {
return "", fmt.Errorf("missing header %q", i)
}
b.WriteString(i)
b.WriteString(headerFieldDelimiter)
for i, v := range hv {
b.WriteString(strings.TrimSpace(v))
if i < len(hv)-1 {
b.WriteString(headerValueDelimiter)
}
}
}
if n < len(include)-1 {
b.WriteString(headersDelimiter)
}
}
return b.String(), nil
}
+184
View File
@@ -0,0 +1,184 @@
package httpsig
import (
"crypto"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
var _ Verifier = &verifier{}
type verifier struct {
header http.Header
kId string
signature string
created int64
expires int64
headers []string
sigStringFn func(http.Header, []string, int64, int64) (string, error)
}
func newVerifier(h http.Header, sigStringFn func(http.Header, []string, int64, int64) (string, error)) (*verifier, error) {
scheme, s, err := getSignatureScheme(h)
if err != nil {
return nil, err
}
kId, sig, headers, created, expires, err := getSignatureComponents(scheme, s)
if created != 0 {
//check if created is not in the future, we assume a maximum clock offset of 10 seconds
now := time.Now().Unix()
if created-now > 10 {
return nil, errors.New("created is in the future")
}
}
if expires != 0 {
//check if expires is in the past, we assume a maximum clock offset of 10 seconds
now := time.Now().Unix()
if now-expires > 10 {
return nil, errors.New("signature expired")
}
}
if err != nil {
return nil, err
}
return &verifier{
header: h,
kId: kId,
signature: sig,
created: created,
expires: expires,
headers: headers,
sigStringFn: sigStringFn,
}, nil
}
func (v *verifier) KeyId() string {
return v.kId
}
func (v *verifier) Verify(pKey crypto.PublicKey, algo Algorithm) error {
s, err := signerFromString(string(algo))
if err == nil {
return v.asymmVerify(s, pKey)
}
m, err := macerFromString(string(algo))
if err == nil {
return v.macVerify(m, pKey)
}
return fmt.Errorf("no crypto implementation available for %q: %s", algo, err)
}
func (v *verifier) macVerify(m macer, pKey crypto.PublicKey) error {
key, ok := pKey.([]byte)
if !ok {
return fmt.Errorf("public key for MAC verifying must be of type []byte")
}
signature, err := v.sigStringFn(v.header, v.headers, v.created, v.expires)
if err != nil {
return err
}
actualMAC, err := base64.StdEncoding.DecodeString(v.signature)
if err != nil {
return err
}
ok, err = m.Equal([]byte(signature), actualMAC, key)
if err != nil {
return err
} else if !ok {
return fmt.Errorf("invalid http signature")
}
return nil
}
func (v *verifier) asymmVerify(s signer, pKey crypto.PublicKey) error {
toHash, err := v.sigStringFn(v.header, v.headers, v.created, v.expires)
if err != nil {
return err
}
signature, err := base64.StdEncoding.DecodeString(v.signature)
if err != nil {
return err
}
err = s.Verify(pKey, []byte(toHash), signature)
if err != nil {
return err
}
return nil
}
func getSignatureScheme(h http.Header) (scheme SignatureScheme, val string, err error) {
s := h.Get(string(Signature))
sigHasAll := strings.Contains(s, keyIdParameter) ||
strings.Contains(s, headersParameter) ||
strings.Contains(s, signatureParameter)
a := h.Get(string(Authorization))
authHasAll := strings.Contains(a, keyIdParameter) ||
strings.Contains(a, headersParameter) ||
strings.Contains(a, signatureParameter)
if sigHasAll && authHasAll {
err = fmt.Errorf("both %q and %q have signature parameters", Signature, Authorization)
return
} else if !sigHasAll && !authHasAll {
err = fmt.Errorf("neither %q nor %q have signature parameters", Signature, Authorization)
return
} else if sigHasAll {
val = s
scheme = Signature
return
} else { // authHasAll
val = a
scheme = Authorization
return
}
}
func getSignatureComponents(scheme SignatureScheme, s string) (kId, sig string, headers []string, created int64, expires int64, err error) {
if as := scheme.authScheme(); len(as) > 0 {
s = strings.TrimPrefix(s, as+prefixSeparater)
}
params := strings.Split(s, parameterSeparater)
for _, p := range params {
kv := strings.SplitN(p, parameterKVSeparater, 2)
if len(kv) != 2 {
err = fmt.Errorf("malformed http signature parameter: %v", kv)
return
}
k := kv[0]
v := strings.Trim(kv[1], parameterValueDelimiter)
switch k {
case keyIdParameter:
kId = v
case createdKey:
created, err = strconv.ParseInt(v, 10, 64)
if err != nil {
return
}
case expiresKey:
expires, err = strconv.ParseInt(v, 10, 64)
if err != nil {
return
}
case algorithmParameter:
// Deprecated, ignore
case headersParameter:
headers = strings.Split(v, headerParameterValueDelim)
case signatureParameter:
sig = v
default:
// Ignore unrecognized parameters
}
}
if len(kId) == 0 {
err = fmt.Errorf("missing %q parameter in http signature", keyIdParameter)
} else if len(sig) == 0 {
err = fmt.Errorf("missing %q parameter in http signature", signatureParameter)
} else if len(headers) == 0 { // Optional
headers = defaultHeaders
}
return
}