From 22b5f0dd8ba6d8b8d65c1b38ead481b97ba2b790 Mon Sep 17 00:00:00 2001 From: Cory Slep Date: Fri, 18 May 2018 00:04:09 +0200 Subject: [PATCH] Finish core tests. Includes bugfixes for things exposed in the testing process. With this commit, the library should be good for first use. --- httpsig.go | 6 +- httpsig_test.go | 357 ++++++++++++++++++++++++++++++++++++++++++++++++ signing.go | 7 +- verifying.go | 19 ++- 4 files changed, 379 insertions(+), 10 deletions(-) create mode 100644 httpsig_test.go diff --git a/httpsig.go b/httpsig.go index 07ddf71..4b14a1f 100644 --- a/httpsig.go +++ b/httpsig.go @@ -113,12 +113,10 @@ type Signer interface { // Algorithm is provided. func NewSigner(prefs []Algorithm, headers []string, scheme SignatureScheme) (Signer, Algorithm, error) { for _, pref := range prefs { - if ok, err := isAvailable(string(pref)); err != nil { - return nil, "", err - } else if !ok { + s, err := newSigner(pref, headers, scheme) + if err != nil { continue } - s, err := newSigner(pref, headers, scheme) return s, pref, err } s, err := newSigner(defaultAlgorithm, headers, scheme) diff --git a/httpsig_test.go b/httpsig_test.go new file mode 100644 index 0000000..d4c5978 --- /dev/null +++ b/httpsig_test.go @@ -0,0 +1,357 @@ +package httpsig + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +const ( + testUrl = "foo.net/bar/baz?q=test&r=ok" + testDate = "Tue, 07 Jun 2014 20:51:35 GMT" + testDigest = "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=" + testMethod = "GET" +) + +type httpsigTest struct { + name string + prefs []Algorithm + headers []string + scheme SignatureScheme + privKey crypto.PrivateKey + pubKey crypto.PublicKey + pubKeyId string + expectedAlgorithm Algorithm + expectErrorSigningResponse bool +} + +var ( + privKey *rsa.PrivateKey + macKey []byte + tests []httpsigTest +) + +func init() { + var err error + privKey, err = rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + macKey = make([]byte, 128) + err = readFullFromCrypto(macKey) + if err != nil { + panic(err) + } + tests = []httpsigTest{ + { + name: "rsa signature", + prefs: []Algorithm{RSA_SHA512}, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + }, + { + name: "hmac signature", + prefs: []Algorithm{HMAC_SHA256}, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: macKey, + pubKey: macKey, + pubKeyId: "pubKeyId", + expectedAlgorithm: HMAC_SHA256, + }, + { + name: "rsa authorization", + prefs: []Algorithm{RSA_SHA512}, + headers: []string{"Date", "Digest"}, + scheme: Authorization, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + }, + { + name: "hmac authorization", + prefs: []Algorithm{HMAC_SHA256}, + headers: []string{"Date", "Digest"}, + scheme: Authorization, + privKey: macKey, + pubKey: macKey, + pubKeyId: "pubKeyId", + expectedAlgorithm: HMAC_SHA256, + }, + { + name: "default algo", + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA256, + }, + { + name: "default headers", + prefs: []Algorithm{RSA_SHA512}, + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + }, + { + name: "different pub key id", + prefs: []Algorithm{RSA_SHA512}, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "i write code that sucks", + expectedAlgorithm: RSA_SHA512, + }, + { + name: "with request target", + prefs: []Algorithm{RSA_SHA512}, + headers: []string{"Date", "Digest", RequestTarget}, + scheme: Signature, + privKey: privKey, + pubKey: privKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + expectErrorSigningResponse: true, + }, + } + +} + +func toSignatureParameter(k, v string) string { + return fmt.Sprintf("%s%s%s%s%s", k, parameterKVSeparater, parameterValueDelimiter, v, parameterValueDelimiter) +} + +func toHeaderSignatureParameters(k string, vals []string) string { + if len(vals) == 0 { + vals = defaultHeaders + } + v := strings.Join(vals, headerParameterValueDelim) + k = strings.ToLower(k) + v = strings.ToLower(v) + return fmt.Sprintf("%s%s%s%s%s", k, parameterKVSeparater, parameterValueDelimiter, v, parameterValueDelimiter) +} + +func TestNewSigner(t *testing.T) { + for _, test := range tests { + s, a, err := NewSigner(test.prefs, test.headers, test.scheme) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + if a != test.expectedAlgorithm { + t.Fatalf("%q: got %s, want %s", test.name, a, test.expectedAlgorithm) + } + // Test request signing + req, err := http.NewRequest(testMethod, testUrl, nil) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + req.Header.Set("Date", testDate) + req.Header.Set("Digest", testDigest) + err = s.SignRequest(test.privKey, test.pubKeyId, req) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + vals, ok := req.Header[string(test.scheme)] + if !ok { + t.Fatalf("%q: not in header %s", test.name, test.scheme) + } + if len(vals) != 1 { + t.Fatalf("%q: too many in header %s: %d", test.name, test.scheme, len(vals)) + } + if p := toSignatureParameter(keyIdParameter, test.pubKeyId); !strings.Contains(vals[0], p) { + t.Fatalf("%q: %s\ndoes not contain\n%s", test.name, vals[0], p) + } else if p := toSignatureParameter(algorithmParameter, string(test.expectedAlgorithm)); !strings.Contains(vals[0], p) { + t.Fatalf("%q: %s\ndoes not contain\n%s", test.name, vals[0], p) + } else if p := toHeaderSignatureParameters(headersParameter, test.headers); !strings.Contains(vals[0], p) { + t.Fatalf("%q: %s\ndoes not contain\n%s", test.name, vals[0], p) + } else if !strings.Contains(vals[0], signatureParameter) { + t.Fatalf("%q: %s\ndoes not contain\n%s", test.name, vals[0], signatureParameter) + } + // 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.expectErrorSigningResponse { + if err != nil { + // Skip rest of testing + continue + } else { + t.Fatalf("%q: expected error, got nil", test.name) + } + } + vals, ok = resp.HeaderMap[string(test.scheme)] + if !ok { + t.Fatalf("%q: not in header %s", test.name, test.scheme) + } + if len(vals) != 1 { + t.Fatalf("%q: too many in header %s: %d", test.name, test.scheme, len(vals)) + } + if p := toSignatureParameter(keyIdParameter, test.pubKeyId); !strings.Contains(vals[0], p) { + t.Fatalf("%q: %s\ndoes not contain\n%s", test.name, vals[0], p) + } else if p := toSignatureParameter(algorithmParameter, string(test.expectedAlgorithm)); !strings.Contains(vals[0], p) { + t.Fatalf("%q: %s\ndoes not contain\n%s", test.name, vals[0], p) + } else if p := toHeaderSignatureParameters(headersParameter, test.headers); !strings.Contains(vals[0], p) { + t.Fatalf("%q: %s\ndoes not contain\n%s", test.name, vals[0], p) + } else if !strings.Contains(vals[0], signatureParameter) { + t.Fatalf("%q: %s\ndoes not contain\n%s", test.name, vals[0], signatureParameter) + } + } +} + +func TestNewSignerRequestMissingHeaders(t *testing.T) { + failingTests := []struct { + name string + prefs []Algorithm + headers []string + scheme SignatureScheme + privKey crypto.PrivateKey + pubKeyId string + expectedAlgorithm Algorithm + }{ + { + name: "wants digest", + prefs: []Algorithm{RSA_SHA512}, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privKey, + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + }, + } + for _, test := range failingTests { + s, a, err := NewSigner(test.prefs, test.headers, test.scheme) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + if a != test.expectedAlgorithm { + t.Fatalf("%q: got %s, want %s", test.name, a, test.expectedAlgorithm) + } + req, err := http.NewRequest(testMethod, testUrl, nil) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + req.Header.Set("Date", testDate) + err = s.SignRequest(test.privKey, test.pubKeyId, req) + if err == nil { + t.Fatalf("%q: expect error but got nil", test.name) + } + } +} + +func TestNewSignerResponseMissingHeaders(t *testing.T) { + failingTests := []struct { + name string + prefs []Algorithm + headers []string + scheme SignatureScheme + privKey crypto.PrivateKey + pubKeyId string + expectedAlgorithm Algorithm + expectErrorSigningResponse bool + }{ + { + name: "want digest", + prefs: []Algorithm{RSA_SHA512}, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privKey, + pubKeyId: "pubKeyId", + expectedAlgorithm: RSA_SHA512, + }, + } + for _, test := range failingTests { + s, a, err := NewSigner(test.prefs, test.headers, test.scheme) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + if a != test.expectedAlgorithm { + t.Fatalf("%q: got %s, want %s", test.name, a, test.expectedAlgorithm) + } + resp := httptest.NewRecorder() + resp.HeaderMap.Set("Date", testDate) + resp.HeaderMap.Set("Digest", testDigest) + err = s.SignResponse(test.privKey, test.pubKeyId, resp) + if err != nil { + t.Fatalf("%q: expected error, got nil", test.name) + } + } +} + +func TestNewVerifier(t *testing.T) { + for _, test := range tests { + // Prepare + req, err := http.NewRequest(testMethod, testUrl, nil) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + req.Header.Set("Date", testDate) + req.Header.Set("Digest", testDigest) + s, _, err := NewSigner(test.prefs, test.headers, test.scheme) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + err = s.SignRequest(test.privKey, test.pubKeyId, req) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + // Test verification + v, err := NewVerifier(req) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + if v.KeyId() != test.pubKeyId { + t.Fatalf("%q: got %s, want %s", test.name, v.KeyId(), test.pubKeyId) + } + err = v.Verify(test.pubKey, test.expectedAlgorithm) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + } +} + +func TestNewResponseVerifier(t *testing.T) { + for _, test := range tests { + if test.expectErrorSigningResponse { + continue + } + // Prepare + resp := httptest.NewRecorder() + resp.HeaderMap.Set("Date", testDate) + resp.HeaderMap.Set("Digest", testDigest) + s, _, err := NewSigner(test.prefs, test.headers, test.scheme) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + err = s.SignResponse(test.privKey, test.pubKeyId, resp) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + // Test verification + v, err := NewResponseVerifier(resp.Result()) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + if v.KeyId() != test.pubKeyId { + t.Fatalf("%q: got %s, want %s", test.name, v.KeyId(), test.pubKeyId) + } + err = v.Verify(test.pubKey, test.expectedAlgorithm) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + } +} diff --git a/signing.go b/signing.go index 47a98af..5da83b0 100644 --- a/signing.go +++ b/signing.go @@ -165,7 +165,12 @@ func setSignatureHeader(h http.Header, targetHeader, pubKeyId, algo, enc string, b.WriteString(headersParameter) b.WriteString(parameterKVSeparater) b.WriteString(parameterValueDelimiter) - b.WriteString(strings.Join(headers, headerParameterValueDelim)) + for i, h := range headers { + b.WriteString(strings.ToLower(h)) + if i != len(headers)-1 { + b.WriteString(headerParameterValueDelim) + } + } b.WriteString(parameterValueDelimiter) b.WriteString(parameterSeparater) // Signature diff --git a/verifying.go b/verifying.go index daf3640..8fbc2f6 100644 --- a/verifying.go +++ b/verifying.go @@ -2,6 +2,7 @@ package httpsig import ( "crypto" + "encoding/base64" "fmt" "net/http" "strings" @@ -56,11 +57,15 @@ func (v *verifier) macVerify(m macer, pKey crypto.PublicKey) error { if !ok { return fmt.Errorf("public key for MAC verifying must be of type []byte") } - actualMAC, err := v.sigStringFn(v.header, v.headers) + signature, err := v.sigStringFn(v.header, v.headers) if err != nil { return err } - ok, err = m.Equal([]byte(v.signature), []byte(actualMAC), key) + actualMAC, err := base64.StdEncoding.DecodeString(v.signature) + if err != nil { + return err + } + ok, err = m.Equal([]byte(signature), actualMAC, key) if err != nil { return err } else if !ok { @@ -74,7 +79,11 @@ func (v *verifier) asymmVerify(s signer, pKey crypto.PublicKey) error { if err != nil { return err } - err = s.Verify(pKey, []byte(toHash), []byte(v.signature)) + signature, err := base64.StdEncoding.DecodeString(v.signature) + if err != nil { + return err + } + err = s.Verify(pKey, []byte(toHash), signature) if err != nil { return err } @@ -104,9 +113,9 @@ func getSignatureScheme(h http.Header) (string, error) { func getSignatureComponents(s string) (kId, sig string, headers []string, err error) { params := strings.Split(s, parameterSeparater) for _, p := range params { - kv := strings.Split(p, parameterKVSeparater) + kv := strings.SplitN(p, parameterKVSeparater, 2) if len(kv) != 2 { - err = fmt.Errorf("malformed http signature parameter: %q", p) + err = fmt.Errorf("malformed http signature parameter: %v", kv) return } k := kv[0]