From 8d61ea6a9aa6290011d37b815fbca0beed6819ff Mon Sep 17 00:00:00 2001 From: Cory Slep Date: Fri, 6 Sep 2019 21:16:36 +0200 Subject: [PATCH] Support signing and verifying the Digest header --- README.md | 15 +++++++-- digest.go | 21 ++++++++++++ httpsig.go | 31 +++++++++++++---- httpsig_test.go | 89 +++++++++++++++++++++++++++++++++++++++---------- signing.go | 37 +++++++++++++++++--- 5 files changed, 161 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 52e7688..3b83854 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ signing of hash schemes. Its goals are: * Remaining flexible with headers included in the signing string * Support both HTTP requests and responses * Explicitly not support known-cryptographically weak algorithms +* Support automatic signing and validating Digest headers ## How to use @@ -25,14 +26,18 @@ Signing a request or response requires creating a new `Signer` and using it: ``` func sign(privateKey crypto.PrivateKey, pubKeyId string, r *http.Request) error { prefs := []httpsig.Algorithm{httpsig.RSA_SHA512, httpsig.RSA_SHA256} + digestAlgorithm := DigestSha256 // The "Date" and "Digest" headers must already be set on r, as well as r.URL. headersToSign := []string{httpsig.RequestTarget, "date", "digest"} - signer, chosenAlgo, err := httpsig.NewSigner(prefs, headersToSign, httpsig.Signature) + signer, chosenAlgo, err := httpsig.NewSigner(prefs, digestAlgorithm, headersToSign, httpsig.Signature) if err != nil { return err } + // To sign the digest, we need to give the signer a copy of the body... + // ...but it is optional, no digest will be signed if given "nil" + body := ... // If r were a http.ResponseWriter, call SignResponse instead. - return signer.SignRequest(privateKey, pubKeyId, r) + return signer.SignRequest(privateKey, pubKeyId, r, body) } ``` @@ -51,7 +56,10 @@ func (s *server) handlerFunc(w http.ResponseWriter, r *http.Request) { // Set headers and such on w s.mu.Lock() defer s.mu.Unlock() - err := s.signer.SignResponse(privateKey, pubKeyId, w) + // To sign the digest, we need to give the signer a copy of the response body... + // ...but it is optional, no digest will be signed if given "nil" + body := ... + err := s.signer.SignResponse(privateKey, pubKeyId, w, body) if err != nil { ... } @@ -76,6 +84,7 @@ func verify(r *http.Request) error { pubKeyId := verifier.KeyId() var algo httpsig.Algorithm = ... var pubKey crypto.PublicKey = ... + // The verifier will verify the Digest in addition to the HTTP signature return verifier.Verify(pubKey, algo) } ``` diff --git a/digest.go b/digest.go index 71000db..9aaeb8c 100644 --- a/digest.go +++ b/digest.go @@ -62,6 +62,27 @@ func addDigest(r *http.Request, algo DigestAlgorithm, b []byte) (err error) { return } +func addDigestResponse(r http.ResponseWriter, algo DigestAlgorithm, b []byte) (err error) { + _, ok := r.Header()[digestHeader] + if ok { + err = fmt.Errorf("cannot add Digest: Digest is already set") + return + } + var h hash.Hash + var a DigestAlgorithm + h, a, err = getHash(algo) + if err != nil { + return + } + sum := h.Sum(b) + r.Header().Add(digestHeader, + fmt.Sprintf("%s%s%s", + a, + digestDelim, + base64.StdEncoding.EncodeToString(sum[:]))) + return +} + func verifyDigest(r *http.Request, body *bytes.Buffer) (err error) { d := r.Header.Get(digestHeader) if len(d) == 0 { diff --git a/httpsig.go b/httpsig.go index efe18be..de9868e 100644 --- a/httpsig.go +++ b/httpsig.go @@ -107,7 +107,14 @@ type Signer interface { // 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. - SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request) error + // + // 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. + SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) 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. @@ -116,7 +123,14 @@ type Signer interface { // 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. - SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter) error + // + // 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. + SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error } // NewSigner creates a new Signer with the provided algorithm preferences to @@ -125,20 +139,23 @@ type Signer interface { // algorithms were available, then the default algorithm is used. The headers // specified will be included into the HTTP signatures. // +// The Digest will also be calculated on a request's body using the provided +// digest algorithm, if "Digest" is one of the headers listed. +// // The provided scheme determines which header is populated with the HTTP // Signature. // // An error is returned if an unknown or a known cryptographically insecure // Algorithm is provided. -func NewSigner(prefs []Algorithm, headers []string, scheme SignatureScheme) (Signer, Algorithm, error) { +func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme) (Signer, Algorithm, error) { for _, pref := range prefs { - s, err := newSigner(pref, headers, scheme) + s, err := newSigner(pref, dAlgo, headers, scheme) if err != nil { continue } return s, pref, err } - s, err := newSigner(defaultAlgorithm, headers, scheme) + s, err := newSigner(defaultAlgorithm, dAlgo, headers, scheme) return s, defaultAlgorithm, err } @@ -187,11 +204,12 @@ func NewResponseVerifier(r *http.Response) (Verifier, error) { }) } -func newSigner(algo Algorithm, headers []string, scheme SignatureScheme) (Signer, error) { +func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme) (Signer, error) { s, err := signerFromString(string(algo)) if err == nil { a := &asymmSigner{ s: s, + dAlgo: dAlgo, headers: headers, targetHeader: scheme, prefix: scheme.authScheme(), @@ -204,6 +222,7 @@ func newSigner(algo Algorithm, headers []string, scheme SignatureScheme) (Signer } c := &macSigner{ m: m, + dAlgo: dAlgo, headers: headers, targetHeader: scheme, prefix: scheme.authScheme(), diff --git a/httpsig_test.go b/httpsig_test.go index d72a47b..f4fe39e 100644 --- a/httpsig_test.go +++ b/httpsig_test.go @@ -29,7 +29,9 @@ const ( type httpsigTest struct { name string prefs []Algorithm + digestAlg DigestAlgorithm headers []string + body []byte scheme SignatureScheme privKey crypto.PrivateKey pubKey crypto.PublicKey @@ -37,6 +39,7 @@ type httpsigTest struct { expectedAlgorithm Algorithm expectErrorSigningResponse bool expectRequestPath bool + expectedDigest string } var ( @@ -62,6 +65,7 @@ func init() { { name: "rsa signature", prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, headers: []string{"Date", "Digest"}, scheme: Signature, privKey: privKey, @@ -69,9 +73,23 @@ func init() { pubKeyId: "pubKeyId", expectedAlgorithm: RSA_SHA512, }, + { + name: "digest on rsa signature", + prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + body: []byte("Last night as I lay dreaming This strangest kind of feeling Revealed its secret meaning And now I know..."), + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + expectedDigest: "SHA-256=TGFzdCBuaWdodCBhcyBJIGxheSBkcmVhbWluZyBUaGlzIHN0cmFuZ2VzdCBraW5kIG9mIGZlZWxpbmcgUmV2ZWFsZWQgaXRzIHNlY3JldCBtZWFuaW5nIEFuZCBub3cgSSBrbm93Li4u47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + }, { name: "hmac signature", prefs: []Algorithm{HMAC_SHA256}, + digestAlg: DigestSha256, headers: []string{"Date", "Digest"}, scheme: Signature, privKey: macKey, @@ -79,9 +97,23 @@ func init() { pubKeyId: "pubKeyId", expectedAlgorithm: HMAC_SHA256, }, + { + name: "digest on hmac signature", + prefs: []Algorithm{HMAC_SHA256}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + body: []byte("I've never ever been to paradise I've never ever seen no angel's eyes You'll never ever let this magic die No matter where you are, you are my lucky star."), + scheme: Signature, + privKey: macKey, + pubKey: macKey, + pubKeyId: "pubKeyId", + expectedAlgorithm: HMAC_SHA256, + expectedDigest: "SHA-256=SSd2ZSBuZXZlciBldmVyIGJlZW4gdG8gcGFyYWRpc2UgSSd2ZSBuZXZlciBldmVyIHNlZW4gbm8gYW5nZWwncyBleWVzIFlvdSdsbCBuZXZlciBldmVyIGxldCB0aGlzIG1hZ2ljIGRpZSBObyBtYXR0ZXIgd2hlcmUgeW91IGFyZSwgeW91IGFyZSBteSBsdWNreSBzdGFyLuOwxEKY/BwUmvv0yJlvuSQnrkHkZJuTTKSVmRt4UrhV", + }, { name: "rsa authorization", prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, headers: []string{"Date", "Digest"}, scheme: Authorization, privKey: privKey, @@ -92,6 +124,7 @@ func init() { { name: "hmac authorization", prefs: []Algorithm{HMAC_SHA256}, + digestAlg: DigestSha256, headers: []string{"Date", "Digest"}, scheme: Authorization, privKey: macKey, @@ -101,6 +134,7 @@ func init() { }, { name: "default algo", + digestAlg: DigestSha256, headers: []string{"Date", "Digest"}, scheme: Signature, privKey: privKey, @@ -111,6 +145,7 @@ func init() { { name: "default headers", prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, scheme: Signature, privKey: privKey, pubKey: privKey.Public(), @@ -120,6 +155,7 @@ func init() { { name: "different pub key id", prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, headers: []string{"Date", "Digest"}, scheme: Signature, privKey: privKey, @@ -130,6 +166,7 @@ func init() { { name: "with request target", prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, headers: []string{"Date", "Digest", RequestTarget}, scheme: Signature, privKey: privKey, @@ -168,7 +205,7 @@ func toHeaderSignatureParameters(k string, vals []string) string { func TestSignerRequest(t *testing.T) { testFn := func(t *testing.T, test httpsigTest) { - s, a, err := NewSigner(test.prefs, test.headers, test.scheme) + s, a, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) if err != nil { t.Fatalf("%s", err) } @@ -181,8 +218,10 @@ func TestSignerRequest(t *testing.T) { t.Fatalf("%s", err) } req.Header.Set("Date", testDate) - req.Header.Set("Digest", testDigest) - err = s.SignRequest(test.privKey, test.pubKeyId, req) + if test.body == nil { + req.Header.Set("Digest", testDigest) + } + err = s.SignRequest(test.privKey, test.pubKeyId, req, test.body) if err != nil { t.Fatalf("%s", err) } @@ -201,6 +240,8 @@ func TestSignerRequest(t *testing.T) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) } else if !strings.Contains(vals[0], signatureParameter) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], signatureParameter) + } else if test.body != nil && req.Header.Get("Digest") != test.expectedDigest { + t.Fatalf("%s\ndoes not match\n%s", req.Header.Get("Digest"), test.expectedDigest) } // For schemes with an authScheme, enforce its is present and at the beginning if len(test.scheme.authScheme()) > 0 { @@ -218,12 +259,14 @@ func TestSignerRequest(t *testing.T) { func TestSignerResponse(t *testing.T) { testFn := func(t *testing.T, test httpsigTest) { - s, _, err := NewSigner(test.prefs, test.headers, test.scheme) + s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) // Test response signing resp := httptest.NewRecorder() resp.HeaderMap.Set("Date", testDate) - resp.HeaderMap.Set("Digest", testDigest) - err = s.SignResponse(test.privKey, test.pubKeyId, resp) + if test.body == nil { + resp.HeaderMap.Set("Digest", testDigest) + } + err = s.SignResponse(test.privKey, test.pubKeyId, resp, test.body) if test.expectErrorSigningResponse { if err != nil { // Skip rest of testing @@ -247,6 +290,8 @@ func TestSignerResponse(t *testing.T) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) } else if !strings.Contains(vals[0], signatureParameter) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], signatureParameter) + } else if test.body != nil && resp.Header().Get("Digest") != test.expectedDigest { + t.Fatalf("%s\ndoes not match\n%s", resp.Header().Get("Digest"), test.expectedDigest) } // For schemes with an authScheme, enforce its is present and at the beginning if len(test.scheme.authScheme()) > 0 { @@ -266,6 +311,7 @@ func TestNewSignerRequestMissingHeaders(t *testing.T) { failingTests := []struct { name string prefs []Algorithm + digestAlg DigestAlgorithm headers []string scheme SignatureScheme privKey crypto.PrivateKey @@ -275,6 +321,7 @@ func TestNewSignerRequestMissingHeaders(t *testing.T) { { name: "wants digest", prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, headers: []string{"Date", "Digest"}, scheme: Signature, privKey: privKey, @@ -285,7 +332,7 @@ func TestNewSignerRequestMissingHeaders(t *testing.T) { for _, test := range failingTests { t.Run(test.name, func(t *testing.T) { test := test - s, a, err := NewSigner(test.prefs, test.headers, test.scheme) + s, a, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) if err != nil { t.Fatalf("%s", err) } @@ -297,7 +344,7 @@ func TestNewSignerRequestMissingHeaders(t *testing.T) { t.Fatalf("%s", err) } req.Header.Set("Date", testDate) - err = s.SignRequest(test.privKey, test.pubKeyId, req) + err = s.SignRequest(test.privKey, test.pubKeyId, req, nil) if err == nil { t.Fatalf("expect error but got nil") } @@ -309,6 +356,7 @@ func TestNewSignerResponseMissingHeaders(t *testing.T) { failingTests := []struct { name string prefs []Algorithm + digestAlg DigestAlgorithm headers []string scheme SignatureScheme privKey crypto.PrivateKey @@ -319,6 +367,7 @@ func TestNewSignerResponseMissingHeaders(t *testing.T) { { name: "want digest", prefs: []Algorithm{RSA_SHA512}, + digestAlg: DigestSha256, headers: []string{"Date", "Digest"}, scheme: Signature, privKey: privKey, @@ -329,7 +378,7 @@ func TestNewSignerResponseMissingHeaders(t *testing.T) { for _, test := range failingTests { t.Run(test.name, func(t *testing.T) { test := test - s, a, err := NewSigner(test.prefs, test.headers, test.scheme) + s, a, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) if err != nil { t.Fatalf("%s", err) } @@ -339,7 +388,7 @@ func TestNewSignerResponseMissingHeaders(t *testing.T) { resp := httptest.NewRecorder() resp.HeaderMap.Set("Date", testDate) resp.HeaderMap.Set("Digest", testDigest) - err = s.SignResponse(test.privKey, test.pubKeyId, resp) + err = s.SignResponse(test.privKey, test.pubKeyId, resp, nil) if err != nil { t.Fatalf("expected error, got nil") } @@ -357,12 +406,14 @@ func TestNewVerifier(t *testing.T) { t.Fatalf("%s", err) } req.Header.Set("Date", testDate) - req.Header.Set("Digest", testDigest) - s, _, err := NewSigner(test.prefs, test.headers, test.scheme) + if test.body == nil { + req.Header.Set("Digest", testDigest) + } + s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) if err != nil { t.Fatalf("%s", err) } - err = s.SignRequest(test.privKey, test.pubKeyId, req) + err = s.SignRequest(test.privKey, test.pubKeyId, req, test.body) if err != nil { t.Fatalf("%s", err) } @@ -392,12 +443,14 @@ func TestNewResponseVerifier(t *testing.T) { // Prepare resp := httptest.NewRecorder() resp.HeaderMap.Set("Date", testDate) - resp.HeaderMap.Set("Digest", testDigest) - s, _, err := NewSigner(test.prefs, test.headers, test.scheme) + if test.body == nil { + resp.HeaderMap.Set("Digest", testDigest) + } + s, _, err := NewSigner(test.prefs, test.digestAlg, test.headers, test.scheme) if err != nil { t.Fatalf("%s", err) } - err = s.SignResponse(test.privKey, test.pubKeyId, resp) + err = s.SignResponse(test.privKey, test.pubKeyId, resp, test.body) if err != nil { t.Fatalf("%s", err) } @@ -465,12 +518,12 @@ func Test_Signing_HTTP_Messages_AppendixC(t *testing.T) { r.Header["Content-Type"] = []string{"application/json"} setDigest(r) - s, _, err := NewSigner([]Algorithm{RSA_SHA256}, test.headers, Authorization) + s, _, err := NewSigner([]Algorithm{RSA_SHA256}, DigestSha256, test.headers, Authorization) if err != nil { t.Fatalf("error creating signer: %s", err) } - if err := s.SignRequest(testSpecRSAPrivateKey, "Test", r); err != nil { + if err := s.SignRequest(testSpecRSAPrivateKey, "Test", r, nil); err != nil { t.Fatalf("error signing request: %s", err) } diff --git a/signing.go b/signing.go index e434e36..f3c9421 100644 --- a/signing.go +++ b/signing.go @@ -40,12 +40,20 @@ var _ Signer = &macSigner{} type macSigner struct { m macer + makeDigest bool + dAlgo DigestAlgorithm headers []string targetHeader SignatureScheme prefix string } -func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request) error { +func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error { + if body != nil { + err := addDigest(r, m.dAlgo, body) + if err != nil { + return err + } + } s, err := m.signatureString(r) if err != nil { return err @@ -58,7 +66,13 @@ func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http return nil } -func (m *macSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter) error { +func (m *macSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error { + if body != nil { + err := addDigestResponse(r, m.dAlgo, body) + if err != nil { + return err + } + } s, err := m.signatureStringResponse(r) if err != nil { return err @@ -96,17 +110,24 @@ var _ Signer = &asymmSigner{} type asymmSigner struct { s signer + makeDigest bool + dAlgo DigestAlgorithm headers []string targetHeader SignatureScheme prefix string } -func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request) error { +func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error { + if body != nil { + err := addDigest(r, a.dAlgo, body) + if err != nil { + return err + } + } s, err := a.signatureString(r) if err != nil { return err } - enc, err := a.signSignature(pKey, s) if err != nil { return err @@ -115,7 +136,13 @@ func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *ht return nil } -func (a *asymmSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter) error { +func (a *asymmSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error { + if body != nil { + err := addDigestResponse(r, a.dAlgo, body) + if err != nil { + return err + } + } s, err := a.signatureStringResponse(r) if err != nil { return err