diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..e6af26d --- /dev/null +++ b/BUILD @@ -0,0 +1,122 @@ +cc_library( + name = "jwt_verify_lib", + srcs = [ + "src/check_audience.cc", + "src/jwks.cc", + "src/jwt.cc", + "src/status.cc", + "src/verify.cc", + ], + hdrs = [ + "jwt_verify_lib/check_audience.h", + "jwt_verify_lib/jwks.h", + "jwt_verify_lib/jwt.h", + "jwt_verify_lib/status.h", + "jwt_verify_lib/verify.h", + ], + visibility = ["//visibility:public"], + deps = [ + "//external:abseil_strings", + "//external:rapidjson", + "//external:ssl", + ], +) + +cc_test( + name = "check_audience_test", + srcs = [ + "src/check_audience_test.cc", + ], + linkopts = [ + "-lm", + "-lpthread", + ], + linkstatic = 1, + deps = [ + ":jwt_verify_lib", + "//external:googletest_main", + ], +) + +cc_test( + name = "jwt_test", + srcs = [ + "src/jwt_test.cc", + ], + linkopts = [ + "-lm", + "-lpthread", + ], + linkstatic = 1, + deps = [ + ":jwt_verify_lib", + "//external:googletest_main", + ], +) + +cc_test( + name = "jwks_test", + srcs = [ + "src/jwks_test.cc", + ], + linkopts = [ + "-lm", + "-lpthread", + ], + linkstatic = 1, + deps = [ + ":jwt_verify_lib", + "//external:googletest_main", + ], +) + +cc_test( + name = "verify_pem_test", + srcs = [ + "src/test_common.h", + "src/verify_pem_test.cc", + ], + linkopts = [ + "-lm", + "-lpthread", + ], + linkstatic = 1, + deps = [ + ":jwt_verify_lib", + "//external:googletest_main", + ], +) + +cc_test( + name = "verify_jwk_rsa_test", + srcs = [ + "src/test_common.h", + "src/verify_jwk_rsa_test.cc", + ], + linkopts = [ + "-lm", + "-lpthread", + ], + linkstatic = 1, + deps = [ + ":jwt_verify_lib", + "//external:googletest_main", + ], +) + +cc_test( + name = "verify_jwk_ec_test", + srcs = [ + "src/test_common.h", + "src/verify_jwk_ec_test.cc", + ], + linkopts = [ + "-lm", + "-lpthread", + ], + linkstatic = 1, + deps = [ + ":jwt_verify_lib", + "//external:googletest_main", + ], +) diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..ffd5f49 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,13 @@ +load( + "//:repositories.bzl", + "boringssl_repositories", + "googletest_repositories", + "rapidjson_repositories", + "abseil_repositories", +) + +boringssl_repositories() +googletest_repositories() +rapidjson_repositories() +abseil_repositories() + diff --git a/googletest.BUILD b/googletest.BUILD new file mode 100644 index 0000000..7df7ab7 --- /dev/null +++ b/googletest.BUILD @@ -0,0 +1,40 @@ + +cc_library( + name = "googletest", + srcs = [ + "googletest/src/gtest-all.cc", + "googlemock/src/gmock-all.cc", + ], + hdrs = glob([ + "googletest/include/**/*.h", + "googlemock/include/**/*.h", + "googletest/src/*.cc", + "googletest/src/*.h", + "googlemock/src/*.cc", + ]), + includes = [ + "googlemock", + "googletest", + "googletest/include", + "googlemock/include", + ], + visibility = ["//visibility:public"], +) + +cc_library( + name = "googletest_main", + srcs = ["googlemock/src/gmock_main.cc"], + visibility = ["//visibility:public"], + deps = [":googletest"], +) + +cc_library( + name = "googletest_prod", + hdrs = [ + "googletest/include/gtest/gtest_prod.h", + ], + includes = [ + "googletest/include", + ], + visibility = ["//visibility:public"], +) diff --git a/jwt_verify_lib/check_audience.h b/jwt_verify_lib/check_audience.h new file mode 100644 index 0000000..a21de6d --- /dev/null +++ b/jwt_verify_lib/check_audience.h @@ -0,0 +1,53 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.#pragma once + +#pragma once + +#include +#include +#include +#include + +#include "jwt_verify_lib/status.h" + +namespace google { +namespace jwt_verify { + +/** + * RFC for JWT `aud `_ only + * specifies case sensitive comparison. But experiences showed that users + * easily add wrong scheme and tailing slash to cause mis-match. + * In this implemeation, scheme portion of URI and tailing slash is removed + * before comparison. + */ +class CheckAudience { + public: + // Construct the object with a list audiences from config. + CheckAudience(const std::vector& config_audiences); + + // Check any of jwt_audiences is matched with one of configurated ones. + bool areAudiencesAllowed(const std::vector& jwt_audiences) const; + + // check if config audiences is empty + bool empty() const { return config_audiences_.empty(); } + + private: + // configured audiences; + std::set config_audiences_; +}; + +typedef std::unique_ptr CheckAudiencePtr; + +} // namespace jwt_verify +} // namespace google diff --git a/jwt_verify_lib/jwks.h b/jwt_verify_lib/jwks.h new file mode 100644 index 0000000..f426687 --- /dev/null +++ b/jwt_verify_lib/jwks.h @@ -0,0 +1,72 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.#pragma once + +#pragma once + +#include +#include + +#include "jwt_verify_lib/status.h" + +#include "openssl/ec.h" +#include "openssl/evp.h" + +namespace google { +namespace jwt_verify { + +/** + * Class to parse and a hold JSON Web Key Set. + * + * Usage example: + * JwksPtr keys = Jwks::createFrom(jwks_string, type); + * if (keys->getStatus() == Status::Ok) { ... } + */ +class Jwks : public WithStatus { + public: + // Format of public key. + enum Type { PEM, JWKS }; + + // Create from string + static std::unique_ptr createFrom(const std::string& pkey, Type type); + + // Struct for JSON Web Key + struct Pubkey { + bssl::UniquePtr evp_pkey_; + bssl::UniquePtr ec_key_; + std::string kid_; + std::string kty_; + std::string alg_; + bool alg_specified_ = false; + bool kid_specified_ = false; + bool pem_format_ = false; + }; + typedef std::unique_ptr PubkeyPtr; + + // Access to list of Jwks + const std::vector& keys() const { return keys_; } + + private: + // Create Pem + void createFromPemCore(const std::string& pkey_pem); + // Create Jwks + void createFromJwksCore(const std::string& pkey_jwks); + + // List of Jwks + std::vector keys_; +}; + +typedef std::unique_ptr JwksPtr; + +} // namespace jwt_verify +} // namespace google diff --git a/jwt_verify_lib/jwt.h b/jwt_verify_lib/jwt.h new file mode 100644 index 0000000..a8c15f7 --- /dev/null +++ b/jwt_verify_lib/jwt.h @@ -0,0 +1,61 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.#pragma once + +#pragma once + +#include +#include + +#include "jwt_verify_lib/status.h" + +namespace google { +namespace jwt_verify { + +/** + * struct to hold a JWT data. + */ +struct Jwt { + // header string + std::string header_str_; + // header base64_url encoded + std::string header_str_base64url_; + + // payload string + std::string payload_str_; + // payload base64_url encoded + std::string payload_str_base64url_; + // signature string + std::string signature_; + // alg + std::string alg_; + // kid + std::string kid_; + // iss + std::string iss_; + // audiences + std::vector audiences_; + // sub + std::string sub_; + // expiration + int64_t exp_ = 0; + + /** + * Parse Jwt from string text + * @return the status. + */ + Status parseFromString(const std::string& jwt); +}; + +} // namespace jwt_verify +} // namespace google diff --git a/jwt_verify_lib/status.h b/jwt_verify_lib/status.h new file mode 100644 index 0000000..040bdc5 --- /dev/null +++ b/jwt_verify_lib/status.h @@ -0,0 +1,165 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +namespace google { +namespace jwt_verify { + +/** + * Define the Jwt verification error status. + */ +enum class Status { + Ok = 0, + + // Jwt errors: + + // Jwt missing. + JwtMissed = 1, + + // Jwt expired. + JwtExpired = 2, + + // JWT is not in the form of Header.Payload.Signature + JwtBadFormat = 3, + + // Jwt header is an invalid Base64url input or an invalid JSON. + JwtHeaderParseError = 4, + + // "alg" in the header is not a string. + JwtHeaderBadAlg = 6, + + // Value of "alg" in the header is invalid. + JwtHeaderNotImplementedAlg = 7, + + // "kid" in the header is not a string. + JwtHeaderBadKid = 8, + + // Jwt payload is an invalid Base64url input or an invalid JSON. + JwtPayloadParseError = 9, + + // Jwt signature is an invalid Base64url input. + JwtSignatureParseError = 10, + + // Issuer is not configured. + JwtUnknownIssuer = 11, + + // Audience is not allowed. + JwtAudienceNotAllowed = 12, + + // Jwt verification fails. + JwtVerificationFail = 13, + + // Jwks errors + + // Jwks is an invalid JSON. + JwksParseError = 14, + + // Jwks does not have "keys". + JwksNoKeys = 15, + + // "keys" in Jwks is not an array. + JwksBadKeys = 16, + + // Jwks doesn't have any valid public key. + JwksNoValidKeys = 17, + + // Jwks doesn't have key to match kid or alg from Jwt. + JwksKidAlgMismatch = 18, + + // Jwks PEM public key is an invalid Base64. + JwksPemBadBase64 = 19, + + // Jwks PEM public key parse error. + JwksPemParseError = 19, + + // "n" or "e" field of a Jwk RSA is missing or has a parse error. + JwksRsaParseError = 20, + + // Failed to create a EC_KEY object. + JwksEcCreateKeyFail = 21, + + // "x" or "y" field of a Jwk EC is missing or has a parse error. + JwksEcParseError = 22, + + // Failed to fetch public key + JwksFetchFail = 23, + + // "kty" is missing in "keys". + JwksMissingKty = 24, + // "kty" is not string type in "keys". + JwksBadKty = 25, + // "kty" is not supported in "keys". + JwksNotImplementedKty = 26, + + // "alg" is not started with "RS" for a RSA key + JwksRSAKeyBadAlg = 27, + // "n" field is missing for a RSA key + JwksRSAKeyMissingN = 28, + // "n" field is not string for a RSA key + JwksRSAKeyBadN = 29, + // "e" field is missing for a RSA key + JwksRSAKeyMissingE = 30, + // "e" field is not string for a RSA key + JwksRSAKeyBadE = 31, + + // "alg" is not "ES256" for an EC key + JwksECKeyBadAlg = 32, + // "x" field is missing for an EC key + JwksECKeyMissingX = 33, + // "x" field is not string for an EC key + JwksECKeyBadX = 34, + // "y" field is missing for an EC key + JwksECKeyMissingY = 35, + // "y" field is not string for an EC key + JwksECKeyBadY = 36, +}; + +/** + * Convert enum status to string. + * @param status is the enum status. + * @return the string status. + */ +std::string getStatusString(Status status); + +/** + * Base class to keep the status that represents "OK" or the first failure. + */ +class WithStatus { + public: + WithStatus() : status_(Status::Ok) {} + + /** + * Get the current status. + * @return the enum status. + */ + Status getStatus() const { return status_; } + + protected: + void updateStatus(Status status) { + // Only keep the first failure + if (status_ == Status::Ok) { + status_ = status; + } + } + + private: + // The internal status. + Status status_; +}; + +} // namespace jwt_verify +} // namespace google diff --git a/jwt_verify_lib/verify.h b/jwt_verify_lib/verify.h new file mode 100644 index 0000000..4da629a --- /dev/null +++ b/jwt_verify_lib/verify.h @@ -0,0 +1,34 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "jwt_verify_lib/jwks.h" +#include "jwt_verify_lib/jwt.h" +#include "jwt_verify_lib/status.h" + +namespace google { +namespace jwt_verify { + +/** + * This function verifies JWT signature. + * If verification failed, returns the failture reason. + * @param jwt is Jwt object + * @param jwks is Jwks object + * @return the verification status + */ +Status verifyJwt(const Jwt& jwt, const Jwks& jwks); + +} // namespace jwt_verify +} // namespace google diff --git a/rapidjson.BUILD b/rapidjson.BUILD new file mode 100644 index 0000000..c69d99f --- /dev/null +++ b/rapidjson.BUILD @@ -0,0 +1,6 @@ +cc_library( + name = "rapidjson", + hdrs = glob(["include/rapidjson/**/*.h"]), + includes = ["include"], + visibility = ["//visibility:public"], +) diff --git a/repositories.bzl b/repositories.bzl new file mode 100644 index 0000000..5bb8015 --- /dev/null +++ b/repositories.bzl @@ -0,0 +1,63 @@ +def boringssl_repositories(bind=True): + native.git_repository( + name = "boringssl", + commit = "9df0c47bc034d60d73d216cd0e090707b3fbea58", # same as Envoy + remote = "https://boringssl.googlesource.com/boringssl", + ) + + if bind: + native.bind( + name = "ssl", + actual = "@boringssl//:ssl", + ) + +def googletest_repositories(bind=True): + native.new_git_repository( + name = "googletest_git", + build_file = "googletest.BUILD", + commit = "43863938377a9ea1399c0596269e0890b5c5515a", + remote = "https://github.com/google/googletest.git", + ) + + if bind: + native.bind( + name = "googletest", + actual = "@googletest_git//:googletest", + ) + + native.bind( + name = "googletest_main", + actual = "@googletest_git//:googletest_main", + ) + + native.bind( + name = "googletest_prod", + actual = "@googletest_git//:googletest_prod", + ) + +def rapidjson_repositories(bind=True): + native.new_git_repository( + name = "com_github_tencent_rapidjson", + build_file = "rapidjson.BUILD", + commit = "f54b0e47a08782a6131cc3d60f94d038fa6e0a51", + remote = "https://github.com/tencent/rapidjson.git", + ) + + if bind: + native.bind( + name = "rapidjson", + actual = "@com_github_tencent_rapidjson//:rapidjson", + ) + +def abseil_repositories(bind=True): + native.git_repository( + name = "com_google_absl", + commit = "787891a3882795cee0364e8a0f0dda315578d155", + remote = "https://github.com/abseil/abseil-cpp", + ) + + if bind: + native.bind( + name = "abseil_strings", + actual = "@com_google_absl//absl/strings:strings", + ) diff --git a/src/check_audience.cc b/src/check_audience.cc new file mode 100644 index 0000000..5109e4b --- /dev/null +++ b/src/check_audience.cc @@ -0,0 +1,79 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jwt_verify_lib/check_audience.h" +#include "absl/strings/match.h" + +namespace google { +namespace jwt_verify { +namespace { + +// HTTP Protocol scheme prefix in JWT aud claim. +constexpr absl::string_view HTTPSchemePrefix("http://"); + +// HTTPS Protocol scheme prefix in JWT aud claim. +constexpr absl::string_view HTTPSSchemePrefix("https://"); + +std::string sanitizeAudience(const std::string& aud) { + if (aud.empty()) { + return aud; + } + + size_t beg_pos = 0; + bool sanitized = false; + // Point beg to first character after protocol scheme prefix in audience. + if (absl::StartsWith(aud, HTTPSchemePrefix)) { + beg_pos = HTTPSchemePrefix.size(); + sanitized = true; + } else if (absl::StartsWith(aud, HTTPSSchemePrefix)) { + beg_pos = HTTPSSchemePrefix.size(); + sanitized = true; + } + + // Point end to trailing slash in aud. + size_t end_pos = aud.length(); + if (aud[end_pos - 1] == '/') { + --end_pos; + sanitized = true; + } + if (sanitized) { + return aud.substr(beg_pos, end_pos - beg_pos); + } + return aud; +} + +} // namespace + +CheckAudience::CheckAudience(const std::vector& config_audiences) { + for (const auto& aud : config_audiences) { + config_audiences_.insert(sanitizeAudience(aud)); + } +} + +bool CheckAudience::areAudiencesAllowed( + const std::vector& jwt_audiences) const { + if (config_audiences_.empty()) { + return true; + } + for (const auto& aud : jwt_audiences) { + if (config_audiences_.find(sanitizeAudience(aud)) != + config_audiences_.end()) { + return true; + } + } + return false; +} + +} // namespace jwt_verify +} // namespace google diff --git a/src/check_audience_test.cc b/src/check_audience_test.cc new file mode 100644 index 0000000..1bd0124 --- /dev/null +++ b/src/check_audience_test.cc @@ -0,0 +1,78 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jwt_verify_lib/check_audience.h" +#include "gtest/gtest.h" + +namespace google { +namespace jwt_verify { +namespace { + +TEST(CheckAudienceTest, TestConfigNotPrefixNotTailing) { + CheckAudience checker({"example_service"}); + EXPECT_TRUE(checker.areAudiencesAllowed({"http://example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"https://example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"example_service/"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"http://example_service/"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"https://example_service/"})); +} + +TEST(CheckAudienceTest, TestConfigHttpPrefixNotTailing) { + CheckAudience checker({"http://example_service"}); + EXPECT_TRUE(checker.areAudiencesAllowed({"example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"https://example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"example_service/"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"http://example_service/"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"https://example_service/"})); +} + +TEST(CheckAudienceTest, TestConfigHttpsPrefixNotTailing) { + CheckAudience checker({"https://example_service"}); + EXPECT_TRUE(checker.areAudiencesAllowed({"example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"http://example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"example_service/"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"http://example_service/"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"https://example_service/"})); +} + +TEST(CheckAudienceTest, TestConfigNotPrefixWithTailing) { + CheckAudience checker({"example_service/"}); + EXPECT_TRUE(checker.areAudiencesAllowed({"example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"http://example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"https://example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"http://example_service/"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"https://example_service/"})); +} + +TEST(CheckAudienceTest, TestConfigHttpPrefixWithTailing) { + CheckAudience checker({"http://example_service/"}); + EXPECT_TRUE(checker.areAudiencesAllowed({"example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"http://example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"https://example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"example_service/"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"https://example_service/"})); +} + +TEST(CheckAudienceTest, TestConfigHttpsPrefixWithTailing) { + CheckAudience checker({"https://example_service/"}); + EXPECT_TRUE(checker.areAudiencesAllowed({"example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"http://example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"https://example_service"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"example_service/"})); + EXPECT_TRUE(checker.areAudiencesAllowed({"http://example_service/"})); +} + +} // namespace +} // namespace jwt_verify +} // namespace google diff --git a/src/jwks.cc b/src/jwks.cc new file mode 100644 index 0000000..ba4ebf5 --- /dev/null +++ b/src/jwks.cc @@ -0,0 +1,301 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "jwt_verify_lib/jwks.h" + +#include "absl/strings/escaping.h" +#include "rapidjson/document.h" + +#include "openssl/bn.h" +#include "openssl/ecdsa.h" +#include "openssl/evp.h" +#include "openssl/rsa.h" +#include "openssl/sha.h" + +namespace google { +namespace jwt_verify { + +namespace { + +// A convinence inline cast function. +inline const uint8_t* castToUChar(const std::string& str) { + return reinterpret_cast(str.c_str()); +} + +/** Class to create EVP_PKEY object from string of public key, formatted in PEM + * or JWKs. + * If it failed, status_ holds the failure reason. + * + * Usage example: + * EvpPkeyGetter e; + * bssl::UniquePtr pkey = + * e.createEvpPkeyFromStr(pem_formatted_public_key); + * (You can use createEvpPkeyFromJwkRSA() or createEcKeyFromJwkEC() for JWKs) + */ +class EvpPkeyGetter : public WithStatus { + public: + // Create EVP_PKEY from PEM string + bssl::UniquePtr createEvpPkeyFromStr(const std::string& pkey_pem) { + // Header "-----BEGIN CERTIFICATE ---"and tailer "-----END CERTIFICATE ---" + // should have been removed. + std::string pkey_der; + if (!absl::Base64Unescape(pkey_pem, &pkey_der)) { + updateStatus(Status::JwksPemBadBase64); + return nullptr; + } + auto rsa = bssl::UniquePtr( + RSA_public_key_from_bytes(castToUChar(pkey_der), pkey_der.length())); + if (!rsa) { + updateStatus(Status::JwksPemParseError); + return nullptr; + } + return createEvpPkeyFromRsa(rsa.get()); + } + + bssl::UniquePtr createEvpPkeyFromJwkRSA(const std::string& n, + const std::string& e) { + return createEvpPkeyFromRsa(createRsaFromJwk(n, e).get()); + } + + bssl::UniquePtr createEcKeyFromJwkEC(const std::string& x, + const std::string& y) { + bssl::UniquePtr ec_key( + EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); + if (!ec_key) { + updateStatus(Status::JwksEcCreateKeyFail); + return nullptr; + } + bssl::UniquePtr bn_x = createBigNumFromBase64UrlString(x); + bssl::UniquePtr bn_y = createBigNumFromBase64UrlString(y); + if (!bn_x || !bn_y) { + // EC public key field is missing or has parse error. + updateStatus(Status::JwksEcParseError); + return nullptr; + } + + if (EC_KEY_set_public_key_affine_coordinates(ec_key.get(), bn_x.get(), + bn_y.get()) == 0) { + updateStatus(Status::JwksEcParseError); + return nullptr; + } + return ec_key; + } + + private: + bssl::UniquePtr createEvpPkeyFromRsa(RSA* rsa) { + if (!rsa) { + return nullptr; + } + bssl::UniquePtr key(EVP_PKEY_new()); + EVP_PKEY_set1_RSA(key.get(), rsa); + return key; + } + + bssl::UniquePtr createBigNumFromBase64UrlString( + const std::string& s) { + std::string s_decoded; + if (!absl::WebSafeBase64Unescape(s, &s_decoded)) { + return nullptr; + } + return bssl::UniquePtr( + BN_bin2bn(castToUChar(s_decoded), s_decoded.length(), NULL)); + }; + + bssl::UniquePtr createRsaFromJwk(const std::string& n, + const std::string& e) { + bssl::UniquePtr rsa(RSA_new()); + rsa->n = createBigNumFromBase64UrlString(n).release(); + rsa->e = createBigNumFromBase64UrlString(e).release(); + if (rsa->n == nullptr || rsa->e == nullptr) { + // RSA public key field is missing or has parse error. + updateStatus(Status::JwksRsaParseError); + return nullptr; + } + if (BN_cmp_word(rsa->e, 3) != 0 && BN_cmp_word(rsa->e, 65537) != 0) { + // non-standard key; reject it early. + updateStatus(Status::JwksRsaParseError); + return nullptr; + } + return rsa; + } +}; + +Status extractJwkFromJwkRSA(const rapidjson::Value& jwk_json, + Jwks::Pubkey* jwk) { + if (jwk->alg_specified_ && + (jwk->alg_.size() < 2 || jwk->alg_.compare(0, 2, "RS") != 0)) { + return Status::JwksRSAKeyBadAlg; + } + + if (!jwk_json.HasMember("n")) { + return Status::JwksRSAKeyMissingN; + } + const auto& n_value = jwk_json["n"]; + if (!n_value.IsString()) { + return Status::JwksRSAKeyBadN; + } + std::string n_str = n_value.GetString(); + + if (!jwk_json.HasMember("e")) { + return Status::JwksRSAKeyMissingE; + } + const auto& e_value = jwk_json["e"]; + if (!e_value.IsString()) { + return Status::JwksRSAKeyBadE; + } + std::string e_str = e_value.GetString(); + + EvpPkeyGetter e; + jwk->evp_pkey_ = e.createEvpPkeyFromJwkRSA(n_str, e_str); + return e.getStatus(); +} + +Status extractJwkFromJwkEC(const rapidjson::Value& jwk_json, + Jwks::Pubkey* jwk) { + if (jwk->alg_specified_ && jwk->alg_ != "ES256") { + return Status::JwksECKeyBadAlg; + } + + if (!jwk_json.HasMember("x")) { + return Status::JwksECKeyMissingX; + } + const auto& x_value = jwk_json["x"]; + if (!x_value.IsString()) { + return Status::JwksECKeyBadX; + } + std::string x_str = x_value.GetString(); + + if (!jwk_json.HasMember("y")) { + return Status::JwksECKeyMissingY; + } + const auto& y_value = jwk_json["y"]; + if (!y_value.IsString()) { + return Status::JwksECKeyBadY; + } + std::string y_str = y_value.GetString(); + + EvpPkeyGetter e; + jwk->ec_key_ = e.createEcKeyFromJwkEC(x_str, y_str); + return e.getStatus(); +} + +Status extractJwk(const rapidjson::Value& jwk_json, Jwks::Pubkey* jwk) { + // Check "kty" parameter, it should exist. + // https://tools.ietf.org/html/rfc7517#section-4.1 + if (!jwk_json.HasMember("kty")) { + return Status::JwksMissingKty; + } + const auto& kty_value = jwk_json["kty"]; + if (!kty_value.IsString()) { + return Status::JwksBadKty; + } + jwk->kty_ = kty_value.GetString(); + + // "kid" and "alg" are optional, if they do not exist, set them to empty. + // https://tools.ietf.org/html/rfc7517#page-8 + if (jwk_json.HasMember("kid")) { + const auto& kid_value = jwk_json["kid"]; + if (kid_value.IsString()) { + jwk->kid_ = kid_value.GetString(); + jwk->kid_specified_ = true; + } + } + if (jwk_json.HasMember("alg")) { + const auto& alg_value = jwk_json["alg"]; + if (alg_value.IsString()) { + jwk->alg_ = alg_value.GetString(); + jwk->alg_specified_ = true; + } + } + + // Extract public key according to "kty" value. + // https://tools.ietf.org/html/rfc7518#section-6.1 + if (jwk->kty_ == "EC") { + return extractJwkFromJwkEC(jwk_json, jwk); + } else if (jwk->kty_ == "RSA") { + return extractJwkFromJwkRSA(jwk_json, jwk); + } + return Status::JwksNotImplementedKty; +} + +} // namespace + +JwksPtr Jwks::createFrom(const std::string& pkey, Type type) { + JwksPtr keys(new Jwks()); + switch (type) { + case Type::JWKS: + keys->createFromJwksCore(pkey); + break; + case Type::PEM: + keys->createFromPemCore(pkey); + break; + default: + break; + } + return keys; +} + +void Jwks::createFromPemCore(const std::string& pkey_pem) { + keys_.clear(); + PubkeyPtr key_ptr(new Pubkey()); + EvpPkeyGetter e; + key_ptr->evp_pkey_ = e.createEvpPkeyFromStr(pkey_pem); + key_ptr->pem_format_ = true; + updateStatus(e.getStatus()); + assert((key_ptr->evp_pkey_ == nullptr) == (e.getStatus() != Status::Ok)); + if (e.getStatus() == Status::Ok) { + keys_.push_back(std::move(key_ptr)); + } +} + +void Jwks::createFromJwksCore(const std::string& pkey_jwks) { + keys_.clear(); + + rapidjson::Document jwks_json; + if (jwks_json.Parse(pkey_jwks.c_str()).HasParseError()) { + updateStatus(Status::JwksParseError); + return; + } + + if (!jwks_json.HasMember("keys")) { + updateStatus(Status::JwksNoKeys); + return; + } + const auto& keys_value = jwks_json["keys"]; + if (!keys_value.IsArray()) { + updateStatus(Status::JwksBadKeys); + return; + } + + for (auto key_it = keys_value.Begin(); key_it != keys_value.End(); ++key_it) { + PubkeyPtr key_ptr(new Pubkey()); + Status status = extractJwk(*key_it, key_ptr.get()); + if (status == Status::Ok) { + keys_.push_back(std::move(key_ptr)); + } else { + updateStatus(status); + break; + } + } + + if (keys_.size() == 0) { + updateStatus(Status::JwksNoValidKeys); + } +} + +} // namespace jwt_verify +} // namespace google diff --git a/src/jwks_test.cc b/src/jwks_test.cc new file mode 100644 index 0000000..0833efe --- /dev/null +++ b/src/jwks_test.cc @@ -0,0 +1,267 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jwt_verify_lib/jwks.h" +#include "gtest/gtest.h" + +namespace google { +namespace jwt_verify { +namespace { + +TEST(JwksParseTest, GoodPem) { + // Public key PEM + const std::string jwks_text = + "MIIBCgKCAQEAtw7MNxUTxmzWROCD5BqJxmzT7xqc9KsnAjbXCoqEEHDx4WBlfcwk" + "XHt9e/2+Uwi3Arz3FOMNKwGGlbr7clBY3utsjUs8BTF0kO/poAmSTdSuGeh2mSbc" + "VHvmQ7X/kichWwx5Qj0Xj4REU3Gixu1gQIr3GATPAIULo5lj/ebOGAa+l0wIG80N" + "zz1pBtTIUx68xs5ZGe7cIJ7E8n4pMX10eeuh36h+aossePeuHulYmjr4N0/1jG7a" + "+hHYL6nqwOR3ej0VqCTLS0OloC0LuCpLV7CnSpwbp2Qg/c+MDzQ0TH8g8drIzR5h" + "Fe9a3NlNRMXgUU5RqbLnR9zfXr7b9oEszQIDAQAB"; + + auto jwks = Jwks::createFrom(jwks_text, Jwks::PEM); + EXPECT_EQ(jwks->getStatus(), Status::Ok); + EXPECT_EQ(jwks->keys().size(), 1); + EXPECT_TRUE(jwks->keys()[0]->pem_format_); +} + +TEST(JwksParseTest, EmptyPem) { + auto jwks = Jwks::createFrom("", Jwks::PEM); + EXPECT_EQ(jwks->getStatus(), Status::JwksPemBadBase64); +} + +TEST(JwksParseTest, BadPem) { + // U2lnbmF0dXJl is Base64 of "Signature" + auto jwks = Jwks::createFrom("U2lnbmF0dXJl", Jwks::PEM); + EXPECT_EQ(jwks->getStatus(), Status::JwksPemParseError); +} + +TEST(JwksParseTest, GoodJwks) { + const std::string jwks_text = R"( + { + "keys": [ + { + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "62a93512c9ee4c7f8067b5a216dade2763d32a47", + "n": "0YWnm_eplO9BFtXszMRQNL5UtZ8HJdTH2jK7vjs4XdLkPW7YBkkm_2xNgcaVpkW0VT2l4mU3KftR-6s3Oa5Rnz5BrWEUkCTVVolR7VYksfqIB2I_x5yZHdOiomMTcm3DheUUCgbJRv5OKRnNqszA4xHn3tA3Ry8VO3X7BgKZYAUh9fyZTFLlkeAh0-bLK5zvqCmKW5QgDIXSxUTJxPjZCgfx1vmAfGqaJb-nvmrORXQ6L284c73DUL7mnt6wj3H6tVqPKA27j56N0TB1Hfx4ja6Slr8S4EB3F1luYhATa1PKUSH8mYDW11HolzZmTQpRoLV8ZoHbHEaTfqX_aYahIw", + "e": "AQAB" + }, + { + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "b3319a147514df7ee5e4bcdee51350cc890cc89e", + "n": "qDi7Tx4DhNvPQsl1ofxxc2ePQFcs-L0mXYo6TGS64CY_2WmOtvYlcLNZjhuddZVV2X88m0MfwaSA16wE-RiKM9hqo5EY8BPXj57CMiYAyiHuQPp1yayjMgoE1P2jvp4eqF-BTillGJt5W5RuXti9uqfMtCQdagB8EC3MNRuU_KdeLgBy3lS3oo4LOYd-74kRBVZbk2wnmmb7IhP9OoLc1-7-9qU1uhpDxmE6JwBau0mDSwMnYDS4G_ML17dC-ZDtLd1i24STUw39KH0pcSdfFbL2NtEZdNeam1DDdk0iUtJSPZliUHJBI_pj8M-2Mn_oA8jBuI8YKwBqYkZCN1I95Q", + "e": "AQAB" + } + ] + } +)"; + + auto jwks = Jwks::createFrom(jwks_text, Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::Ok); + EXPECT_EQ(jwks->keys().size(), 2); + + EXPECT_EQ(jwks->keys()[0]->alg_, "RS256"); + EXPECT_EQ(jwks->keys()[0]->kid_, "62a93512c9ee4c7f8067b5a216dade2763d32a47"); + EXPECT_TRUE(jwks->keys()[0]->alg_specified_); + EXPECT_TRUE(jwks->keys()[0]->kid_specified_); + EXPECT_FALSE(jwks->keys()[0]->pem_format_); + + EXPECT_EQ(jwks->keys()[1]->alg_, "RS256"); + EXPECT_EQ(jwks->keys()[1]->kid_, "b3319a147514df7ee5e4bcdee51350cc890cc89e"); + EXPECT_TRUE(jwks->keys()[1]->alg_specified_); + EXPECT_TRUE(jwks->keys()[1]->kid_specified_); + EXPECT_FALSE(jwks->keys()[1]->pem_format_); +} + +TEST(JwksParseTest, GoodEC) { + // Public key JwkEC + const std::string jwks_text = R"( + { + "keys": [ + { + "kty": "EC", + "crv": "P-256", + "x": "EB54wykhS7YJFD6RYJNnwbWEz3cI7CF5bCDTXlrwI5k", + "y": "92bCBTvMFQ8lKbS2MbgjT3YfmYo6HnPEE2tsAqWUJw8", + "alg": "ES256", + "kid": "abc" + }, + { + "kty": "EC", + "crv": "P-256", + "x": "EB54wykhS7YJFD6RYJNnwbWEz3cI7CF5bCDTXlrwI5k", + "y": "92bCBTvMFQ8lKbS2MbgjT3YfmYo6HnPEE2tsAqWUJw8", + "alg": "ES256", + "kid": "xyz" + } + ] + } +)"; + auto jwks = Jwks::createFrom(jwks_text, Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::Ok); + EXPECT_EQ(jwks->keys().size(), 2); + + EXPECT_EQ(jwks->keys()[0]->alg_, "ES256"); + EXPECT_EQ(jwks->keys()[0]->kid_, "abc"); + EXPECT_EQ(jwks->keys()[0]->kty_, "EC"); + EXPECT_TRUE(jwks->keys()[0]->alg_specified_); + EXPECT_TRUE(jwks->keys()[0]->kid_specified_); + EXPECT_FALSE(jwks->keys()[0]->pem_format_); + + EXPECT_EQ(jwks->keys()[1]->alg_, "ES256"); + EXPECT_EQ(jwks->keys()[1]->kid_, "xyz"); + EXPECT_EQ(jwks->keys()[1]->kty_, "EC"); + EXPECT_TRUE(jwks->keys()[1]->alg_specified_); + EXPECT_TRUE(jwks->keys()[1]->kid_specified_); + EXPECT_FALSE(jwks->keys()[1]->pem_format_); +} + +TEST(JwksParseTest, EmptyJwks) { + auto jwks = Jwks::createFrom("", Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::JwksParseError); +} + +TEST(JwksParseTest, JwksNoKeys) { + auto jwks = Jwks::createFrom("{}", Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::JwksNoKeys); +} + +TEST(JwksParseTest, JwksWrongKeys) { + auto jwks = Jwks::createFrom(R"({"keys": 123})", Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::JwksBadKeys); +} + +TEST(JwksParseTest, JwksInvalidKty) { + // Invalid kty field + const std::string jwks_text = R"( + { + "keys": [ + { + "kty": "XYZ", + "crv": "P-256", + "x": "EB54wykhS7YJFD6RYJNnwbWEz3cI7CF5bCDTXlrwI5k", + "y": "92bCBTvMFQ8lKbS2MbgjT3YfmYo6HnPEE2tsAqWUJw8", + "alg": "ES256", + "kid": "abc" + } + ] + } +)"; + auto jwks = Jwks::createFrom(jwks_text, Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::JwksNotImplementedKty); +} + +TEST(JwksParseTest, JwksMismatchKty1) { + // kty doesn't match with alg + const std::string jwks_text = R"( + { + "keys": [ + { + "kty": "RSA", + "alg": "ES256" + } + ] + } +)"; + auto jwks = Jwks::createFrom(jwks_text, Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::JwksRSAKeyBadAlg); +} + +TEST(JwksParseTest, JwksMismatchKty2) { + // kty doesn't match with alg + const std::string jwks_text = R"( + { + "keys": [ + { + "kty": "EC", + "alg": "RS256" + } + ] + } +)"; + + auto jwks = Jwks::createFrom(jwks_text, Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::JwksECKeyBadAlg); +} + +TEST(JwksParseTest, JwksECNoXY) { + const std::string jwks_text = R"( + { + "keys": [ + { + "kty": "EC", + "alg": "ES256" + } + ] + } +)"; + auto jwks = Jwks::createFrom(jwks_text, Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::JwksECKeyMissingX); +} + +TEST(JwksParseTest, JwksRSANoNE) { + const std::string jwks_text = R"( + { + "keys": [ + { + "kty": "RSA", + "alg": "RS256" + } + ] + } +)"; + auto jwks = Jwks::createFrom(jwks_text, Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::JwksRSAKeyMissingN); +} + +TEST(JwksParseTest, JwksECWrongXY) { + const std::string jwks_text = R"( + { + "keys": [ + { + "kty": "EC", + "x": "EB54wykhS7YJFD6RYJNnwbWEz3cI7CF5bCDTXlrwI5k111", + "y": "92bCBTvMFQ8lKbS2MbgjT3YfmYo6HnPEE2tsAqWUJw8111", + "alg": "ES256" + } + ] + } +)"; + auto jwks = Jwks::createFrom(jwks_text, Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::JwksEcParseError); +} + +TEST(JwksParseTest, JwksRSAWrongNE) { + const std::string jwks_text = R"( + { + "keys": [ + { + "kty": "RSA", + "n": "EB54wykhS7YJFD6RYJNnwbW", + "e": "92bCBTvMFQ8lKbS2MbgjT3YfmY", + "alg": "RS256" + } + ] + } +)"; + auto jwks = Jwks::createFrom(jwks_text, Jwks::JWKS); + EXPECT_EQ(jwks->getStatus(), Status::JwksRsaParseError); +} + +} // namespace +} // namespace jwt_verify +} // namespace google diff --git a/src/jwt.cc b/src/jwt.cc new file mode 100644 index 0000000..b47ec0f --- /dev/null +++ b/src/jwt.cc @@ -0,0 +1,135 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "absl/strings/escaping.h" +#include "absl/strings/str_split.h" +#include "jwt_verify_lib/jwt.h" +#include "rapidjson/document.h" + +namespace google { +namespace jwt_verify { + +// Maximum Jwt size to prevent JSON parser attack: +// stack overflow crash if a document contains heavily nested arrays. +// [[... repeat 100,000 times ... [[[0]]]]]]]]]]]]]]]]]]]]]]]]]]].. +const size_t kMaxJwtSize = 8096; + +Status Jwt::parseFromString(const std::string& jwt) { + if (jwt.size() >= kMaxJwtSize) { + return Status::JwtBadFormat; + } + // jwt must have exactly 2 dots + if (std::count(jwt.begin(), jwt.end(), '.') != 2) { + return Status::JwtBadFormat; + } + std::vector jwt_split = + absl::StrSplit(jwt, absl::ByAnyChar("."), absl::SkipEmpty()); + if (jwt_split.size() != 3) { + return Status::JwtBadFormat; + } + + // Parse header json + header_str_base64url_ = std::string(jwt_split[0]); + if (!absl::WebSafeBase64Unescape(header_str_base64url_, &header_str_)) { + return Status::JwtHeaderParseError; + } + rapidjson::Document header_json; + if (header_json.Parse(header_str_.c_str()).HasParseError()) { + return Status::JwtHeaderParseError; + } + + // Header should contain "alg" and should be a string. + if (!header_json.HasMember("alg") || !header_json["alg"].IsString()) { + return Status::JwtHeaderBadAlg; + } + alg_ = header_json["alg"].GetString(); + + if (alg_ != "RS256" && alg_ != "ES256") { + return Status::JwtHeaderNotImplementedAlg; + } + + // Header may contain "kid", should be a string if exists. + if (header_json.HasMember("kid")) { + if (!header_json["kid"].IsString()) { + return Status::JwtHeaderBadKid; + } + kid_ = header_json["kid"].GetString(); + } + + // Parse payload json + payload_str_base64url_ = std::string(jwt_split[1]); + if (!absl::WebSafeBase64Unescape(payload_str_base64url_, &payload_str_)) { + return Status::JwtPayloadParseError; + } + + rapidjson::Document payload_json; + if (payload_json.Parse(payload_str_.c_str()).HasParseError()) { + return Status::JwtPayloadParseError; + } + + if (payload_json.HasMember("iss")) { + if (payload_json["iss"].IsString()) { + iss_ = payload_json["iss"].GetString(); + } else { + return Status::JwtPayloadParseError; + } + } + if (payload_json.HasMember("sub")) { + if (payload_json["sub"].IsString()) { + sub_ = payload_json["sub"].GetString(); + } else { + return Status::JwtPayloadParseError; + } + } + if (payload_json.HasMember("exp")) { + if (payload_json["exp"].IsInt()) { + exp_ = payload_json["exp"].GetInt(); + } else { + return Status::JwtPayloadParseError; + } + } else { + exp_ = 0; + } + + // "aud" can be either string array or string. + // Try as string array, read it as empty array if doesn't exist. + if (payload_json.HasMember("aud")) { + const auto& aud_value = payload_json["aud"]; + if (aud_value.IsArray()) { + for (auto it = aud_value.Begin(); it != aud_value.End(); ++it) { + if (it->IsString()) { + audiences_.push_back(it->GetString()); + } else { + return Status::JwtPayloadParseError; + } + } + } else if (aud_value.IsString()) { + audiences_.push_back(aud_value.GetString()); + } else { + return Status::JwtPayloadParseError; + } + } + + // Set up signature + if (!absl::WebSafeBase64Unescape(jwt_split[2], &signature_)) { + // Signature is a bad Base64url input. + return Status::JwtSignatureParseError; + } + return Status::Ok; +} + +} // namespace jwt_verify +} // namespace google diff --git a/src/jwt_test.cc b/src/jwt_test.cc new file mode 100644 index 0000000..bfdea70 --- /dev/null +++ b/src/jwt_test.cc @@ -0,0 +1,176 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jwt_verify_lib/jwt.h" +#include "gtest/gtest.h" + +namespace google { +namespace jwt_verify { +namespace { + +TEST(JwtParseTest, GoodJwt) { + // JWT with + // Header: {"alg":"RS256","typ":"JWT"} + // Payload: + // {"iss":"https://example.com","sub":"test@example.com","exp":1501281058} + const std::string jwt_text = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0.U2lnbmF0dXJl"; + + Jwt jwt; + ASSERT_EQ(jwt.parseFromString(jwt_text), Status::Ok); + + EXPECT_EQ(jwt.alg_, "RS256"); + EXPECT_EQ(jwt.kid_, ""); + EXPECT_EQ(jwt.iss_, "https://example.com"); + EXPECT_EQ(jwt.sub_, "test@example.com"); + EXPECT_EQ(jwt.audiences_, std::vector()); + EXPECT_EQ(jwt.exp_, 1501281058); + EXPECT_EQ(jwt.signature_, "Signature"); +} + +TEST(JwtParseTest, GoodJwtWithMultiAud) { + // aud: [aud1, aud2] + const std::string jwt_text = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFmMDZjMTlmOGU1YjMzMTUyMT" + "ZkZjAxMGZkMmI5YTkzYmFjMTM1YzgifQ.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tI" + "iwiaWF0IjoxNTE3ODc1MDU5LCJhdWQiOlsiYXVkMSIsImF1ZDIiXSwiZXhwIjoxNTE3ODc" + "4NjU5LCJzdWIiOiJodHRwczovL2V4YW1wbGUuY29tIn0.U2lnbmF0dXJl"; + + Jwt jwt; + ASSERT_EQ(jwt.parseFromString(jwt_text), Status::Ok); + + EXPECT_EQ(jwt.alg_, "RS256"); + EXPECT_EQ(jwt.kid_, "af06c19f8e5b3315216df010fd2b9a93bac135c8"); + EXPECT_EQ(jwt.iss_, "https://example.com"); + EXPECT_EQ(jwt.sub_, "https://example.com"); + EXPECT_EQ(jwt.audiences_, std::vector({"aud1", "aud2"})); + EXPECT_EQ(jwt.exp_, 1517878659); + EXPECT_EQ(jwt.signature_, "Signature"); +} + +TEST(JwtParseTest, EmptyJwt) { + Jwt jwt; + ASSERT_EQ(jwt.parseFromString(""), Status::JwtBadFormat); +} + +TEST(JwtParseTest, TooLargeJwt) { + Jwt jwt; + // string > 8096 of MaxJwtSize + std::string jwt_str(10240, 'c'); + ASSERT_EQ(jwt.parseFromString(jwt_str), Status::JwtBadFormat); +} + +TEST(JwtParseTest, BadJsonHeader) { + /* + * jwt with header replaced by + * "{"alg":"RS256","typ":"JWT", this is a invalid json}" + */ + const std::string jwt_text = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIHRoaXMgaXMgYSBpbnZhbGlkIGpzb259." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0.VGVzdFNpZ25hdHVyZQ"; + + Jwt jwt; + ASSERT_EQ(jwt.parseFromString(jwt_text), Status::JwtHeaderParseError); +} + +TEST(JwtParseTest, BadJsonPayload) { + /* + * jwt with payload replaced by + * "this is not a json" + */ + const std::string jwt_text = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.dGhpcyBpcyBub3QgYSBqc29u." + "VGVzdFNpZ25hdHVyZQ"; + + Jwt jwt; + ASSERT_EQ(jwt.parseFromString(jwt_text), Status::JwtPayloadParseError); +} + +TEST(JwtParseTest, AbsentAlg) { + /* + * jwt with header replaced by + * "{"typ":"JWT"}" + */ + const std::string jwt_text = + "eyJ0eXAiOiJKV1QifQ." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0" + ".VGVzdFNpZ25hdHVyZQ"; + + Jwt jwt; + ASSERT_EQ(jwt.parseFromString(jwt_text), Status::JwtHeaderBadAlg); +} + +TEST(JwtParseTest, AlgIsNotString) { + /* + * jwt with header replaced by + * "{"alg":256,"typ":"JWT"}" + */ + const std::string jwt_text = + "eyJhbGciOjI1NiwidHlwIjoiSldUIn0." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0.VGVzdFNpZ25hdHVyZQ"; + + Jwt jwt; + ASSERT_EQ(jwt.parseFromString(jwt_text), Status::JwtHeaderBadAlg); +} + +TEST(JwtParseTest, InvalidAlg) { + /* + * jwt with header replaced by + * "{"alg":"InvalidAlg","typ":"JWT"}" + */ + const std::string jwt_text = + "eyJhbGciOiJJbnZhbGlkQWxnIiwidHlwIjoiSldUIn0." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0.VGVzdFNpZ25hdHVyZQ"; + + Jwt jwt; + ASSERT_EQ(jwt.parseFromString(jwt_text), Status::JwtHeaderNotImplementedAlg); +} + +TEST(JwtParseTest, BadFormatKid) { + // JWT with bad-formatted kid + // Header: {"alg":"RS256","typ":"JWT","kid":1} + // Payload: + // {"iss":"https://example.com","sub":"test@example.com","exp":1501281058} + const std::string jwt_text = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6MX0." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0.VGVzdFNpZ25hdHVyZQ"; + + Jwt jwt; + ASSERT_EQ(jwt.parseFromString(jwt_text), Status::JwtHeaderBadKid); +} + +TEST(JwtParseTest, InvalidSignature) { + // {"iss":"https://example.com","sub":"test@example.com","exp":1501281058, + // aud: [aud1, aud2] } + // signature part is invalid. + const std::string jwt_text = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFmMDZjMTlmOGU1YjMzMTUyMT" + "ZkZjAxMGZkMmI5YTkzYmFjMTM1YzgifQ.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tI" + "iwiaWF0IjoxNTE3ODc1MDU5LCJhdWQiOlsiYXVkMSIsImF1ZDIiXSwiZXhwIjoxNTE3ODc" + "4NjU5LCJzdWIiOiJodHRwczovL2V4YW1wbGUuY29tIn0.invalid-signature"; + + Jwt jwt; + ASSERT_EQ(jwt.parseFromString(jwt_text), Status::JwtSignatureParseError); +} + +} // namespace +} // namespace jwt_verify +} // namespace google diff --git a/src/status.cc b/src/status.cc new file mode 100644 index 0000000..c94194b --- /dev/null +++ b/src/status.cc @@ -0,0 +1,80 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include "jwt_verify_lib/status.h" + +namespace google { +namespace jwt_verify { + +std::string getStatusString(Status status) { + static std::map table = { + {Status::Ok, "OK"}, + + {Status::JwtMissed, "Jwt missing"}, + {Status::JwtExpired, "Jwt expired"}, + {Status::JwtBadFormat, + "Jwt is not in the form of Header.Payload.Signature"}, + {Status::JwtHeaderParseError, + "Jwt header is an invalid Base64url input or an invalid JSON"}, + {Status::JwtHeaderBadAlg, "Jwt header [alg] field is not a string"}, + {Status::JwtHeaderNotImplementedAlg, + "Jwt header [alg] field value is invalid"}, + {Status::JwtHeaderBadKid, "Jwt header [kid] field is not a string"}, + {Status::JwtPayloadParseError, + "Jwt payload is an invalid Base64url input or an invalid JSON"}, + {Status::JwtSignatureParseError, + "Jwt signature is an invalid Base64url input"}, + {Status::JwtUnknownIssuer, "Jwt issuer is not configured"}, + {Status::JwtAudienceNotAllowed, "Audience in Jwt is not allowed"}, + {Status::JwtVerificationFail, "Jwt verification fails"}, + + {Status::JwksParseError, "Jwks is an invalid JSON"}, + {Status::JwksNoKeys, "Jwks does not have [keys] field"}, + {Status::JwksBadKeys, "[keys] in Jwks is not an array"}, + {Status::JwksNoValidKeys, "Jwks doesn't have any valid public key"}, + {Status::JwksKidAlgMismatch, + "Jwks doesn't have key to match kid or alg from Jwt"}, + {Status::JwksPemBadBase64, "Jwks PEM public key is an invalid Base64"}, + {Status::JwksPemParseError, "Jwks PEM public key parse error"}, + {Status::JwksRsaParseError, + "Jwks RSA [n] or [e] field is missing or has a parse error"}, + {Status::JwksEcCreateKeyFail, "Jwks EC create key fail"}, + {Status::JwksEcParseError, + "Jwks EC [x] or [y] field is missing or has a parse error."}, + {Status::JwksFetchFail, "Jwks fetch fail"}, + + {Status::JwksMissingKty, "[kty] is missing in [keys]"}, + {Status::JwksBadKty, "[kty] is missing in [keys]"}, + {Status::JwksNotImplementedKty, "[kty] is not supported in [keys]"}, + + {Status::JwksRSAKeyBadAlg, + "[alg] is not started with [RS] for a RSA key"}, + {Status::JwksRSAKeyMissingN, "[n] field is missing for a RSA key"}, + {Status::JwksRSAKeyBadN, "[n] field is not string for a RSA key"}, + {Status::JwksRSAKeyMissingE, "[e] field is missing for a RSA key"}, + {Status::JwksRSAKeyBadE, "[e] field is not string for a RSA key"}, + + {Status::JwksECKeyBadAlg, "[alg] is not [ES256] for an EC key"}, + {Status::JwksECKeyMissingX, "[x] field is missing for an EC key"}, + {Status::JwksECKeyBadX, "[x] field is not string for an EC key"}, + {Status::JwksECKeyMissingY, "[y] field is missing for an EC key"}, + {Status::JwksECKeyBadY, "[y] field is not string for an EC key"}, + }; + return table[status]; +} + +} // namespace jwt_verify +} // namespace google diff --git a/src/test_common.h b/src/test_common.h new file mode 100644 index 0000000..f644ecd --- /dev/null +++ b/src/test_common.h @@ -0,0 +1,50 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.#pragma once + +#pragma once + +#include +#include "jwt_verify_lib/jwt.h" + +namespace google { +namespace jwt_verify { + +/** + * This funciton fuzz the signature in two loops + */ +void fuzzJwtSignature(const Jwt& jwt, + std::function test_fn) { + // alter 1 bit + for (size_t b = 0; b < jwt.signature_.size(); ++b) { + for (int bit = 0; bit < 8; ++bit) { + Jwt fuzz_jwt = jwt; + unsigned char bb = fuzz_jwt.signature_[b]; + bb ^= (unsigned char)(1 << bit); + fuzz_jwt.signature_[b] = (char)bb; + test_fn(fuzz_jwt); + } + } + + // truncate bytes + for (size_t pos = 0; pos < jwt.signature_.size(); ++pos) { + for (size_t count = 1; count < jwt.signature_.size() - pos; ++count) { + Jwt fuzz_jwt = jwt; + fuzz_jwt.signature_ = jwt.signature_.substr(pos, count); + test_fn(fuzz_jwt); + } + } +} + +} // namespace jwt_verify +} // namespace google diff --git a/src/verify.cc b/src/verify.cc new file mode 100644 index 0000000..0717478 --- /dev/null +++ b/src/verify.cc @@ -0,0 +1,125 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jwt_verify_lib/verify.h" +#include "absl/strings/string_view.h" + +#include "openssl/bn.h" +#include "openssl/ecdsa.h" +#include "openssl/evp.h" +#include "openssl/rsa.h" +#include "openssl/sha.h" + +namespace google { +namespace jwt_verify { +namespace { + +// A convinence inline cast function. +inline const uint8_t* castToUChar(const absl::string_view& str) { + return reinterpret_cast(str.data()); +} + +bool verifySignatureRSA(EVP_PKEY* key, const EVP_MD* md, + const uint8_t* signature, size_t signature_len, + const uint8_t* signed_data, size_t signed_data_len) { + if (key == nullptr || md == nullptr || signature == nullptr || + signed_data == nullptr) { + return false; + } + bssl::UniquePtr md_ctx(EVP_MD_CTX_create()); + + EVP_DigestVerifyInit(md_ctx.get(), nullptr, md, nullptr, key); + EVP_DigestVerifyUpdate(md_ctx.get(), signed_data, signed_data_len); + return (EVP_DigestVerifyFinal(md_ctx.get(), signature, signature_len) == 1); +} + +bool verifySignatureRSA(EVP_PKEY* key, const EVP_MD* md, + absl::string_view signature, + absl::string_view signed_data) { + return verifySignatureRSA(key, md, castToUChar(signature), signature.length(), + castToUChar(signed_data), signed_data.length()); +} + +bool verifySignatureEC(EC_KEY* key, const uint8_t* signature, + size_t signature_len, const uint8_t* signed_data, + size_t signed_data_len) { + if (key == nullptr || signature == nullptr || signed_data == nullptr) { + return false; + } + // ES256 signature should be 64 bytes. + if (signature_len != 2 * 32) { + return false; + } + + uint8_t digest[SHA256_DIGEST_LENGTH]; + SHA256(signed_data, signed_data_len, digest); + + bssl::UniquePtr ecdsa_sig(ECDSA_SIG_new()); + if (!ecdsa_sig) { + return false; + } + + if (BN_bin2bn(signature, 32, ecdsa_sig->r) == nullptr || + BN_bin2bn(signature + 32, 32, ecdsa_sig->s) == nullptr) { + return false; + } + return (ECDSA_do_verify(digest, SHA256_DIGEST_LENGTH, ecdsa_sig.get(), key) == + 1); +} + +bool verifySignatureEC(EC_KEY* key, absl::string_view signature, + absl::string_view signed_data) { + return verifySignatureEC(key, castToUChar(signature), signature.length(), + castToUChar(signed_data), signed_data.length()); +} + +} // namespace + +Status verifyJwt(const Jwt& jwt, const Jwks& jwks) { + std::string signed_data = + jwt.header_str_base64url_ + '.' + jwt.payload_str_base64url_; + bool kid_alg_matched = false; + for (const auto& jwk : jwks.keys()) { + // If kid is specified in JWT, JWK with the same kid is used for + // verification. + // If kid is not specified in JWT, try all JWK. + if (!jwt.kid_.empty() && jwk->kid_specified_ && jwk->kid_ != jwt.kid_) { + continue; + } + + // The same alg must be used. + if (jwk->alg_specified_ && jwk->alg_ != jwt.alg_) { + continue; + } + kid_alg_matched = true; + + if (jwk->kty_ == "EC" && + verifySignatureEC(jwk->ec_key_.get(), jwt.signature_, signed_data)) { + // Verification succeeded. + return Status::Ok; + } else if ((jwk->pem_format_ || jwk->kty_ == "RSA") && + verifySignatureRSA(jwk->evp_pkey_.get(), EVP_sha256(), + jwt.signature_, signed_data)) { + // Verification succeeded. + return Status::Ok; + } + } + + // Verification failed. + return kid_alg_matched ? Status::JwtVerificationFail + : Status::JwksKidAlgMismatch; +} + +} // namespace jwt_verify +} // namespace google diff --git a/src/verify_jwk_ec_test.cc b/src/verify_jwk_ec_test.cc new file mode 100644 index 0000000..937b871 --- /dev/null +++ b/src/verify_jwk_ec_test.cc @@ -0,0 +1,171 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jwt_verify_lib/verify.h" +#include "gtest/gtest.h" +#include "src/test_common.h" + +namespace google { +namespace jwt_verify { +namespace { + +// Please see jwt_generator.py and jwk_generator.py under /tools/. +// for ES256-signed jwt token and public jwk generation, respectively. +// jwt_generator.py uses ES256 private key file to generate JWT token. +// ES256 private key file can be generated by: +// $ openssl ecparam -genkey -name prime256v1 -noout -out private_key.pem +// jwk_generator.py uses ES256 public key file to generate JWK. ES256 +// public key file can be generated by: +// $ openssl ec -in private_key.pem -pubout -out public_key.pem. + +// ES256 private key: +// "-----BEGIN EC PRIVATE KEY-----" +// "MHcCAQEEIOyf96eKdFeSFYeHiM09vGAylz+/auaXKEr+fBZssFsJoAoGCCqGSM49" +// "AwEHoUQDQgAEEB54wykhS7YJFD6RYJNnwbWEz3cI7CF5bCDTXlrwI5n3ZsIFO8wV" +// "DyUptLYxuCNPdh+Zijoec8QTa2wCpZQnDw==" +// "-----END EC PRIVATE KEY-----" + +// ES256 public key: +// "-----BEGIN PUBLIC KEY-----" +// "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEB54wykhS7YJFD6RYJNnwbWEz3cI" +// "7CF5bCDTXlrwI5n3ZsIFO8wVDyUptLYxuCNPdh+Zijoec8QTa2wCpZQnDw==" +// "-----END PUBLIC KEY-----" + +const std::string PublicKeyJwkEC = R"( +{ + "keys": [ + { + "kty": "EC", + "crv": "P-256", + "alg": "ES256", + "kid": "abc", + "x": "EB54wykhS7YJFD6RYJNnwbWEz3cI7CF5bCDTXlrwI5k", + "y": "92bCBTvMFQ8lKbS2MbgjT3YfmYo6HnPEE2tsAqWUJw8" + }, + { + "kty": "EC", + "crv": "P-256", + "alg": "ES256", + "kid": "xyz", + "x": "EB54wykhS7YJFD6RYJNnwbWEz3cI7CF5bCDTXlrwI5k", + "y": "92bCBTvMFQ8lKbS2MbgjT3YfmYo6HnPEE2tsAqWUJw8" + } + ] +} +)"; + +// "{"kid":"abc"}" +const std::string JwtTextEC = + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYyJ9.eyJpc3MiOiI2Mj" + "g2NDU3NDE4ODEtbm9hYml1MjNmNWE4bThvdmQ4dWN2Njk4bGo3OHZ2MGxAZGV2ZWxvc" + "GVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzdWIiOiI2Mjg2NDU3NDE4ODEtbm9hYml1" + "MjNmNWE4bThvdmQ4dWN2Njk4bGo3OHZ2MGxAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3V" + "udC5jb20iLCJhdWQiOiJodHRwOi8vbXlzZXJ2aWNlLmNvbS9teWFwaSJ9.T2KAwChqg" + "o2ZSXyLh3IcMBQNSeRZRe5Z-MUDl-s-F99XGoyutqA6lq8bKZ6vmjZAlpVG8AGRZW9J" + "Gp9lq3cbEw"; + +// "{"kid":"abcdef"}" +const std::string JwtTextWithNonExistKidEC = + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiY2RlZiJ9.eyJpc3MiOi" + "I2Mjg2NDU3NDE4ODEtbm9hYml1MjNmNWE4bThvdmQ4dWN2Njk4bGo3OHZ2MGxAZ" + "GV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzdWIiOiI2Mjg2NDU3NDE4" + "ODEtbm9hYml1MjNmNWE4bThvdmQ4dWN2Njk4bGo3OHZ2MGxAZGV2ZWxvcGVyLmd" + "zZXJ2aWNlYWNjb3VudC5jb20iLCJhdWQiOiJodHRwOi8vbXlzZXJ2aWNlLmNvbS" + "9teWFwaSJ9.rWSoOV5j7HxHc4yVgZEZYUSgY7AUarG3HxdfPON1mw6II_pNUsc8" + "_sVf7Yv2-jeVhmf8BtR99wnOwEDhVYrVpQ"; + +const std::string JwtTextECNoKid = + "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2Mjg2NDU3NDE4ODEtbm" + "9hYml1MjNmNWE4bThvdmQ4dWN2Njk4bGo3OHZ2MGxAZGV2ZWxvcGVyLmdzZXJ2a" + "WNlYWNjb3VudC5jb20iLCJzdWIiOiI2Mjg2NDU3NDE4ODEtbm9hYml1MjNmNWE4" + "bThvdmQ4dWN2Njk4bGo3OHZ2MGxAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5" + "jb20iLCJhdWQiOiJodHRwOi8vbXlzZXJ2aWNlLmNvbS9teWFwaSJ9.zlFcET8Fi" + "OYcKe30A7qOD4TIBvtb9zIVhDcM8pievKs1Te-UOBcklQxhwXMnRSSEBY4P0pfZ" + "qWJT_V5IVrKrdQ"; + +class VerifyJwkECTest : public testing::Test { + protected: + void SetUp() { + jwks_ = Jwks::createFrom(PublicKeyJwkEC, Jwks::Type::JWKS); + EXPECT_EQ(jwks_->getStatus(), Status::Ok); + } + + JwksPtr jwks_; +}; + +TEST_F(VerifyJwkECTest, KidOK) { + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextEC), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::Ok); + + fuzzJwtSignature(jwt, [this](const Jwt& jwt) { + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::JwtVerificationFail); + }); +} + +TEST_F(VerifyJwkECTest, NoKidOK) { + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextECNoKid), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::Ok); + + fuzzJwtSignature(jwt, [this](const Jwt& jwt) { + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::JwtVerificationFail); + }); +} + +TEST_F(VerifyJwkECTest, NonExistKidFail) { + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextWithNonExistKidEC), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::JwksKidAlgMismatch); +} + +TEST_F(VerifyJwkECTest, PubkeyNoAlgOK) { + // Remove "alg" claim from public key. + std::string alg_claim = R"("alg": "ES256",)"; + std::string pubkey_no_alg = PublicKeyJwkEC; + std::size_t alg_pos = pubkey_no_alg.find(alg_claim); + while (alg_pos != std::string::npos) { + pubkey_no_alg.erase(alg_pos, alg_claim.length()); + alg_pos = pubkey_no_alg.find(alg_claim); + } + + jwks_ = Jwks::createFrom(pubkey_no_alg, Jwks::Type::JWKS); + EXPECT_EQ(jwks_->getStatus(), Status::Ok); + + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextEC), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::Ok); +} + +TEST_F(VerifyJwkECTest, PubkeyNoKidOK) { + // Remove "kid" claim from public key. + std::string kid_claim1 = R"("kid": "abc",)"; + std::string kid_claim2 = R"("kid": "xyz",)"; + std::string pubkey_no_kid = PublicKeyJwkEC; + std::size_t kid_pos = pubkey_no_kid.find(kid_claim1); + pubkey_no_kid.erase(kid_pos, kid_claim1.length()); + kid_pos = pubkey_no_kid.find(kid_claim2); + pubkey_no_kid.erase(kid_pos, kid_claim2.length()); + + jwks_ = Jwks::createFrom(pubkey_no_kid, Jwks::Type::JWKS); + EXPECT_EQ(jwks_->getStatus(), Status::Ok); + + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextEC), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::Ok); +} + +} // namespace +} // namespace jwt_verify +} // namespace google diff --git a/src/verify_jwk_rsa_test.cc b/src/verify_jwk_rsa_test.cc new file mode 100644 index 0000000..ef39ebd --- /dev/null +++ b/src/verify_jwk_rsa_test.cc @@ -0,0 +1,272 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jwt_verify_lib/verify.h" +#include "gtest/gtest.h" +#include "src/test_common.h" + +namespace google { +namespace jwt_verify { +namespace { + +// private key: +// "-----BEGIN RSA PRIVATE KEY-----" +// "MIIEowIBAAKCAQEAtw7MNxUTxmzWROCD5BqJxmzT7xqc9KsnAjbXCoqEEHDx4WBl" +// "fcwkXHt9e/2+Uwi3Arz3FOMNKwGGlbr7clBY3utsjUs8BTF0kO/poAmSTdSuGeh2" +// "mSbcVHvmQ7X/kichWwx5Qj0Xj4REU3Gixu1gQIr3GATPAIULo5lj/ebOGAa+l0wI" +// "G80Nzz1pBtTIUx68xs5ZGe7cIJ7E8n4pMX10eeuh36h+aossePeuHulYmjr4N0/1" +// "jG7a+hHYL6nqwOR3ej0VqCTLS0OloC0LuCpLV7CnSpwbp2Qg/c+MDzQ0TH8g8drI" +// "zR5hFe9a3NlNRMXgUU5RqbLnR9zfXr7b9oEszQIDAQABAoIBAQCgQQ8cRZJrSkqG" +// "P7qWzXjBwfIDR1wSgWcD9DhrXPniXs4RzM7swvMuF1myW1/r1xxIBF+V5HNZq9tD" +// "Z07LM3WpqZX9V9iyfyoZ3D29QcPX6RGFUtHIn5GRUGoz6rdTHnh/+bqJ92uR02vx" +// "VPD4j0SNHFrWpxcE0HRxA07bLtxLgNbzXRNmzAB1eKMcrTu/W9Q1zI1opbsQbHbA" +// "CjbPEdt8INi9ij7d+XRO6xsnM20KgeuKx1lFebYN9TKGEEx8BCGINOEyWx1lLhsm" +// "V6S0XGVwWYdo2ulMWO9M0lNYPzX3AnluDVb3e1Yq2aZ1r7t/GrnGDILA1N2KrAEb" +// "AAKHmYNNAoGBAPAv9qJqf4CP3tVDdto9273DA4Mp4Kjd6lio5CaF8jd/4552T3UK" +// "N0Q7N6xaWbRYi6xsCZymC4/6DhmLG/vzZOOhHkTsvLshP81IYpWwjm4rF6BfCSl7" +// "ip+1z8qonrElxes68+vc1mNhor6GGsxyGe0C18+KzpQ0fEB5J4p0OHGnAoGBAMMb" +// "/fpr6FxXcjUgZzRlxHx1HriN6r8Jkzc+wAcQXWyPUOD8OFLcRuvikQ16sa+SlN4E" +// "HfhbFn17ABsikUAIVh0pPkHqMsrGFxDn9JrORXUpNhLdBHa6ZH+we8yUe4G0X4Mc" +// "R7c8OT26p2zMg5uqz7bQ1nJ/YWlP4nLqIytehnRrAoGAT6Rn0JUlsBiEmAylxVoL" +// "mhGnAYAKWZQ0F6/w7wEtPs/uRuYOFM4NY1eLb2AKLK3LqqGsUkAQx23v7PJelh2v" +// "z3bmVY52SkqNIGGnJuGDaO5rCCdbH2EypyCfRSDCdhUDWquSpBv3Dr8aOri2/CG9" +// "jQSLUOtC8ouww6Qow1UkPjMCgYB8kTicU5ysqCAAj0mVCIxkMZqFlgYUJhbZpLSR" +// "Tf93uiCXJDEJph2ZqLOXeYhMYjetb896qx02y/sLWAyIZ0ojoBthlhcLo2FCp/Vh" +// "iOSLot4lOPsKmoJji9fei8Y2z2RTnxCiik65fJw8OG6mSm4HeFoSDAWzaQ9Y8ue1" +// "XspVNQKBgAiHh4QfiFbgyFOlKdfcq7Scq98MA3mlmFeTx4Epe0A9xxhjbLrn362+" +// "ZSCUhkdYkVkly4QVYHJ6Idzk47uUfEC6WlLEAnjKf9LD8vMmZ14yWR2CingYTIY1" +// "LL2jMkSYEJx102t2088meCuJzEsF3BzEWOP8RfbFlciT7FFVeiM4" +// "-----END RSA PRIVATE KEY-----"; + +// The following public key jwk and token are taken from +// https://github.com/cloudendpoints/esp/blob/master/src/api_manager/auth/lib/auth_jwt_validator_test.cc +const std::string PublicKeyRSA = R"( +{ + "keys": [ + { + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "62a93512c9ee4c7f8067b5a216dade2763d32a47", + "n": "0YWnm_eplO9BFtXszMRQNL5UtZ8HJdTH2jK7vjs4XdLkPW7YBkkm_2xNgcaVpkW0VT2l4mU3KftR-6s3Oa5Rnz5BrWEUkCTVVolR7VYksfqIB2I_x5yZHdOiomMTcm3DheUUCgbJRv5OKRnNqszA4xHn3tA3Ry8VO3X7BgKZYAUh9fyZTFLlkeAh0-bLK5zvqCmKW5QgDIXSxUTJxPjZCgfx1vmAfGqaJb-nvmrORXQ6L284c73DUL7mnt6wj3H6tVqPKA27j56N0TB1Hfx4ja6Slr8S4EB3F1luYhATa1PKUSH8mYDW11HolzZmTQpRoLV8ZoHbHEaTfqX_aYahIw", + "e": "AQAB" + }, + { + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "b3319a147514df7ee5e4bcdee51350cc890cc89e", + "n": "qDi7Tx4DhNvPQsl1ofxxc2ePQFcs-L0mXYo6TGS64CY_2WmOtvYlcLNZjhuddZVV2X88m0MfwaSA16wE-RiKM9hqo5EY8BPXj57CMiYAyiHuQPp1yayjMgoE1P2jvp4eqF-BTillGJt5W5RuXti9uqfMtCQdagB8EC3MNRuU_KdeLgBy3lS3oo4LOYd-74kRBVZbk2wnmmb7IhP9OoLc1-7-9qU1uhpDxmE6JwBau0mDSwMnYDS4G_ML17dC-ZDtLd1i24STUw39KH0pcSdfFbL2NtEZdNeam1DDdk0iUtJSPZliUHJBI_pj8M-2Mn_oA8jBuI8YKwBqYkZCN1I95Q", + "e": "AQAB" + } + ] +} +)"; + +// private key: +// "-----BEGIN PRIVATE KEY-----\n" +// "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCoOLtPHgOE289C\n" +// "yXWh/HFzZ49AVyz4vSZdijpMZLrgJj/ZaY629iVws1mOG511lVXZfzybQx/BpIDX\n" +// "rAT5GIoz2GqjkRjwE9ePnsIyJgDKIe5A+nXJrKMyCgTU/aO+nh6oX4FOKWUYm3lb\n" +// "lG5e2L26p8y0JB1qAHwQLcw1G5T8p14uAHLeVLeijgs5h37viREFVluTbCeaZvsi\n" +// "E/06gtzX7v72pTW6GkPGYTonAFq7SYNLAydgNLgb8wvXt0L5kO0t3WLbhJNTDf0o\n" +// "fSlxJ18VsvY20Rl015qbUMN2TSJS0lI9mWJQckEj+mPwz7Yyf+gDyMG4jxgrAGpi\n" +// "RkI3Uj3lAgMBAAECggEAOuaaVyp4KvXYDVeC07QTeUgCdZHQkkuQemIi5YrDkCZ0\n" +// "Zsi6CsAG/f4eVk6/BGPEioItk2OeY+wYnOuDVkDMazjUpe7xH2ajLIt3DZ4W2q+k\n" +// "v6WyxmmnPqcZaAZjZiPxMh02pkqCNmqBxJolRxp23DtSxqR6lBoVVojinpnIwem6\n" +// "xyUl65u0mvlluMLCbKeGW/K9bGxT+qd3qWtYFLo5C3qQscXH4L0m96AjGgHUYW6M\n" +// "Ffs94ETNfHjqICbyvXOklabSVYenXVRL24TOKIHWkywhi1wW+Q6zHDADSdDVYw5l\n" +// "DaXz7nMzJ2X7cuRP9zrPpxByCYUZeJDqej0Pi7h7ZQKBgQDdI7Yb3xFXpbuPd1VS\n" +// "tNMltMKzEp5uQ7FXyDNI6C8+9TrjNMduTQ3REGqEcfdWA79FTJq95IM7RjXX9Aae\n" +// "p6cLekyH8MDH/SI744vCedkD2bjpA6MNQrzNkaubzGJgzNiZhjIAqnDAD3ljHI61\n" +// "NbADc32SQMejb6zlEh8hssSsXwKBgQDCvXhTIO/EuE/y5Kyb/4RGMtVaQ2cpPCoB\n" +// "GPASbEAHcsRk+4E7RtaoDQC1cBRy+zmiHUA9iI9XZyqD2xwwM89fzqMj5Yhgukvo\n" +// "XMxvMh8NrTneK9q3/M3mV1AVg71FJQ2oBr8KOXSEbnF25V6/ara2+EpH2C2GDMAo\n" +// "pgEnZ0/8OwKBgFB58IoQEdWdwLYjLW/d0oGEWN6mRfXGuMFDYDaGGLuGrxmEWZdw\n" +// "fzi4CquMdgBdeLwVdrLoeEGX+XxPmCEgzg/FQBiwqtec7VpyIqhxg2J9V2elJS9s\n" +// "PB1rh9I4/QxRP/oO9h9753BdsUU6XUzg7t8ypl4VKRH3UCpFAANZdW1tAoGAK4ad\n" +// "tjbOYHGxrOBflB5wOiByf1JBZH4GBWjFf9iiFwgXzVpJcC5NHBKL7gG3EFwGba2M\n" +// "BjTXlPmCDyaSDlQGLavJ2uQar0P0Y2MabmANgMkO/hFfOXBPtQQe6jAfxayaeMvJ\n" +// "N0fQOylUQvbRTodTf2HPeG9g/W0sJem0qFH3FrECgYEAnwixjpd1Zm/diJuP0+Lb\n" +// "YUzDP+Afy78IP3mXlbaQ/RVd7fJzMx6HOc8s4rQo1m0Y84Ztot0vwm9+S54mxVSo\n" +// "6tvh9q0D7VLDgf+2NpnrDW7eMB3n0SrLJ83Mjc5rZ+wv7m033EPaWSr/TFtc/MaF\n" +// "aOI20MEe3be96HHuWD3lTK0=\n" +// "-----END PRIVATE KEY-----"; + +// JWT without kid +// Header: {"alg":"RS256","typ":"JWT"} +// Payload: +// {"iss":"https://example.com","sub":"test@example.com","exp":1501281058} +const std::string JwtTextNoKid = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0.XYPg6VPrq-H1Kl-kgmAfGFomVpnmdZLIAo0g6dhJb2Be_" + "koZ2T76xg5_Lr828hsLKxUfzwNxl5-k1cdz_kAst6vei0hdnOYqRQ8EhkZS_" + "5Y2vWMrzGHw7AUPKCQvSnNqJG5HV8YdeOfpsLhQTd-" + "tG61q39FWzJ5Ra5lkxWhcrVDQFtVy7KQrbm2dxhNEHAR2v6xXP21p1T5xFBdmGZbHFiH63N9" + "dwdRgWjkvPVTUqxrZil7PSM2zg_GTBETp_" + "qS7Wwf8C0V9o2KZu0KDV0j0c9nZPWTv3IMlaGZAtQgJUeyemzRDtf4g2yG3xBZrLm3AzDUj_" + "EX_pmQAHA5ZjPVCAw"; + +// JWT without kid with long exp +// Header: {"alg":"RS256","typ":"JWT"} +// Payload: +// {"iss":"https://example.com","sub":"test@example.com","aud":"example_service","exp":2001001001} +const std::string JwtTextNoKidLongExp = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImF1ZCI6ImV4YW1wbGVfc2VydmljZSIsImV4cCI6MjAwMTAwMTAwMX0." + "n45uWZfIBZwCIPiL0K8Ca3tmm-ZlsDrC79_" + "vXCspPwk5oxdSn983tuC9GfVWKXWUMHe11DsB02b19Ow-" + "fmoEzooTFn65Ml7G34nW07amyM6lETiMhNzyiunctplOr6xKKJHmzTUhfTirvDeG-q9n24-" + "8lH7GP8GgHvDlgSM9OY7TGp81bRcnZBmxim_UzHoYO3_" + "c8OP4ZX3xG5PfihVk5G0g6wcHrO70w0_64JgkKRCrLHMJSrhIgp9NHel_" + "CNOnL0AjQKe9IGblJrMuouqYYS0zEWwmOVUWUSxQkoLpldQUVefcfjQeGjz8IlvktRa77FYe" + "xfP590ACPyXrivtsxg"; + +// JWT with correct kid +// Header: +// {"alg":"RS256","typ":"JWT","kid":"b3319a147514df7ee5e4bcdee51350cc890cc89e"} +// Payload: +// {"iss":"https://example.com","sub":"test@example.com","exp":1501281058} +const std::string JwtTextWithCorrectKid = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImIzMzE5YTE0NzUxNGRmN2VlNWU0" + "YmNkZWU1MTM1MGNjODkwY2M4OWUifQ." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0.QYWtQR2JNhLBJXtpJfFisF0WSyzLbD-9dynqwZt_" + "KlQZAIoZpr65BRNEyRzpt0jYrk7RA7hUR2cS9kB3AIKuWA8kVZubrVhSv_fiX6phjf_" + "bZYj92kDtMiPJf7RCuGyMgKXwwf4b1Sr67zamcTmQXf26DT415rnrUHVqTlOIW50TjNa1bbO" + "fNyKZC3LFnKGEzkfaIeXYdGiSERVOTtOFF5cUtZA2OVyeAT3mE1NuBWxz0v7xJ4zdIwHwxFU" + "wd_5tB57j_" + "zCEC9NwnwTiZ8wcaSyMWc4GJUn4bJs22BTNlRt5ElWl6RuBohxZA7nXwWig5CoLZmCpYpb8L" + "fBxyCpqJQ"; + +// JWT with existing but incorrect kid +// Header: +// {"alg":"RS256","typ":"JWT","kid":"62a93512c9ee4c7f8067b5a216dade2763d32a47"} +// Payload: +// {"iss":"https://example.com","sub":"test@example.com","exp":1501281058} +const std::string JwtTextWithIncorrectKid = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjYyYTkzNTEyYzllZTRjN2Y4MDY3" + "YjVhMjE2ZGFkZTI3NjNkMzJhNDcifQ." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0." + "adrKqsjKh4zdOuw9rMZr0Kn2LLYG1OUfDuvnO6tk75NKCHpKX6oI8moNYhgcCQU4AoCKXZ_" + "u-oMl54QTx9lX9xZ2VUWKTxcJEOnpoJb-DVv_FgIG9ETe5wcCS8Y9pQ2-hxtO1_LWYok1-" + "A01Q4929u6WNw_Og4rFXR6VSpZxXHOQrEwW44D2-Lngu1PtPjWIz3rO6cOiYaTGCS6-" + "TVeLFnB32KQg823WhFhWzzHjhYRO7NOrl-IjfGn3zYD_" + "DfSoMY3A6LeOFCPp0JX1gcKcs2mxaF6e3LfVoBiOBZGvgG_" + "jx3y85hF2BZiANbSf1nlLQFdjk_CWbLPhTWeSfLXMOg"; + +// JWT with nonexist kid +// Header: {"alg":"RS256","typ":"JWT","kid":"blahblahblah"} +// Payload: +// {"iss":"https://example.com","sub":"test@example.com","exp":1501281058} +const std::string JwtTextWithNonExistKid = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJsYWhibGFoYmxhaCJ9." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0.digk0Fr_IdcWgJNVyeVDw2dC1cQG6LsHwg5pIN93L4_" + "xhEDI3ZFoZ8aE44kvQHWLicnHDlhELqtF-" + "TqxrhfnitpLE7jiyknSu6NVXxtRBcZ3dOTKryVJDvDXcYXOaaP8infnh82loHfhikgg1xmk9" + "rcH50jtc3BkxWNbpNgPyaAAE2tEisIInaxeX0gqkwiNVrLGe1hfwdtdlWFL1WENGlyniQBvB" + "Mwi8DgG_F0eyFKTSRWoaNQQXQruEK0YIcwDj9tkYOXq8cLAnRK9zSYc5-" + "15Hlzfb8eE77pID0HZN-Axeui4IY22I_kYftd0OEqlwXJv_v5p6kNaHsQ9QbtAkw"; + +class VerifyJwkRsaTest : public testing::Test { + protected: + void SetUp() { + jwks_ = Jwks::createFrom(PublicKeyRSA, Jwks::Type::JWKS); + EXPECT_EQ(jwks_->getStatus(), Status::Ok); + } + + JwksPtr jwks_; +}; + +TEST_F(VerifyJwkRsaTest, NoKidOK) { + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextNoKid), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::Ok); + + fuzzJwtSignature(jwt, [this](const Jwt& jwt) { + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::JwtVerificationFail); + }); +} + +TEST_F(VerifyJwkRsaTest, NoKidLongExpOK) { + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextNoKidLongExp), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::Ok); + + fuzzJwtSignature(jwt, [this](const Jwt& jwt) { + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::JwtVerificationFail); + }); +} + +TEST_F(VerifyJwkRsaTest, CorrectKidOK) { + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextWithCorrectKid), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::Ok); + + fuzzJwtSignature(jwt, [this](const Jwt& jwt) { + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::JwtVerificationFail); + }); +} + +TEST_F(VerifyJwkRsaTest, NonExistKidFail) { + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextWithNonExistKid), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::JwksKidAlgMismatch); +} + +TEST_F(VerifyJwkRsaTest, OkPublicKeyNotAlg) { + // Remove "alg" claim from public key. + std::string alg_claim = R"("alg": "RS256",)"; + std::string pubkey_no_alg = PublicKeyRSA; + std::size_t alg_pos = pubkey_no_alg.find(alg_claim); + while (alg_pos != std::string::npos) { + pubkey_no_alg.erase(alg_pos, alg_claim.length()); + alg_pos = pubkey_no_alg.find(alg_claim); + } + + jwks_ = Jwks::createFrom(pubkey_no_alg, Jwks::Type::JWKS); + EXPECT_EQ(jwks_->getStatus(), Status::Ok); + + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextNoKid), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::Ok); +} + +TEST_F(VerifyJwkRsaTest, OkPublicKeyNotKid) { + // Remove "kid" claim from public key. + std::string kid_claim1 = + R"("kid": "62a93512c9ee4c7f8067b5a216dade2763d32a47",)"; + std::string kid_claim2 = + R"("kid": "b3319a147514df7ee5e4bcdee51350cc890cc89e",)"; + std::string pubkey_no_kid = PublicKeyRSA; + std::size_t kid_pos = pubkey_no_kid.find(kid_claim1); + pubkey_no_kid.erase(kid_pos, kid_claim1.length()); + kid_pos = pubkey_no_kid.find(kid_claim2); + pubkey_no_kid.erase(kid_pos, kid_claim2.length()); + + jwks_ = Jwks::createFrom(pubkey_no_kid, Jwks::Type::JWKS); + EXPECT_EQ(jwks_->getStatus(), Status::Ok); + + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtTextNoKid), Status::Ok); + EXPECT_EQ(verifyJwt(jwt, *jwks_), Status::Ok); +} + +} // namespace +} // namespace jwt_verify +} // namespace google diff --git a/src/verify_pem_test.cc b/src/verify_pem_test.cc new file mode 100644 index 0000000..937f748 --- /dev/null +++ b/src/verify_pem_test.cc @@ -0,0 +1,61 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "jwt_verify_lib/verify.h" +#include "gtest/gtest.h" +#include "src/test_common.h" + +namespace google { +namespace jwt_verify { +namespace { + +// JWT with +// Header: {"alg":"RS256","typ":"JWT"} +// Payload: +// {"iss":"https://example.com","sub":"test@example.com","exp":1501281058} +const std::string JwtPem = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIs" + "ImV4cCI6MTUwMTI4MTA1OH0.FxT92eaBr9thDpeWaQh0YFhblVggn86DBpnTa_" + "DVO4mNoGEkdpuhYq3epHPAs9EluuxdSkDJ3fCoI758ggGDw8GbqyJAcOsH10fBOrQbB7EFRB" + "CI1xz6-6GEUac5PxyDnwy3liwC_" + "gK6p4yqOD13EuEY5aoYkeM382tDFiz5Jkh8kKbqKT7h0bhIimniXLDz6iABeNBFouczdPf04" + "N09hdvlCtAF87Fu1qqfwEQ93A-J7m08bZJoyIPcNmTcYGHwfMR4-lcI5cC_93C_" + "5BGE1FHPLOHpNghLuM6-rhOtgwZc9ywupn_bBK3QzuAoDnYwpqQhgQL_CdUD_bSHcmWFkw"; + +const std::string PublicKeyPem = + "MIIBCgKCAQEAtw7MNxUTxmzWROCD5BqJxmzT7xqc9KsnAjbXCoqEEHDx4WBlfcwk" + "XHt9e/2+Uwi3Arz3FOMNKwGGlbr7clBY3utsjUs8BTF0kO/poAmSTdSuGeh2mSbc" + "VHvmQ7X/kichWwx5Qj0Xj4REU3Gixu1gQIr3GATPAIULo5lj/ebOGAa+l0wIG80N" + "zz1pBtTIUx68xs5ZGe7cIJ7E8n4pMX10eeuh36h+aossePeuHulYmjr4N0/1jG7a" + "+hHYL6nqwOR3ej0VqCTLS0OloC0LuCpLV7CnSpwbp2Qg/c+MDzQ0TH8g8drIzR5h" + "Fe9a3NlNRMXgUU5RqbLnR9zfXr7b9oEszQIDAQAB"; + +TEST(VerifyPemTest, OKPem) { + Jwt jwt; + EXPECT_EQ(jwt.parseFromString(JwtPem), Status::Ok); + + auto jwks = Jwks::createFrom(PublicKeyPem, Jwks::Type::PEM); + EXPECT_EQ(jwks->getStatus(), Status::Ok); + + EXPECT_EQ(verifyJwt(jwt, *jwks), Status::Ok); + + fuzzJwtSignature(jwt, [&jwks](const Jwt& jwt) { + EXPECT_EQ(verifyJwt(jwt, *jwks), Status::JwtVerificationFail); + }); +} + +} // namespace +} // namespace jwt_verify +} // namespace google