mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +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 -->
476 lines
16 KiB
Go
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)
|
|
}
|