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 -->
200 lines
4.3 KiB
Go
200 lines
4.3 KiB
Go
package httpsig
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"net/http"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/remitly-oss/httpsig-go/sigtest"
|
|
)
|
|
|
|
// FuzzSigningOptions fuzzes the basic user input to SigningOptions
|
|
func FuzzSigningOptions1(f *testing.F) {
|
|
testcases := [][]string{
|
|
{"", "", "", ""},
|
|
{"", "0", "0", "\xde"},
|
|
{"", "\n", "0", "0"},
|
|
{"", "", "0", "@"},
|
|
{"", "@query-param", "0", "0"},
|
|
{string(Algo_ECDSA_P256_SHA256), "@query", "0", "0"},
|
|
{"any", "@query", "0", "0"},
|
|
{string(Algo_ED25519), "@query", "0", "0"},
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
f.Add(tc[0], tc[1], tc[2], tc[3])
|
|
}
|
|
|
|
reqtxt, err := os.ReadFile("testdata/rfc-test-request.txt")
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, algo, label, keyID, tag string) {
|
|
t.Logf("Label: %s\n", label)
|
|
t.Logf("keyid: %s\n", keyID)
|
|
t.Logf("tag: %s\n", tag)
|
|
|
|
fields := Fields(label, keyID, tag)
|
|
fields = append(fields, SignedField{
|
|
Name: label,
|
|
Parameters: map[string]any{
|
|
keyID: tag,
|
|
},
|
|
})
|
|
privKey := sigtest.ReadTestPrivateKey(t, "test-key-ed25519.key")
|
|
so := SigningProfile{
|
|
Algorithm: Algo_ED25519,
|
|
Fields: Fields(label, keyID, tag),
|
|
Metadata: []Metadata{MetaKeyID, MetaTag},
|
|
Label: label,
|
|
}
|
|
sk := SigningKey{
|
|
Key: privKey,
|
|
MetaKeyID: keyID,
|
|
MetaTag: tag,
|
|
}
|
|
if so.validate(sk) != nil {
|
|
// Catching invalidate signing options is good.
|
|
return
|
|
}
|
|
|
|
req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(reqtxt)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = Sign(req, so, sk)
|
|
if err != nil {
|
|
if _, ok := err.(*SignatureError); ok {
|
|
// Handled error
|
|
return
|
|
}
|
|
// Unhandled error
|
|
t.Error(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func FuzzSigningOptionsFields(f *testing.F) {
|
|
testcases := [][]string{
|
|
{"", "", ""},
|
|
{"0", "0", "\xde"},
|
|
{"\n", "0", "0"},
|
|
{"", "0", "@"},
|
|
{"@query-param", "name", "0"},
|
|
{"@query", "0", "0"},
|
|
{"@method", "", ""},
|
|
{"@status", "", ""},
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
f.Add(tc[0], tc[1], tc[2])
|
|
}
|
|
|
|
reqtxt, err := os.ReadFile("testdata/rfc-test-request.txt")
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, field, tagName, tagValue string) {
|
|
t.Logf("field: %s\n", field)
|
|
t.Logf("tag: %s:%s\n", tagName, tagValue)
|
|
fields := []SignedField{}
|
|
if tagName == "" {
|
|
fields = append(fields, SignedField{
|
|
Name: field,
|
|
})
|
|
} else {
|
|
fields = append(fields, SignedField{
|
|
Name: field,
|
|
Parameters: map[string]any{
|
|
tagName: tagValue,
|
|
},
|
|
})
|
|
}
|
|
|
|
so := SigningProfile{
|
|
Algorithm: Algo_ED25519,
|
|
Fields: fields,
|
|
}
|
|
sk := SigningKey{
|
|
Key: sigtest.ReadTestPrivateKey(t, "test-key-ed25519.key"),
|
|
}
|
|
if so.validate(sk) != nil {
|
|
// Catching invalidate signing options is good.
|
|
return
|
|
}
|
|
|
|
req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(reqtxt)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = Sign(req, so, sk)
|
|
if err != nil {
|
|
if _, ok := err.(*SignatureError); ok {
|
|
// Handled error
|
|
return
|
|
}
|
|
// Unhandled error
|
|
t.Error(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func FuzzExtractSignatures(f *testing.F) {
|
|
testcases := []struct {
|
|
SignatureHeader string
|
|
SignatureInputHeader string
|
|
}{
|
|
{
|
|
SignatureHeader: "",
|
|
SignatureInputHeader: "",
|
|
},
|
|
{
|
|
SignatureHeader: "sig-b24=(\"@status\" \"content-type\" \"content-digest\" \"content-length\");created=1618884473;keyid=\"test-key-ecc-p256\"",
|
|
SignatureInputHeader: "sig-b24=:wNmSUAhwb5LxtOtOpNa6W5xj067m5hFrj0XQ4fvpaCLx0NKocgPquLgyahnzDnDAUy5eCdlYUEkLIj+32oiasw==:",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testcases {
|
|
f.Add(tc.SignatureHeader, tc.SignatureInputHeader)
|
|
}
|
|
|
|
reqtxt, err := os.ReadFile("testdata/rfc-test-request.txt")
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, sigHeader, sigInputHeader string) {
|
|
t.Logf("signature header: %s\n", sigHeader)
|
|
t.Logf("signature input header: %s\n", sigInputHeader)
|
|
|
|
req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(reqtxt)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req.Header.Set("signature", sigHeader)
|
|
req.Header.Set("signature-input", sigInputHeader)
|
|
|
|
sigSFV, err := parseSignaturesFromRequest(req.Header)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, label := range sigSFV.Sigs.Names() {
|
|
_, err = unmarshalSignature(sigSFV, label)
|
|
if err != nil {
|
|
if _, ok := err.(*SignatureError); ok {
|
|
// Handled error
|
|
return
|
|
}
|
|
// Unhandled error
|
|
t.Error(err)
|
|
}
|
|
}
|
|
})
|
|
}
|