From 330a7f79126e2a353fceccf51d339d4675dfe7f0 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Sat, 9 Dec 2023 08:52:32 -0500 Subject: [PATCH] feat: add option to exclude query params from signature This is technically optional in the IETF spec. Of note, Mastodon does not include the query parameters in the signature, so this is important for interoperability. Signed-off-by: Milas Bowman --- httpsig.go | 64 ++++++++++++++++++++++++++++++++++++++++++++----- httpsig_test.go | 39 +++++++++++++++++++++++------- signing.go | 36 ++++++++++++++++++++-------- verifying.go | 22 ++++++++++------- 4 files changed, 128 insertions(+), 33 deletions(-) diff --git a/httpsig.go b/httpsig.go index 17310c8..ea3736c 100644 --- a/httpsig.go +++ b/httpsig.go @@ -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. // @@ -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 @@ -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 { @@ -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. @@ -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) }) } @@ -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 { diff --git a/httpsig_test.go b/httpsig_test.go index dca48e7..09b93ff 100644 --- a/httpsig_test.go +++ b/httpsig_test.go @@ -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", @@ -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"}, @@ -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) } @@ -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", @@ -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"}, @@ -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) } }) diff --git a/signing.go b/signing.go index e18db41..a2fa38c 100644 --- a/signing.go +++ b/signing.go @@ -39,7 +39,7 @@ const ( var defaultHeaders = []string{dateHeader} -var _ Signer = &macSigner{} +var _ SignerWithOptions = &macSigner{} type macSigner struct { m macer @@ -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 } @@ -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 { @@ -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 @@ -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 } @@ -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 { @@ -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) { @@ -269,7 +285,7 @@ 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) @@ -277,7 +293,7 @@ func addRequestTarget(r *http.Request) func(b *bytes.Buffer) error { 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) } diff --git a/verifying.go b/verifying.go index e39b9dc..52a142a 100644 --- a/verifying.go +++ b/verifying.go @@ -11,7 +11,7 @@ import ( "time" ) -var _ Verifier = &verifier{} +var _ VerifierWithOptions = &verifier{} type verifier struct { header http.Header @@ -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 @@ -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 } @@ -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 }