fleet/third_party/httpsig-go/verify.go
Victor Lyuboslavsky c25fed2492
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 20:26:50 +02:00

476 lines
16 KiB
Go

package httpsig
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/hmac"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"fmt"
"math/big"
"net/http"
"slices"
"time"
sfv "github.com/dunglas/httpsfv"
)
var (
DefaultVerifyProfile = VerifyProfile{
SignatureLabel: DefaultSignatureLabel,
AllowedAlgorithms: []Algorithm{Algo_ECDSA_P256_SHA256, Algo_ECDSA_P384_SHA384, Algo_ED25519, Algo_HMAC_SHA256},
RequiredFields: DefaultRequiredFields,
RequiredMetadata: []Metadata{MetaCreated, MetaKeyID},
DisallowedMetadata: []Metadata{MetaAlgorithm}, // The algorithm should be looked up from the keyid not an explicit setting.
CreatedValidDuration: time.Minute * 5, // Signatures must have been created within within the last 5 minutes
DateFieldSkew: time.Minute, // If the created parameter is present, the Date header cannot be more than a minute off.
}
// DefaultRequiredFields covers the request body with 'content-digest' the method and full URI.
// As per the specification Date is not covered in favor of using the 'created' metadata parameter.
DefaultRequiredFields = Fields("content-digest", "@method", "@target-uri")
ctxKeyAddDebug = struct{}{}
)
// KeySpec is the per-key information needed to verify a signature.
type KeySpec struct {
KeyID string
Algo Algorithm
PubKey crypto.PublicKey
Secret []byte // shared secret for symmetric algorithms
}
// KeySpec implements KeySpecer
func (ks KeySpec) KeySpec() (KeySpec, error) {
return ks, nil
}
// KeySpecer should be implemented by your key/credential store
type KeySpecer interface {
KeySpec() (KeySpec, error)
}
type KeyErrorReason string
type KeyError struct {
error
Reason KeyErrorReason
Message string
}
type KeyFetcher interface {
// FetchByKeyID looks up a KeySpec from the 'keyid' metadata parameter on the signature.
FetchByKeyID(ctx context.Context, rh http.Header, keyID string) (KeySpecer, error)
// Fetch looks up a KeySpec when the keyid is not in the signature.
Fetch(ctx context.Context, rh http.Header, md MetadataProvider) (KeySpecer, error)
}
// VerifyProfile sets the parameters for a fully valid request or response.
// A valid signature is a relatively easy accomplishment. Did the signature include all the important parts of the request? Did it use a strong enough algorithm? Was it signed 41 days ago? There are choices to make about what constitutes a valid signed request or response beyond just a verified signature.
type VerifyProfile struct {
SignatureLabel string // Which signature this profile applies to. '*' applies to all
RequiredFields []SignedField
RequiredMetadata []Metadata
DisallowedMetadata []Metadata
AllowedAlgorithms []Algorithm // Which algorithms are allowed, either from keyid meta or in the KeySpec
// Timing enforcement options
DisableTimeEnforcement bool // If true do no time enforcement on any fields
DisableExpirationEnforcement bool // If expiration is present default to enforce
CreatedValidDuration time.Duration // Duration allowed for between time.Now and the created time
ExpiredSkew time.Duration // Maximum duration allowed between time.Now and the expired time
DateFieldSkew time.Duration // Maximum duration allowed between Date field and created
}
type VerifyResult struct {
Verified bool
Label string
KeySpecer KeySpecer
DebugInfo VerifyDebugInfo // Present if the verifier debug flag is set and the signature was valid.
MetadataProvider
}
type VerifyDebugInfo struct {
SignatureBase string // The signature base derived from the request.
}
type Verifier struct {
keys KeyFetcher
profile VerifyProfile
}
// Verify validates the signatures in a request and ensured the signature meets the required profile.
func Verify(req *http.Request, kf KeyFetcher, profile VerifyProfile) (VerifyResult, error) {
ver, err := NewVerifier(kf, profile)
if err != nil {
return VerifyResult{}, err
}
return ver.Verify(req)
}
func VerifyResponse(resp *http.Response, kf KeyFetcher, profile VerifyProfile) (VerifyResult, error) {
ver, err := NewVerifier(kf, profile)
if err != nil {
return VerifyResult{}, err
}
return ver.VerifyResponse(resp)
}
func NewVerifier(kf KeyFetcher, profile VerifyProfile) (*Verifier, error) {
if kf == nil {
return nil, newError(ErrSigKeyFetch, "KeyFetcher cannot be nil")
}
return &Verifier{
keys: kf,
profile: profile,
}, nil
}
// Verify verifies the signature(s) in an http request. Any invalid signature will return an error.
// A valid VerifyResult is returned even if error is also returned.
func (ver *Verifier) Verify(req *http.Request) (VerifyResult, error) {
return ver.verify(httpMessage{
Req: req,
})
}
func (ver *Verifier) VerifyResponse(resp *http.Response) (VerifyResult, error) {
return ver.verify(httpMessage{
IsResponse: true,
Resp: resp,
})
}
// verify verifies the request or response.
func (ver *Verifier) verify(hrr httpMessage) (VerifyResult, error) {
vr := VerifyResult{}
/* calculate content digest if needed */
if hrr.Headers().Get("Content-Digest") != "" {
digestAlgo, expectedDigest, err := getSupportedDigestFromHeader(hrr.Headers().Values("Content-Digest"))
if err != nil {
return vr, err
}
di, err := digestBody(digestAlgo, hrr.Body())
if err != nil {
return vr, err
}
hrr.SetBody(di.NewBody)
if !bytes.Equal(expectedDigest, di.Digest) {
return vr, newError(ErrNoSigWrongDigest, "Digest does not match")
}
}
/* parse and extract the signature */
sigsfv, err := parseSignaturesFromRequest(hrr.Headers())
if err != nil {
return vr, err
}
sig, err := unmarshalSignature(sigsfv, ver.profile.SignatureLabel)
if err != nil {
return vr, err
}
vr.Label = sig.Label
vr.MetadataProvider = sig.Input.MetadataValues
/* verify and validate */
base, err := calculateSignatureBase(hrr, sig.Input)
if err != nil {
return vr, err
}
if hrr.isDebug() {
vr.DebugInfo = VerifyDebugInfo{
SignatureBase: string(base.base),
}
}
keyspec, err := ver.verifySignature(hrr, sig, base)
vr.KeySpecer = keyspec
if err != nil {
return vr, err
}
if err := ver.profile.validate(sig); err != nil {
return vr, err
}
return VerifyResult{
Verified: true,
Label: sig.Label,
KeySpecer: keyspec,
MetadataProvider: sig.Input.MetadataValues,
DebugInfo: vr.DebugInfo,
}, nil
}
type extractedSignature struct {
Label string
Signature []byte
Input sigBaseInput
}
// signaturesSFV is the structured field value representation of all the signatures of a request.
type signaturesSFV struct {
SigInputs *sfv.Dictionary
Sigs *sfv.Dictionary
}
func parseSignaturesFromRequest(headers http.Header) (signaturesSFV, error) {
psd := signaturesSFV{}
/* Pull signature and signature-input header */
sigHeader := headers.Get("signature")
if sigHeader == "" {
return psd, newError(ErrNoSigMissingSignature, "Missing signature header")
}
sigInputHeader := headers.Get("signature-input")
if sigInputHeader == "" {
return psd, newError(ErrNoSigMissingSignature, "Missing signature-input header")
}
/* Parse headers into their appropriate HTTP structured field values */
// signature-input must be a HTTP structured field value of type Dictionary.
var err error
psd.SigInputs, err = sfv.UnmarshalDictionary([]string{sigInputHeader})
if err != nil {
return psd, newError(ErrNoSigInvalidSignature, "Invalid signature-input header. Not a valid Dictionary", err)
}
// signature must be a HTTP structured field value of type Dictionary.
psd.Sigs, err = sfv.UnmarshalDictionary([]string{sigHeader})
if err != nil {
return psd, newError(ErrNoSigInvalidSignature, "Invalid signature header. Not a valid Dictionary", err)
}
return psd, nil
}
// unmarshalSignature unmarshals a signature from hhtp structured field value (sfv) format.
func unmarshalSignature(sigs signaturesSFV, label string) (extractedSignature, error) {
sigInfo := extractedSignature{
Label: label,
}
sigMember, found := sigs.Sigs.Get(label)
if !found {
return sigInfo, newError(ErrNoSigMissingSignature, fmt.Sprintf("The signature for label '%s' not found", label))
}
sigInputMember, found := sigs.SigInputs.Get(label)
if !found {
return sigInfo, newError(ErrNoSigMissingSignature, fmt.Sprintf("The signature-input for label '%s' not found", label))
}
// The signature must be of sfv type 'Item'
sigItem, isItem := sigMember.(sfv.Item)
if !isItem {
return sigInfo, newError(ErrSigInvalidSignature, fmt.Sprintf("The signature for label '%s' must be type Item. It was type %T", label, sigMember))
}
// Signatures must be byte sequences. The sfv library uses []byte for byte sequences.
sigBytes, isByteSequence := sigItem.Value.([]byte)
if !isByteSequence {
return sigInfo, newError(ErrSigInvalidSignature, fmt.Sprintf("The signature for label '%s' was not a byte sequence. It was type %T", label, sigItem.Value))
}
sigInfo.Signature = sigBytes
// The signature input must be of sfv type InnerList
sigInputList, isList := sigInputMember.(sfv.InnerList)
if !isList {
return sigInfo, newError(ErrSigInvalidSignature, fmt.Sprintf("The signature-input for label '%s' must be type InnerList. It was type '%T'.", label, sigInputMember))
}
cIDs := []componentID{}
for _, componentItem := range sigInputList.Items {
name, ok := componentItem.Value.(string)
if !ok {
return sigInfo, newError(ErrSigInvalidSignature, fmt.Sprintf("signature components must be string types"))
}
cIDs = append(cIDs, componentID{
Name: name,
Item: componentItem,
})
}
mds := []Metadata{}
for _, name := range sigInputList.Params.Names() {
mds = append(mds, Metadata(name))
}
sigInfo.Input = sigBaseInput{
Components: cIDs,
MetadataParams: mds,
MetadataValues: metadataProviderFromParams{sigInputList.Params},
}
return sigInfo, nil
}
func (ver *Verifier) verifySignature(r httpMessage, sig extractedSignature, base signatureBase) (KeySpecer, error) {
var specer KeySpecer
var ks KeySpec
var err error
// Get keyspec
if slices.Contains(sig.Input.MetadataParams, MetaKeyID) {
keyid, err := sig.Input.MetadataValues.KeyID()
if err != nil {
return nil, newError(ErrSigKeyFetch, "Could not get keyid from signature metadata", err)
}
specer, err = ver.keys.FetchByKeyID(r.Context(), r.Headers(), keyid)
if err != nil {
return nil, newError(ErrSigKeyFetch, fmt.Sprintf("Failed to fetch key for keyid '%s'", keyid), err)
}
ks, err = specer.KeySpec()
if err != nil {
return nil, newError(ErrSigKeyFetch, fmt.Sprintf("Failed to fetch key for keyid '%s'", keyid), err)
}
} else {
specer, err = ver.keys.Fetch(r.Context(), r.Headers(), sig.Input.MetadataValues)
if err != nil {
return specer, newError(ErrSigKeyFetch, fmt.Sprintf("Failed to fetch key for signature without a keyid and with label '%s'\n", sig.Label), err)
}
ks, err = specer.KeySpec()
if err != nil {
return specer, newError(ErrSigKeyFetch, fmt.Sprintf("Failed to fetch key for signature without a keyid and with label '%s'\n", sig.Label), err)
}
}
switch ks.Algo {
case Algo_RSA_PSS_SHA512:
if rsapub, ok := ks.PubKey.(*rsa.PublicKey); ok {
opts := &rsa.PSSOptions{
SaltLength: 64,
Hash: crypto.SHA512,
}
msgHash := sha512.Sum512(base.base)
err := rsa.VerifyPSS(rsapub, crypto.SHA512, msgHash[:], sig.Signature, opts)
if err != nil {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
return specer, nil
} else {
return specer, newError(ErrSigPublicKey, fmt.Sprintf("Invalid public key. Requires rsa.PublicKey but got type: %T", ks.PubKey))
}
case Algo_RSA_v1_5_sha256:
if rsapub, ok := ks.PubKey.(*rsa.PublicKey); ok {
msgHash := sha256.Sum256(base.base)
err := rsa.VerifyPKCS1v15(rsapub, crypto.SHA256, msgHash[:], sig.Signature)
if err != nil {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
return specer, nil
} else {
return specer, newError(ErrSigPublicKey, fmt.Sprintf("Invalid public key. Requires rsa.PublicKey but got type: %T", ks.PubKey))
}
case Algo_HMAC_SHA256:
if len(ks.Secret) == 0 {
return specer, newError(ErrInvalidSignatureOptions, fmt.Sprintf("No secret provided for symmetric algorithm '%s'", Algo_HMAC_SHA256))
}
msgHash := hmac.New(sha256.New, ks.Secret)
msgHash.Write(base.base) // write does not return an error per hash.Hash documentation
calcualtedSignature := msgHash.Sum(nil)
if !hmac.Equal(calcualtedSignature, sig.Signature) {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
case Algo_ECDSA_P256_SHA256:
if epub, ok := ks.PubKey.(*ecdsa.PublicKey); ok {
if len(sig.Signature) != 64 {
return specer, newError(ErrSigInvalidSignature, fmt.Sprintf("Signature must be 64 bytes for algorithm '%s'", Algo_ECDSA_P256_SHA256))
}
msgHash := sha256.Sum256(base.base)
// Concatenate r and s to form the signature as per the spec. r and s and *not* ANS1 encoded.
r := new(big.Int)
r.SetBytes(sig.Signature[0:32])
s := new(big.Int)
s.SetBytes(sig.Signature[32:64])
if !ecdsa.Verify(epub, msgHash[:], r, s) {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
} else {
return specer, newError(ErrSigPublicKey, fmt.Sprintf("Invalid public key. Requires *ecdsa.PublicKey but got type: %T", ks.PubKey))
}
case Algo_ECDSA_P384_SHA384:
if epub, ok := ks.PubKey.(*ecdsa.PublicKey); ok {
if len(sig.Signature) != 96 {
return specer, newError(ErrSigInvalidSignature, fmt.Sprintf("Signature must be 96 bytes for algorithm '%s'", Algo_ECDSA_P256_SHA256))
}
msgHash := sha512.Sum384(base.base)
// Concatenate r and s to form the signature as per the spec. r and s and *not* ANS1 encoded.
r := new(big.Int)
r.SetBytes(sig.Signature[0:48])
s := new(big.Int)
s.SetBytes(sig.Signature[48:96])
if !ecdsa.Verify(epub, msgHash[:], r, s) {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
} else {
return specer, newError(ErrSigPublicKey, fmt.Sprintf("Invalid public key. Requires *ecdsa.PublicKey but got type: %T", ks.PubKey))
}
case Algo_ED25519:
if edpubkey, ok := ks.PubKey.(ed25519.PublicKey); ok {
if !ed25519.Verify(edpubkey, base.base, sig.Signature) {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
} else {
return specer, newError(ErrSigPublicKey, fmt.Sprintf("Invalid public key. Requires ed25519.PublicKey but got type: %T", ks.PubKey))
}
default:
return specer, newError(ErrSigUnsupportedAlgorithm, fmt.Sprintf("Invalid verification algorithm '%s'", ks.Algo))
}
return specer, nil
}
func (vp VerifyProfile) validate(sig extractedSignature) error {
return nil
}
type metadataProviderFromParams struct {
Params *sfv.Params
}
func (mp metadataProviderFromParams) Created() (int, error) {
if val, ok := mp.Params.Get(string(MetaCreated)); ok {
return int(val.(int64)), nil
}
return 0, fmt.Errorf("No created value")
}
func (mp metadataProviderFromParams) Expires() (int, error) {
if val, ok := mp.Params.Get(string(MetaExpires)); ok {
return int(val.(int64)), nil
}
return 0, fmt.Errorf("No expires value")
}
func (mp metadataProviderFromParams) Nonce() (string, error) {
if val, ok := mp.Params.Get(string(MetaNonce)); ok {
return val.(string), nil
}
return "", fmt.Errorf("No nonce value")
}
func (mp metadataProviderFromParams) Alg() (string, error) {
if val, ok := mp.Params.Get(string(MetaAlgorithm)); ok {
return val.(string), nil
}
return "", fmt.Errorf("No alg value")
}
func (mp metadataProviderFromParams) KeyID() (string, error) {
if val, ok := mp.Params.Get(string(MetaKeyID)); ok {
return val.(string), nil
}
return "", fmt.Errorf("No keyid value")
}
func (mp metadataProviderFromParams) Tag() (string, error) {
if val, ok := mp.Params.Get(string(MetaTag)); ok {
return val.(string), nil
}
return "", fmt.Errorf("No tag value")
}
func SetAddDebugInfo(ctx context.Context) context.Context {
return context.WithValue(ctx, ctxKeyAddDebug, true)
}