/* * * Copyright 2015, 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 disclaimser. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimser * 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 #include #include "src/core/httpcli/httpcli.h" #include "src/core/security/base64.h" #include #include #include #include #include /* --- 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), (int)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: { : , : , ... } */ 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(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(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( &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(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( &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_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( 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); }