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

233 lines
6.6 KiB
Go

package httpsig_test
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/remitly-oss/httpsig-go"
"github.com/remitly-oss/httpsig-go/keyman"
"github.com/remitly-oss/httpsig-go/keyutil"
"github.com/remitly-oss/httpsig-go/sigtest"
)
func TestVerify(t *testing.T) {
testcases := []struct {
Name string
RequestFile string
Label string
AddDebugInfo bool
Keys httpsig.KeyFetcher
Expected httpsig.VerifyResult
}{
{
Name: "OneValid",
Label: "sig-b21",
RequestFile: "verify_request1.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-rsa-pss": {
KeyID: "test-key-rsa-pss",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
}),
Expected: httpsig.VerifyResult{
Verified: true,
Label: "sig-b21",
MetadataProvider: &fixedMetadataProvider{map[httpsig.Metadata]any{
httpsig.MetaKeyID: "test-key-rsa-pss",
httpsig.MetaCreated: int64(1618884473),
httpsig.MetaNonce: "b3k2pp5k7z-50gnwp.yemd",
}},
KeySpecer: httpsig.KeySpec{
KeyID: "test-key-rsa-pss",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
},
},
{
Name: "OneValidDebug",
Label: "sig-b21",
RequestFile: "verify_request1.txt",
AddDebugInfo: true,
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-rsa-pss": {
KeyID: "test-key-rsa-pss",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
}),
Expected: httpsig.VerifyResult{
Verified: true,
Label: "sig-b21",
MetadataProvider: &fixedMetadataProvider{map[httpsig.Metadata]any{
httpsig.MetaKeyID: "test-key-rsa-pss",
httpsig.MetaCreated: int64(1618884473),
httpsig.MetaNonce: "b3k2pp5k7z-50gnwp.yemd",
}},
KeySpecer: httpsig.KeySpec{
KeyID: "test-key-rsa-pss",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
DebugInfo: httpsig.VerifyDebugInfo{
SignatureBase: `"@signature-params": ();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"`,
},
},
},
}
for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
req := sigtest.ReadRequest(t, tc.RequestFile)
if tc.AddDebugInfo {
req = req.WithContext(httpsig.SetAddDebugInfo(req.Context()))
}
actual, err := httpsig.Verify(req, tc.Keys, httpsig.VerifyProfile{SignatureLabel: tc.Label})
if err != nil {
t.Fatal(err)
}
// VerifyResult is returned even when error is also returned.
// Because VerifryResult embed Metadataprovider we first need diff ignoring the MetadataProvider
sigtest.Diff(t, tc.Expected, actual, "Did not match",
cmp.FilterPath(func(p cmp.Path) bool {
return p.String() == "MetadataProvider"
}, cmp.Ignore()))
// Then diff the metadata provider
sigtest.Diff(t, tc.Expected, actual, "Did not match", getCmdOpts()...)
})
}
}
func TestVerifyInvalid(t *testing.T) {
testcases := []struct {
Name string
RequestFile string
Label string
Keys httpsig.KeyFetcher
Expected httpsig.ErrCode
}{
{
Name: "SignatureVerificationFailure",
RequestFile: "verify_request2.txt",
Label: "bad-sig",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-rsa-pss": {
KeyID: "test-key-rsa-pss",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
}),
Expected: httpsig.ErrSigVerification,
},
{
Name: "KeyFetchError",
RequestFile: "verify_request2.txt",
Label: "sig-b21",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{}),
Expected: httpsig.ErrSigKeyFetch,
},
{
Name: "KeyFetchError2",
RequestFile: "verify_request2.txt",
Label: "bad-sig",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{}),
Expected: httpsig.ErrSigKeyFetch,
},
}
for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
_, err := httpsig.Verify(sigtest.ReadRequest(t, tc.RequestFile), tc.Keys, httpsig.VerifyProfile{SignatureLabel: tc.Label})
if err == nil {
t.Fatal("Expected err")
}
if sigerr, ok := err.(*httpsig.SignatureError); ok {
sigtest.Diff(t, tc.Expected, sigerr.Code, "Did not match")
} else {
sigtest.Diff(t, tc.Expected, sigerr, "Did not match")
}
})
}
}
type fixedMetadataProvider struct {
values map[httpsig.Metadata]any
}
func (fmp fixedMetadataProvider) Created() (int, error) {
if val, ok := fmp.values[httpsig.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[httpsig.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[httpsig.MetaNonce]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No nonce value")
}
func (fmp fixedMetadataProvider) Alg() (string, error) {
if val, ok := fmp.values[httpsig.MetaAlgorithm]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No alg value")
}
func (fmp fixedMetadataProvider) KeyID() (string, error) {
if val, ok := fmp.values[httpsig.MetaKeyID]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No keyid value")
}
func (fmp fixedMetadataProvider) Tag() (string, error) {
if val, ok := fmp.values[httpsig.MetaTag]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No tag value")
}
func metaVal[E comparable](f1 func() (E, error)) any {
val, err := f1()
if err != nil {
return err.Error()
}
return val
}
func getCmdOpts() []cmp.Option {
return []cmp.Option{
// This gets used for *ANY* struct assignable to MetadataProvider including other structres
// that embed it!
cmp.Transformer("MetadataProvider", TransformMeta),
}
}
func TransformMeta(md httpsig.MetadataProvider) map[string]any {
out := map[string]any{}
if md == nil {
return out
}
out[string(httpsig.MetaCreated)] = metaVal(md.Created)
out[string(httpsig.MetaExpires)] = metaVal(md.Expires)
out[string(httpsig.MetaNonce)] = metaVal(md.Nonce)
out[string(httpsig.MetaAlgorithm)] = metaVal(md.Alg)
out[string(httpsig.MetaKeyID)] = metaVal(md.KeyID)
out[string(httpsig.MetaTag)] = metaVal(md.Tag)
return out
}