| 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);
|
| +}
|
|
|