Skip to content

feat: add option to exclude query params from signature #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
64 changes: 58 additions & 6 deletions httpsig.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ func (s SignatureScheme) authScheme() string {
}
}

type SignatureOption struct {
// ExcludeQueryStringFromPathPseudoHeader omits the query parameters from the
// `:path` pseudo-header in the HTTP signature.
//
// The query string is optional in the `:path` pseudo-header.
// https://www.rfc-editor.org/rfc/rfc9113#section-8.3.1-2.4.1
ExcludeQueryStringFromPathPseudoHeader bool
}

// Signers will sign HTTP requests or responses based on the algorithms and
// headers selected at creation time.
//
Expand Down Expand Up @@ -148,6 +157,43 @@ type Signer interface {
SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error
}

type SignerWithOptions interface {
Signer

// SignRequest signs the request using a private key. The public key id
// is used by the HTTP server to identify which key to use to verify the
// signature.
//
// If the Signer was created using a MAC based algorithm, then the key
// is expected to be of type []byte. If the Signer was created using an
// RSA based algorithm, then the private key is expected to be of type
// *rsa.PrivateKey.
//
// A Digest (RFC 3230) will be added to the request. The body provided
// must match the body used in the request, and is allowed to be nil.
// The Digest ensures the request body is not tampered with in flight,
// and if the signer is created to also sign the "Digest" header, the
// HTTP Signature will then ensure both the Digest and body are not both
// modified to maliciously represent different content.
SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte, opts SignatureOption) error
// SignResponse signs the response using a private key. The public key
// id is used by the HTTP client to identify which key to use to verify
// the signature.
//
// If the Signer was created using a MAC based algorithm, then the key
// is expected to be of type []byte. If the Signer was created using an
// RSA based algorithm, then the private key is expected to be of type
// *rsa.PrivateKey.
//
// A Digest (RFC 3230) will be added to the response. The body provided
// must match the body written in the response, and is allowed to be
// nil. The Digest ensures the response body is not tampered with in
// flight, and if the signer is created to also sign the "Digest"
// header, the HTTP Signature will then ensure both the Digest and body
// are not both modified to maliciously represent different content.
SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte, opts SignatureOption) error
}

// NewSigner creates a new Signer with the provided algorithm preferences to
// make HTTP signatures. Only the first available algorithm will be used, which
// is returned by this function along with the Signer. If none of the preferred
Expand All @@ -162,7 +208,7 @@ type Signer interface {
//
// An error is returned if an unknown or a known cryptographically insecure
// Algorithm is provided.
func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, Algorithm, error) {
func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SignerWithOptions, Algorithm, error) {
for _, pref := range prefs {
s, err := newSigner(pref, dAlgo, headers, scheme, expiresIn)
if err != nil {
Expand Down Expand Up @@ -267,6 +313,12 @@ type Verifier interface {
Verify(pKey crypto.PublicKey, algo Algorithm) error
}

type VerifierWithOptions interface {
Verifier

VerifyWithOptions(pKey crypto.PublicKey, algo Algorithm, opts SignatureOption) error
}

const (
// host is treated specially because golang may not include it in the
// request header map on the server side of a request.
Expand All @@ -277,20 +329,20 @@ const (
// Signature parameters are not present in any headers, are present in more than
// one header, are malformed, or are missing required parameters. It ignores
// unknown HTTP Signature parameters.
func NewVerifier(r *http.Request) (Verifier, error) {
func NewVerifier(r *http.Request) (VerifierWithOptions, error) {
h := r.Header
if _, hasHostHeader := h[hostHeader]; len(r.Host) > 0 && !hasHostHeader {
h[hostHeader] = []string{r.Host}
}
return newVerifier(h, func(h http.Header, toInclude []string, created int64, expires int64) (string, error) {
return signatureString(h, toInclude, addRequestTarget(r), created, expires)
return newVerifier(h, func(h http.Header, toInclude []string, created int64, expires int64, opts SignatureOption) (string, error) {
return signatureString(h, toInclude, addRequestTarget(r, opts), created, expires)
})
}

// NewResponseVerifier verifies the given response. It returns errors under the
// same conditions as NewVerifier.
func NewResponseVerifier(r *http.Response) (Verifier, error) {
return newVerifier(r.Header, func(h http.Header, toInclude []string, created int64, expires int64) (string, error) {
return newVerifier(r.Header, func(h http.Header, toInclude []string, created int64, expires int64, _ SignatureOption) (string, error) {
return signatureString(h, toInclude, requestTargetNotPermitted, created, expires)
})
}
Expand Down Expand Up @@ -323,7 +375,7 @@ func newSSHSigner(sshSigner ssh.Signer, algo Algorithm, dAlgo DigestAlgorithm, h
return a, nil
}

func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, error) {
func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SignerWithOptions, error) {

var expires, created int64 = 0, 0
if expiresIn != 0 {
Expand Down
39 changes: 31 additions & 8 deletions httpsig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,9 +562,10 @@ func TestNewResponseVerifier(t *testing.T) {
// https://tools.ietf.org/html/draft-cavage-http-signatures-10#appendix-C
func Test_Signing_HTTP_Messages_AppendixC(t *testing.T) {
specTests := []struct {
name string
headers []string
expectedSignature string
name string
headers []string
excludeQueryString bool
expectedSignature string
}{
{
name: "C.1. Default Test",
Expand All @@ -584,6 +585,12 @@ func Test_Signing_HTTP_Messages_AppendixC(t *testing.T) {
headers: []string{"(request-target)", "host", "date"},
expectedSignature: `Authorization: Signature keyId="Test",algorithm="hs2019",headers="(request-target) host date",signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="`,
},
{
name: "C.2. Basic Test - Exclude Query String",
headers: []string{"(request-target)", "host", "date"},
excludeQueryString: true,
expectedSignature: `Authorization: Signature keyId="Test",algorithm="hs2019",headers="(request-target) host date",signature="UTZ/RcfAkDv1tlRZ2R/lYxZ8c9H34hnp7eM4v/U9GC61CKIgZKTN3HTLK0Zd0Lg5QoGK78kNUmhBspkKJ27n9fTzEFb56DHKeilgt/SnT7PuL+E5U9ttm66l+RpF4IhPe0DV8VuYeb2UCNUw7UyyqrOQZtMe7CJVgBb0dn/92Z8="`,
},
{
name: "C.3. All Headers Test",
headers: []string{"(request-target)", "host", "date", "content-type", "digest", "content-length"},
Expand All @@ -610,7 +617,11 @@ func Test_Signing_HTTP_Messages_AppendixC(t *testing.T) {
t.Fatalf("error creating signer: %s", err)
}

if err := s.SignRequest(testSpecRSAPrivateKey, "Test", r, nil); err != nil {
opts := SignatureOption{
ExcludeQueryStringFromPathPseudoHeader: test.excludeQueryString,
}

if err := s.SignRequestWithOptions(testSpecRSAPrivateKey, "Test", r, nil, opts); err != nil {
t.Fatalf("error signing request: %s", err)
}

Expand Down Expand Up @@ -691,9 +702,10 @@ func TestSigningEd25519(t *testing.T) {
// https://tools.ietf.org/html/draft-cavage-http-signatures-10#appendix-C
func Test_Verifying_HTTP_Messages_AppendixC(t *testing.T) {
specTests := []struct {
name string
headers []string
signature string
name string
headers []string
excludeQueryString bool
signature string
}{
{
name: "C.1. Default Test",
Expand All @@ -705,6 +717,12 @@ func Test_Verifying_HTTP_Messages_AppendixC(t *testing.T) {
headers: []string{"(request-target)", "host", "date"},
signature: `Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="`,
},
{
name: "C.2. Basic Test - Exclude Query String",
headers: []string{"(request-target)", "host", "date"},
excludeQueryString: true,
signature: `Signature keyId="Test",algorithm="hs2019",headers="(request-target) host date",signature="UTZ/RcfAkDv1tlRZ2R/lYxZ8c9H34hnp7eM4v/U9GC61CKIgZKTN3HTLK0Zd0Lg5QoGK78kNUmhBspkKJ27n9fTzEFb56DHKeilgt/SnT7PuL+E5U9ttm66l+RpF4IhPe0DV8VuYeb2UCNUw7UyyqrOQZtMe7CJVgBb0dn/92Z8="`,
},
{
name: "C.3. All Headers Test",
headers: []string{"(request-target)", "host", "date", "content-type", "digest", "content-length"},
Expand Down Expand Up @@ -735,7 +753,12 @@ func Test_Verifying_HTTP_Messages_AppendixC(t *testing.T) {
if "Test" != v.KeyId() {
t.Errorf("KeyId mismatch\nGot: %s\nWant: Test", v.KeyId())
}
if err := v.Verify(testSpecRSAPublicKey, RSA_SHA256); err != nil {

opts := SignatureOption{
ExcludeQueryStringFromPathPseudoHeader: test.excludeQueryString,
}

if err := v.VerifyWithOptions(testSpecRSAPublicKey, RSA_SHA256, opts); err != nil {
t.Errorf("Verification failure: %s", err)
}
})
Expand Down
36 changes: 26 additions & 10 deletions signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const (

var defaultHeaders = []string{dateHeader}

var _ Signer = &macSigner{}
var _ SignerWithOptions = &macSigner{}

type macSigner struct {
m macer
Expand All @@ -53,13 +53,17 @@ type macSigner struct {
}

func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error {
return m.SignRequestWithOptions(pKey, pubKeyId, r, body, SignatureOption{})
}

func (m *macSigner) SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte, opts SignatureOption) error {
if body != nil {
err := addDigest(r, m.dAlgo, body)
if err != nil {
return err
}
}
s, err := m.signatureString(r)
s, err := m.signatureString(r, opts)
if err != nil {
return err
}
Expand All @@ -72,6 +76,10 @@ func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http
}

func (m *macSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error {
return m.SignResponseWithOptions(pKey, pubKeyId, r, body, SignatureOption{})
}

func (m *macSigner) SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte, _ SignatureOption) error {
if body != nil {
err := addDigestResponse(r, m.dAlgo, body)
if err != nil {
Expand Down Expand Up @@ -103,15 +111,15 @@ func (m *macSigner) signSignature(pKey crypto.PrivateKey, s string) (string, err
return enc, nil
}

func (m *macSigner) signatureString(r *http.Request) (string, error) {
return signatureString(r.Header, m.headers, addRequestTarget(r), m.created, m.expires)
func (m *macSigner) signatureString(r *http.Request, opts SignatureOption) (string, error) {
return signatureString(r.Header, m.headers, addRequestTarget(r, opts), m.created, m.expires)
}

func (m *macSigner) signatureStringResponse(r http.ResponseWriter) (string, error) {
return signatureString(r.Header(), m.headers, requestTargetNotPermitted, m.created, m.expires)
}

var _ Signer = &asymmSigner{}
var _ SignerWithOptions = &asymmSigner{}

type asymmSigner struct {
s signer
Expand All @@ -125,13 +133,17 @@ type asymmSigner struct {
}

func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error {
return a.SignRequestWithOptions(pKey, pubKeyId, r, body, SignatureOption{})
}

func (a *asymmSigner) SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte, opts SignatureOption) error {
if body != nil {
err := addDigest(r, a.dAlgo, body)
if err != nil {
return err
}
}
s, err := a.signatureString(r)
s, err := a.signatureString(r, opts)
if err != nil {
return err
}
Expand All @@ -144,6 +156,10 @@ func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *ht
}

func (a *asymmSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error {
return a.SignResponseWithOptions(pKey, pubKeyId, r, body, SignatureOption{})
}

func (a *asymmSigner) SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte, _ SignatureOption) error {
if body != nil {
err := addDigestResponse(r, a.dAlgo, body)
if err != nil {
Expand Down Expand Up @@ -171,8 +187,8 @@ func (a *asymmSigner) signSignature(pKey crypto.PrivateKey, s string) (string, e
return enc, nil
}

func (a *asymmSigner) signatureString(r *http.Request) (string, error) {
return signatureString(r.Header, a.headers, addRequestTarget(r), a.created, a.expires)
func (a *asymmSigner) signatureString(r *http.Request, opts SignatureOption) (string, error) {
return signatureString(r.Header, a.headers, addRequestTarget(r, opts), a.created, a.expires)
}

func (a *asymmSigner) signatureStringResponse(r http.ResponseWriter) (string, error) {
Expand Down Expand Up @@ -269,15 +285,15 @@ func requestTargetNotPermitted(b *bytes.Buffer) error {
return fmt.Errorf("cannot sign with %q on anything other than an http request", RequestTarget)
}

func addRequestTarget(r *http.Request) func(b *bytes.Buffer) error {
func addRequestTarget(r *http.Request, opts SignatureOption) func(b *bytes.Buffer) error {
return func(b *bytes.Buffer) error {
b.WriteString(RequestTarget)
b.WriteString(headerFieldDelimiter)
b.WriteString(strings.ToLower(r.Method))
b.WriteString(requestTargetSeparator)
b.WriteString(r.URL.Path)

if r.URL.RawQuery != "" {
if !opts.ExcludeQueryStringFromPathPseudoHeader && r.URL.RawQuery != "" {
b.WriteString("?")
b.WriteString(r.URL.RawQuery)
}
Expand Down
22 changes: 13 additions & 9 deletions verifying.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"time"
)

var _ Verifier = &verifier{}
var _ VerifierWithOptions = &verifier{}

type verifier struct {
header http.Header
Expand All @@ -20,10 +20,10 @@ type verifier struct {
created int64
expires int64
headers []string
sigStringFn func(http.Header, []string, int64, int64) (string, error)
sigStringFn func(http.Header, []string, int64, int64, SignatureOption) (string, error)
}

func newVerifier(h http.Header, sigStringFn func(http.Header, []string, int64, int64) (string, error)) (*verifier, error) {
func newVerifier(h http.Header, sigStringFn func(http.Header, []string, int64, int64, SignatureOption) (string, error)) (*verifier, error) {
scheme, s, err := getSignatureScheme(h)
if err != nil {
return nil, err
Expand Down Expand Up @@ -62,23 +62,27 @@ func (v *verifier) KeyId() string {
}

func (v *verifier) Verify(pKey crypto.PublicKey, algo Algorithm) error {
return v.VerifyWithOptions(pKey, algo, SignatureOption{})
}

func (v *verifier) VerifyWithOptions(pKey crypto.PublicKey, algo Algorithm, opts SignatureOption) error {
s, err := signerFromString(string(algo))
if err == nil {
return v.asymmVerify(s, pKey)
return v.asymmVerify(s, pKey, opts)
}
m, err := macerFromString(string(algo))
if err == nil {
return v.macVerify(m, pKey)
return v.macVerify(m, pKey, opts)
}
return fmt.Errorf("no crypto implementation available for %q: %s", algo, err)
}

func (v *verifier) macVerify(m macer, pKey crypto.PublicKey) error {
func (v *verifier) macVerify(m macer, pKey crypto.PublicKey, opts SignatureOption) error {
key, ok := pKey.([]byte)
if !ok {
return fmt.Errorf("public key for MAC verifying must be of type []byte")
}
signature, err := v.sigStringFn(v.header, v.headers, v.created, v.expires)
signature, err := v.sigStringFn(v.header, v.headers, v.created, v.expires, opts)
if err != nil {
return err
}
Expand All @@ -95,8 +99,8 @@ func (v *verifier) macVerify(m macer, pKey crypto.PublicKey) error {
return nil
}

func (v *verifier) asymmVerify(s signer, pKey crypto.PublicKey) error {
toHash, err := v.sigStringFn(v.header, v.headers, v.created, v.expires)
func (v *verifier) asymmVerify(s signer, pKey crypto.PublicKey, opts SignatureOption) error {
toHash, err := v.sigStringFn(v.header, v.headers, v.created, v.expires, opts)
if err != nil {
return err
}
Expand Down