diff --git a/wfe/jose.go b/wfe/jose.go new file mode 100644 index 000000000..22a548593 --- /dev/null +++ b/wfe/jose.go @@ -0,0 +1,48 @@ +package wfe + +import ( + "crypto/rsa" + "fmt" + + "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" + "github.com/letsencrypt/boulder/core" +) + +func algorithmForKey(key *jose.JsonWebKey) (string, error) { + // TODO(https://github.com/letsencrypt/boulder/issues/792): Support EC. + switch key.Key.(type) { + case *rsa.PublicKey: + return string(jose.RS256), nil + } + return "", core.SignatureValidationError("no signature algorithms suitable for given key type") +} + +const ( + noAlgorithmForKey = "WFE.Errors.NoAlgorithmForKey" + invalidJWSAlgorithm = "WFE.Errors.InvalidJWSAlgorithm" + invalidAlgorithmOnKey = "WFE.Errors.InvalidAlgorithmOnKey" +) + +// Check that (1) there is a suitable algorithm for the provided key based on its +// Golang type, (2) the Algorithm field on the JWK is either absent, or matches +// that algorithm, and (3) the Algorithm field on the JWK is present and matches +// that algorithm. Precondition: parsedJws must have exactly one signature on +// it. Returns stat name to increment if err is non-nil. +func checkAlgorithm(key *jose.JsonWebKey, parsedJws *jose.JsonWebSignature) (string, error) { + algorithm, err := algorithmForKey(key) + if err != nil { + return noAlgorithmForKey, err + } + jwsAlgorithm := parsedJws.Signatures[0].Header.Algorithm + if jwsAlgorithm != algorithm { + return invalidJWSAlgorithm, + core.SignatureValidationError(fmt.Sprintf( + "algorithm '%s' in JWS header not acceptable", jwsAlgorithm)) + } + if key.Algorithm != "" && key.Algorithm != algorithm { + return invalidAlgorithmOnKey, + core.SignatureValidationError(fmt.Sprintf( + "algorithm '%s' on JWK is unacceptable", key.Algorithm)) + } + return "", nil +} diff --git a/wfe/jose_test.go b/wfe/jose_test.go new file mode 100644 index 000000000..8a3695550 --- /dev/null +++ b/wfe/jose_test.go @@ -0,0 +1,176 @@ +package wfe + +import ( + "crypto/ecdsa" + "crypto/rsa" + "testing" + + "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" +) + +func TestRejectsNone(t *testing.T) { + wfe, _ := setupWFE(t) + _, _, _, err := wfe.verifyPOST(newRequestEvent(), makePostRequest(` + { + "header": { + "alg": "none", + "jwk": { + "kty": "RSA", + "n": "vrjT", + "e": "AQAB" + } + }, + "payload": "aGkK", + "signature": "" + } + `), true, "foo") + if err == nil { + t.Fatalf("verifyPOST did not reject JWS with alg: 'none'") + } + if err.Error() != "algorithm 'none' in JWS header not acceptable" { + t.Fatalf("verifyPOST rejected JWS with alg: 'none', but for wrong reason: %s", err) + } +} + +func TestRejectsHS256(t *testing.T) { + wfe, _ := setupWFE(t) + _, _, _, err := wfe.verifyPOST(newRequestEvent(), makePostRequest(` + { + "header": { + "alg": "HS256", + "jwk": { + "kty": "RSA", + "n": "vrjT", + "e": "AQAB" + } + }, + "payload": "aGkK", + "signature": "" + } + `), true, "foo") + if err == nil { + t.Fatalf("verifyPOST did not reject JWS with alg: 'HS256'") + } + expected := "algorithm 'HS256' in JWS header not acceptable" + if err.Error() != expected { + t.Fatalf("verifyPOST rejected JWS with alg: 'none', but for wrong reason: got '%s', wanted %s", err, expected) + } +} + +func TestCheckAlgorithm(t *testing.T) { + testCases := []struct { + key jose.JsonWebKey + jws jose.JsonWebSignature + expectedErr string + expectedStat string + }{ + { + jose.JsonWebKey{ + Algorithm: "ES256", + Key: &ecdsa.PublicKey{}, + }, + jose.JsonWebSignature{}, + "no signature algorithms suitable for given key type", + "WFE.Errors.NoAlgorithmForKey", + }, + { + jose.JsonWebKey{ + Algorithm: "HS256", + }, + jose.JsonWebSignature{}, + "no signature algorithms suitable for given key type", + "WFE.Errors.NoAlgorithmForKey", + }, + { + jose.JsonWebKey{ + Key: &rsa.PublicKey{}, + }, + jose.JsonWebSignature{ + Signatures: []jose.Signature{ + jose.Signature{ + Header: jose.JoseHeader{ + Algorithm: "HS256", + }, + }, + }, + }, + "algorithm 'HS256' in JWS header not acceptable", + "WFE.Errors.InvalidJWSAlgorithm", + }, + { + jose.JsonWebKey{ + Algorithm: "HS256", + Key: &rsa.PublicKey{}, + }, + jose.JsonWebSignature{ + Signatures: []jose.Signature{ + jose.Signature{ + Header: jose.JoseHeader{ + Algorithm: "HS256", + }, + }, + }, + }, + "algorithm 'HS256' in JWS header not acceptable", + "WFE.Errors.InvalidJWSAlgorithm", + }, + { + jose.JsonWebKey{ + Algorithm: "HS256", + Key: &rsa.PublicKey{}, + }, + jose.JsonWebSignature{ + Signatures: []jose.Signature{ + jose.Signature{ + Header: jose.JoseHeader{ + Algorithm: "RS256", + }, + }, + }, + }, + "algorithm 'HS256' on JWK is unacceptable", + "WFE.Errors.InvalidAlgorithmOnKey", + }, + } + for i, tc := range testCases { + stat, err := checkAlgorithm(&tc.key, &tc.jws) + if tc.expectedErr != "" && err.Error() != tc.expectedErr { + t.Errorf("TestCheckAlgorithm %d: Expected '%s', got '%s'", i, tc.expectedErr, err) + } + if tc.expectedStat != "" && stat != tc.expectedStat { + t.Errorf("TestCheckAlgorithm %d: Expected stat '%s', got '%s'", i, tc.expectedStat, stat) + } + } +} + +func TestCheckAlgorithmSuccess(t *testing.T) { + _, err := checkAlgorithm(&jose.JsonWebKey{ + Algorithm: "RS256", + Key: &rsa.PublicKey{}, + }, &jose.JsonWebSignature{ + Signatures: []jose.Signature{ + jose.Signature{ + Header: jose.JoseHeader{ + Algorithm: "RS256", + }, + }, + }, + }) + if err != nil { + t.Errorf("RS256 key: Expected nil error, got '%s'", err) + } + _, err = checkAlgorithm(&jose.JsonWebKey{ + Key: &rsa.PublicKey{}, + }, &jose.JsonWebSignature{ + Signatures: []jose.Signature{ + jose.Signature{ + Header: jose.JoseHeader{ + Algorithm: "RS256", + }, + }, + }, + }) + if err != nil { + t.Errorf("RS256 key: Expected nil error, got '%s'", err) + } +} diff --git a/wfe/web-front-end.go b/wfe/web-front-end.go index a2ce1227f..b1b2a4c27 100644 --- a/wfe/web-front-end.go +++ b/wfe/web-front-end.go @@ -7,7 +7,6 @@ package wfe import ( "bytes" - "crypto/rsa" "crypto/x509" "encoding/json" "fmt" @@ -317,38 +316,6 @@ const ( malformedJWS = "Unable to read/verify body" ) -func algorithmForKey(key *jose.JsonWebKey) (string, error) { - // TODO(https://github.com/letsencrypt/boulder/issues/792): Support EC. - switch key.Key.(type) { - case *rsa.PublicKey: - return "RS256", nil - } - return "", core.SignatureValidationError("no signature algorithms suitable for given key type") -} - -// Check that (1) there is a suitable algorithm for the provided key based on its -// Golang type, (2) the Algorithm field on the JWK is either absent, or matches -// that algorithm, and (3) the Algorithm field on the JWK is present and matches -// that algorithm. -func checkAlgorithm(key *jose.JsonWebKey, parsedJws *jose.JsonWebSignature) (string, error) { - algorithm, err := algorithmForKey(key) - if err != nil { - return "NoAlgorithmForKey", err - } - jwsAlgorithm := parsedJws.Signatures[0].Header.Algorithm - if jwsAlgorithm != algorithm { - return "InvalidJWSAlgorithm", - core.SignatureValidationError(fmt.Sprintf( - "algorithm '%s' in JWS header not acceptable", jwsAlgorithm)) - } - if key.Algorithm != "" && key.Algorithm != algorithm { - return "InvalidAlgorithmOnKey", - core.SignatureValidationError(fmt.Sprintf( - "algorithm '%s' on JWK is unacceptable", key.Algorithm)) - } - return "", nil -} - // verifyPOST reads and parses the request body, looks up the Registration // corresponding to its JWK, verifies the JWS signature, checks that the // resource field is present and correct in the JWS protected header, and @@ -456,7 +423,7 @@ func (wfe *WebFrontEndImpl) verifyPOST(logEvent *requestEvent, request *http.Req } if statName, err := checkAlgorithm(key, parsedJws); err != nil { - wfe.stats.Inc(fmt.Sprintf("WFE.Errors.%s", statName), 1, 1.0) + wfe.stats.Inc(statName, 1, 1.0) return nil, nil, reg, err } diff --git a/wfe/web-front-end_test.go b/wfe/web-front-end_test.go index 1665fdec6..f1822ba0f 100644 --- a/wfe/web-front-end_test.go +++ b/wfe/web-front-end_test.go @@ -7,7 +7,6 @@ package wfe import ( "bytes" - "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/json" @@ -1388,157 +1387,6 @@ func TestVerifyPOSTUsesStoredKey(t *testing.T) { test.AssertError(t, err, "No error returned when provided key differed from stored key.") } -func TestRejectsNone(t *testing.T) { - wfe, _ := setupWFE(t) - _, _, _, err := wfe.verifyPOST(newRequestEvent(), makePostRequest(` - { - "header": { - "alg": "none", - "jwk": { - "kty": "RSA", - "n": "vrjT", - "e": "AQAB" - } - }, - "payload": "aGkK", - "signature": "" - } - `), true, "foo") - if err == nil { - t.Fatalf("verifyPOST did not reject JWS with alg: 'none'") - } - if err.Error() != "algorithm 'none' in JWS header not acceptable" { - t.Fatalf("verifyPOST rejected JWS with alg: 'none', but for wrong reason: %s", err) - } -} - -func TestRejectsHS256(t *testing.T) { - wfe, _ := setupWFE(t) - _, _, _, err := wfe.verifyPOST(newRequestEvent(), makePostRequest(` - { - "header": { - "alg": "HS256", - "jwk": { - "kty": "RSA", - "n": "vrjT", - "e": "AQAB" - } - }, - "payload": "aGkK", - "signature": "" - } - `), true, "foo") - if err == nil { - t.Fatalf("verifyPOST did not reject JWS with alg: 'HS256'") - } - expected := "algorithm 'HS256' in JWS header not acceptable" - if err.Error() != expected { - t.Fatalf("verifyPOST rejected JWS with alg: 'none', but for wrong reason: got '%s', wanted %s", err, expected) - } -} - -func TestCheckAlgorithmES(t *testing.T) { - stat, err := checkAlgorithm(&jose.JsonWebKey{ - Algorithm: "ES256", - Key: &ecdsa.PublicKey{}, - }, &jose.JsonWebSignature{}) - expected := "no signature algorithms suitable for given key type" - if err == nil || err.Error() != expected { - t.Errorf("ES256 key: Expected error '%s', got '%s'", expected, err) - } - if stat != "NoAlgorithmForKey" { - t.Errorf("ES256 key: Expected stat '%s', got '%s'", "NoAlgorithmForKey", stat) - } -} - -func TestCheckAlgorithmHS(t *testing.T) { - stat, err := checkAlgorithm(&jose.JsonWebKey{ - Algorithm: "HS256", - }, &jose.JsonWebSignature{}) - expected := "no signature algorithms suitable for given key type" - if err == nil || err.Error() != expected { - t.Errorf("HS256 key: Expected error '%s', got '%s'", expected, err) - } - if stat != "NoAlgorithmForKey" { - t.Errorf("HS256 key: Expected stat '%s', got '%s'", "NoAlgorithmForKey", stat) - } -} - -func TestCheckAlgorithmBadJWS(t *testing.T) { - stat, err := checkAlgorithm(&jose.JsonWebKey{ - Key: &rsa.PublicKey{}, - }, &jose.JsonWebSignature{ - Signatures: []jose.Signature{ - jose.Signature{ - Header: jose.JoseHeader{ - Algorithm: "HS256", - }, - }, - }, - }) - expected := "algorithm 'HS256' in JWS header not acceptable" - if err == nil || err.Error() != expected { - t.Errorf("HS256 JWS: Expected error '%s', got '%s'", expected, err) - } - if stat != "InvalidJWSAlgorithm" { - t.Errorf("HS256 JWS: Expected stat '%s', got '%s'", "InvalidJWSAlgorithm", stat) - } -} - -func TestCheckAlgorithmBadJWK(t *testing.T) { - stat, err := checkAlgorithm(&jose.JsonWebKey{ - Algorithm: "HS256", - Key: &rsa.PublicKey{}, - }, &jose.JsonWebSignature{ - Signatures: []jose.Signature{ - jose.Signature{ - Header: jose.JoseHeader{ - Algorithm: "RS256", - }, - }, - }, - }) - expected := "algorithm 'HS256' on JWK is unacceptable" - if err == nil || err.Error() != expected { - t.Errorf("HS256 JWS: Expected error '%s', got '%s'", expected, err) - } - if stat != "InvalidAlgorithmOnKey" { - t.Errorf("HS256 JWS: Expected stat '%s', got '%s'", "InvalidAlgorithmOnKey", stat) - } -} - -func TestCheckAlgorithmSuccess(t *testing.T) { - _, err := checkAlgorithm(&jose.JsonWebKey{ - Algorithm: "RS256", - Key: &rsa.PublicKey{}, - }, &jose.JsonWebSignature{ - Signatures: []jose.Signature{ - jose.Signature{ - Header: jose.JoseHeader{ - Algorithm: "RS256", - }, - }, - }, - }) - if err != nil { - t.Errorf("RS256 key: Expected nil error, got '%s'", err) - } - _, err = checkAlgorithm(&jose.JsonWebKey{ - Key: &rsa.PublicKey{}, - }, &jose.JsonWebSignature{ - Signatures: []jose.Signature{ - jose.Signature{ - Header: jose.JoseHeader{ - Algorithm: "RS256", - }, - }, - }, - }) - if err != nil { - t.Errorf("RS256 key: Expected nil error, got '%s'", err) - } -} - func TestBadKeyCSR(t *testing.T) { wfe, _ := setupWFE(t) responseWriter := httptest.NewRecorder()