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 -->
172 lines
5.2 KiB
Go
172 lines
5.2 KiB
Go
package httpsig
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"fmt"
|
|
"time"
|
|
|
|
sfv "github.com/dunglas/httpsfv"
|
|
)
|
|
|
|
// derived component names
|
|
type derived string
|
|
|
|
const (
|
|
sigparams derived = "@signature-params"
|
|
method derived = "@method"
|
|
path derived = "@path"
|
|
targetURI derived = "@target-uri"
|
|
authority derived = "@authority"
|
|
)
|
|
|
|
// MetadataProvider allows customized functions for metadata parameter values. Not needed for default usage.
|
|
type MetadataProvider interface {
|
|
Created() (int, error)
|
|
Expires() (int, error)
|
|
Nonce() (string, error)
|
|
Alg() (string, error)
|
|
KeyID() (string, error)
|
|
Tag() (string, error)
|
|
}
|
|
|
|
type signatureBase struct {
|
|
base []byte // The full signature base. Use this as input to signing and verification
|
|
signatureInput string // The signature-input line
|
|
}
|
|
|
|
type sigParameters struct {
|
|
Base sigBaseInput
|
|
Algo Algorithm
|
|
Label string
|
|
PrivateKey crypto.PrivateKey
|
|
Secret []byte
|
|
}
|
|
|
|
func sign(hrr httpMessage, sp sigParameters) error {
|
|
base, err := calculateSignatureBase(hrr, sp.Base)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var sigBytes []byte
|
|
switch sp.Algo {
|
|
|
|
case Algo_RSA_PSS_SHA512:
|
|
msgHash := sha512.Sum512(base.base)
|
|
opts := &rsa.PSSOptions{
|
|
SaltLength: 64,
|
|
Hash: crypto.SHA512,
|
|
}
|
|
switch rsapk := sp.PrivateKey.(type) {
|
|
case *rsa.PrivateKey:
|
|
sigBytes, err = rsa.SignPSS(rand.Reader, rsapk, crypto.SHA512, msgHash[:], opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case crypto.Signer:
|
|
sigBytes, err = rsapk.Sign(rand.Reader, msgHash[:], crypto.SHA512)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("Invalid private key. Requires *rsa.PrivateKey or crypto.Signer: %T", sp.PrivateKey)
|
|
}
|
|
case Algo_RSA_v1_5_sha256:
|
|
msgHash := sha256.Sum256(base.base)
|
|
switch rsapk := sp.PrivateKey.(type) {
|
|
case *rsa.PrivateKey:
|
|
sigBytes, err = rsa.SignPKCS1v15(rand.Reader, rsapk, crypto.SHA256, msgHash[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case crypto.Signer:
|
|
sigBytes, err = rsapk.Sign(rand.Reader, msgHash[:], crypto.SHA256)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("Invalid private key. Requires *rsa.PrivateKey or crypto.Signer: %T", sp.PrivateKey)
|
|
}
|
|
case Algo_ECDSA_P256_SHA256:
|
|
msgHash := sha256.Sum256(base.base)
|
|
switch eccpk := sp.PrivateKey.(type) {
|
|
case *ecdsa.PrivateKey:
|
|
r, s, err := ecdsa.Sign(rand.Reader, eccpk, msgHash[:])
|
|
if err != nil {
|
|
return newError(ErrInternal, "Failed to sign with ecdsa private key", err)
|
|
}
|
|
// Concatenate r and s to make the signature as per the spec. r and s are *not* encoded in ASN1 format
|
|
sigBytes = make([]byte, 64)
|
|
r.FillBytes(sigBytes[0:32])
|
|
s.FillBytes(sigBytes[32:64])
|
|
case crypto.Signer:
|
|
sigBytes, err = eccpk.Sign(rand.Reader, msgHash[:], crypto.SHA256)
|
|
if err != nil {
|
|
return newError(ErrInternal, "Failed to sign with ecdsa custom signer", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("Invalid private key. Requires *ecdsa.PrivateKey or crypto.Signer: %T", sp.PrivateKey)
|
|
}
|
|
case Algo_ECDSA_P384_SHA384:
|
|
msgHash := sha512.Sum384(base.base)
|
|
switch eccpk := sp.PrivateKey.(type) {
|
|
case *ecdsa.PrivateKey:
|
|
r, s, err := ecdsa.Sign(rand.Reader, eccpk, msgHash[:])
|
|
if err != nil {
|
|
return newError(ErrInternal, "Failed to sign with ecdsa private key", err)
|
|
}
|
|
// Concatenate r and s to make the signature as per the spec. r and s are *not* encoded in ASN1 format
|
|
sigBytes = make([]byte, 96)
|
|
r.FillBytes(sigBytes[0:48])
|
|
s.FillBytes(sigBytes[48:96])
|
|
case crypto.Signer:
|
|
sigBytes, err = eccpk.Sign(rand.Reader, msgHash[:], crypto.SHA384)
|
|
if err != nil {
|
|
return newError(ErrInternal, "Failed to sign with ecdsa custom signer", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("Invalid private key. Requires *ecdsa.PrivateKey or crypto.Signer: %T", sp.PrivateKey)
|
|
}
|
|
case Algo_ED25519:
|
|
switch edpk := sp.PrivateKey.(type) {
|
|
case ed25519.PrivateKey:
|
|
sigBytes = ed25519.Sign(edpk, base.base)
|
|
case crypto.Signer:
|
|
sigBytes, err = edpk.Sign(rand.Reader, base.base, crypto.Hash(0))
|
|
if err != nil {
|
|
return newError(ErrInternal, "Failed to sign with ed25519 custom signer", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("Invalid private key. Requires ed25519.PrivateKey or crypto.Signer: %T", sp.PrivateKey)
|
|
}
|
|
case Algo_HMAC_SHA256:
|
|
if len(sp.Secret) == 0 {
|
|
return newError(ErrInvalidSignatureOptions, fmt.Sprintf("No secret provided for symmetric algorithm '%s'", Algo_HMAC_SHA256))
|
|
}
|
|
msgHash := hmac.New(sha256.New, sp.Secret)
|
|
msgHash.Write(base.base) // write does not return an error per hash.Hash documentation
|
|
sigBytes = msgHash.Sum(nil)
|
|
default:
|
|
return newError(ErrInvalidSignatureOptions, fmt.Sprintf("Signing algorithm not supported: '%s'", sp.Algo))
|
|
}
|
|
sigField := sfv.NewDictionary()
|
|
sigField.Add(sp.Label, sfv.NewItem(sigBytes))
|
|
signature, err := sfv.Marshal(sigField)
|
|
if err != nil {
|
|
return newError(ErrInternal, fmt.Sprintf("Failed to marshal signature for label '%s'", sp.Label), err)
|
|
}
|
|
hrr.Headers().Set("Signature-Input", fmt.Sprintf("%s=%s", sp.Label, base.signatureInput))
|
|
hrr.Headers().Set("Signature", signature)
|
|
return nil
|
|
}
|
|
|
|
func timestamp(nowtime func() time.Time) int {
|
|
return int(nowtime().Unix())
|
|
}
|