fleet/third_party/httpsig-go/sign_test.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

275 lines
6.2 KiB
Go

package httpsig
import (
"bufio"
"fmt"
"net/http"
"os"
"testing"
"github.com/remitly-oss/httpsig-go/sigtest"
)
// testcaseSigBase is a test case for signature bases
type testcaseSigBase struct {
Name string
Params sigBaseInput
IsResponse bool
SourceFile string // defaults to the specification request or response file
ExpectedFile string
ExpectedErr ErrCode
}
func TestSignatureBase(t *testing.T) {
cases := []testcaseSigBase{
{
Name: "RepeatedComponents",
Params: sigBaseInput{
Components: makeComponents("one", "two", "one", "three"),
MetadataParams: []Metadata{},
MetadataValues: emptyMeta,
},
SourceFile: "request_repeated_components.txt",
ExpectedErr: ErrInvalidSignatureOptions,
},
{
Name: "BadComponentName",
Params: sigBaseInput{
Components: makeComponents("\xd3", "two", "one", "three"),
MetadataParams: []Metadata{},
MetadataValues: emptyMeta,
},
ExpectedErr: ErrInvalidComponent,
},
{
Name: "NoMultiValueSuport",
Params: sigBaseInput{
Components: makeComponents("one"),
MetadataParams: []Metadata{},
MetadataValues: emptyMeta,
},
ExpectedErr: ErrUnsupported,
SourceFile: "request_multivalue.txt",
},
{
Name: "BadMeta-Created",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaCreated,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
{
Name: "BadMeta-Expires",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaExpires,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
{
Name: "BadMeta-Nonce",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaNonce,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
{
Name: "BadMeta-Algorithm",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaAlgorithm,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
{
Name: "BadMeta-KeyID",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaKeyID,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
{
Name: "BadMeta-Tag",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaTag,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
runTestSigBase(t, tc)
})
}
}
func runTestSigBase(t *testing.T, tc testcaseSigBase) {
sourceFile := tc.SourceFile
hrr := httpMessage{
IsResponse: tc.IsResponse,
}
if tc.IsResponse {
if sourceFile == "" {
sourceFile = "rfc-test-response.txt"
}
resptxt, err := os.Open(fmt.Sprintf("testdata/%s", sourceFile))
if err != nil {
t.Fatal(err)
}
resp, err := http.ReadResponse(bufio.NewReader(resptxt), nil)
if err != nil {
t.Fatal(err)
}
hrr.Resp = resp
} else {
if sourceFile == "" {
sourceFile = "rfc-test-request.txt"
}
// request
reqtxt, err := os.Open(fmt.Sprintf("testdata/%s", sourceFile))
if err != nil {
t.Fatal(err)
}
req, err := http.ReadRequest(bufio.NewReader(reqtxt))
if err != nil {
t.Fatal(err)
}
hrr.Req = req
}
actualBase, err := calculateSignatureBase(hrr, tc.Params)
if sigtest.Diff(t, tc.ExpectedErr, errCode(err), "Wrong error code") {
return
} else if tc.ExpectedErr != "" {
// If an error is expected and the err Diff check has passed then don't continue on to test the result
return
}
t.Log(string(actualBase.base))
expectedBase, err := os.ReadFile(fmt.Sprintf("testdata/%s", tc.ExpectedFile))
if err != nil {
t.Fatal(err)
}
if sigtest.Diff(t, string(expectedBase), string(actualBase.base), "Signature base did not match") {
t.FailNow()
}
}
type errorMetadataProvider struct{}
func (fmp errorMetadataProvider) Created() (int, error) {
return 0, fmt.Errorf("No created value")
}
func (fmp errorMetadataProvider) Expires() (int, error) {
return 0, fmt.Errorf("No expires value")
}
func (fmp errorMetadataProvider) Nonce() (string, error) {
return "", fmt.Errorf("No nonce value")
}
func (fmp errorMetadataProvider) Alg() (string, error) {
return "", fmt.Errorf("No alg value")
}
func (fmp errorMetadataProvider) KeyID() (string, error) {
return "", fmt.Errorf("No keyid value")
}
func (fmp errorMetadataProvider) Tag() (string, error) {
return "", fmt.Errorf("No tag value")
}
var emptyMeta = fixedMetadataProvider{
values: map[Metadata]any{},
}
type fixedMetadataProvider struct {
values map[Metadata]any
}
func (fmp fixedMetadataProvider) Created() (int, error) {
if val, ok := fmp.values[MetaCreated]; ok {
return int(val.(int64)), nil
}
return 0, fmt.Errorf("No created value")
}
func (fmp fixedMetadataProvider) Expires() (int, error) {
if val, ok := fmp.values[MetaExpires]; ok {
return int(val.(int64)), nil
}
return 0, fmt.Errorf("No expires value")
}
func (fmp fixedMetadataProvider) Nonce() (string, error) {
if val, ok := fmp.values[MetaNonce]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No nonce value")
}
func (fmp fixedMetadataProvider) Alg() (string, error) {
if val, ok := fmp.values[MetaAlgorithm]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No alg value")
}
func (fmp fixedMetadataProvider) KeyID() (string, error) {
if val, ok := fmp.values[MetaKeyID]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No keyid value")
}
func (fmp fixedMetadataProvider) Tag() (string, error) {
if val, ok := fmp.values[MetaTag]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No tag value")
}
func makeComponents(ids ...string) []componentID {
cids := []componentID{}
for _, id := range ids {
cids = append(cids, SignedField{
Name: id,
}.componentID())
}
return cids
}
func makeComponentIDs(sfs ...SignedField) []componentID {
cids := []componentID{}
for _, sf := range sfs {
cids = append(cids, sf.componentID())
}
return cids
}