/* This file is part of GNUnet Copyright (C) 2010-2015 GNUnet e.V. GNUnet is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. GNUnet is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ /** * @file reclaim/oidc_helper.c * @brief helper library for OIDC related functions * @author Martin Schanzenbach */ #include "platform.h" #include "gnunet_util_lib.h" #include "gnunet_signatures.h" #include "gnunet_reclaim_service.h" #include "gnunet_reclaim_attribute_lib.h" #include #include #include "oidc_helper.h" static char* create_jwt_header(void) { json_t *root; char *json_str; root = json_object (); json_object_set_new (root, JWT_ALG, json_string (JWT_ALG_VALUE)); json_object_set_new (root, JWT_TYP, json_string (JWT_TYP_VALUE)); json_str = json_dumps (root, JSON_INDENT(0) | JSON_COMPACT); json_decref (root); return json_str; } static void replace_char(char* str, char find, char replace){ char *current_pos = strchr(str,find); while (current_pos){ *current_pos = replace; current_pos = strchr(current_pos,find); } } //RFC4648 static void fix_base64(char* str) { char *padding; //First, remove trailing padding '=' padding = strtok(str, "="); while (NULL != padding) padding = strtok(NULL, "="); //Replace + with - replace_char (str, '+', '-'); //Replace / with _ replace_char (str, '/', '_'); } /** * Create a JWT from attributes * * @param aud_key the public of the audience * @param sub_key the public key of the subject * @param attrs the attribute list * @param expiration_time the validity of the token * @param secret_key the key used to sign the JWT * @return a new base64-encoded JWT string. */ char* OIDC_id_token_new (const struct GNUNET_CRYPTO_EcdsaPublicKey *aud_key, const struct GNUNET_CRYPTO_EcdsaPublicKey *sub_key, const struct GNUNET_RECLAIM_ATTRIBUTE_ClaimList *attrs, const struct GNUNET_TIME_Relative *expiration_time, const char *nonce, const char *secret_key) { struct GNUNET_RECLAIM_ATTRIBUTE_ClaimListEntry *le; struct GNUNET_HashCode signature; struct GNUNET_TIME_Absolute exp_time; struct GNUNET_TIME_Absolute time_now; char* audience; char* subject; char* header; char* body_str; char* result; char* header_base64; char* body_base64; char* signature_target; char* signature_base64; char* attr_val_str; json_t* body; //iat REQUIRED time now time_now = GNUNET_TIME_absolute_get(); //exp REQUIRED time expired from config exp_time = GNUNET_TIME_absolute_add (time_now, *expiration_time); //auth_time only if max_age //nonce only if nonce // OPTIONAL acr,amr,azp subject = GNUNET_STRINGS_data_to_string_alloc (sub_key, sizeof (struct GNUNET_CRYPTO_EcdsaPublicKey)); audience = GNUNET_STRINGS_data_to_string_alloc (aud_key, sizeof (struct GNUNET_CRYPTO_EcdsaPublicKey)); header = create_jwt_header (); body = json_object (); //iss REQUIRED case sensitive server uri with https //The issuer is the local reclaim instance (e.g. https://reclaim.id/api/openid) json_object_set_new (body, "iss", json_string (SERVER_ADDRESS)); //sub REQUIRED public key identity, not exceed 255 ASCII length json_object_set_new (body, "sub", json_string (subject)); //aud REQUIRED public key client_id must be there json_object_set_new (body, "aud", json_string (audience)); //iat json_object_set_new (body, "iat", json_integer (time_now.abs_value_us / (1000*1000))); //exp json_object_set_new (body, "exp", json_integer (exp_time.abs_value_us / (1000*1000))); //nbf json_object_set_new (body, "nbf", json_integer (time_now.abs_value_us / (1000*1000))); //nonce if (NULL != nonce) json_object_set_new (body, "nonce", json_string (nonce)); for (le = attrs->list_head; NULL != le; le = le->next) { attr_val_str = GNUNET_RECLAIM_ATTRIBUTE_value_to_string (le->claim->type, le->claim->data, le->claim->data_size); json_object_set_new (body, le->claim->name, json_string (attr_val_str)); GNUNET_free (attr_val_str); } body_str = json_dumps (body, JSON_INDENT(0) | JSON_COMPACT); json_decref (body); GNUNET_STRINGS_base64_encode (header, strlen (header), &header_base64); fix_base64(header_base64); GNUNET_STRINGS_base64_encode (body_str, strlen (body_str), &body_base64); fix_base64(body_base64); GNUNET_free (subject); GNUNET_free (audience); /** * Creating the JWT signature. This might not be * standards compliant, check. */ GNUNET_asprintf (&signature_target, "%s.%s", header_base64, body_base64); GNUNET_CRYPTO_hmac_raw (secret_key, strlen (secret_key), signature_target, strlen (signature_target), &signature); GNUNET_STRINGS_base64_encode ((const char*)&signature, sizeof (struct GNUNET_HashCode), &signature_base64); fix_base64(signature_base64); GNUNET_asprintf (&result, "%s.%s.%s", header_base64, body_base64, signature_base64); GNUNET_free (signature_target); GNUNET_free (header); GNUNET_free (body_str); GNUNET_free (signature_base64); GNUNET_free (body_base64); GNUNET_free (header_base64); return result; } /** * Builds an OIDC authorization code including * a reclaim ticket and nonce * * @param issuer the issuer of the ticket, used to sign the ticket and nonce * @param ticket the ticket to include in the code * @param nonce the nonce to include in the code * @return a new authorization code (caller must free) */ char* OIDC_build_authz_code (const struct GNUNET_CRYPTO_EcdsaPrivateKey *issuer, const struct GNUNET_RECLAIM_Ticket *ticket, const char* nonce) { char *ticket_str; json_t *code_json; char *signature_payload; char *signature_str; char *authz_code; size_t signature_payload_len; struct GNUNET_CRYPTO_EcdsaSignature signature; struct GNUNET_CRYPTO_EccSignaturePurpose *purpose; signature_payload_len = sizeof (struct GNUNET_RECLAIM_Ticket); if (NULL != nonce) signature_payload_len += strlen (nonce); signature_payload = GNUNET_malloc (sizeof (struct GNUNET_CRYPTO_EccSignaturePurpose) + signature_payload_len); purpose = (struct GNUNET_CRYPTO_EccSignaturePurpose *)signature_payload; purpose->size = htonl (sizeof (struct GNUNET_CRYPTO_EccSignaturePurpose) + signature_payload_len); purpose->purpose = htonl (GNUNET_SIGNATURE_PURPOSE_RECLAIM_CODE_SIGN); memcpy (&purpose[1], ticket, sizeof (struct GNUNET_RECLAIM_Ticket)); if (NULL != nonce) memcpy (((char*)&purpose[1]) + sizeof (struct GNUNET_RECLAIM_Ticket), nonce, strlen (nonce)); if (GNUNET_SYSERR == GNUNET_CRYPTO_ecdsa_sign (issuer, purpose, &signature)) { GNUNET_free (signature_payload); return NULL; } signature_str = GNUNET_STRINGS_data_to_string_alloc (&signature, sizeof (signature)); ticket_str = GNUNET_STRINGS_data_to_string_alloc (ticket, sizeof (struct GNUNET_RECLAIM_Ticket)); code_json = json_object (); json_object_set_new (code_json, "ticket", json_string (ticket_str)); if (NULL != nonce) json_object_set_new (code_json, "nonce", json_string (nonce)); json_object_set_new (code_json, "signature", json_string (signature_str)); authz_code = json_dumps (code_json, JSON_INDENT(0) | JSON_COMPACT); GNUNET_free (signature_payload); GNUNET_free (signature_str); GNUNET_free (ticket_str); json_decref (code_json); return authz_code; } /** * Parse reclaim ticket and nonce from * authorization code. * This also verifies the signature in the code. * * @param audience the expected audience of the code * @param code the string representation of the code * @param ticket where to store the ticket * @param nonce where to store the nonce * @return GNUNET_OK if successful, else GNUNET_SYSERR */ int OIDC_parse_authz_code (const struct GNUNET_CRYPTO_EcdsaPublicKey *audience, const char* code, struct GNUNET_RECLAIM_Ticket **ticket, char **nonce) { json_error_t error; json_t *code_json; json_t *ticket_json; json_t *nonce_json; json_t *signature_json; const char *ticket_str; const char *signature_str; const char *nonce_str; char *code_output; struct GNUNET_CRYPTO_EccSignaturePurpose *purpose; struct GNUNET_CRYPTO_EcdsaSignature signature; size_t signature_payload_len; code_output = NULL; GNUNET_STRINGS_base64_decode (code, strlen(code), (void**)&code_output); code_json = json_loads (code_output, 0 , &error); GNUNET_free (code_output); ticket_json = json_object_get (code_json, "ticket"); nonce_json = json_object_get (code_json, "nonce"); signature_json = json_object_get (code_json, "signature"); *ticket = NULL; *nonce = NULL; if ((NULL == ticket_json || !json_is_string (ticket_json)) || (NULL == signature_json || !json_is_string (signature_json))) { json_decref (code_json); return GNUNET_SYSERR; } ticket_str = json_string_value (ticket_json); signature_str = json_string_value (signature_json); nonce_str = NULL; if (NULL != nonce_json) nonce_str = json_string_value (nonce_json); signature_payload_len = sizeof (struct GNUNET_RECLAIM_Ticket); if (NULL != nonce_str) signature_payload_len += strlen (nonce_str); purpose = GNUNET_malloc (sizeof (struct GNUNET_CRYPTO_EccSignaturePurpose) + signature_payload_len); purpose->size = htonl (sizeof (struct GNUNET_CRYPTO_EccSignaturePurpose) + signature_payload_len); purpose->purpose = htonl (GNUNET_SIGNATURE_PURPOSE_RECLAIM_CODE_SIGN); if (GNUNET_OK != GNUNET_STRINGS_string_to_data (ticket_str, strlen (ticket_str), &purpose[1], sizeof (struct GNUNET_RECLAIM_Ticket))) { GNUNET_free (purpose); json_decref (code_json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Cannot parse ticket!\n"); return GNUNET_SYSERR; } if (GNUNET_OK != GNUNET_STRINGS_string_to_data (signature_str, strlen (signature_str), &signature, sizeof (struct GNUNET_CRYPTO_EcdsaSignature))) { GNUNET_free (purpose); json_decref (code_json); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Cannot parse signature!\n"); return GNUNET_SYSERR; } *ticket = GNUNET_new (struct GNUNET_RECLAIM_Ticket); memcpy (*ticket, &purpose[1], sizeof (struct GNUNET_RECLAIM_Ticket)); if (0 != memcmp (audience, &(*ticket)->audience, sizeof (struct GNUNET_CRYPTO_EcdsaPublicKey))) { GNUNET_free (purpose); GNUNET_free (*ticket); json_decref (code_json); *ticket = NULL; GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Audience in ticket does not match client!\n"); return GNUNET_SYSERR; } if (NULL != nonce_str) memcpy (((char*)&purpose[1]) + sizeof (struct GNUNET_RECLAIM_Ticket), nonce_str, strlen (nonce_str)); if (GNUNET_OK != GNUNET_CRYPTO_ecdsa_verify (GNUNET_SIGNATURE_PURPOSE_RECLAIM_CODE_SIGN, purpose, &signature, &(*ticket)->identity)) { GNUNET_free (purpose); GNUNET_free (*ticket); json_decref (code_json); *ticket = NULL; GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Signature of authZ code invalid!\n"); return GNUNET_SYSERR; } *nonce = GNUNET_strdup (nonce_str); return GNUNET_OK; } /** * Build a token response for a token request * TODO: Maybe we should add the scope here? * * @param access_token the access token to include * @param id_token the id_token to include * @param expiration_time the expiration time of the token(s) * @param token_response where to store the response */ void OIDC_build_token_response (const char *access_token, const char *id_token, const struct GNUNET_TIME_Relative *expiration_time, char **token_response) { json_t *root_json; root_json = json_object (); GNUNET_assert (NULL != access_token); GNUNET_assert (NULL != id_token); GNUNET_assert (NULL != expiration_time); json_object_set_new (root_json, "access_token", json_string (access_token)); json_object_set_new (root_json, "token_type", json_string ("Bearer")); json_object_set_new (root_json, "expires_in", json_integer (expiration_time->rel_value_us / (1000 * 1000))); json_object_set_new (root_json, "id_token", json_string (id_token)); *token_response = json_dumps (root_json, JSON_INDENT(0) | JSON_COMPACT); json_decref (root_json); } /** * Generate a new access token */ char* OIDC_access_token_new () { char* access_token_number; char* access_token; uint64_t random_number; random_number = GNUNET_CRYPTO_random_u64(GNUNET_CRYPTO_QUALITY_NONCE, UINT64_MAX); GNUNET_asprintf (&access_token_number, "%" PRIu64, random_number); GNUNET_STRINGS_base64_encode(access_token_number,strlen(access_token_number),&access_token); return access_token; }