mirror of https://github.com/grpc/grpc.git
Still missing: - Caching of the already checked JWTs (although it could be done at an upper layer). - Caching of the jwks_uri to avoid 2 roundtrips for each verification.pull/2252/head
parent
a6de02f5ef
commit
feca1bf06c
19 changed files with 1832 additions and 11 deletions
@ -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); |
||||
} |
||||
|
@ -0,0 +1,136 @@ |
||||
/*
|
||||
* |
||||
* 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. |
||||
* |
||||
*/ |
||||
|
||||
#ifndef GRPC_INTERNAL_CORE_SECURITY_JWT_VERIFIER_H |
||||
#define GRPC_INTERNAL_CORE_SECURITY_JWT_VERIFIER_H |
||||
|
||||
#include "src/core/iomgr/pollset.h" |
||||
#include "src/core/json/json.h" |
||||
|
||||
#include <grpc/support/slice.h> |
||||
#include <grpc/support/time.h> |
||||
|
||||
/* --- Constants. --- */ |
||||
|
||||
#define GRPC_OPENID_CONFIG_URL_SUFFIX "/.well-known/openid-configuration" |
||||
#define GRPC_GOOGLE_SERVICE_ACCOUNTS_EMAIL_DOMAIN \ |
||||
"developer.gserviceaccount.com" |
||||
#define GRPC_GOOGLE_SERVICE_ACCOUNTS_KEY_URL_PREFIX \ |
||||
"www.googleapis.com/robot/v1/metadata/x509" |
||||
|
||||
/* --- grpc_jwt_verifier_status. --- */ |
||||
|
||||
typedef enum { |
||||
GRPC_JWT_VERIFIER_OK = 0, |
||||
GRPC_JWT_VERIFIER_BAD_SIGNATURE, |
||||
GRPC_JWT_VERIFIER_BAD_FORMAT, |
||||
GRPC_JWT_VERIFIER_BAD_AUDIENCE, |
||||
GRPC_JWT_VERIFIER_KEY_RETRIEVAL_ERROR, |
||||
GRPC_JWT_VERIFIER_TIME_CONSTRAINT_FAILURE, |
||||
GRPC_JWT_VERIFIER_GENERIC_ERROR |
||||
} grpc_jwt_verifier_status; |
||||
|
||||
const char *grpc_jwt_verifier_status_to_string(grpc_jwt_verifier_status status); |
||||
|
||||
/* --- grpc_jwt_claims. --- */ |
||||
|
||||
typedef struct grpc_jwt_claims grpc_jwt_claims; |
||||
|
||||
void grpc_jwt_claims_destroy(grpc_jwt_claims *claims); |
||||
|
||||
/* Returns the whole JSON tree of the claims. */ |
||||
const grpc_json *grpc_jwt_claims_json(const grpc_jwt_claims *claims); |
||||
|
||||
/* Access to registered claims in https://tools.ietf.org/html/rfc7519#page-9 */ |
||||
const char *grpc_jwt_claims_subject(const grpc_jwt_claims *claims); |
||||
const char *grpc_jwt_claims_issuer(const grpc_jwt_claims *claims); |
||||
const char *grpc_jwt_claims_id(const grpc_jwt_claims *claims); |
||||
const char *grpc_jwt_claims_audience(const grpc_jwt_claims *claims); |
||||
gpr_timespec grpc_jwt_claims_issued_at(const grpc_jwt_claims *claims); |
||||
gpr_timespec grpc_jwt_claims_expires_at(const grpc_jwt_claims *claims); |
||||
gpr_timespec grpc_jwt_claims_not_before(const grpc_jwt_claims *claims); |
||||
|
||||
/* --- grpc_jwt_verifier. --- */ |
||||
|
||||
typedef struct grpc_jwt_verifier grpc_jwt_verifier; |
||||
|
||||
typedef struct { |
||||
/* The email domain is the part after the @ sign. */ |
||||
const char *email_domain; |
||||
|
||||
/* The key url prefix will be used to get the public key from the issuer:
|
||||
https://<key_url_prefix>/<issuer_email>
|
||||
Therefore the key_url_prefix must NOT contain https://. */
|
||||
const char *key_url_prefix; |
||||
} grpc_jwt_verifier_email_domain_key_url_mapping; |
||||
|
||||
/* Globals to control the verifier. Not thread-safe. */ |
||||
extern gpr_timespec grpc_jwt_verifier_clock_skew; |
||||
extern gpr_timespec grpc_jwt_verifier_max_delay; |
||||
|
||||
/* The verifier can be created with some custom mappings to help with key
|
||||
discovery in the case where the issuer is an email address. |
||||
mappings can be NULL in which case num_mappings MUST be 0. |
||||
A verifier object has one built-in mapping (unless overridden): |
||||
GRPC_GOOGLE_SERVICE_ACCOUNTS_EMAIL_DOMAIN -> |
||||
GRPC_GOOGLE_SERVICE_ACCOUNTS_KEY_URL_PREFIX.*/ |
||||
grpc_jwt_verifier *grpc_jwt_verifier_create( |
||||
const grpc_jwt_verifier_email_domain_key_url_mapping *mappings, |
||||
size_t num_mappings); |
||||
|
||||
/*The verifier must not be destroyed if there are still outstanding callbacks.*/ |
||||
void grpc_jwt_verifier_destroy(grpc_jwt_verifier *verifier); |
||||
|
||||
/* User provided callback that will be called when the verification of the JWT
|
||||
is done (maybe in another thread). |
||||
It is the responsibility of the callee to call grpc_jwt_claims_destroy on |
||||
the claims. */ |
||||
typedef void (*grpc_jwt_verification_done_cb)(void *user_data, |
||||
grpc_jwt_verifier_status status, |
||||
grpc_jwt_claims *claims); |
||||
|
||||
/* Verifies for the JWT for the given expected audience. */ |
||||
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); |
||||
|
||||
/* --- TESTING ONLY exposed functions. --- */ |
||||
|
||||
grpc_jwt_claims *grpc_jwt_claims_from_json(grpc_json *json, gpr_slice buffer); |
||||
grpc_jwt_verifier_status grpc_jwt_claims_check(const grpc_jwt_claims *claims, |
||||
const char *audience); |
||||
|
||||
#endif /* GRPC_INTERNAL_CORE_SECURITY_JWT_VERIFIER_H */ |
||||
|
@ -0,0 +1,565 @@ |
||||
/*
|
||||
* |
||||
* 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 disclaimer. |
||||
* * Redistributions in binary form must reproduce the above |
||||
* copyright notice, this list of conditions and the following disclaimer |
||||
* in the documentation and/or other materials provided with the |
||||
* distribution. |
||||
* * Neither the name of Google Inc. nor the names of its |
||||
* contributors may be used to endorse or promote products derived from |
||||
* this software without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
* |
||||
*/ |
||||
|
||||
#include "src/core/security/jwt_verifier.h" |
||||
|
||||
#include <string.h> |
||||
|
||||
#include "src/core/httpcli/httpcli.h" |
||||
#include "src/core/security/base64.h" |
||||
#include "src/core/security/json_token.h" |
||||
#include "test/core/util/test_config.h" |
||||
|
||||
#include <grpc/support/alloc.h> |
||||
#include <grpc/support/log.h> |
||||
#include <grpc/support/slice.h> |
||||
#include <grpc/support/string_util.h> |
||||
|
||||
/* This JSON key was generated with the GCE console and revoked immediately.
|
||||
The identifiers have been changed as well. |
||||
Maximum size for a string literal is 509 chars in C89, yay! */ |
||||
static const char json_key_str_part1[] = |
||||
"{ \"private_key\": \"-----BEGIN PRIVATE KEY-----" |
||||
"\\nMIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAOEvJsnoHnyHkXcp\\n7mJE" |
||||
"qg" |
||||
"WGjiw71NfXByguekSKho65FxaGbsnSM9SMQAqVk7Q2rG+I0OpsT0LrWQtZ\\nyjSeg/" |
||||
"rWBQvS4hle4LfijkP3J5BG+" |
||||
"IXDMP8RfziNRQsenAXDNPkY4kJCvKux2xdD\\nOnVF6N7dL3nTYZg+" |
||||
"uQrNsMTz9UxVAgMBAAECgYEAzbLewe1xe9vy+2GoSsfib+28\\nDZgSE6Bu/" |
||||
"zuFoPrRc6qL9p2SsnV7txrunTyJkkOnPLND9ABAXybRTlcVKP/sGgza\\n/" |
||||
"8HpCqFYM9V8f34SBWfD4fRFT+n/" |
||||
"73cfRUtGXdXpseva2lh8RilIQfPhNZAncenU\\ngqXjDvpkypEusgXAykECQQD+"; |
||||
static const char json_key_str_part2[] = |
||||
"53XxNVnxBHsYb+AYEfklR96yVi8HywjVHP34+OQZ\\nCslxoHQM8s+" |
||||
"dBnjfScLu22JqkPv04xyxmt0QAKm9+vTdAkEA4ib7YvEAn2jXzcCI\\nEkoy2L/" |
||||
"XydR1GCHoacdfdAwiL2npOdnbvi4ZmdYRPY1LSTO058tQHKVXV7NLeCa3\\nAARh2QJBAMKeDA" |
||||
"G" |
||||
"W303SQv2cZTdbeaLKJbB5drz3eo3j7dDKjrTD9JupixFbzcGw\\n8FZi5c8idxiwC36kbAL6Hz" |
||||
"A" |
||||
"ZoX+ofI0CQE6KCzPJTtYNqyShgKAZdJ8hwOcvCZtf\\n6z8RJm0+" |
||||
"6YBd38lfh5j8mZd7aHFf6I17j5AQY7oPEc47TjJj/" |
||||
"5nZ68ECQQDvYuI3\\nLyK5fS8g0SYbmPOL9TlcHDOqwG0mrX9qpg5DC2fniXNSrrZ64GTDKdzZ" |
||||
"Y" |
||||
"Ap6LI9W\\nIqv4vr6y38N79TTC\\n-----END PRIVATE KEY-----\\n\", "; |
||||
static const char json_key_str_part3_for_google_email_issuer[] = |
||||
"\"private_key_id\": \"e6b5137873db8d2ef81e06a47289e6434ec8a165\", " |
||||
"\"client_email\": " |
||||
"\"777-abaslkan11hlb6nmim3bpspl31ud@developer.gserviceaccount." |
||||
"com\", \"client_id\": " |
||||
"\"777-abaslkan11hlb6nmim3bpspl31ud.apps.googleusercontent." |
||||
"com\", \"type\": \"service_account\" }"; |
||||
/* Trick our JWT library into issuing a JWT with iss=accounts.google.com. */ |
||||
static const char json_key_str_part3_for_url_issuer[] = |
||||
"\"private_key_id\": \"e6b5137873db8d2ef81e06a47289e6434ec8a165\", " |
||||
"\"client_email\": \"accounts.google.com\", " |
||||
"\"client_id\": " |
||||
"\"777-abaslkan11hlb6nmim3bpspl31ud.apps.googleusercontent." |
||||
"com\", \"type\": \"service_account\" }"; |
||||
static const char json_key_str_part3_for_custom_email_issuer[] = |
||||
"\"private_key_id\": \"e6b5137873db8d2ef81e06a47289e6434ec8a165\", " |
||||
"\"client_email\": " |
||||
"\"foo@bar.com\", \"client_id\": " |
||||
"\"777-abaslkan11hlb6nmim3bpspl31ud.apps.googleusercontent." |
||||
"com\", \"type\": \"service_account\" }"; |
||||
|
||||
static grpc_jwt_verifier_email_domain_key_url_mapping custom_mapping = { |
||||
"bar.com", "keys.bar.com/jwk" |
||||
}; |
||||
|
||||
static const char expected_user_data[] = "user data"; |
||||
|
||||
static const char good_jwk_set[] = |
||||
"{" |
||||
" \"keys\": [" |
||||
" {" |
||||
" \"kty\": \"RSA\"," |
||||
" \"alg\": \"RS256\"," |
||||
" \"use\": \"sig\"," |
||||
" \"kid\": \"e6b5137873db8d2ef81e06a47289e6434ec8a165\"," |
||||
" \"n\": " |
||||
"\"4S8myegefIeRdynuYkSqBYaOLDvU19cHKC56RIqGjrkXFoZuydIz1IxACpWTtDasb4jQ6mxP" |
||||
"QutZC1nKNJ6D-tYFC9LiGV7gt-KOQ_cnkEb4hcMw_xF_OI1FCx6cBcM0-" |
||||
"RjiQkK8q7HbF0M6dUXo3t0vedNhmD65Cs2wxPP1TFU=\"," |
||||
" \"e\": \"AQAB\"" |
||||
" }" |
||||
" ]" |
||||
"}"; |
||||
|
||||
static gpr_timespec expected_lifetime = {3600, 0}; |
||||
|
||||
static const char good_google_email_keys_part1[] = |
||||
"{\"e6b5137873db8d2ef81e06a47289e6434ec8a165\": \"-----BEGIN " |
||||
"CERTIFICATE-----" |
||||
"\\nMIICATCCAWoCCQDEywLhxvHjnDANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB\\nVTET" |
||||
"MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0\\ncyBQdHkgTHR" |
||||
"kMB4XDTE1MDYyOTA4Mzk1MFoXDTI1MDYyNjA4Mzk1MFowRTELMAkG\\nA1UEBhMCQVUxEzARBg" |
||||
"NVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0\\nIFdpZGdpdHMgUHR5IEx0ZDCBn" |
||||
"zANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA4S8m\\nyegefIeRdynuYkSqBYaOLDvU19cHKC56" |
||||
"RIqGjrkXFoZuydIz1IxACpWTtDasb4jQ\\n6mxPQutZC1nKNJ6D+tYFC9LiGV7gt+KOQ/"; |
||||
|
||||
static const char good_google_email_keys_part2[] = |
||||
"cnkEb4hcMw/xF/OI1FCx6cBcM0+" |
||||
"Rji\\nQkK8q7HbF0M6dUXo3t0vedNhmD65Cs2wxPP1TFUCAwEAATANBgkqhkiG9w0BAQsF\\nA" |
||||
"AOBgQBfu69FkPmBknbKNFgurPz78kbs3VNN+k/" |
||||
"PUgO5DHKskJmgK2TbtvX2VMpx\\nkftmHGzgzMzUlOtigCaGMgHWjfqjpP9uuDbahXrZBJzB8c" |
||||
"Oq7MrQF8r17qVvo3Ue\\nPjTKQMAsU8uxTEMmeuz9L6yExs0rfd6bPOrQkAoVfFfiYB3/" |
||||
"pA==\\n-----END CERTIFICATE-----\\n\"}"; |
||||
|
||||
static const char expected_audience[] = "https://foo.com"; |
||||
|
||||
static const char good_openid_config[] = |
||||
"{" |
||||
" \"issuer\": \"https://accounts.google.com\"," |
||||
" \"authorization_endpoint\": " |
||||
"\"https://accounts.google.com/o/oauth2/v2/auth\"," |
||||
" \"token_endpoint\": \"https://www.googleapis.com/oauth2/v4/token\"," |
||||
" \"userinfo_endpoint\": \"https://www.googleapis.com/oauth2/v3/userinfo\"," |
||||
" \"revocation_endpoint\": \"https://accounts.google.com/o/oauth2/revoke\"," |
||||
" \"jwks_uri\": \"https://www.googleapis.com/oauth2/v3/certs\"" |
||||
"}"; |
||||
|
||||
static const char expired_claims[] = |
||||
"{ \"aud\": \"https://foo.com\"," |
||||
" \"iss\": \"blah.foo.com\"," |
||||
" \"sub\": \"juju@blah.foo.com\"," |
||||
" \"jti\": \"jwtuniqueid\"," |
||||
" \"iat\": 100," /* Way back in the past... */ |
||||
" \"exp\": 120," |
||||
" \"nbf\": 60," |
||||
" \"foo\": \"bar\"}"; |
||||
|
||||
static const char claims_without_time_constraint[] = |
||||
"{ \"aud\": \"https://foo.com\"," |
||||
" \"iss\": \"blah.foo.com\"," |
||||
" \"sub\": \"juju@blah.foo.com\"," |
||||
" \"jti\": \"jwtuniqueid\"," |
||||
" \"foo\": \"bar\"}"; |
||||
|
||||
static const char invalid_claims[] = |
||||
"{ \"aud\": \"https://foo.com\"," |
||||
" \"iss\": 46," /* Issuer cannot be a number. */ |
||||
" \"sub\": \"juju@blah.foo.com\"," |
||||
" \"jti\": \"jwtuniqueid\"," |
||||
" \"foo\": \"bar\"}"; |
||||
|
||||
typedef struct { |
||||
grpc_jwt_verifier_status expected_status; |
||||
const char *expected_issuer; |
||||
const char *expected_subject; |
||||
} verifier_test_config; |
||||
|
||||
static void test_claims_success(void) { |
||||
grpc_jwt_claims *claims; |
||||
gpr_slice s = gpr_slice_from_copied_string(claims_without_time_constraint); |
||||
grpc_json *json = grpc_json_parse_string_with_len( |
||||
(char *)GPR_SLICE_START_PTR(s), GPR_SLICE_LENGTH(s)); |
||||
GPR_ASSERT(json != NULL); |
||||
claims = grpc_jwt_claims_from_json(json, s); |
||||
GPR_ASSERT(claims != NULL); |
||||
GPR_ASSERT(grpc_jwt_claims_json(claims) == json); |
||||
GPR_ASSERT(strcmp(grpc_jwt_claims_audience(claims), "https://foo.com") == 0); |
||||
GPR_ASSERT(strcmp(grpc_jwt_claims_issuer(claims), "blah.foo.com") == 0); |
||||
GPR_ASSERT(strcmp(grpc_jwt_claims_subject(claims), "juju@blah.foo.com") == 0); |
||||
GPR_ASSERT(strcmp(grpc_jwt_claims_id(claims), "jwtuniqueid") == 0); |
||||
GPR_ASSERT(grpc_jwt_claims_check(claims, "https://foo.com") == |
||||
GRPC_JWT_VERIFIER_OK); |
||||
grpc_jwt_claims_destroy(claims); |
||||
} |
||||
|
||||
static void test_expired_claims_failure(void) { |
||||
grpc_jwt_claims *claims; |
||||
gpr_slice s = gpr_slice_from_copied_string(expired_claims); |
||||
grpc_json *json = grpc_json_parse_string_with_len( |
||||
(char *)GPR_SLICE_START_PTR(s), GPR_SLICE_LENGTH(s)); |
||||
gpr_timespec exp_iat = {100, 0}; |
||||
gpr_timespec exp_exp = {120, 0}; |
||||
gpr_timespec exp_nbf = {60, 0}; |
||||
GPR_ASSERT(json != NULL); |
||||
claims = grpc_jwt_claims_from_json(json, s); |
||||
GPR_ASSERT(claims != NULL); |
||||
GPR_ASSERT(grpc_jwt_claims_json(claims) == json); |
||||
GPR_ASSERT(strcmp(grpc_jwt_claims_audience(claims), "https://foo.com") == 0); |
||||
GPR_ASSERT(strcmp(grpc_jwt_claims_issuer(claims), "blah.foo.com") == 0); |
||||
GPR_ASSERT(strcmp(grpc_jwt_claims_subject(claims), "juju@blah.foo.com") == 0); |
||||
GPR_ASSERT(strcmp(grpc_jwt_claims_id(claims), "jwtuniqueid") == 0); |
||||
GPR_ASSERT(gpr_time_cmp(grpc_jwt_claims_issued_at(claims), exp_iat) == 0); |
||||
GPR_ASSERT(gpr_time_cmp(grpc_jwt_claims_expires_at(claims), exp_exp) == 0); |
||||
GPR_ASSERT(gpr_time_cmp(grpc_jwt_claims_not_before(claims), exp_nbf) == 0); |
||||
|
||||
GPR_ASSERT(grpc_jwt_claims_check(claims, "https://foo.com") == |
||||
GRPC_JWT_VERIFIER_TIME_CONSTRAINT_FAILURE); |
||||
grpc_jwt_claims_destroy(claims); |
||||
} |
||||
|
||||
static void test_invalid_claims_failure(void) { |
||||
gpr_slice s = gpr_slice_from_copied_string(invalid_claims); |
||||
grpc_json *json = grpc_json_parse_string_with_len( |
||||
(char *)GPR_SLICE_START_PTR(s), GPR_SLICE_LENGTH(s)); |
||||
GPR_ASSERT(grpc_jwt_claims_from_json(json, s) == NULL); |
||||
} |
||||
|
||||
static void test_bad_audience_claims_failure(void) { |
||||
grpc_jwt_claims *claims; |
||||
gpr_slice s = gpr_slice_from_copied_string(claims_without_time_constraint); |
||||
grpc_json *json = grpc_json_parse_string_with_len( |
||||
(char *)GPR_SLICE_START_PTR(s), GPR_SLICE_LENGTH(s)); |
||||
GPR_ASSERT(json != NULL); |
||||
claims = grpc_jwt_claims_from_json(json, s); |
||||
GPR_ASSERT(claims != NULL); |
||||
GPR_ASSERT(grpc_jwt_claims_check(claims, "https://bar.com") == |
||||
GRPC_JWT_VERIFIER_BAD_AUDIENCE); |
||||
grpc_jwt_claims_destroy(claims); |
||||
} |
||||
|
||||
static char *json_key_str(const char *last_part) { |
||||
size_t result_len = strlen(json_key_str_part1) + strlen(json_key_str_part2) + |
||||
strlen(last_part); |
||||
char *result = gpr_malloc(result_len + 1); |
||||
char *current = result; |
||||
strcpy(result, json_key_str_part1); |
||||
current += strlen(json_key_str_part1); |
||||
strcpy(current, json_key_str_part2); |
||||
current += strlen(json_key_str_part2); |
||||
strcpy(current, last_part); |
||||
return result; |
||||
} |
||||
|
||||
static char *good_google_email_keys(void) { |
||||
size_t result_len = strlen(good_google_email_keys_part1) + |
||||
strlen(good_google_email_keys_part2); |
||||
char *result = gpr_malloc(result_len + 1); |
||||
char *current = result; |
||||
strcpy(result, good_google_email_keys_part1); |
||||
current += strlen(good_google_email_keys_part1); |
||||
strcpy(current, good_google_email_keys_part2); |
||||
return result; |
||||
} |
||||
|
||||
static grpc_httpcli_response http_response(int status, char *body) { |
||||
grpc_httpcli_response response; |
||||
memset(&response, 0, sizeof(grpc_httpcli_response)); |
||||
response.status = status; |
||||
response.body = body; |
||||
response.body_length = strlen(body); |
||||
return response; |
||||
} |
||||
|
||||
static int httpcli_post_should_not_be_called( |
||||
const grpc_httpcli_request *request, const char *body_bytes, |
||||
size_t body_size, gpr_timespec deadline, |
||||
grpc_httpcli_response_cb on_response, void *user_data) { |
||||
GPR_ASSERT("HTTP POST should not be called" == NULL); |
||||
return 1; |
||||
} |
||||
|
||||
static int httpcli_get_google_keys_for_email( |
||||
const grpc_httpcli_request *request, gpr_timespec deadline, |
||||
grpc_httpcli_response_cb on_response, void *user_data) { |
||||
grpc_httpcli_response response = http_response(200, good_google_email_keys()); |
||||
GPR_ASSERT(request->use_ssl); |
||||
GPR_ASSERT(strcmp(request->host, "www.googleapis.com") == 0); |
||||
GPR_ASSERT(strcmp(request->path, |
||||
"/robot/v1/metadata/x509/" |
||||
"777-abaslkan11hlb6nmim3bpspl31ud@developer." |
||||
"gserviceaccount.com") == 0); |
||||
on_response(user_data, &response); |
||||
gpr_free(response.body); |
||||
return 1; |
||||
} |
||||
|
||||
static void on_verification_success(void *user_data, |
||||
grpc_jwt_verifier_status status, |
||||
grpc_jwt_claims *claims) { |
||||
GPR_ASSERT(status == GRPC_JWT_VERIFIER_OK); |
||||
GPR_ASSERT(claims != NULL); |
||||
GPR_ASSERT(user_data == (void *)expected_user_data); |
||||
GPR_ASSERT(strcmp(grpc_jwt_claims_audience(claims), expected_audience) == 0); |
||||
grpc_jwt_claims_destroy(claims); |
||||
} |
||||
|
||||
static void test_jwt_verifier_google_email_issuer_success(void) { |
||||
grpc_jwt_verifier *verifier = grpc_jwt_verifier_create(NULL, 0); |
||||
char *jwt = NULL; |
||||
char *key_str = json_key_str(json_key_str_part3_for_google_email_issuer); |
||||
grpc_auth_json_key key = grpc_auth_json_key_create_from_string(key_str); |
||||
gpr_free(key_str); |
||||
GPR_ASSERT(grpc_auth_json_key_is_valid(&key)); |
||||
grpc_httpcli_set_override(httpcli_get_google_keys_for_email, |
||||
httpcli_post_should_not_be_called); |
||||
jwt = |
||||
grpc_jwt_encode_and_sign(&key, expected_audience, expected_lifetime, NULL); |
||||
grpc_auth_json_key_destruct(&key); |
||||
GPR_ASSERT(jwt != NULL); |
||||
grpc_jwt_verifier_verify(verifier, NULL, jwt, expected_audience, |
||||
on_verification_success, (void *)expected_user_data); |
||||
gpr_free(jwt); |
||||
grpc_jwt_verifier_destroy(verifier); |
||||
grpc_httpcli_set_override(NULL, NULL); |
||||
} |
||||
|
||||
static int httpcli_get_custom_keys_for_email( |
||||
const grpc_httpcli_request *request, gpr_timespec deadline, |
||||
grpc_httpcli_response_cb on_response, void *user_data) { |
||||
grpc_httpcli_response response = http_response(200, gpr_strdup(good_jwk_set)); |
||||
GPR_ASSERT(request->use_ssl); |
||||
GPR_ASSERT(strcmp(request->host, "keys.bar.com") == 0); |
||||
GPR_ASSERT(strcmp(request->path, "/jwk/foo@bar.com") == 0); |
||||
on_response(user_data, &response); |
||||
gpr_free(response.body); |
||||
return 1; |
||||
} |
||||
|
||||
static void test_jwt_verifier_custom_email_issuer_success(void) { |
||||
grpc_jwt_verifier *verifier = grpc_jwt_verifier_create(&custom_mapping, 1); |
||||
char *jwt = NULL; |
||||
char *key_str = json_key_str(json_key_str_part3_for_custom_email_issuer); |
||||
grpc_auth_json_key key = grpc_auth_json_key_create_from_string(key_str); |
||||
gpr_free(key_str); |
||||
GPR_ASSERT(grpc_auth_json_key_is_valid(&key)); |
||||
grpc_httpcli_set_override(httpcli_get_custom_keys_for_email, |
||||
httpcli_post_should_not_be_called); |
||||
jwt = |
||||
grpc_jwt_encode_and_sign(&key, expected_audience, expected_lifetime, NULL); |
||||
grpc_auth_json_key_destruct(&key); |
||||
GPR_ASSERT(jwt != NULL); |
||||
grpc_jwt_verifier_verify(verifier, NULL, jwt, expected_audience, |
||||
on_verification_success, (void *)expected_user_data); |
||||
gpr_free(jwt); |
||||
grpc_jwt_verifier_destroy(verifier); |
||||
grpc_httpcli_set_override(NULL, NULL); |
||||
} |
||||
|
||||
static int httpcli_get_jwk_set( |
||||
const grpc_httpcli_request *request, gpr_timespec deadline, |
||||
grpc_httpcli_response_cb on_response, void *user_data) { |
||||
grpc_httpcli_response response = http_response(200, gpr_strdup(good_jwk_set)); |
||||
GPR_ASSERT(request->use_ssl); |
||||
GPR_ASSERT(strcmp(request->host, "www.googleapis.com") == 0); |
||||
GPR_ASSERT(strcmp(request->path, "/oauth2/v3/certs") == 0); |
||||
on_response(user_data, &response); |
||||
gpr_free(response.body); |
||||
return 1; |
||||
} |
||||
|
||||
static int httpcli_get_openid_config(const grpc_httpcli_request *request, |
||||
gpr_timespec deadline, |
||||
grpc_httpcli_response_cb on_response, |
||||
void *user_data) { |
||||
grpc_httpcli_response response = |
||||
http_response(200, gpr_strdup(good_openid_config)); |
||||
GPR_ASSERT(request->use_ssl); |
||||
GPR_ASSERT(strcmp(request->host, "accounts.google.com") == 0); |
||||
GPR_ASSERT(strcmp(request->path, GRPC_OPENID_CONFIG_URL_SUFFIX) == 0); |
||||
grpc_httpcli_set_override(httpcli_get_jwk_set, |
||||
httpcli_post_should_not_be_called); |
||||
on_response(user_data, &response); |
||||
gpr_free(response.body); |
||||
return 1; |
||||
} |
||||
|
||||
static void test_jwt_verifier_url_issuer_success(void) { |
||||
grpc_jwt_verifier *verifier = grpc_jwt_verifier_create(NULL, 0); |
||||
char *jwt = NULL; |
||||
char *key_str = json_key_str(json_key_str_part3_for_url_issuer); |
||||
grpc_auth_json_key key = grpc_auth_json_key_create_from_string(key_str); |
||||
gpr_free(key_str); |
||||
GPR_ASSERT(grpc_auth_json_key_is_valid(&key)); |
||||
grpc_httpcli_set_override(httpcli_get_openid_config, |
||||
httpcli_post_should_not_be_called); |
||||
jwt = |
||||
grpc_jwt_encode_and_sign(&key, expected_audience, expected_lifetime, NULL); |
||||
grpc_auth_json_key_destruct(&key); |
||||
GPR_ASSERT(jwt != NULL); |
||||
grpc_jwt_verifier_verify(verifier, NULL, jwt, expected_audience, |
||||
on_verification_success, (void *)expected_user_data); |
||||
gpr_free(jwt); |
||||
grpc_jwt_verifier_destroy(verifier); |
||||
grpc_httpcli_set_override(NULL, NULL); |
||||
} |
||||
|
||||
static void on_verification_key_retrieval_error(void *user_data, |
||||
grpc_jwt_verifier_status status, |
||||
grpc_jwt_claims *claims) { |
||||
GPR_ASSERT(status == GRPC_JWT_VERIFIER_KEY_RETRIEVAL_ERROR); |
||||
GPR_ASSERT(claims == NULL); |
||||
GPR_ASSERT(user_data == (void *)expected_user_data); |
||||
} |
||||
|
||||
static int httpcli_get_bad_json(const grpc_httpcli_request *request, |
||||
gpr_timespec deadline, |
||||
grpc_httpcli_response_cb on_response, |
||||
void *user_data) { |
||||
grpc_httpcli_response response = |
||||
http_response(200, gpr_strdup("{\"bad\": \"stuff\"}")); |
||||
GPR_ASSERT(request->use_ssl); |
||||
on_response(user_data, &response); |
||||
gpr_free(response.body); |
||||
return 1; |
||||
} |
||||
|
||||
static void test_jwt_verifier_url_issuer_bad_config(void) { |
||||
grpc_jwt_verifier *verifier = grpc_jwt_verifier_create(NULL, 0); |
||||
char *jwt = NULL; |
||||
char *key_str = json_key_str(json_key_str_part3_for_url_issuer); |
||||
grpc_auth_json_key key = grpc_auth_json_key_create_from_string(key_str); |
||||
gpr_free(key_str); |
||||
GPR_ASSERT(grpc_auth_json_key_is_valid(&key)); |
||||
grpc_httpcli_set_override(httpcli_get_bad_json, |
||||
httpcli_post_should_not_be_called); |
||||
jwt = |
||||
grpc_jwt_encode_and_sign(&key, expected_audience, expected_lifetime, NULL); |
||||
grpc_auth_json_key_destruct(&key); |
||||
GPR_ASSERT(jwt != NULL); |
||||
grpc_jwt_verifier_verify(verifier, NULL, jwt, expected_audience, |
||||
on_verification_key_retrieval_error, |
||||
(void *)expected_user_data); |
||||
gpr_free(jwt); |
||||
grpc_jwt_verifier_destroy(verifier); |
||||
grpc_httpcli_set_override(NULL, NULL); |
||||
} |
||||
|
||||
static void test_jwt_verifier_bad_json_key(void) { |
||||
grpc_jwt_verifier *verifier = grpc_jwt_verifier_create(NULL, 0); |
||||
char *jwt = NULL; |
||||
char *key_str = json_key_str(json_key_str_part3_for_google_email_issuer); |
||||
grpc_auth_json_key key = grpc_auth_json_key_create_from_string(key_str); |
||||
gpr_free(key_str); |
||||
GPR_ASSERT(grpc_auth_json_key_is_valid(&key)); |
||||
grpc_httpcli_set_override(httpcli_get_bad_json, |
||||
httpcli_post_should_not_be_called); |
||||
jwt = |
||||
grpc_jwt_encode_and_sign(&key, expected_audience, expected_lifetime, NULL); |
||||
grpc_auth_json_key_destruct(&key); |
||||
GPR_ASSERT(jwt != NULL); |
||||
grpc_jwt_verifier_verify(verifier, NULL, jwt, expected_audience, |
||||
on_verification_key_retrieval_error, |
||||
(void *)expected_user_data); |
||||
gpr_free(jwt); |
||||
grpc_jwt_verifier_destroy(verifier); |
||||
grpc_httpcli_set_override(NULL, NULL); |
||||
} |
||||
|
||||
static void corrupt_jwt_sig(char *jwt) { |
||||
gpr_slice sig; |
||||
char *bad_b64_sig; |
||||
gpr_uint8 *sig_bytes; |
||||
char *last_dot = strrchr(jwt, '.'); |
||||
GPR_ASSERT(last_dot != NULL); |
||||
sig = grpc_base64_decode(last_dot + 1, 1); |
||||
GPR_ASSERT(!GPR_SLICE_IS_EMPTY(sig)); |
||||
sig_bytes = GPR_SLICE_START_PTR(sig); |
||||
(*sig_bytes)++; /* Corrupt first byte. */ |
||||
bad_b64_sig = |
||||
grpc_base64_encode(GPR_SLICE_START_PTR(sig), GPR_SLICE_LENGTH(sig), 1, 0); |
||||
memcpy(last_dot + 1, bad_b64_sig, strlen(bad_b64_sig)); |
||||
gpr_free(bad_b64_sig); |
||||
gpr_slice_unref(sig); |
||||
} |
||||
|
||||
static void on_verification_bad_signature(void *user_data, |
||||
grpc_jwt_verifier_status status, |
||||
grpc_jwt_claims *claims) { |
||||
GPR_ASSERT(status == GRPC_JWT_VERIFIER_BAD_SIGNATURE); |
||||
GPR_ASSERT(claims == NULL); |
||||
GPR_ASSERT(user_data == (void *)expected_user_data); |
||||
} |
||||
|
||||
static void test_jwt_verifier_bad_signature(void) { |
||||
grpc_jwt_verifier *verifier = grpc_jwt_verifier_create(NULL, 0); |
||||
char *jwt = NULL; |
||||
char *key_str = json_key_str(json_key_str_part3_for_url_issuer); |
||||
grpc_auth_json_key key = grpc_auth_json_key_create_from_string(key_str); |
||||
gpr_free(key_str); |
||||
GPR_ASSERT(grpc_auth_json_key_is_valid(&key)); |
||||
grpc_httpcli_set_override(httpcli_get_openid_config, |
||||
httpcli_post_should_not_be_called); |
||||
jwt = |
||||
grpc_jwt_encode_and_sign(&key, expected_audience, expected_lifetime, NULL); |
||||
grpc_auth_json_key_destruct(&key); |
||||
corrupt_jwt_sig(jwt); |
||||
GPR_ASSERT(jwt != NULL); |
||||
grpc_jwt_verifier_verify(verifier, NULL, jwt, expected_audience, |
||||
on_verification_bad_signature, |
||||
(void *)expected_user_data); |
||||
gpr_free(jwt); |
||||
grpc_jwt_verifier_destroy(verifier); |
||||
grpc_httpcli_set_override(NULL, NULL); |
||||
} |
||||
|
||||
static int httpcli_get_should_not_be_called( |
||||
const grpc_httpcli_request *request, gpr_timespec deadline, |
||||
grpc_httpcli_response_cb on_response, void *user_data) { |
||||
GPR_ASSERT(0); |
||||
return 1; |
||||
} |
||||
|
||||
static void on_verification_bad_format(void *user_data, |
||||
grpc_jwt_verifier_status status, |
||||
grpc_jwt_claims *claims) { |
||||
GPR_ASSERT(status == GRPC_JWT_VERIFIER_BAD_FORMAT); |
||||
GPR_ASSERT(claims == NULL); |
||||
GPR_ASSERT(user_data == (void *)expected_user_data); |
||||
} |
||||
|
||||
static void test_jwt_verifier_bad_format(void) { |
||||
grpc_jwt_verifier *verifier = grpc_jwt_verifier_create(NULL, 0); |
||||
grpc_httpcli_set_override(httpcli_get_should_not_be_called, |
||||
httpcli_post_should_not_be_called); |
||||
grpc_jwt_verifier_verify(verifier, NULL, "bad jwt", expected_audience, |
||||
on_verification_bad_format, |
||||
(void *)expected_user_data); |
||||
grpc_jwt_verifier_destroy(verifier); |
||||
grpc_httpcli_set_override(NULL, NULL); |
||||
} |
||||
|
||||
/* find verification key: bad jks, cannot find key in jks */ |
||||
/* bad signature custom provided email*/ |
||||
/* bad key */ |
||||
|
||||
|
||||
int main(int argc, char **argv) { |
||||
grpc_test_init(argc, argv); |
||||
test_claims_success(); |
||||
test_expired_claims_failure(); |
||||
test_invalid_claims_failure(); |
||||
test_bad_audience_claims_failure(); |
||||
test_jwt_verifier_google_email_issuer_success(); |
||||
test_jwt_verifier_custom_email_issuer_success(); |
||||
test_jwt_verifier_url_issuer_success(); |
||||
test_jwt_verifier_url_issuer_bad_config(); |
||||
test_jwt_verifier_bad_json_key(); |
||||
test_jwt_verifier_bad_signature(); |
||||
test_jwt_verifier_bad_format(); |
||||
return 0; |
||||
} |
||||
|
@ -0,0 +1,119 @@ |
||||
/*
|
||||
* |
||||
* 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 disclaimer. |
||||
* * Redistributions in binary form must reproduce the above |
||||
* copyright notice, this list of conditions and the following disclaimer |
||||
* in the documentation and/or other materials provided with the |
||||
* distribution. |
||||
* * Neither the name of Google Inc. nor the names of its |
||||
* contributors may be used to endorse or promote products derived from |
||||
* this software without specific prior written permission. |
||||
* |
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
* |
||||
*/ |
||||
|
||||
#include <stdio.h> |
||||
#include <string.h> |
||||
|
||||
#include "src/core/security/jwt_verifier.h" |
||||
#include <grpc/grpc.h> |
||||
#include <grpc/grpc_security.h> |
||||
#include <grpc/support/alloc.h> |
||||
#include <grpc/support/cmdline.h> |
||||
#include <grpc/support/log.h> |
||||
#include <grpc/support/slice.h> |
||||
#include <grpc/support/sync.h> |
||||
|
||||
typedef struct { |
||||
grpc_pollset pollset; |
||||
int is_done; |
||||
int success; |
||||
} synchronizer; |
||||
|
||||
static void print_usage_and_exit(gpr_cmdline *cl, const char *argv0) { |
||||
char *usage = gpr_cmdline_usage_string(cl, argv0); |
||||
fprintf(stderr, "%s", usage); |
||||
gpr_free(usage); |
||||
gpr_cmdline_destroy(cl); |
||||
exit(1); |
||||
} |
||||
|
||||
static void on_jwt_verification_done(void *user_data, |
||||
grpc_jwt_verifier_status status, |
||||
grpc_jwt_claims *claims) { |
||||
synchronizer *sync = user_data; |
||||
|
||||
sync->success = (status == GRPC_JWT_VERIFIER_OK); |
||||
if (sync->success) { |
||||
char *claims_str; |
||||
GPR_ASSERT(claims != NULL); |
||||
claims_str = |
||||
grpc_json_dump_to_string((grpc_json *)grpc_jwt_claims_json(claims), 2); |
||||
printf("Claims: \n\n%s\n", claims_str); |
||||
gpr_free(claims_str); |
||||
grpc_jwt_claims_destroy(claims); |
||||
} else { |
||||
GPR_ASSERT(claims == NULL); |
||||
fprintf(stderr, "Verification failed with error %s\n", |
||||
grpc_jwt_verifier_status_to_string(status)); |
||||
} |
||||
|
||||
gpr_mu_lock(GRPC_POLLSET_MU(&sync->pollset)); |
||||
sync->is_done = 1; |
||||
grpc_pollset_kick(&sync->pollset); |
||||
gpr_mu_unlock(GRPC_POLLSET_MU(&sync->pollset)); |
||||
} |
||||
|
||||
int main(int argc, char **argv) { |
||||
synchronizer sync; |
||||
grpc_jwt_verifier *verifier; |
||||
gpr_cmdline *cl; |
||||
char *jwt = NULL; |
||||
char *aud = NULL; |
||||
|
||||
cl = gpr_cmdline_create("JWT verifier tool"); |
||||
gpr_cmdline_add_string(cl, "jwt", "JSON web token to verify", &jwt); |
||||
gpr_cmdline_add_string(cl, "aud", "Audience for the JWT", &aud); |
||||
gpr_cmdline_parse(cl, argc, argv); |
||||
if (jwt == NULL || aud == NULL) { |
||||
print_usage_and_exit(cl, argv[0]); |
||||
} |
||||
|
||||
verifier = grpc_jwt_verifier_create(NULL, 0); |
||||
|
||||
grpc_init(); |
||||
|
||||
grpc_pollset_init(&sync.pollset); |
||||
sync.is_done = 0; |
||||
|
||||
grpc_jwt_verifier_verify(verifier, &sync.pollset, jwt, aud, |
||||
on_jwt_verification_done, &sync); |
||||
|
||||
gpr_mu_lock(GRPC_POLLSET_MU(&sync.pollset)); |
||||
while (!sync.is_done) grpc_pollset_work(&sync.pollset, gpr_inf_future); |
||||
gpr_mu_unlock(GRPC_POLLSET_MU(&sync.pollset)); |
||||
|
||||
grpc_jwt_verifier_destroy(verifier); |
||||
gpr_cmdline_destroy(cl); |
||||
return !sync.success; |
||||
} |
||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue