mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 14:58:33 +00:00
For #30473 This change adds a vendored `httpsig-go` library to our repo. We cannot use the upstream library because it has not merged the change we need: https://github.com/remitly-oss/httpsig-go/pull/25 Thus, we need our own copy at this point. The instructions for keeping this library up to date (if needed) are in `UPDATE_INSTRUCTIONS`. None of the coderabbitai review comments are relevant to the code/features we are going to use for HTTP message signatures. We will use this library in subsequent PRs for the TPM-backed HTTP message signature feature. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a Go library for HTTP message signing and verification, supporting multiple cryptographic algorithms (RSA, ECDSA, Ed25519, HMAC). * Added utilities for key management, including JWK and PEM key handling. * Provided HTTP client and server helpers for automatic request signing and signature verification. * Implemented structured error handling and metadata extraction for signatures. * **Documentation** * Added comprehensive README, usage examples, and update instructions. * Included license and configuration files for third-party and testing tools. * **Tests** * Added extensive unit, integration, and fuzz tests covering signing, verification, and key handling. * Included official RFC test vectors and various test data files for robust validation. * **Chores** * Integrated continuous integration workflows and ignore files for code quality and security analysis. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
123 lines
3.3 KiB
Go
123 lines
3.3 KiB
Go
package httpsig
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
sfv "github.com/dunglas/httpsfv"
|
|
)
|
|
|
|
const (
|
|
digestAlgoSHA256 = "sha-256"
|
|
digestAlgoSHA512 = "sha-512"
|
|
)
|
|
|
|
var (
|
|
emptySHA256 = sha256.Sum256([]byte{})
|
|
emptySHA512 = sha512.Sum512([]byte{})
|
|
)
|
|
|
|
// digestBody reads the entire body to calculate the digest and returns a new io.ReaderCloser which can be set as the new request body.
|
|
type digestInfo struct {
|
|
Digest []byte
|
|
NewBody io.ReadCloser // NewBody is intended as the http.Request Body replacement. Calculating the digest requires reading the body.
|
|
}
|
|
|
|
func digestBody(digAlgo Digest, body io.ReadCloser) (digestInfo, error) {
|
|
var digest []byte
|
|
// client GET requests have a nil body
|
|
// received/server GET requests have a body but its NoBody
|
|
if body == nil || body == http.NoBody {
|
|
switch digAlgo {
|
|
case DigestSHA256:
|
|
digest = emptySHA256[:]
|
|
case DigestSHA512:
|
|
digest = emptySHA512[:]
|
|
default:
|
|
return digestInfo{}, newError(ErrNoSigUnsupportedDigest, fmt.Sprintf("Unsupported digest algorithm '%s'", digAlgo))
|
|
}
|
|
return digestInfo{
|
|
Digest: digest,
|
|
NewBody: body,
|
|
}, nil
|
|
}
|
|
var buf bytes.Buffer
|
|
if _, err := buf.ReadFrom(body); err != nil {
|
|
return digestInfo{}, newError(ErrNoSigMessageBody, "Failed to read message body to calculate digest", err)
|
|
}
|
|
if err := body.Close(); err != nil {
|
|
return digestInfo{}, newError(ErrNoSigMessageBody, "Failed to close message body to calculate digest", err)
|
|
}
|
|
|
|
switch digAlgo {
|
|
case DigestSHA256:
|
|
d := sha256.Sum256(buf.Bytes())
|
|
digest = d[:]
|
|
case DigestSHA512:
|
|
d := sha512.Sum512(buf.Bytes())
|
|
digest = d[:]
|
|
default:
|
|
return digestInfo{}, newError(ErrNoSigUnsupportedDigest, fmt.Sprintf("Unsupported digest algorithm '%s'", digAlgo))
|
|
}
|
|
|
|
return digestInfo{
|
|
Digest: digest,
|
|
NewBody: io.NopCloser(bytes.NewReader(buf.Bytes())),
|
|
}, nil
|
|
}
|
|
|
|
func createDigestHeader(algo Digest, digest []byte) (string, error) {
|
|
sfValue := sfv.NewItem(digest)
|
|
header := sfv.NewDictionary()
|
|
switch algo {
|
|
case DigestSHA256:
|
|
header.Add(digestAlgoSHA256, sfValue)
|
|
case DigestSHA512:
|
|
header.Add(digestAlgoSHA512, sfValue)
|
|
default:
|
|
return "", newError(ErrNoSigUnsupportedDigest, fmt.Sprintf("Unsupported digest algorithm '%s'", algo))
|
|
}
|
|
value, err := sfv.Marshal(header)
|
|
if err != nil {
|
|
return "", newError(ErrInternal, "Failed to marshal digest", err)
|
|
}
|
|
return value, nil
|
|
|
|
}
|
|
|
|
// getSupportedDigestFromHeader returns the first supported digest from the supplied header. If no supported header is found a nil digest is returned.
|
|
func getSupportedDigestFromHeader(contentDigestHeader []string) (algo Digest, digest []byte, err error) {
|
|
digestDict, err := sfv.UnmarshalDictionary(contentDigestHeader)
|
|
if err != nil {
|
|
return "", nil, newError(ErrNoSigInvalidHeader, "Could not parse Content-Digest header", err)
|
|
}
|
|
|
|
for _, algo := range digestDict.Names() {
|
|
switch Digest(algo) {
|
|
case DigestSHA256:
|
|
fallthrough
|
|
case DigestSHA512:
|
|
member, ok := digestDict.Get(algo)
|
|
if !ok {
|
|
continue
|
|
}
|
|
item, ok := member.(sfv.Item)
|
|
if !ok {
|
|
// If not a an Item it's not a valid header value. Skip
|
|
continue
|
|
}
|
|
if digest, ok := item.Value.([]byte); ok {
|
|
return Digest(algo), digest, nil
|
|
}
|
|
default:
|
|
// Unsupported
|
|
continue
|
|
}
|
|
}
|
|
|
|
return "", nil, nil
|
|
}
|