fleet/third_party/httpsig-go/digest.go

124 lines
3.3 KiB
Go
Raw Normal View History

Added a vendored version of httpsig-go. (#30820) 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 -->
2025-07-14 18:26:50 +00:00
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
}