Index: third_party/grpc/src/core/security/jwt_verifier.c |
diff --git a/third_party/grpc/src/core/security/jwt_verifier.c b/third_party/grpc/src/core/security/jwt_verifier.c |
new file mode 100644 |
index 0000000000000000000000000000000000000000..928c6c148dedc24d25a6a51b44ff9e5d7ca2721d |
--- /dev/null |
+++ b/third_party/grpc/src/core/security/jwt_verifier.c |
@@ -0,0 +1,843 @@ |
+/* |
+ * |
+ * Copyright 2015-2016, Google Inc. |
+ * All rights reserved. |
+ * |
+ * Redistribution and use in source and binary forms, with or without |
+ * modification, are permitted provided that the following conditions are |
+ * met: |
+ * |
+ * * Redistributions of source code must retain the above copyright |
+ * notice, this list of conditions and the following disclaimer. |
+ * * Redistributions in binary form must reproduce the above |
+ * copyright notice, this list of conditions and the following disclaimer |
+ * in the documentation and/or other materials provided with the |
+ * distribution. |
+ * * Neither the name of Google Inc. nor the names of its |
+ * contributors may be used to endorse or promote products derived from |
+ * this software without specific prior written permission. |
+ * |
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+ * |
+ */ |
+ |
+#include "src/core/security/jwt_verifier.h" |
+ |
+#include <limits.h> |
+#include <string.h> |
+ |
+#include "src/core/httpcli/httpcli.h" |
+#include "src/core/security/b64.h" |
+#include "src/core/tsi/ssl_types.h" |
+ |
+#include <grpc/support/alloc.h> |
+#include <grpc/support/log.h> |
+#include <grpc/support/string_util.h> |
+#include <grpc/support/sync.h> |
+#include <openssl/pem.h> |
+ |
+/* --- Utils. --- */ |
+ |
+const char *grpc_jwt_verifier_status_to_string( |
+ grpc_jwt_verifier_status status) { |
+ switch (status) { |
+ case GRPC_JWT_VERIFIER_OK: |
+ return "OK"; |
+ case GRPC_JWT_VERIFIER_BAD_SIGNATURE: |
+ return "BAD_SIGNATURE"; |
+ case GRPC_JWT_VERIFIER_BAD_FORMAT: |
+ return "BAD_FORMAT"; |
+ case GRPC_JWT_VERIFIER_BAD_AUDIENCE: |
+ return "BAD_AUDIENCE"; |
+ case GRPC_JWT_VERIFIER_KEY_RETRIEVAL_ERROR: |
+ return "KEY_RETRIEVAL_ERROR"; |
+ case GRPC_JWT_VERIFIER_TIME_CONSTRAINT_FAILURE: |
+ return "TIME_CONSTRAINT_FAILURE"; |
+ case GRPC_JWT_VERIFIER_GENERIC_ERROR: |
+ return "GENERIC_ERROR"; |
+ default: |
+ return "UNKNOWN"; |
+ } |
+} |
+ |
+static const EVP_MD *evp_md_from_alg(const char *alg) { |
+ if (strcmp(alg, "RS256") == 0) { |
+ return EVP_sha256(); |
+ } else if (strcmp(alg, "RS384") == 0) { |
+ return EVP_sha384(); |
+ } else if (strcmp(alg, "RS512") == 0) { |
+ return EVP_sha512(); |
+ } else { |
+ return NULL; |
+ } |
+} |
+ |
+static grpc_json *parse_json_part_from_jwt(const char *str, size_t len, |
+ gpr_slice *buffer) { |
+ grpc_json *json; |
+ |
+ *buffer = grpc_base64_decode_with_len(str, len, 1); |
+ if (GPR_SLICE_IS_EMPTY(*buffer)) { |
+ gpr_log(GPR_ERROR, "Invalid base64."); |
+ return NULL; |
+ } |
+ json = grpc_json_parse_string_with_len((char *)GPR_SLICE_START_PTR(*buffer), |
+ GPR_SLICE_LENGTH(*buffer)); |
+ if (json == NULL) { |
+ gpr_slice_unref(*buffer); |
+ gpr_log(GPR_ERROR, "JSON parsing error."); |
+ } |
+ return json; |
+} |
+ |
+static const char *validate_string_field(const grpc_json *json, |
+ const char *key) { |
+ if (json->type != GRPC_JSON_STRING) { |
+ gpr_log(GPR_ERROR, "Invalid %s field [%s]", key, json->value); |
+ return NULL; |
+ } |
+ return json->value; |
+} |
+ |
+static gpr_timespec validate_time_field(const grpc_json *json, |
+ const char *key) { |
+ gpr_timespec result = gpr_time_0(GPR_CLOCK_REALTIME); |
+ if (json->type != GRPC_JSON_NUMBER) { |
+ gpr_log(GPR_ERROR, "Invalid %s field [%s]", key, json->value); |
+ return result; |
+ } |
+ result.tv_sec = strtol(json->value, NULL, 10); |
+ return result; |
+} |
+ |
+/* --- JOSE header. see http://tools.ietf.org/html/rfc7515#section-4 --- */ |
+ |
+typedef struct { |
+ const char *alg; |
+ const char *kid; |
+ const char *typ; |
+ /* TODO(jboeuf): Add others as needed (jku, jwk, x5u, x5c and so on...). */ |
+ gpr_slice buffer; |
+} jose_header; |
+ |
+static void jose_header_destroy(jose_header *h) { |
+ gpr_slice_unref(h->buffer); |
+ gpr_free(h); |
+} |
+ |
+/* Takes ownership of json and buffer. */ |
+static jose_header *jose_header_from_json(grpc_json *json, gpr_slice buffer) { |
+ grpc_json *cur; |
+ jose_header *h = gpr_malloc(sizeof(jose_header)); |
+ memset(h, 0, sizeof(jose_header)); |
+ h->buffer = buffer; |
+ for (cur = json->child; cur != NULL; cur = cur->next) { |
+ if (strcmp(cur->key, "alg") == 0) { |
+ /* We only support RSA-1.5 signatures for now. |
+ Beware of this if we add HMAC support: |
+ https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ |
+ */ |
+ if (cur->type != GRPC_JSON_STRING || strncmp(cur->value, "RS", 2) || |
+ evp_md_from_alg(cur->value) == NULL) { |
+ gpr_log(GPR_ERROR, "Invalid alg field [%s]", cur->value); |
+ goto error; |
+ } |
+ h->alg = cur->value; |
+ } else if (strcmp(cur->key, "typ") == 0) { |
+ h->typ = validate_string_field(cur, "typ"); |
+ if (h->typ == NULL) goto error; |
+ } else if (strcmp(cur->key, "kid") == 0) { |
+ h->kid = validate_string_field(cur, "kid"); |
+ if (h->kid == NULL) goto error; |
+ } |
+ } |
+ if (h->alg == NULL) { |
+ gpr_log(GPR_ERROR, "Missing alg field."); |
+ goto error; |
+ } |
+ grpc_json_destroy(json); |
+ h->buffer = buffer; |
+ return h; |
+ |
+error: |
+ grpc_json_destroy(json); |
+ jose_header_destroy(h); |
+ return NULL; |
+} |
+ |
+/* --- JWT claims. see http://tools.ietf.org/html/rfc7519#section-4.1 */ |
+ |
+struct grpc_jwt_claims { |
+ /* Well known properties already parsed. */ |
+ const char *sub; |
+ const char *iss; |
+ const char *aud; |
+ const char *jti; |
+ gpr_timespec iat; |
+ gpr_timespec exp; |
+ gpr_timespec nbf; |
+ |
+ grpc_json *json; |
+ gpr_slice buffer; |
+}; |
+ |
+void grpc_jwt_claims_destroy(grpc_jwt_claims *claims) { |
+ grpc_json_destroy(claims->json); |
+ gpr_slice_unref(claims->buffer); |
+ gpr_free(claims); |
+} |
+ |
+const grpc_json *grpc_jwt_claims_json(const grpc_jwt_claims *claims) { |
+ if (claims == NULL) return NULL; |
+ return claims->json; |
+} |
+ |
+const char *grpc_jwt_claims_subject(const grpc_jwt_claims *claims) { |
+ if (claims == NULL) return NULL; |
+ return claims->sub; |
+} |
+ |
+const char *grpc_jwt_claims_issuer(const grpc_jwt_claims *claims) { |
+ if (claims == NULL) return NULL; |
+ return claims->iss; |
+} |
+ |
+const char *grpc_jwt_claims_id(const grpc_jwt_claims *claims) { |
+ if (claims == NULL) return NULL; |
+ return claims->jti; |
+} |
+ |
+const char *grpc_jwt_claims_audience(const grpc_jwt_claims *claims) { |
+ if (claims == NULL) return NULL; |
+ return claims->aud; |
+} |
+ |
+gpr_timespec grpc_jwt_claims_issued_at(const grpc_jwt_claims *claims) { |
+ if (claims == NULL) return gpr_inf_past(GPR_CLOCK_REALTIME); |
+ return claims->iat; |
+} |
+ |
+gpr_timespec grpc_jwt_claims_expires_at(const grpc_jwt_claims *claims) { |
+ if (claims == NULL) return gpr_inf_future(GPR_CLOCK_REALTIME); |
+ return claims->exp; |
+} |
+ |
+gpr_timespec grpc_jwt_claims_not_before(const grpc_jwt_claims *claims) { |
+ if (claims == NULL) return gpr_inf_past(GPR_CLOCK_REALTIME); |
+ return claims->nbf; |
+} |
+ |
+/* Takes ownership of json and buffer even in case of failure. */ |
+grpc_jwt_claims *grpc_jwt_claims_from_json(grpc_json *json, gpr_slice buffer) { |
+ grpc_json *cur; |
+ grpc_jwt_claims *claims = gpr_malloc(sizeof(grpc_jwt_claims)); |
+ memset(claims, 0, sizeof(grpc_jwt_claims)); |
+ claims->json = json; |
+ claims->buffer = buffer; |
+ claims->iat = gpr_inf_past(GPR_CLOCK_REALTIME); |
+ claims->nbf = gpr_inf_past(GPR_CLOCK_REALTIME); |
+ claims->exp = gpr_inf_future(GPR_CLOCK_REALTIME); |
+ |
+ /* Per the spec, all fields are optional. */ |
+ for (cur = json->child; cur != NULL; cur = cur->next) { |
+ if (strcmp(cur->key, "sub") == 0) { |
+ claims->sub = validate_string_field(cur, "sub"); |
+ if (claims->sub == NULL) goto error; |
+ } else if (strcmp(cur->key, "iss") == 0) { |
+ claims->iss = validate_string_field(cur, "iss"); |
+ if (claims->iss == NULL) goto error; |
+ } else if (strcmp(cur->key, "aud") == 0) { |
+ claims->aud = validate_string_field(cur, "aud"); |
+ if (claims->aud == NULL) goto error; |
+ } else if (strcmp(cur->key, "jti") == 0) { |
+ claims->jti = validate_string_field(cur, "jti"); |
+ if (claims->jti == NULL) goto error; |
+ } else if (strcmp(cur->key, "iat") == 0) { |
+ claims->iat = validate_time_field(cur, "iat"); |
+ if (gpr_time_cmp(claims->iat, gpr_time_0(GPR_CLOCK_REALTIME)) == 0) |
+ goto error; |
+ } else if (strcmp(cur->key, "exp") == 0) { |
+ claims->exp = validate_time_field(cur, "exp"); |
+ if (gpr_time_cmp(claims->exp, gpr_time_0(GPR_CLOCK_REALTIME)) == 0) |
+ goto error; |
+ } else if (strcmp(cur->key, "nbf") == 0) { |
+ claims->nbf = validate_time_field(cur, "nbf"); |
+ if (gpr_time_cmp(claims->nbf, gpr_time_0(GPR_CLOCK_REALTIME)) == 0) |
+ goto error; |
+ } |
+ } |
+ return claims; |
+ |
+error: |
+ grpc_jwt_claims_destroy(claims); |
+ return NULL; |
+} |
+ |
+grpc_jwt_verifier_status grpc_jwt_claims_check(const grpc_jwt_claims *claims, |
+ const char *audience) { |
+ gpr_timespec skewed_now; |
+ int audience_ok; |
+ |
+ GPR_ASSERT(claims != NULL); |
+ |
+ skewed_now = |
+ gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), grpc_jwt_verifier_clock_skew); |
+ if (gpr_time_cmp(skewed_now, claims->nbf) < 0) { |
+ gpr_log(GPR_ERROR, "JWT is not valid yet."); |
+ return GRPC_JWT_VERIFIER_TIME_CONSTRAINT_FAILURE; |
+ } |
+ skewed_now = |
+ gpr_time_sub(gpr_now(GPR_CLOCK_REALTIME), grpc_jwt_verifier_clock_skew); |
+ if (gpr_time_cmp(skewed_now, claims->exp) > 0) { |
+ gpr_log(GPR_ERROR, "JWT is expired."); |
+ return GRPC_JWT_VERIFIER_TIME_CONSTRAINT_FAILURE; |
+ } |
+ |
+ if (audience == NULL) { |
+ audience_ok = claims->aud == NULL; |
+ } else { |
+ audience_ok = claims->aud != NULL && strcmp(audience, claims->aud) == 0; |
+ } |
+ if (!audience_ok) { |
+ gpr_log(GPR_ERROR, "Audience mismatch: expected %s and found %s.", |
+ audience == NULL ? "NULL" : audience, |
+ claims->aud == NULL ? "NULL" : claims->aud); |
+ return GRPC_JWT_VERIFIER_BAD_AUDIENCE; |
+ } |
+ return GRPC_JWT_VERIFIER_OK; |
+} |
+ |
+/* --- verifier_cb_ctx object. --- */ |
+ |
+typedef struct { |
+ grpc_jwt_verifier *verifier; |
+ grpc_pollset *pollset; |
+ jose_header *header; |
+ grpc_jwt_claims *claims; |
+ char *audience; |
+ gpr_slice signature; |
+ gpr_slice signed_data; |
+ void *user_data; |
+ grpc_jwt_verification_done_cb user_cb; |
+} verifier_cb_ctx; |
+ |
+/* Takes ownership of the header, claims and signature. */ |
+static verifier_cb_ctx *verifier_cb_ctx_create( |
+ grpc_jwt_verifier *verifier, grpc_pollset *pollset, jose_header *header, |
+ grpc_jwt_claims *claims, const char *audience, gpr_slice signature, |
+ const char *signed_jwt, size_t signed_jwt_len, void *user_data, |
+ grpc_jwt_verification_done_cb cb) { |
+ verifier_cb_ctx *ctx = gpr_malloc(sizeof(verifier_cb_ctx)); |
+ memset(ctx, 0, sizeof(verifier_cb_ctx)); |
+ ctx->verifier = verifier; |
+ ctx->pollset = pollset; |
+ ctx->header = header; |
+ ctx->audience = gpr_strdup(audience); |
+ ctx->claims = claims; |
+ ctx->signature = signature; |
+ ctx->signed_data = gpr_slice_from_copied_buffer(signed_jwt, signed_jwt_len); |
+ ctx->user_data = user_data; |
+ ctx->user_cb = cb; |
+ return ctx; |
+} |
+ |
+void verifier_cb_ctx_destroy(verifier_cb_ctx *ctx) { |
+ if (ctx->audience != NULL) gpr_free(ctx->audience); |
+ if (ctx->claims != NULL) grpc_jwt_claims_destroy(ctx->claims); |
+ gpr_slice_unref(ctx->signature); |
+ gpr_slice_unref(ctx->signed_data); |
+ jose_header_destroy(ctx->header); |
+ /* TODO: see what to do with claims... */ |
+ gpr_free(ctx); |
+} |
+ |
+/* --- grpc_jwt_verifier object. --- */ |
+ |
+/* Clock skew defaults to one minute. */ |
+gpr_timespec grpc_jwt_verifier_clock_skew = {60, 0, GPR_TIMESPAN}; |
+ |
+/* Max delay defaults to one minute. */ |
+gpr_timespec grpc_jwt_verifier_max_delay = {60, 0, GPR_TIMESPAN}; |
+ |
+typedef struct { |
+ char *email_domain; |
+ char *key_url_prefix; |
+} email_key_mapping; |
+ |
+struct grpc_jwt_verifier { |
+ email_key_mapping *mappings; |
+ size_t num_mappings; /* Should be very few, linear search ok. */ |
+ size_t allocated_mappings; |
+ grpc_httpcli_context http_ctx; |
+}; |
+ |
+static grpc_json *json_from_http(const grpc_httpcli_response *response) { |
+ grpc_json *json = NULL; |
+ |
+ if (response == NULL) { |
+ gpr_log(GPR_ERROR, "HTTP response is NULL."); |
+ return NULL; |
+ } |
+ if (response->status != 200) { |
+ gpr_log(GPR_ERROR, "Call to http server failed with error %d.", |
+ response->status); |
+ return NULL; |
+ } |
+ |
+ json = grpc_json_parse_string_with_len(response->body, response->body_length); |
+ if (json == NULL) { |
+ gpr_log(GPR_ERROR, "Invalid JSON found in response."); |
+ } |
+ return json; |
+} |
+ |
+static const grpc_json *find_property_by_name(const grpc_json *json, |
+ const char *name) { |
+ const grpc_json *cur; |
+ for (cur = json->child; cur != NULL; cur = cur->next) { |
+ if (strcmp(cur->key, name) == 0) return cur; |
+ } |
+ return NULL; |
+} |
+ |
+static EVP_PKEY *extract_pkey_from_x509(const char *x509_str) { |
+ X509 *x509 = NULL; |
+ EVP_PKEY *result = NULL; |
+ BIO *bio = BIO_new(BIO_s_mem()); |
+ size_t len = strlen(x509_str); |
+ GPR_ASSERT(len < INT_MAX); |
+ BIO_write(bio, x509_str, (int)len); |
+ x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL); |
+ if (x509 == NULL) { |
+ gpr_log(GPR_ERROR, "Unable to parse x509 cert."); |
+ goto end; |
+ } |
+ result = X509_get_pubkey(x509); |
+ if (result == NULL) { |
+ gpr_log(GPR_ERROR, "Cannot find public key in X509 cert."); |
+ } |
+ |
+end: |
+ BIO_free(bio); |
+ if (x509 != NULL) X509_free(x509); |
+ return result; |
+} |
+ |
+static BIGNUM *bignum_from_base64(const char *b64) { |
+ BIGNUM *result = NULL; |
+ gpr_slice bin; |
+ |
+ if (b64 == NULL) return NULL; |
+ bin = grpc_base64_decode(b64, 1); |
+ if (GPR_SLICE_IS_EMPTY(bin)) { |
+ gpr_log(GPR_ERROR, "Invalid base64 for big num."); |
+ return NULL; |
+ } |
+ result = BN_bin2bn(GPR_SLICE_START_PTR(bin), |
+ TSI_SIZE_AS_SIZE(GPR_SLICE_LENGTH(bin)), NULL); |
+ gpr_slice_unref(bin); |
+ return result; |
+} |
+ |
+static EVP_PKEY *pkey_from_jwk(const grpc_json *json, const char *kty) { |
+ const grpc_json *key_prop; |
+ RSA *rsa = NULL; |
+ EVP_PKEY *result = NULL; |
+ |
+ GPR_ASSERT(kty != NULL && json != NULL); |
+ if (strcmp(kty, "RSA") != 0) { |
+ gpr_log(GPR_ERROR, "Unsupported key type %s.", kty); |
+ goto end; |
+ } |
+ rsa = RSA_new(); |
+ if (rsa == NULL) { |
+ gpr_log(GPR_ERROR, "Could not create rsa key."); |
+ goto end; |
+ } |
+ for (key_prop = json->child; key_prop != NULL; key_prop = key_prop->next) { |
+ if (strcmp(key_prop->key, "n") == 0) { |
+ rsa->n = bignum_from_base64(validate_string_field(key_prop, "n")); |
+ if (rsa->n == NULL) goto end; |
+ } else if (strcmp(key_prop->key, "e") == 0) { |
+ rsa->e = bignum_from_base64(validate_string_field(key_prop, "e")); |
+ if (rsa->e == NULL) goto end; |
+ } |
+ } |
+ if (rsa->e == NULL || rsa->n == NULL) { |
+ gpr_log(GPR_ERROR, "Missing RSA public key field."); |
+ goto end; |
+ } |
+ result = EVP_PKEY_new(); |
+ EVP_PKEY_set1_RSA(result, rsa); /* uprefs rsa. */ |
+ |
+end: |
+ if (rsa != NULL) RSA_free(rsa); |
+ return result; |
+} |
+ |
+static EVP_PKEY *find_verification_key(const grpc_json *json, |
+ const char *header_alg, |
+ const char *header_kid) { |
+ const grpc_json *jkey; |
+ const grpc_json *jwk_keys; |
+ /* Try to parse the json as a JWK set: |
+ https://tools.ietf.org/html/rfc7517#section-5. */ |
+ jwk_keys = find_property_by_name(json, "keys"); |
+ if (jwk_keys == NULL) { |
+ /* Use the google proprietary format which is: |
+ { <kid1>: <x5091>, <kid2>: <x5092>, ... } */ |
+ const grpc_json *cur = find_property_by_name(json, header_kid); |
+ if (cur == NULL) return NULL; |
+ return extract_pkey_from_x509(cur->value); |
+ } |
+ |
+ if (jwk_keys->type != GRPC_JSON_ARRAY) { |
+ gpr_log(GPR_ERROR, |
+ "Unexpected value type of keys property in jwks key set."); |
+ return NULL; |
+ } |
+ /* Key format is specified in: |
+ https://tools.ietf.org/html/rfc7518#section-6. */ |
+ for (jkey = jwk_keys->child; jkey != NULL; jkey = jkey->next) { |
+ grpc_json *key_prop; |
+ const char *alg = NULL; |
+ const char *kid = NULL; |
+ const char *kty = NULL; |
+ |
+ if (jkey->type != GRPC_JSON_OBJECT) continue; |
+ for (key_prop = jkey->child; key_prop != NULL; key_prop = key_prop->next) { |
+ if (strcmp(key_prop->key, "alg") == 0 && |
+ key_prop->type == GRPC_JSON_STRING) { |
+ alg = key_prop->value; |
+ } else if (strcmp(key_prop->key, "kid") == 0 && |
+ key_prop->type == GRPC_JSON_STRING) { |
+ kid = key_prop->value; |
+ } else if (strcmp(key_prop->key, "kty") == 0 && |
+ key_prop->type == GRPC_JSON_STRING) { |
+ kty = key_prop->value; |
+ } |
+ } |
+ if (alg != NULL && kid != NULL && kty != NULL && |
+ strcmp(kid, header_kid) == 0 && strcmp(alg, header_alg) == 0) { |
+ return pkey_from_jwk(jkey, kty); |
+ } |
+ } |
+ gpr_log(GPR_ERROR, |
+ "Could not find matching key in key set for kid=%s and alg=%s", |
+ header_kid, header_alg); |
+ return NULL; |
+} |
+ |
+static int verify_jwt_signature(EVP_PKEY *key, const char *alg, |
+ gpr_slice signature, gpr_slice signed_data) { |
+ EVP_MD_CTX *md_ctx = EVP_MD_CTX_create(); |
+ const EVP_MD *md = evp_md_from_alg(alg); |
+ int result = 0; |
+ |
+ GPR_ASSERT(md != NULL); /* Checked before. */ |
+ if (md_ctx == NULL) { |
+ gpr_log(GPR_ERROR, "Could not create EVP_MD_CTX."); |
+ goto end; |
+ } |
+ if (EVP_DigestVerifyInit(md_ctx, NULL, md, NULL, key) != 1) { |
+ gpr_log(GPR_ERROR, "EVP_DigestVerifyInit failed."); |
+ goto end; |
+ } |
+ if (EVP_DigestVerifyUpdate(md_ctx, GPR_SLICE_START_PTR(signed_data), |
+ GPR_SLICE_LENGTH(signed_data)) != 1) { |
+ gpr_log(GPR_ERROR, "EVP_DigestVerifyUpdate failed."); |
+ goto end; |
+ } |
+ if (EVP_DigestVerifyFinal(md_ctx, GPR_SLICE_START_PTR(signature), |
+ GPR_SLICE_LENGTH(signature)) != 1) { |
+ gpr_log(GPR_ERROR, "JWT signature verification failed."); |
+ goto end; |
+ } |
+ result = 1; |
+ |
+end: |
+ if (md_ctx != NULL) EVP_MD_CTX_destroy(md_ctx); |
+ return result; |
+} |
+ |
+static void on_keys_retrieved(grpc_exec_ctx *exec_ctx, void *user_data, |
+ const grpc_httpcli_response *response) { |
+ grpc_json *json = json_from_http(response); |
+ verifier_cb_ctx *ctx = (verifier_cb_ctx *)user_data; |
+ EVP_PKEY *verification_key = NULL; |
+ grpc_jwt_verifier_status status = GRPC_JWT_VERIFIER_GENERIC_ERROR; |
+ grpc_jwt_claims *claims = NULL; |
+ |
+ if (json == NULL) { |
+ status = GRPC_JWT_VERIFIER_KEY_RETRIEVAL_ERROR; |
+ goto end; |
+ } |
+ verification_key = |
+ find_verification_key(json, ctx->header->alg, ctx->header->kid); |
+ if (verification_key == NULL) { |
+ gpr_log(GPR_ERROR, "Could not find verification key with kid %s.", |
+ ctx->header->kid); |
+ status = GRPC_JWT_VERIFIER_KEY_RETRIEVAL_ERROR; |
+ goto end; |
+ } |
+ |
+ if (!verify_jwt_signature(verification_key, ctx->header->alg, ctx->signature, |
+ ctx->signed_data)) { |
+ status = GRPC_JWT_VERIFIER_BAD_SIGNATURE; |
+ goto end; |
+ } |
+ |
+ status = grpc_jwt_claims_check(ctx->claims, ctx->audience); |
+ if (status == GRPC_JWT_VERIFIER_OK) { |
+ /* Pass ownership. */ |
+ claims = ctx->claims; |
+ ctx->claims = NULL; |
+ } |
+ |
+end: |
+ if (json != NULL) grpc_json_destroy(json); |
+ if (verification_key != NULL) EVP_PKEY_free(verification_key); |
+ ctx->user_cb(ctx->user_data, status, claims); |
+ verifier_cb_ctx_destroy(ctx); |
+} |
+ |
+static void on_openid_config_retrieved(grpc_exec_ctx *exec_ctx, void *user_data, |
+ const grpc_httpcli_response *response) { |
+ const grpc_json *cur; |
+ grpc_json *json = json_from_http(response); |
+ verifier_cb_ctx *ctx = (verifier_cb_ctx *)user_data; |
+ grpc_httpcli_request req; |
+ const char *jwks_uri; |
+ |
+ /* TODO(jboeuf): Cache the jwks_uri in order to avoid this hop next time. */ |
+ if (json == NULL) goto error; |
+ cur = find_property_by_name(json, "jwks_uri"); |
+ if (cur == NULL) { |
+ gpr_log(GPR_ERROR, "Could not find jwks_uri in openid config."); |
+ goto error; |
+ } |
+ jwks_uri = validate_string_field(cur, "jwks_uri"); |
+ if (jwks_uri == NULL) goto error; |
+ if (strstr(jwks_uri, "https://") != jwks_uri) { |
+ gpr_log(GPR_ERROR, "Invalid non https jwks_uri: %s.", jwks_uri); |
+ goto error; |
+ } |
+ jwks_uri += 8; |
+ req.handshaker = &grpc_httpcli_ssl; |
+ req.host = gpr_strdup(jwks_uri); |
+ req.path = strchr(jwks_uri, '/'); |
+ if (req.path == NULL) { |
+ req.path = ""; |
+ } else { |
+ *(req.host + (req.path - jwks_uri)) = '\0'; |
+ } |
+ grpc_httpcli_get( |
+ exec_ctx, &ctx->verifier->http_ctx, ctx->pollset, &req, |
+ gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), grpc_jwt_verifier_max_delay), |
+ on_keys_retrieved, ctx); |
+ grpc_json_destroy(json); |
+ gpr_free(req.host); |
+ return; |
+ |
+error: |
+ if (json != NULL) grpc_json_destroy(json); |
+ ctx->user_cb(ctx->user_data, GRPC_JWT_VERIFIER_KEY_RETRIEVAL_ERROR, NULL); |
+ verifier_cb_ctx_destroy(ctx); |
+} |
+ |
+static email_key_mapping *verifier_get_mapping(grpc_jwt_verifier *v, |
+ const char *email_domain) { |
+ size_t i; |
+ if (v->mappings == NULL) return NULL; |
+ for (i = 0; i < v->num_mappings; i++) { |
+ if (strcmp(email_domain, v->mappings[i].email_domain) == 0) { |
+ return &v->mappings[i]; |
+ } |
+ } |
+ return NULL; |
+} |
+ |
+static void verifier_put_mapping(grpc_jwt_verifier *v, const char *email_domain, |
+ const char *key_url_prefix) { |
+ email_key_mapping *mapping = verifier_get_mapping(v, email_domain); |
+ GPR_ASSERT(v->num_mappings < v->allocated_mappings); |
+ if (mapping != NULL) { |
+ gpr_free(mapping->key_url_prefix); |
+ mapping->key_url_prefix = gpr_strdup(key_url_prefix); |
+ return; |
+ } |
+ v->mappings[v->num_mappings].email_domain = gpr_strdup(email_domain); |
+ v->mappings[v->num_mappings].key_url_prefix = gpr_strdup(key_url_prefix); |
+ v->num_mappings++; |
+ GPR_ASSERT(v->num_mappings <= v->allocated_mappings); |
+} |
+ |
+/* Takes ownership of ctx. */ |
+static void retrieve_key_and_verify(grpc_exec_ctx *exec_ctx, |
+ verifier_cb_ctx *ctx) { |
+ const char *at_sign; |
+ grpc_httpcli_response_cb http_cb; |
+ char *path_prefix = NULL; |
+ const char *iss; |
+ grpc_httpcli_request req; |
+ memset(&req, 0, sizeof(grpc_httpcli_request)); |
+ req.handshaker = &grpc_httpcli_ssl; |
+ |
+ GPR_ASSERT(ctx != NULL && ctx->header != NULL && ctx->claims != NULL); |
+ iss = ctx->claims->iss; |
+ if (ctx->header->kid == NULL) { |
+ gpr_log(GPR_ERROR, "Missing kid in jose header."); |
+ goto error; |
+ } |
+ if (iss == NULL) { |
+ gpr_log(GPR_ERROR, "Missing iss in claims."); |
+ goto error; |
+ } |
+ |
+ /* This code relies on: |
+ https://openid.net/specs/openid-connect-discovery-1_0.html |
+ Nobody seems to implement the account/email/webfinger part 2. of the spec |
+ so we will rely instead on email/url mappings if we detect such an issuer. |
+ Part 4, on the other hand is implemented by both google and salesforce. */ |
+ |
+ /* Very non-sophisticated way to detect an email address. Should be good |
+ enough for now... */ |
+ at_sign = strchr(iss, '@'); |
+ if (at_sign != NULL) { |
+ email_key_mapping *mapping; |
+ const char *email_domain = at_sign + 1; |
+ GPR_ASSERT(ctx->verifier != NULL); |
+ mapping = verifier_get_mapping(ctx->verifier, email_domain); |
+ if (mapping == NULL) { |
+ gpr_log(GPR_ERROR, "Missing mapping for issuer email."); |
+ goto error; |
+ } |
+ req.host = gpr_strdup(mapping->key_url_prefix); |
+ path_prefix = strchr(req.host, '/'); |
+ if (path_prefix == NULL) { |
+ gpr_asprintf(&req.path, "/%s", iss); |
+ } else { |
+ *(path_prefix++) = '\0'; |
+ gpr_asprintf(&req.path, "/%s/%s", path_prefix, iss); |
+ } |
+ http_cb = on_keys_retrieved; |
+ } else { |
+ req.host = gpr_strdup(strstr(iss, "https://") == iss ? iss + 8 : iss); |
+ path_prefix = strchr(req.host, '/'); |
+ if (path_prefix == NULL) { |
+ req.path = gpr_strdup(GRPC_OPENID_CONFIG_URL_SUFFIX); |
+ } else { |
+ *(path_prefix++) = 0; |
+ gpr_asprintf(&req.path, "/%s%s", path_prefix, |
+ GRPC_OPENID_CONFIG_URL_SUFFIX); |
+ } |
+ http_cb = on_openid_config_retrieved; |
+ } |
+ |
+ grpc_httpcli_get( |
+ exec_ctx, &ctx->verifier->http_ctx, ctx->pollset, &req, |
+ gpr_time_add(gpr_now(GPR_CLOCK_REALTIME), grpc_jwt_verifier_max_delay), |
+ http_cb, ctx); |
+ gpr_free(req.host); |
+ gpr_free(req.path); |
+ return; |
+ |
+error: |
+ ctx->user_cb(ctx->user_data, GRPC_JWT_VERIFIER_KEY_RETRIEVAL_ERROR, NULL); |
+ verifier_cb_ctx_destroy(ctx); |
+} |
+ |
+void grpc_jwt_verifier_verify(grpc_exec_ctx *exec_ctx, |
+ grpc_jwt_verifier *verifier, |
+ grpc_pollset *pollset, const char *jwt, |
+ const char *audience, |
+ grpc_jwt_verification_done_cb cb, |
+ void *user_data) { |
+ const char *dot = NULL; |
+ grpc_json *json; |
+ jose_header *header = NULL; |
+ grpc_jwt_claims *claims = NULL; |
+ gpr_slice header_buffer; |
+ gpr_slice claims_buffer; |
+ gpr_slice signature; |
+ size_t signed_jwt_len; |
+ const char *cur = jwt; |
+ |
+ GPR_ASSERT(verifier != NULL && jwt != NULL && audience != NULL && cb != NULL); |
+ dot = strchr(cur, '.'); |
+ if (dot == NULL) goto error; |
+ json = parse_json_part_from_jwt(cur, (size_t)(dot - cur), &header_buffer); |
+ if (json == NULL) goto error; |
+ header = jose_header_from_json(json, header_buffer); |
+ if (header == NULL) goto error; |
+ |
+ cur = dot + 1; |
+ dot = strchr(cur, '.'); |
+ if (dot == NULL) goto error; |
+ json = parse_json_part_from_jwt(cur, (size_t)(dot - cur), &claims_buffer); |
+ if (json == NULL) goto error; |
+ claims = grpc_jwt_claims_from_json(json, claims_buffer); |
+ if (claims == NULL) goto error; |
+ |
+ signed_jwt_len = (size_t)(dot - jwt); |
+ cur = dot + 1; |
+ signature = grpc_base64_decode(cur, 1); |
+ if (GPR_SLICE_IS_EMPTY(signature)) goto error; |
+ retrieve_key_and_verify( |
+ exec_ctx, |
+ verifier_cb_ctx_create(verifier, pollset, header, claims, audience, |
+ signature, jwt, signed_jwt_len, user_data, cb)); |
+ return; |
+ |
+error: |
+ if (header != NULL) jose_header_destroy(header); |
+ if (claims != NULL) grpc_jwt_claims_destroy(claims); |
+ cb(user_data, GRPC_JWT_VERIFIER_BAD_FORMAT, NULL); |
+} |
+ |
+grpc_jwt_verifier *grpc_jwt_verifier_create( |
+ const grpc_jwt_verifier_email_domain_key_url_mapping *mappings, |
+ size_t num_mappings) { |
+ grpc_jwt_verifier *v = gpr_malloc(sizeof(grpc_jwt_verifier)); |
+ memset(v, 0, sizeof(grpc_jwt_verifier)); |
+ grpc_httpcli_context_init(&v->http_ctx); |
+ |
+ /* We know at least of one mapping. */ |
+ v->allocated_mappings = 1 + num_mappings; |
+ v->mappings = gpr_malloc(v->allocated_mappings * sizeof(email_key_mapping)); |
+ verifier_put_mapping(v, GRPC_GOOGLE_SERVICE_ACCOUNTS_EMAIL_DOMAIN, |
+ GRPC_GOOGLE_SERVICE_ACCOUNTS_KEY_URL_PREFIX); |
+ /* User-Provided mappings. */ |
+ if (mappings != NULL) { |
+ size_t i; |
+ for (i = 0; i < num_mappings; i++) { |
+ verifier_put_mapping(v, mappings[i].email_domain, |
+ mappings[i].key_url_prefix); |
+ } |
+ } |
+ return v; |
+} |
+ |
+void grpc_jwt_verifier_destroy(grpc_jwt_verifier *v) { |
+ size_t i; |
+ if (v == NULL) return; |
+ grpc_httpcli_context_destroy(&v->http_ctx); |
+ if (v->mappings != NULL) { |
+ for (i = 0; i < v->num_mappings; i++) { |
+ gpr_free(v->mappings[i].email_domain); |
+ gpr_free(v->mappings[i].key_url_prefix); |
+ } |
+ gpr_free(v->mappings); |
+ } |
+ gpr_free(v); |
+} |