Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions base.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func calculateSignatureBase(msg httpMessage, bp sigBaseInput) (signatureBase, er
base.WriteString(fmt.Sprintf("\"%s\": %s", sigparams, paramsOut))
return signatureBase{
base: []byte(base.String()),
signatureInput: paramsOut,
signatureInput: signatureParams,
}, nil
}

Expand Down Expand Up @@ -218,11 +218,11 @@ func deriveComponentValueRequest(req *http.Request, component componentID) (stri
case "@query-param":
paramKey, found := component.Item.Params.Get("name")
if !found {
return "", newError(ErrInvalidSignatureOptions, fmt.Sprintf("@query-param specified but missing 'name' parameter to indicate which parameter."))
return "", newError(ErrInvalidSignatureOptions, "@query-param specified but missing 'name' parameter to indicate which parameter.")
}
paramName, ok := paramKey.(string)
if !ok {
return "", newError(ErrInvalidSignatureOptions, fmt.Sprintf("@query-param specified but the 'name' parameter must be a string to indicate which parameter."))
return "", newError(ErrInvalidSignatureOptions, "@query-param specified but the 'name' parameter must be a string to indicate which parameter.")
}
paramValue := req.URL.Query().Get(paramName)
// TODO support empty - is this still a string with a space in it?
Expand Down
6 changes: 5 additions & 1 deletion fz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ func FuzzExtractSignatures(f *testing.F) {
{"sig1=:dGVzdA==:", "sig1=invalid"},
{"=:dGVzdA==:", "=(\"@method\")"},
{"sig1=:dGVzdA==:", "sig1=("},
{
SignatureInputHeader: `tst-ecdsa-0=("content-digest" "@method" "@target-uri");created=1755899955;keyid="test-key-ecdsa", tst-ecdsa-1=("content-digest" "@method" "@target-uri");created=1755899955;keyid="test-key-ecdsa", tst-ecdsa-2=("content-digest" "@method" "@target-uri");created=1755899955;keyid="test-key-ecdsa", tst-ecdsa-3=("content-digest" "@method" "@target-uri");created=1755899955;keyid="test-key-ecdsa", tst-ecdsa-4=("content-digest" "@method" "@target-uri");created=1755899955;keyid="test-key-ecdsa", tst-ecdsa-5=("content-digest" "@method" "@target-uri");created=1755899955;keyid="test-key-ecdsa", tst-ecdsa-6=("content-digest" "@method" "@target-uri");created=1755899955;keyid="test-key-ecdsa", tst-ecdsa-7=("content-digest" "@method" "@target-uri");created=1755899955;keyid="test-key-ecdsa"`,
SignatureHeader: `tst-ecdsa-0=:ktNUTq0MYt1iIDU8AxBi39Nrw6vVLAD7BL1EMuHxtmpDatYx7CFxxFee1cPqE3hkOKknGnQgS6Umrv0ct3wq3A==:, tst-ecdsa-1=:XFoz0EbrdxZFhsvVi0xhRBI2F5NNaQ5SsdRUjmyBiaTqZvC7Ud15gD+AmTipZxyjx5iX9YNKDNAnILv0fViWHA==:, tst-ecdsa-2=:EmUtrfTosRFr1uIKCu89ggqpfUClw4La3C3esWuXYdcg7xntY5dpqoqFCwyrlwxHveLcBxoHcVp1trSGsUJGAA==:, tst-ecdsa-3=:jJw65CIeODTbYxTAqBPof9U70FU1+uL/k8GLLJR3YMOHbk5enJqi9GCI6Gpn7SUamw1d3u6YqUb9BYo2C1WdTA==:, tst-ecdsa-4=:yhFGqoxuKpzApUccnjspLYhneYXf9Y/CTNbrCrQuzRUf6OzDGJFxJB1deDwdLJzIGB2DnICNVtHc3Zp/MI5+jQ==:, tst-ecdsa-5=:jxZ+G1ZFY9yc0GyhTXG5vSKtHTEQ2Slacb6AJzV8lHuuoTJpn08eZeJijLXhiRzBNzHEoFc9PwcsKehIp5dGjA==:, tst-ecdsa-6=:HLFRyXXkrIZbRYEoVQl3aL1EpFtg52JkAK8wad+wHnehVFYzO3M7tyEpdGBcr/6ZpUr1rkc1J1Ru9wRa3WkqRQ==:, tst-ecdsa-7=:b7+mkQt02cvV/cucQcSn9J+lJ9/cRkWcVX8mAdNh+p3avD1ULj/hg0z7ZkgEnlfDuWaTskdu+CLncxseLsEIWQ==:`,
},
}

for _, tc := range testcases {
Expand All @@ -193,7 +197,7 @@ func FuzzExtractSignatures(f *testing.F) {
req.Header.Set("signature", sigHeader)
req.Header.Set("signature-input", sigInputHeader)

sigSFV, err := parseSignaturesFromRequest(req.Header)
sigSFV, err := parseSignaturesFromRequest(req.Header, false)
if err != nil {
return
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/remitly-oss/httpsig-go

go 1.21.4
go 1.22

require (
github.com/dunglas/httpsfv v1.0.2
Expand Down
117 changes: 117 additions & 0 deletions roundtrip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package httpsig_test
import (
"crypto"
"crypto/ecdsa"
"fmt"
"io"
"math/big"
"testing"
Expand Down Expand Up @@ -339,6 +340,122 @@ func TestRoundTrip(t *testing.T) {
}
}

func TestRoundTripMultiSig(t *testing.T) {

testcases := []struct {
Name string
NumberOfSignatures int
PrivateKey crypto.PrivateKey
SigningOpts httpsig.SigningKeyOpts
MetaKeyID string
Secret []byte
SignProfile httpsig.SigningProfile
RequestFile string
Keys httpsig.KeyFetcher
Profile httpsig.VerifyProfile
ExpectedErrCodeVerify httpsig.ErrCode
}{
{
Name: "RSA-PSS",
NumberOfSignatures: 2,
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-rsa-pss.key"),
MetaKeyID: "test-key-rsa",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_RSA_PSS_SHA512,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-rsa-pss-%d",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-rsa": {
KeyID: "test-key-rsa",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
}),
Profile: httpsig.VerifyProfile{
SignatureLabel: "tst-rsa-pss-%d",
AllowedAlgorithms: []httpsig.Algorithm{httpsig.Algo_RSA_PSS_SHA512},
},
},
{
Name: "ECDSA-p256",
NumberOfSignatures: 8,
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-ecc-p256.key"),
MetaKeyID: "test-key-ecdsa",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_ECDSA_P256_SHA256,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-ecdsa-%d",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-ecdsa": {
KeyID: "test-key-ecds",
Algo: httpsig.Algo_ECDSA_P256_SHA256,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-ecc-p256.pub"),
},
}),
Profile: createVerifyProfile("tst-ecdsa-%d"),
},
}

for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
req := sigtest.ReadRequest(t, tc.RequestFile)

for i := range tc.NumberOfSignatures {
var signer *httpsig.Signer
sk := httpsig.SigningKey{
Key: tc.PrivateKey,
Opts: tc.SigningOpts,
Secret: tc.Secret,
MetaKeyID: tc.MetaKeyID,
}

sp := tc.SignProfile
sp.Label = fmt.Sprintf(sp.Label, i)
signer, err := httpsig.NewSigner(sp, sk)
if err != nil {
t.Fatal(err)
}
err = signer.Sign(req)
if err != nil {
t.Fatalf("%#v", err)
}
}

t.Log(req.Header.Get("Signature-Input"))
t.Log(req.Header.Get("Signature"))

for i := range tc.NumberOfSignatures {
vpf := tc.Profile
vpf.SignatureLabel = fmt.Sprintf(vpf.SignatureLabel, i)
ver, err := httpsig.NewVerifier(tc.Keys, vpf)
if err != nil {
t.Fatal(err)
}
vf, err := ver.Verify(req)
if err != nil {
if tc.ExpectedErrCodeVerify != "" {
if sigerr, ok := err.(*httpsig.SignatureError); ok {
sigtest.Diff(t, tc.ExpectedErrCodeVerify, sigerr.Code, "Wrong err code")
}
} else {
t.Fatalf("%#v", err)
}
} else if tc.ExpectedErrCodeVerify != "" {
t.Fatal("Expected error")
}
t.Logf("%+v\n", vf)
}
})

}
}

func createVerifyProfile(label string) httpsig.VerifyProfile {
vp := httpsig.DefaultVerifyProfile
vp.SignatureLabel = label
Expand Down
76 changes: 51 additions & 25 deletions signatures.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ type MetadataProvider interface {
}

type signatureBase struct {
base []byte // The full signature base. Use this as input to signing and verification
signatureInput string // The signature-input line
base []byte // The full signature base. Use this as input to signing and verification
signatureInput sfv.InnerList // The signature-input line
}

type sigParameters struct {
Expand All @@ -56,12 +56,45 @@ type sigParameters struct {
}

func sign(hrr httpMessage, sp sigParameters) error {
// get the signature base
base, err := calculateSignatureBase(hrr, sp.Base)
if err != nil {
return err
}
// create the signature from the base
sigBytes, err := createSignature(sp, base)
if err != nil {
return err
}

// Add it to the request
sigsSFV, err := parseSignaturesFromRequest(hrr.Headers(), true)
if err != nil {
return err
}

// Note this will overwrite an existing signature with the same label.
sigsSFV.SigInputs.Add(sp.Label, base.signatureInput)
sigsSFV.Sigs.Add(sp.Label, sfv.NewItem(sigBytes))

sigInput, err := sfv.Marshal(sigsSFV.SigInputs)
if err != nil {
return newError(ErrInternal, "Failed to marshal signature-input for http header value", err)
}
sigs, err := sfv.Marshal(sigsSFV.Sigs)
if err != nil {
return newError(ErrInternal, "Failed to marshal signature for http header value.", err)
}

hrr.Headers().Set("Signature-Input", sigInput)
hrr.Headers().Set("Signature", sigs)

return nil
}

func createSignature(sp sigParameters, base signatureBase) (sig []byte, e error) {
var sigBytes []byte
var err error
pkSigner := sp.Opts.Signer // Use crypto.Signer interface if set.

// Use the crypto.Signer generic `Sign` method if crypto.Signer is provided in the SigningKeyOpts
Expand All @@ -72,7 +105,7 @@ func sign(hrr httpMessage, sp sigParameters) error {
if rsapk, ok := sp.PrivateKey.(*rsa.PrivateKey); ok {
pkSigner = rsapk
} else {
return newError(ErrInternal, fmt.Sprintf("Invalid private key. Requires *rsa.PrivateKey: %T", sp.PrivateKey))
return nil, newError(ErrInternal, fmt.Sprintf("Invalid private key. Requires *rsa.PrivateKey: %T", sp.PrivateKey))
}
}

Expand All @@ -83,20 +116,20 @@ func sign(hrr httpMessage, sp sigParameters) error {
}
sigBytes, err = pkSigner.Sign(rand.Reader, msgHash[:], opts)
if err != nil {
return newError(ErrInternal, fmt.Sprintf("Failed to sign algorithm '%s'", Algo_RSA_PSS_SHA512), err)
return nil, newError(ErrInternal, fmt.Sprintf("Failed to sign algorithm '%s'", Algo_RSA_PSS_SHA512), err)
}
case Algo_RSA_v1_5_sha256:
if pkSigner == nil {
if rsapk, ok := sp.PrivateKey.(*rsa.PrivateKey); ok {
pkSigner = rsapk
} else {
return newError(ErrInternal, fmt.Sprintf("Invalid private key. Requires *rsa.PrivateKey: %T", sp.PrivateKey))
return nil, newError(ErrInternal, fmt.Sprintf("Invalid private key. Requires *rsa.PrivateKey: %T", sp.PrivateKey))
}
}
msgHash := sha256.Sum256(base.base)
sigBytes, err = pkSigner.Sign(rand.Reader, msgHash[:], crypto.SHA256)
if err != nil {
return newError(ErrInternal, fmt.Sprintf("Failed to sign algorithm '%s'", Algo_RSA_v1_5_sha256), err)
return nil, newError(ErrInternal, fmt.Sprintf("Failed to sign algorithm '%s'", Algo_RSA_v1_5_sha256), err)
}
case Algo_ECDSA_P256_SHA256:
msgHash := sha256.Sum256(base.base)
Expand All @@ -105,19 +138,19 @@ func sign(hrr httpMessage, sp sigParameters) error {
// Use the native ecdsa.Sign method to avoid needing to decode ASN.1 result.
r, s, err := ecdsa.Sign(rand.Reader, eccpk, msgHash[:])
if err != nil {
return newError(ErrInternal, fmt.Sprintf("Failed to sign algorithm '%s'", Algo_ECDSA_P256_SHA256), err)
return nil, newError(ErrInternal, fmt.Sprintf("Failed to sign algorithm '%s'", Algo_ECDSA_P256_SHA256), err)
}
// Concatenate r and s to make the signature as per the spec. r and s are *not* encoded in ASN1 format
sigBytes = ecdsaConcatRS(r, s, ecdsaP256SignatureSize)
} else {
return newError(ErrInternal, "Invalid private key. Requires *ecdsa.PrivateKey")
return nil, newError(ErrInternal, "Invalid private key. Requires *ecdsa.PrivateKey")
}
} else {
// crypto.Signer for ECDSA may return the signature in ASN.1 format
sigBytes, err = pkSigner.Sign(rand.Reader, msgHash[:], crypto.SHA256)
sigBytes, err = sp.ecdsaHandleASN1(err, sigBytes, ecdsaP256SignatureSize)
if err != nil {
return newError(ErrInternal, fmt.Sprintf("Failed to sign algorithm '%s'", Algo_ECDSA_P256_SHA256), err)
return nil, newError(ErrInternal, fmt.Sprintf("Failed to sign algorithm '%s'", Algo_ECDSA_P256_SHA256), err)
}
}
case Algo_ECDSA_P384_SHA384:
Expand All @@ -126,54 +159,47 @@ func sign(hrr httpMessage, sp sigParameters) error {
if eccpk, ok := sp.PrivateKey.(*ecdsa.PrivateKey); ok {
r, s, err := ecdsa.Sign(rand.Reader, eccpk, msgHash[:])
if err != nil {
return newError(ErrInternal, "Failed to sign with ecdsa private key", err)
return nil, 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 = ecdsaConcatRS(r, s, ecdsaP384SignatureSize)
} else {
return newError(ErrInternal, "Invalid private key. Requires *ecdsa.PrivateKey")
return nil, newError(ErrInternal, "Invalid private key. Requires *ecdsa.PrivateKey")
}
} else {
// crypto.Signer for ECDSA may return the signature in ASN.1 format
sigBytes, err = pkSigner.Sign(rand.Reader, msgHash[:], crypto.SHA384)
sigBytes, err = sp.ecdsaHandleASN1(err, sigBytes, ecdsaP384SignatureSize)
if err != nil {
return newError(ErrInternal, fmt.Sprintf("Failed to sign algorithm '%s'", Algo_ECDSA_P384_SHA384), err)
return nil, newError(ErrInternal, fmt.Sprintf("Failed to sign algorithm '%s'", Algo_ECDSA_P384_SHA384), err)
}
}
case Algo_ED25519:
if pkSigner == nil {
if edpk, ok := sp.PrivateKey.(ed25519.PrivateKey); ok {
sigBytes = ed25519.Sign(edpk, base.base)
} else {
return newError(ErrInternal, "Invalid private key. Requires ed25519.PrivateKey")
return nil, newError(ErrInternal, "Invalid private key. Requires ed25519.PrivateKey")
}
} else {
// No prehash function per the spec.
sigBytes, err = pkSigner.Sign(nil, base.base, crypto.Hash(0))
if err != nil {
return newError(ErrInternal, fmt.Sprintf("Failed to sign with crypto.Signer. Algorithm '%s'", Algo_ED25519), err)
return nil, newError(ErrInternal, fmt.Sprintf("Failed to sign with crypto.Signer. Algorithm '%s'", Algo_ED25519), err)
}
}
case Algo_HMAC_SHA256:
if len(sp.Secret) == 0 {
return newError(ErrInvalidSignatureOptions, fmt.Sprintf("No secret provided for symmetric algorithm '%s'", Algo_HMAC_SHA256))
return nil, 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)
return nil, newError(ErrInvalidSignatureOptions, fmt.Sprintf("Signing algorithm not supported: '%s'", sp.Algo))
}
hrr.Headers().Set("Signature-Input", fmt.Sprintf("%s=%s", sp.Label, base.signatureInput))
hrr.Headers().Set("Signature", signature)
return nil

return sigBytes, nil
}

// ecdsaHandleASN1 returns the R | S concatenated signature. If sigParameters expect an ASN1 encoded signature it will decode it first. 'origErr' can be passed in to avoid an extra error check step.
Expand Down
Loading
Loading