/* SSSD KCM Server - ccache JSON (un)marshalling for storing ccaches in sssd-secrets Copyright (C) Red Hat, 2017 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "config.h" #include #include #include #include "util/util.h" #include "util/util_creds.h" #include "util/crypto/sss_crypto.h" #include "responder/kcm/kcmsrv_ccache_pvt.h" /* The base for storing secrets is: * http://localhost/kcm/persistent/$uid * * Under $base, there are two containers: * /ccache - stores the ccaches * /ntlm - stores NTLM creds [Not implement yet] * * There is also a special entry that contains the UUID of the default * cache for this UID: * /default - stores the UUID of the default ccache for this UID * * Each ccache has a name and an UUID. On the secrets level, the 'secret' * is a concatenation of the stringified UUID and the name separated * by a plus-sign. */ #define KCM_SEC_URL "http://localhost/kcm/persistent" #define KCM_SEC_BASE_FMT KCM_SEC_URL"/%"SPRIuid"/" #define KCM_SEC_CCACHE_FMT KCM_SEC_BASE_FMT"ccache/" #define KCM_SEC_DFL_FMT KCM_SEC_BASE_FMT"default" /* * We keep the JSON representation of the ccache versioned to allow * us to modify the format in a future version */ #define KS_JSON_VERSION 1 /* * The secrets store is a key-value store at heart. We store the UUID * and the name in the key to allow easy lookups be either key */ #define SEC_KEY_SEPARATOR '-' /* Compat definition of json_array_foreach for older systems */ #ifndef json_array_foreach #define json_array_foreach(array, idx, value) \ for(idx = 0; \ idx < json_array_size(array) && (value = json_array_get(array, idx)); \ idx++) #endif const char *sec_container_url_create(TALLOC_CTX *mem_ctx, struct cli_creds *client) { return talloc_asprintf(mem_ctx, KCM_SEC_CCACHE_FMT, cli_creds_get_uid(client)); } const char *sec_cc_url_create(TALLOC_CTX *mem_ctx, struct cli_creds *client, const char *sec_key) { return talloc_asprintf(mem_ctx, KCM_SEC_CCACHE_FMT"%s", cli_creds_get_uid(client), sec_key); } const char *sec_dfl_url_create(TALLOC_CTX *mem_ctx, struct cli_creds *client) { return talloc_asprintf(mem_ctx, KCM_SEC_DFL_FMT, cli_creds_get_uid(client)); } static const char *sec_key_create(TALLOC_CTX *mem_ctx, const char *name, uuid_t uuid) { char uuid_str[UUID_STR_SIZE]; uuid_unparse(uuid, uuid_str); return talloc_asprintf(mem_ctx, "%s%c%s", uuid_str, SEC_KEY_SEPARATOR, name); } static bool sec_key_valid(const char *sec_key) { if (sec_key == NULL) { return false; } if (strlen(sec_key) < UUID_STR_SIZE + 1) { /* One char for separator (at UUID_STR_SIZE, because strlen doesn't * include the '\0', but UUID_STR_SIZE does) and at least one for * the name */ DEBUG(SSSDBG_CRIT_FAILURE, "Key %s is too short\n", sec_key); return false; } if (sec_key[UUID_STR_SIZE - 1] != SEC_KEY_SEPARATOR) { DEBUG(SSSDBG_CRIT_FAILURE, "Key doesn't contain the separator\n"); return false; } return true; } static errno_t sec_key_parse(TALLOC_CTX *mem_ctx, const char *sec_key, const char **_name, uuid_t uuid) { char uuid_str[UUID_STR_SIZE]; if (!sec_key_valid(sec_key)) { return EINVAL; } strncpy(uuid_str, sec_key, sizeof(uuid_str)-1); if (sec_key[UUID_STR_SIZE - 1] != SEC_KEY_SEPARATOR) { DEBUG(SSSDBG_CRIT_FAILURE, "Key doesn't contain the separator\n"); return EINVAL; } uuid_str[UUID_STR_SIZE-1] = '\0'; *_name = talloc_strdup(mem_ctx, sec_key + UUID_STR_SIZE); if (*_name == NULL) { return ENOMEM; } uuid_parse(uuid_str, uuid); return EOK; } errno_t sec_key_get_uuid(const char *sec_key, uuid_t uuid) { char uuid_str[UUID_STR_SIZE]; if (!sec_key_valid(sec_key)) { return EINVAL; } strncpy(uuid_str, sec_key, UUID_STR_SIZE-1); uuid_str[UUID_STR_SIZE-1] = '\0'; uuid_parse(uuid_str, uuid); return EOK; } const char *sec_key_get_name(const char *sec_key) { if (!sec_key_valid(sec_key)) { return NULL; } return sec_key + UUID_STR_SIZE; } bool sec_key_match_name(const char *sec_key, const char *name) { if (!sec_key_valid(sec_key) || name == NULL) { return false; } return strcmp(sec_key + UUID_STR_SIZE, name) == 0; } bool sec_key_match_uuid(const char *sec_key, uuid_t uuid) { errno_t ret; uuid_t key_uuid; ret = sec_key_get_uuid(sec_key, key_uuid); if (ret != EOK) { DEBUG(SSSDBG_MINOR_FAILURE, "Cannot convert key to UUID\n"); return false; } return uuid_compare(key_uuid, uuid) == 0; } /* * Creates an array of principal elements that will be used later * in the form of: * "componenets": [ "elem1", "elem2", ...] */ static json_t *princ_data_to_json(TALLOC_CTX *mem_ctx, krb5_principal princ) { json_t *jdata = NULL; json_t *data_array = NULL; int ret; char *str_princ_data; data_array = json_array(); if (data_array == NULL) { return NULL; } for (ssize_t i = 0; i < princ->length; i++) { /* FIXME - it might be cleaner to use stringn here, but the libjansson * version on RHEL-7 doesn't support that */ str_princ_data = talloc_zero_array(mem_ctx, char, princ->data[i].length + 1); if (str_princ_data == NULL) { return NULL; } memcpy(str_princ_data, princ->data[i].data, princ->data[i].length); str_princ_data[princ->data[i].length] = '\0'; jdata = json_string(str_princ_data); talloc_free(str_princ_data); if (jdata == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot convert principal data to string\n"); json_decref(data_array); return NULL; } ret = json_array_append_new(data_array, jdata); if (ret != 0) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot append principal data to array\n"); json_decref(jdata); json_decref(data_array); return NULL; } /* data_array now owns the reference to jdata */ } return data_array; } /* Creates: * { * "type": "number", * "realm": "string", * "componenents": [ "elem1", "elem2", ...] * } */ static json_t *princ_to_json(TALLOC_CTX *mem_ctx, krb5_principal princ) { json_t *jprinc = NULL; json_t *components = NULL; json_error_t error; char *str_realm_data; components = princ_data_to_json(mem_ctx, princ); if (components == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot convert principal data to JSON\n"); return NULL; } /* FIXME - it might be cleaner to use the s% specifier here, but the libjansson * version on RHEL-7 doesn't support that */ str_realm_data = talloc_zero_array(mem_ctx, char, princ->realm.length + 1); if (str_realm_data == NULL) { return NULL; } memcpy(str_realm_data, princ->realm.data, princ->realm.length); str_realm_data[princ->realm.length] = '\0'; jprinc = json_pack_ex(&error, JSON_STRICT, "{s:i, s:s, s:o}", "type", princ->type, "realm", str_realm_data, "components", components); talloc_free(str_realm_data); if (jprinc == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Failed to pack JSON princ structure on line %d: %s\n", error.line, error.text); json_decref(components); return NULL; } return jprinc; } /* Creates: * { * "uuid": , * "payload": , * }, */ static json_t *cred_to_json(struct kcm_cred *crd) { char uuid_str[UUID_STR_SIZE]; uint8_t *cred_blob_data; size_t cred_blob_size; json_t *jcred; json_error_t error; char *base64_cred_blob; uuid_unparse(crd->uuid, uuid_str); cred_blob_data = sss_iobuf_get_data(crd->cred_blob); cred_blob_size = sss_iobuf_get_size(crd->cred_blob); base64_cred_blob = sss_base64_encode(crd, cred_blob_data, cred_blob_size); if (base64_cred_blob == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot base64 encode the certificate blob\n"); return NULL; } jcred = json_pack_ex(&error, JSON_STRICT, "{s:s, s:s}", "uuid", uuid_str, "payload", base64_cred_blob); talloc_free(base64_cred_blob); if (jcred == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Failed to pack JSON cred structure on line %d: %s\n", error.line, error.text); return NULL; } return jcred; } /* * Creates: * [ * { * "uuid": , * "payload": , * }, * ... * ] */ static json_t *creds_to_json_array(struct kcm_cred *creds) { struct kcm_cred *crd; json_t *array; json_t *jcred; array = json_array(); if (array == NULL) { return NULL; } DLIST_FOR_EACH(crd, creds) { jcred = cred_to_json(crd); if (jcred == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot convert credentials to JSON\n"); json_decref(array); return NULL; } json_array_append_new(array, jcred); /* array now owns jcred */ jcred = NULL; } return array; } /* * The ccache is formatted in JSON as: * { * version: number * kdc_offset: number * principal : { * "type": "number", * "realm": "string", * "componenents": [ "elem1", "elem2", ...] * } * creds : [ * { * "uuid": , * "payload": , * }, * { * ... * } * ] * } * } */ static json_t *ccache_to_json(struct kcm_ccache *cc) { json_t *princ = NULL; json_t *creds = NULL; json_t *jcc = NULL; json_error_t error; princ = princ_to_json(cc, cc->client); if (princ == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot convert princ to JSON\n"); return NULL; } creds = creds_to_json_array(cc->creds); if (creds == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot convert creds to JSON array\n"); json_decref(princ); return NULL; } jcc = json_pack_ex(&error, JSON_STRICT, "{s:i, s:i, s:o, s:o}", "version", KS_JSON_VERSION, "kdc_offset", cc->kdc_offset, "principal", princ, "creds", creds); if (jcc == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Failed to pack JSON ccache structure on line %d: %s\n", error.line, error.text); json_decref(creds); json_decref(princ); return NULL; } return jcc; } static errno_t ccache_to_sec_kv(TALLOC_CTX *mem_ctx, struct kcm_ccache *cc, const char **_sec_key, const char **_sec_value) { json_t *jcc = NULL; char *jdump; jcc = ccache_to_json(cc); if (jcc == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot convert ccache to JSON\n"); return ERR_JSON_ENCODING; } /* it would be more efficient to learn the size with json_dumpb and * a NULL buffer, but that's only available since 2.10 */ jdump = json_dumps(jcc, JSON_INDENT(4) | JSON_ENSURE_ASCII); if (jdump == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot dump JSON\n"); return ERR_JSON_ENCODING; } *_sec_key = sec_key_create(mem_ctx, cc->name, cc->uuid); *_sec_value = talloc_strdup(mem_ctx, jdump); free(jdump); json_decref(jcc); if (*_sec_key == NULL || *_sec_value == NULL) { return ENOMEM; } return EOK; } errno_t kcm_ccache_to_sec_input(TALLOC_CTX *mem_ctx, struct kcm_ccache *cc, struct cli_creds *client, const char **_url, struct sss_iobuf **_payload) { errno_t ret; const char *key; const char *value; const char *url; struct sss_iobuf *payload; TALLOC_CTX *tmp_ctx; tmp_ctx = talloc_new(mem_ctx); if (tmp_ctx == NULL) { return ENOMEM; } ret = ccache_to_sec_kv(mem_ctx, cc, &key, &value); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot convert cache %s to JSON [%d]: %s\n", cc->name, ret, sss_strerror(ret)); goto done; } url = sec_cc_url_create(tmp_ctx, client, key); if (url == NULL) { ret = ENOMEM; goto done; } payload = sss_iobuf_init_readonly(tmp_ctx, (const uint8_t *) value, strlen(value)+1); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot create payload buffer\n"); goto done; } ret = EOK; *_url = talloc_steal(mem_ctx, url); *_payload = talloc_steal(mem_ctx, payload); done: talloc_free(tmp_ctx); return ret; } static errno_t sec_value_to_json(const char *input, json_t **_root) { json_t *root = NULL; json_error_t error; int ok; root = json_loads(input, 0, &error); if (root == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Failed to parse JSON payload on line %d: %s\n", error.line, error.text); return ERR_JSON_DECODING; } ok = json_is_object(root); if (!ok) { DEBUG(SSSDBG_CRIT_FAILURE, "Json data is not an object.\n"); json_decref(root); return ERR_JSON_DECODING; } *_root = root; return EOK; } /* * ccache unmarshalling from JSON */ static errno_t json_element_to_krb5_data(TALLOC_CTX *mem_ctx, json_t *element, krb5_data *data) { const char *str_value; size_t str_len; /* FIXME - it might be cleaner to use stringn here, but the libjansson * version on RHEL-7 doesn't support that */ str_value = json_string_value(element); if (str_value == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "JSON element not a string\n"); return EINVAL; } str_len = strlen(str_value); data->data = talloc_strndup(mem_ctx, str_value, str_len); if (data->data == NULL) { return ENOMEM; } data->length = str_len; return EOK; } static errno_t json_array_to_krb5_data(TALLOC_CTX *mem_ctx, json_t *array, krb5_data **_data, size_t *_len) { errno_t ret; int ok; size_t len; size_t idx; json_t *element; krb5_data *data; ok = json_is_array(array); if (!ok) { DEBUG(SSSDBG_CRIT_FAILURE, "Json object is not an array.\n"); return ERR_JSON_DECODING; } len = json_array_size(array); if (len == 0) { *_data = NULL; *_len = 0; return EOK; } data = talloc_zero_array(mem_ctx, krb5_data, len); if (data == NULL) { return ENOMEM; } json_array_foreach(array, idx, element) { ret = json_element_to_krb5_data(data, element, &data[idx]); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot convert krb5 data element from JSON"); talloc_free(data); return ret; } } *_data = data; *_len = len; return EOK; } static errno_t json_to_princ(TALLOC_CTX *mem_ctx, json_t *js_princ, krb5_principal *_princ) { errno_t ret; json_t *components = NULL; int ok; krb5_principal princ = NULL; TALLOC_CTX *tmp_ctx = NULL; char *realm_str; size_t realm_size; json_error_t error; ok = json_is_object(js_princ); if (!ok) { DEBUG(SSSDBG_CRIT_FAILURE, "Json principal is not an object.\n"); ret = ERR_JSON_DECODING; goto done; } tmp_ctx = talloc_new(mem_ctx); if (tmp_ctx == NULL) { ret = ENOMEM; goto done; } princ = talloc_zero(tmp_ctx, struct krb5_principal_data); if (princ == NULL) { return ENOMEM; } princ->magic = KV5M_PRINCIPAL; /* FIXME - it might be cleaner to use the s% specifier here, but the libjansson * version on RHEL-7 doesn't support that */ ret = json_unpack_ex(js_princ, &error, JSON_STRICT, "{s:i, s:s, s:o}", "type", &princ->type, "realm", &realm_str, "components", &components); if (ret != 0) { DEBUG(SSSDBG_CRIT_FAILURE, "Failed to unpack JSON princ structure on line %d: %s\n", error.line, error.text); ret = EINVAL; goto done; } realm_size = strlen(realm_str); princ->realm.data = talloc_strndup(mem_ctx, realm_str, realm_size); if (princ->realm.data == NULL) { return ENOMEM; } princ->realm.length = realm_size; princ->realm.magic = 0; ret = json_array_to_krb5_data(princ, components, &princ->data, (size_t *) &princ->length); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot convert principal from JSON"); ret = EINVAL; goto done; } *_princ = talloc_steal(mem_ctx, princ); ret = EOK; done: talloc_free(tmp_ctx); return ret; } static errno_t json_elem_to_cred(TALLOC_CTX *mem_ctx, json_t *element, struct kcm_cred **_crd) { errno_t ret; char *uuid_str; json_error_t error; uuid_t uuid; struct sss_iobuf *cred_blob; const char *base64_cred_blob; struct kcm_cred *crd; uint8_t *outbuf; size_t outbuf_size; TALLOC_CTX *tmp_ctx = NULL; ret = json_unpack_ex(element, &error, JSON_STRICT, "{s:s, s:s}", "uuid", &uuid_str, "payload", &base64_cred_blob); if (ret != 0) { DEBUG(SSSDBG_CRIT_FAILURE, "Failed to unpack JSON cred structure on line %d: %s\n", error.line, error.text); return EINVAL; } uuid_parse(uuid_str, uuid); tmp_ctx = talloc_new(mem_ctx); if (tmp_ctx == NULL) { ret = ENOMEM; goto done; } outbuf = sss_base64_decode(tmp_ctx, base64_cred_blob, &outbuf_size); if (outbuf == NULL) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot decode cred blob\n"); ret = EIO; goto done; } cred_blob = sss_iobuf_init_readonly(tmp_ctx, outbuf, outbuf_size); if (cred_blob == NULL) { ret = ENOMEM; goto done; } crd = kcm_cred_new(tmp_ctx, uuid, cred_blob); if (crd == NULL) { ret = ENOMEM; goto done; } ret = EOK; *_crd = talloc_steal(mem_ctx, crd); done: talloc_free(tmp_ctx); return ret; } static errno_t json_to_creds(struct kcm_ccache *cc, json_t *jcreds) { errno_t ret; int ok; size_t idx; json_t *value; struct kcm_cred *crd; ok = json_is_array(jcreds); if (!ok) { DEBUG(SSSDBG_CRIT_FAILURE, "Json creds object is not an array.\n"); return ERR_JSON_DECODING; } json_array_foreach(jcreds, idx, value) { ret = json_elem_to_cred(cc, value, &crd); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot convert JSON cred element [%d]: %s\n", ret, sss_strerror(ret)); return ret; } ret = kcm_cc_store_creds(cc, crd); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot store creds in ccache [%d]: %s\n", ret, sss_strerror(ret)); return ret; } } return EOK; } static errno_t sec_json_value_to_ccache(struct kcm_ccache *cc, json_t *root) { errno_t ret; json_t *princ = NULL; json_t *creds = NULL; json_error_t error; int version; ret = json_unpack_ex(root, &error, JSON_STRICT, "{s:i, s:i, s:o, s:o}", "version", &version, "kdc_offset", &cc->kdc_offset, "principal", &princ, "creds", &creds); if (ret != 0) { DEBUG(SSSDBG_CRIT_FAILURE, "Failed to unpack JSON creds structure on line %d: %s\n", error.line, error.text); return EINVAL; } if (version != KS_JSON_VERSION) { DEBUG(SSSDBG_CRIT_FAILURE, "Expected version %d, received version %d\n", KS_JSON_VERSION, version); return EINVAL; } ret = json_to_princ(cc, princ, &cc->client); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot store JSON to principal [%d]: %s\n", ret, sss_strerror(ret)); return ret; } ret = json_to_creds(cc, creds); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot store JSON to creds [%d]: %s\n", ret, sss_strerror(ret)); return EOK; } return EOK; } /* * sec_key is a concatenation of the ccache's UUID and name * sec_value is the JSON dump of the ccache contents */ errno_t sec_kv_to_ccache(TALLOC_CTX *mem_ctx, const char *sec_key, const char *sec_value, struct cli_creds *client, struct kcm_ccache **_cc) { errno_t ret; json_t *root = NULL; struct kcm_ccache *cc = NULL; TALLOC_CTX *tmp_ctx = NULL; ret = sec_value_to_json(sec_value, &root); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannot store secret to JSN [%d]: %s\n", ret, sss_strerror(ret)); goto done; } tmp_ctx = talloc_new(mem_ctx); if (tmp_ctx == NULL) { ret = ENOMEM; goto done; } cc = talloc_zero(tmp_ctx, struct kcm_ccache); if (cc == NULL) { ret = ENOMEM; goto done; } /* We rely on sssd-secrets only searching the user's subtree so we * set the ownership to the client */ cc->owner.uid = cli_creds_get_uid(client); cc->owner.gid = cli_creds_get_gid(client); ret = sec_key_parse(cc, sec_key, &cc->name, cc->uuid); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannt parse secret key [%d]: %s\n", ret, sss_strerror(ret)); goto done; } ret = sec_json_value_to_ccache(cc, root); if (ret != EOK) { DEBUG(SSSDBG_CRIT_FAILURE, "Cannt parse secret value [%d]: %s\n", ret, sss_strerror(ret)); goto done; } ret = EOK; *_cc = talloc_steal(mem_ctx, cc); done: talloc_free(tmp_ctx); json_decref(root); return ret; }