|  | @@ -0,0 +1,830 @@
 | 
	
		
			
				|  |  | +/*
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * 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 <string.h>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +#include "src/core/httpcli/httpcli.h"
 | 
	
		
			
				|  |  | +#include "src/core/security/base64.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;
 | 
	
		
			
				|  |  | +  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;
 | 
	
		
			
				|  |  | +  return claims->iat;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +gpr_timespec grpc_jwt_claims_expires_at(const grpc_jwt_claims *claims) {
 | 
	
		
			
				|  |  | +  if (claims == NULL) return gpr_inf_future;
 | 
	
		
			
				|  |  | +  return claims->exp;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +gpr_timespec grpc_jwt_claims_not_before(const grpc_jwt_claims *claims) {
 | 
	
		
			
				|  |  | +  if (claims == NULL) return gpr_inf_past;
 | 
	
		
			
				|  |  | +  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;
 | 
	
		
			
				|  |  | +  claims->nbf = gpr_inf_past;
 | 
	
		
			
				|  |  | +  claims->exp = gpr_inf_future;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  /* 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) == 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) == 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) == 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(), 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(), 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};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/* Max delay defaults to one minute. */
 | 
	
		
			
				|  |  | +gpr_timespec grpc_jwt_verifier_max_delay = {60, 0};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +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());
 | 
	
		
			
				|  |  | +  BIO_write(bio, x509_str, strlen(x509_str));
 | 
	
		
			
				|  |  | +  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), 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(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.use_ssl = 1;
 | 
	
		
			
				|  |  | +  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(), 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.use_ssl = 1;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  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(), 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, 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, 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);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 |