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 }