blob: 7a45ff9343edc58c5097a41d9abb43368cf1192c [file] [log] [blame]
/*
*
* 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 <limits.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 (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:
{ <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 (&ctx->verifier->http_ctx, ctx->pollset, &req, gpr_time_add (gpr_now (exec_ctx, 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 (&ctx->verifier->http_ctx, ctx->pollset, &req, gpr_time_add (gpr_now (exec_ctx, 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 (verifier_cb_ctx_create (exec_ctx, 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);
}