diff options
Diffstat (limited to 'src/libdnssec/keystore')
-rw-r--r-- | src/libdnssec/keystore/internal.h | 52 | ||||
-rw-r--r-- | src/libdnssec/keystore/keystore.c | 183 | ||||
-rw-r--r-- | src/libdnssec/keystore/pkcs11.c | 440 | ||||
-rw-r--r-- | src/libdnssec/keystore/pkcs8.c | 211 | ||||
-rw-r--r-- | src/libdnssec/keystore/pkcs8_dir.c | 379 |
5 files changed, 1265 insertions, 0 deletions
diff --git a/src/libdnssec/keystore/internal.h b/src/libdnssec/keystore/internal.h new file mode 100644 index 0000000..a87dd03 --- /dev/null +++ b/src/libdnssec/keystore/internal.h @@ -0,0 +1,52 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <http://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include <gnutls/gnutls.h> +#include <gnutls/abstract.h> + +#include "libdnssec/binary.h" +#include "libdnssec/key.h" +#include "libdnssec/keystore.h" +#include "libdnssec/list.h" + +typedef struct keystore_functions { + // construction of internal context + int (*ctx_new)(void **ctx_ptr, void *data); + int (*ctx_free)(void *ctx); + // keystore init/open/close + int (*init)(void *ctx, const char *config); + int (*open)(void *ctx, const char *config); + int (*close)(void *ctx); + // keystore access + int (*list_keys)(void *ctx, dnssec_list_t **list); + int (*generate_key)(void *ctx, gnutls_pk_algorithm_t algorithm, + unsigned bits, char **id_ptr); + int (*import_key)(void *ctx, const dnssec_binary_t *pem, char **id_ptr); + int (*remove_key)(void *ctx, const char *id); + // private key access + int (*get_private)(void *ctx, const char *id, gnutls_privkey_t *key_ptr); +} keystore_functions_t; + +struct dnssec_keystore { + const keystore_functions_t *functions; + void *ctx; +}; + +int keystore_create(dnssec_keystore_t **store_ptr, + const keystore_functions_t *functions, + void *ctx_custom_data); diff --git a/src/libdnssec/keystore/keystore.c b/src/libdnssec/keystore/keystore.c new file mode 100644 index 0000000..203441a --- /dev/null +++ b/src/libdnssec/keystore/keystore.c @@ -0,0 +1,183 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <assert.h> +#include <stdlib.h> + +#include "libdnssec/error.h" +#include "libdnssec/key.h" +#include "libdnssec/key/algorithm.h" +#include "libdnssec/key/dnskey.h" +#include "libdnssec/key/internal.h" +#include "libdnssec/key/privkey.h" +#include "libdnssec/keyid.h" +#include "libdnssec/keystore.h" +#include "libdnssec/keystore/internal.h" +#include "libdnssec/shared/shared.h" + +/* -- internal API --------------------------------------------------------- */ + +int keystore_create(dnssec_keystore_t **store_ptr, + const keystore_functions_t *functions, + void *ctx_custom_data) +{ + assert(store_ptr); + assert(functions); + + dnssec_keystore_t *store = calloc(1, sizeof(*store)); + if (!store) { + return DNSSEC_ENOMEM; + } + + store->functions = functions; + + int result = functions->ctx_new(&store->ctx, ctx_custom_data); + if (result != DNSSEC_EOK) { + free(store); + return DNSSEC_ENOMEM; + } + + *store_ptr = store; + return DNSSEC_EOK; +} + +/* -- public API ----------------------------------------------------------- */ + +_public_ +int dnssec_keystore_deinit(dnssec_keystore_t *store) +{ + if (!store) { + return DNSSEC_EINVAL; + } + + dnssec_keystore_close(store); + store->functions->ctx_free(store->ctx); + + free(store); + + return DNSSEC_EOK; +} + +_public_ +int dnssec_keystore_init(dnssec_keystore_t *store, const char *config) +{ + if (!store) { + return DNSSEC_EINVAL; + } + + return store->functions->init(store->ctx, config); +} + +_public_ +int dnssec_keystore_open(dnssec_keystore_t *store, const char *config) +{ + if (!store) { + return DNSSEC_EINVAL; + } + + return store->functions->open(store->ctx, config); +} + +_public_ +int dnssec_keystore_close(dnssec_keystore_t *store) +{ + if (!store) { + return DNSSEC_EINVAL; + } + + return store->functions->close(store->ctx); +} + +_public_ +int dnssec_keystore_list_keys(dnssec_keystore_t *store, dnssec_list_t **list) +{ + if (!store || !list) { + return DNSSEC_EINVAL; + } + + return store->functions->list_keys(store->ctx, list); +} + +_public_ +int dnssec_keystore_generate_key(dnssec_keystore_t *store, + dnssec_key_algorithm_t _algorithm, + unsigned bits, char **id_ptr) +{ + if (!store || !_algorithm || !id_ptr) { + return DNSSEC_EINVAL; + } + + // prepare parameters + + gnutls_pk_algorithm_t algorithm = algorithm_to_gnutls(_algorithm); + if (algorithm == GNUTLS_PK_UNKNOWN) { + return DNSSEC_INVALID_KEY_ALGORITHM; + } + + if (!dnssec_algorithm_key_size_check(_algorithm, bits)) { + return DNSSEC_INVALID_KEY_SIZE; + } + + return store->functions->generate_key(store->ctx, algorithm, bits, id_ptr); +} + +_public_ +int dnssec_keystore_import(dnssec_keystore_t *store, const dnssec_binary_t *pem, + char **id_ptr) +{ + if (!store || !pem || !id_ptr) { + return DNSSEC_EINVAL; + } + + return store->functions->import_key(store->ctx, pem, id_ptr); +} + +_public_ +int dnssec_keystore_remove_key(dnssec_keystore_t *store, const char *key_id) +{ + if (!store || !key_id) { + return DNSSEC_EINVAL; + } + + return store->functions->remove_key(store->ctx, key_id); +} + +_public_ +int dnssec_key_import_keystore(dnssec_key_t *key, dnssec_keystore_t *store, + const char *id) +{ + if (!key || !store || !id || dnssec_key_get_algorithm(key) == 0) { + return DNSSEC_EINVAL; + } + + if (key->private_key) { + return DNSSEC_KEY_ALREADY_PRESENT; + } + + gnutls_privkey_t privkey = NULL; + int r = store->functions->get_private(store->ctx, id, &privkey); + if (r != DNSSEC_EOK) { + return r; + } + + r = key_set_private_key(key, privkey); + if (r != DNSSEC_EOK) { + gnutls_privkey_deinit(privkey); + return r; + } + + return DNSSEC_EOK; +} diff --git a/src/libdnssec/keystore/pkcs11.c b/src/libdnssec/keystore/pkcs11.c new file mode 100644 index 0000000..dbe1a37 --- /dev/null +++ b/src/libdnssec/keystore/pkcs11.c @@ -0,0 +1,440 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <gnutls/gnutls.h> +#include <pthread.h> + +#include "libdnssec/error.h" +#include "libdnssec/shared/hex.h" +#include "libdnssec/keyid.h" +#include "libdnssec/shared/keyid_gnutls.h" +#include "libdnssec/keystore.h" +#include "libdnssec/keystore/internal.h" +#include "libdnssec/p11/p11.h" +#include "libdnssec/shared/pem.h" +#include "libdnssec/shared/shared.h" + +#ifdef ENABLE_PKCS11 + +struct pkcs11_ctx { + char *url; +}; + +typedef struct pkcs11_ctx pkcs11_ctx_t; + +/*! + * Flags used when generating/import key into the token. + */ +static const int TOKEN_ADD_FLAGS = GNUTLS_PKCS11_OBJ_FLAG_MARK_SENSITIVE + | GNUTLS_PKCS11_OBJ_FLAG_MARK_PRIVATE; + +static int key_url(const char *token_uri, const char *key_id, char **url_ptr) +{ + assert(token_uri); + assert(key_id); + assert(url_ptr); + + if (!dnssec_keyid_is_valid(key_id)) { + return DNSSEC_INVALID_KEY_ID; + } + + size_t token_len = strlen(token_uri); + size_t id_len = strlen(key_id); + + // url: <token-url>;id=%aa%bb%cc.. + + size_t len = token_len + 4 + (id_len / 2 * 3); + char *url = malloc(len + 1); + if (!url) { + return DNSSEC_ENOMEM; + } + + size_t prefix = snprintf(url, len, "%s;id=", token_uri); + if (prefix != token_len + 4) { + free(url); + return DNSSEC_ENOMEM; + } + + assert(id_len % 2 == 0); + char *pos = url + prefix; + for (int i = 0; i < id_len; i += 2, pos += 3) { + pos[0] = '%'; + pos[1] = key_id[i]; + pos[2] = key_id[i+1]; + } + assert(url + len == pos); + url[len] = '\0'; + + *url_ptr = url; + return DNSSEC_EOK; +} + +/** + * Parse configuration string. Accepted format: "<pkcs11-url> <module-path>" + */ +static int parse_config(const char *config, char **uri_ptr, char **module_ptr) +{ + const char *space = strchr(config, ' '); + if (!space) { + return DNSSEC_KEYSTORE_INVALID_CONFIG; + } + + char *url = strndup(config, space - config); + char *module = strdup(space + 1); + + if (!url || !module) { + free(url); + free(module); + return DNSSEC_ENOMEM; + } + + *uri_ptr = url; + *module_ptr = module; + + return DNSSEC_EOK; +} + +/*! + * Load PKCS #11 module and check if the token is available. + */ +static int safe_open(const char *config, char **url_ptr) +{ + char *url = NULL; + char *module = NULL; + + int r = parse_config(config, &url, &module); + if (r != DNSSEC_EOK) { + return r; + } + + r = p11_load_module(module); + free(module); + if (r != GNUTLS_E_SUCCESS) { + free(url); + return DNSSEC_P11_FAILED_TO_LOAD_MODULE; + } + + unsigned int flags = 0; + r = gnutls_pkcs11_token_get_flags(url, &flags); + if (r != GNUTLS_E_SUCCESS) { + free(url); + return DNSSEC_P11_TOKEN_NOT_AVAILABLE; + } + + *url_ptr = url; + + return DNSSEC_EOK; +} + +/* -- internal API --------------------------------------------------------- */ + +static void disable_pkcs11_callbacks(void) +{ + gnutls_pkcs11_set_pin_function(NULL, NULL); + gnutls_pkcs11_set_token_function(NULL, NULL); +} + +static int pkcs11_ctx_new(void **ctx_ptr, _unused_ void *data) +{ + static pthread_once_t once = PTHREAD_ONCE_INIT; + pthread_once(&once, disable_pkcs11_callbacks); + + pkcs11_ctx_t *ctx = calloc(1, sizeof(*ctx)); + if (!ctx) { + return DNSSEC_ENOMEM; + } + + *ctx_ptr = ctx; + + return DNSSEC_EOK; +} + +static int pkcs11_ctx_free(void *ctx) +{ + if (ctx) { + free(ctx); + } + + return DNSSEC_EOK; +} + +static int pkcs11_init(void *ctx, const char *config) +{ + /* + * Current keystore initialization is idempotent. We don't really + * initialize the token because don't want to wipe the data. We just + * check that the token is available the same way pkcs11_open() does. + */ + + _cleanup_free_ char *url = NULL; + + return safe_open(config, &url); +} + +static int pkcs11_open(void *_ctx, const char *config) +{ + pkcs11_ctx_t *ctx = _ctx; + + return safe_open(config, &ctx->url); +} + +static int pkcs11_close(void *_ctx) +{ + pkcs11_ctx_t *ctx = _ctx; + + free(ctx->url); + clear_struct(ctx); + + return DNSSEC_EOK; +} + +static char *get_object_id(gnutls_pkcs11_obj_t object) +{ + assert(object); + + uint8_t buffer[DNSSEC_KEYID_BINARY_SIZE] = { 0 }; + size_t size = sizeof(buffer); + + int r = gnutls_pkcs11_obj_get_info(object, GNUTLS_PKCS11_OBJ_ID, buffer, &size); + if (r != GNUTLS_E_SUCCESS || size != sizeof(buffer)) { + return NULL; + } + + const dnssec_binary_t bin = { .data = buffer, .size = sizeof(buffer) }; + char *id = NULL; + if (bin_to_hex(&bin, &id) != DNSSEC_EOK) { + return NULL; + } + + assert(strlen(id) == DNSSEC_KEYID_SIZE); + + return id; +} + +static int pkcs11_list_keys(void *_ctx, dnssec_list_t **list) +{ + pkcs11_ctx_t *ctx = _ctx; + + dnssec_list_t *ids = dnssec_list_new(); + if (!ids) { + return DNSSEC_ENOMEM; + } + + gnutls_pkcs11_obj_t *objects = NULL; + unsigned count = 0; + + int flags = GNUTLS_PKCS11_OBJ_FLAG_PRIVKEY | + GNUTLS_PKCS11_OBJ_FLAG_LOGIN; + + int r = gnutls_pkcs11_obj_list_import_url4(&objects, &count, ctx->url, flags); + if (r != GNUTLS_E_SUCCESS) { + dnssec_list_free(ids); + return DNSSEC_P11_TOKEN_NOT_AVAILABLE; + } + + for (unsigned i = 0; i < count; i++) { + gnutls_pkcs11_obj_t object = objects[i]; + char *id = get_object_id(object); + dnssec_list_append(ids, id); + gnutls_pkcs11_obj_deinit(object); + } + + gnutls_free(objects); + + *list = ids; + return DNSSEC_EOK; +} + +static int pkcs11_generate_key(void *_ctx, gnutls_pk_algorithm_t algorithm, + unsigned bits, char **id_ptr) +{ + pkcs11_ctx_t *ctx = _ctx; + + uint8_t buf[20] = { 0 }; + gnutls_rnd(GNUTLS_RND_RANDOM, buf, sizeof(buf)); + dnssec_binary_t cka_id = { .data = buf, .size = sizeof(buf) }; + + int flags = TOKEN_ADD_FLAGS | GNUTLS_PKCS11_OBJ_FLAG_LOGIN; + gnutls_datum_t gt_cka_id = binary_to_datum(&cka_id); + int r = gnutls_pkcs11_privkey_generate3(ctx->url, algorithm, bits, NULL, >_cka_id, 0, NULL, 0, flags); + if (r != GNUTLS_E_SUCCESS) { + return DNSSEC_KEY_GENERATE_ERROR; + } + + char *id = NULL; + r = bin_to_hex(&cka_id, &id); + if (r != DNSSEC_EOK) { + return DNSSEC_ENOMEM; + } + + *id_ptr = id; + + return DNSSEC_EOK; +} + +static int import_pem(const dnssec_binary_t *pem, + gnutls_x509_privkey_t *key_ptr, + gnutls_pubkey_t *pubkey_ptr) +{ + gnutls_x509_privkey_t x509_key = NULL; + gnutls_privkey_t key = NULL; + gnutls_pubkey_t pubkey = NULL; + + int r = pem_x509(pem, &x509_key); + if (r != DNSSEC_EOK) { + goto fail; + } + + if (gnutls_privkey_init(&key) != GNUTLS_E_SUCCESS || + gnutls_pubkey_init(&pubkey) != GNUTLS_E_SUCCESS + ) { + r = DNSSEC_ENOMEM; + goto fail; + } + + if (gnutls_privkey_import_x509(key, x509_key, 0) != GNUTLS_E_SUCCESS || + gnutls_pubkey_import_privkey(pubkey, key, 0, 0) != GNUTLS_E_SUCCESS + ) { + r = DNSSEC_KEY_IMPORT_ERROR; + goto fail; + } + +fail: + gnutls_privkey_deinit(key); + + if (r == DNSSEC_EOK) { + *key_ptr = x509_key; + *pubkey_ptr = pubkey; + } else { + gnutls_x509_privkey_deinit(x509_key); + gnutls_pubkey_deinit(pubkey); + } + + return r; +} + +static int pkcs11_import_key(void *_ctx, const dnssec_binary_t *pem, char **id_ptr) +{ + pkcs11_ctx_t *ctx = _ctx; + + _cleanup_x509_privkey_ gnutls_x509_privkey_t key = NULL; + _cleanup_pubkey_ gnutls_pubkey_t pubkey = NULL; + + int r = import_pem(pem, &key, &pubkey); + if (r != DNSSEC_EOK) { + return r; + } + + _cleanup_binary_ dnssec_binary_t id = { 0 }; + r = keyid_x509(key, &id); + if (r != DNSSEC_EOK) { + return r; + } + + int flags = TOKEN_ADD_FLAGS | GNUTLS_PKCS11_OBJ_FLAG_LOGIN; + gnutls_datum_t gid = binary_to_datum(&id); + + r = gnutls_pkcs11_copy_x509_privkey2(ctx->url, key, NULL, &gid, 0, flags); + if (r != GNUTLS_E_SUCCESS) { + return DNSSEC_KEY_IMPORT_ERROR; + } + + r = gnutls_pkcs11_copy_pubkey(ctx->url, pubkey, NULL, &gid, 0, flags); + if (r != GNUTLS_E_SUCCESS) { + // note, we result with dangling private key in the token + return DNSSEC_KEY_IMPORT_ERROR; + } + + return bin_to_hex(&id, id_ptr); +} + +static int pkcs11_remove_key(void *_ctx, const char *id) +{ + pkcs11_ctx_t *ctx = _ctx; + + _cleanup_free_ char *url = NULL; + int r = key_url(ctx->url, id, &url); + if (r != DNSSEC_EOK) { + return r; + } + + r = gnutls_pkcs11_delete_url(url, GNUTLS_PKCS11_OBJ_FLAG_LOGIN); + if (r < 0) { + return DNSSEC_ERROR; + } else if (r == 0) { + return DNSSEC_ENOENT; + } + + return DNSSEC_EOK; +} + +static int pkcs11_get_private(void *_ctx, const char *id, gnutls_privkey_t *key_ptr) +{ + pkcs11_ctx_t *ctx = _ctx; + + _cleanup_free_ char *url = NULL; + int r = key_url(ctx->url, id, &url); + if (r != DNSSEC_EOK) { + return r; + } + + gnutls_privkey_t key = NULL; + r = gnutls_privkey_init(&key); + if (r != GNUTLS_E_SUCCESS) { + return DNSSEC_ENOMEM; + } + + r = gnutls_privkey_import_pkcs11_url(key, url); + if (r != GNUTLS_E_SUCCESS) { + gnutls_privkey_deinit(key); + return DNSSEC_NOT_FOUND; + } + + *key_ptr = key; + + return DNSSEC_EOK; +} + +/* -- public API ----------------------------------------------------------- */ + +_public_ +int dnssec_keystore_init_pkcs11(dnssec_keystore_t **store_ptr) +{ + static const keystore_functions_t IMPLEMENTATION = { + .ctx_new = pkcs11_ctx_new, + .ctx_free = pkcs11_ctx_free, + .init = pkcs11_init, + .open = pkcs11_open, + .close = pkcs11_close, + .list_keys = pkcs11_list_keys, + .generate_key = pkcs11_generate_key, + .import_key = pkcs11_import_key, + .remove_key = pkcs11_remove_key, + .get_private = pkcs11_get_private, + }; + + return keystore_create(store_ptr, &IMPLEMENTATION, NULL); +} + +#else // !ENABLE_PKCS11 + +_public_ +int dnssec_keystore_init_pkcs11(dnssec_keystore_t **store_ptr) +{ + return DNSSEC_NOT_IMPLEMENTED_ERROR; +} + +#endif diff --git a/src/libdnssec/keystore/pkcs8.c b/src/libdnssec/keystore/pkcs8.c new file mode 100644 index 0000000..f05eda0 --- /dev/null +++ b/src/libdnssec/keystore/pkcs8.c @@ -0,0 +1,211 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <assert.h> + +#include "libdnssec/error.h" +#include "libdnssec/key/algorithm.h" +#include "libdnssec/keyid.h" +#include "libdnssec/keystore.h" +#include "libdnssec/keystore/internal.h" +#include "libdnssec/shared/pem.h" +#include "libdnssec/shared/shared.h" + +/*! + * PKCS #8 key store context. + */ +typedef struct pkcs8_ctx { + /*! Storage implementation callbacks. */ + const dnssec_keystore_pkcs8_functions_t *functions; + /*! Implementation specific context data. */ + void *data; +} pkcs8_ctx_t; + +/* -- internal API --------------------------------------------------------- */ + +static int pkcs8_ctx_new(void **ctx_ptr, void *_functions) +{ + assert(ctx_ptr); + assert(_functions); + + pkcs8_ctx_t *ctx = calloc(1, sizeof(*ctx)); + if (!ctx) { + return DNSSEC_ENOMEM; + } + + ctx->functions = _functions; + + int r = ctx->functions->handle_new(&ctx->data); + if (r != DNSSEC_EOK) { + free(ctx); + return r; + } + + *ctx_ptr = ctx; + + return DNSSEC_EOK; +} + +static int pkcs8_ctx_free(void *_ctx) +{ + pkcs8_ctx_t *ctx = _ctx; + int r = ctx->functions->handle_free(ctx->data); + + free(ctx); + + return r; +} + +static int pkcs8_init(void *_ctx, const char *config) +{ + pkcs8_ctx_t *ctx = _ctx; + return ctx->functions->init(ctx->data, config); +} + +static int pkcs8_open(void *_ctx, const char *config) +{ + pkcs8_ctx_t *ctx = _ctx; + return ctx->functions->open(ctx->data, config); +} + +static int pkcs8_close(void *_ctx) +{ + pkcs8_ctx_t *ctx = _ctx; + return ctx->functions->close(ctx->data); +} + +static int pkcs8_list_keys(void *_ctx, dnssec_list_t **list) +{ + pkcs8_ctx_t *ctx = _ctx; + return ctx->functions->list(ctx->data, list); +} + +static int pkcs8_generate_key(void *_ctx, gnutls_pk_algorithm_t algorithm, + unsigned bits, char **id_ptr) +{ + assert(_ctx); + assert(id_ptr); + + pkcs8_ctx_t *ctx = _ctx; + + // generate key + + char *new_id = NULL; + _cleanup_binary_ dnssec_binary_t data = { 0 }; + int r = pem_generate(algorithm, bits, &data, &new_id); + if (r != DNSSEC_EOK) { + return r; + } + + // save key + + r = ctx->functions->write(ctx->data, new_id, &data); + if (r != DNSSEC_EOK) { + return r; + } + + // finish + + *id_ptr = new_id; + + return DNSSEC_EOK; +} + +static int pkcs8_import_key(void *_ctx, const dnssec_binary_t *pem, char **id_ptr) +{ + pkcs8_ctx_t *ctx = _ctx; + + // retrieve key ID + + char *id = NULL; + int r = pem_keyid(pem, &id); + if (r != DNSSEC_EOK) { + return r; + } + + // save the key + + r = ctx->functions->write(ctx->data, id, pem); + if (r != DNSSEC_EOK) { + free(id); + return r; + } + + *id_ptr = id; + + return DNSSEC_EOK; +} + +static int pkcs8_remove_key(void *_ctx, const char *id) +{ + pkcs8_ctx_t *ctx = _ctx; + return ctx->functions->remove(ctx->data, id); +} + +static int pkcs8_get_private(void *_ctx, const char *id, gnutls_privkey_t *key_ptr) +{ + assert(_ctx); + assert(id); + assert(key_ptr); + + pkcs8_ctx_t *ctx = _ctx; + + // load private key data + + _cleanup_binary_ dnssec_binary_t pem = { 0 }; + int r = ctx->functions->read(ctx->data, id, &pem); + if (r != DNSSEC_EOK) { + return r; + } + + // construct the key + + gnutls_privkey_t key = NULL; + r = pem_privkey(&pem, &key); + if (r != DNSSEC_EOK) { + return r; + } + + *key_ptr = key; + + return DNSSEC_EOK; +} + +static const keystore_functions_t PKCS8_FUNCTIONS = { + .ctx_new = pkcs8_ctx_new, + .ctx_free = pkcs8_ctx_free, + .init = pkcs8_init, + .open = pkcs8_open, + .close = pkcs8_close, + .list_keys = pkcs8_list_keys, + .generate_key = pkcs8_generate_key, + .import_key = pkcs8_import_key, + .remove_key = pkcs8_remove_key, + .get_private = pkcs8_get_private, +}; + +/* -- public API ----------------------------------------------------------- */ + +_public_ +int dnssec_keystore_init_pkcs8_custom(dnssec_keystore_t **store_ptr, + const dnssec_keystore_pkcs8_functions_t *store_functions) +{ + if (!store_ptr || !store_functions) { + return DNSSEC_EINVAL; + } + + return keystore_create(store_ptr, &PKCS8_FUNCTIONS, (void *)store_functions); +} diff --git a/src/libdnssec/keystore/pkcs8_dir.c b/src/libdnssec/keystore/pkcs8_dir.c new file mode 100644 index 0000000..bc67ad5 --- /dev/null +++ b/src/libdnssec/keystore/pkcs8_dir.c @@ -0,0 +1,379 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <http://www.gnu.org/licenses/>. +*/ + +#include <assert.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include "libdnssec/binary.h" +#include "libdnssec/error.h" +#include "libdnssec/shared/fs.h" +#include "libdnssec/key.h" +#include "libdnssec/keystore.h" +#include "libdnssec/keystore/internal.h" +#include "libdnssec/shared/shared.h" + +#define DIR_INIT_MODE 0750 + +/*! + * Context for PKCS #8 key directory. + */ +typedef struct pkcs8_dir_handle { + char *dir_name; +} pkcs8_dir_handle_t; + +/* -- internal functions --------------------------------------------------- */ + +/*! + * Get path to a private key in PKCS #8 PEM format. + */ +static char *key_path(const char *dir, const char *id) +{ + char *strp = NULL; + + int ret = asprintf(&strp, "%s/%s.pem", dir, id); + if (ret < 0) { + return NULL; + } + return strp; +} + +/*! + * Get size of the file and reset the position to the beginning of the file. + */ +static int file_size(int fd, size_t *size) +{ + off_t offset = lseek(fd, 0, SEEK_END); + if (offset == -1) { + return dnssec_errno_to_error(errno); + } + + if (lseek(fd, 0, SEEK_SET) == -1) { + return dnssec_errno_to_error(errno); + } + + assert(offset >= 0); + *size = offset; + + return DNSSEC_EOK; +} + +/*! + * Open a key file and get the descriptor. + */ +static int key_open(const char *dir_name, const char *id, int flags, + mode_t mode, int *fd_ptr) +{ + assert(dir_name); + assert(id); + assert(fd_ptr); + + _cleanup_free_ char *filename = key_path(dir_name, id); + if (!filename) { + return DNSSEC_ENOMEM; + } + + int fd = open(filename, flags, mode); + if (fd == -1) { + return dnssec_errno_to_error(errno); + } + + *fd_ptr = fd; + + return DNSSEC_EOK; +} + +static int key_open_read(const char *dir_name, const char *id, int *fd_ptr) +{ + return key_open(dir_name, id, O_RDONLY, 0, fd_ptr); +} + +static int key_open_write(const char *dir_name, const char *id, int *fd_ptr) +{ + return key_open(dir_name, id, O_WRONLY|O_CREAT|O_EXCL, + S_IRUSR|S_IWUSR|S_IRGRP, fd_ptr); +} + +/*! + * Strip '.pem' suffix, basename has to be valid key ID. + */ +static char *filename_to_keyid(const char *filename) +{ + assert(filename); + + size_t len = strlen(filename); + + const char ext[] = ".pem"; + const size_t ext_len = sizeof(ext) - 1; + + if (len < ext_len || strcmp(filename + len - ext_len, ext) != 0) { + return NULL; + } + + return strndup(filename, len - ext_len); +} + +/* -- PKCS #8 dir access API ----------------------------------------------- */ + +static int pkcs8_dir_handle_new(void **handle_ptr) +{ + if (!handle_ptr) { + return DNSSEC_EINVAL; + } + + pkcs8_dir_handle_t *handle = calloc(1, sizeof(*handle)); + if (!handle) { + return DNSSEC_ENOMEM; + } + + *handle_ptr = handle; + + return DNSSEC_EOK; +} + +static int pkcs8_dir_handle_free(void *handle) +{ + free(handle); + + return DNSSEC_EOK; +} + +static int pkcs8_dir_init(void *handle, const char *path) +{ + if (!handle || !path) { + return DNSSEC_EINVAL; + } + + return fs_mkdir(path, DIR_INIT_MODE, true); +} + +static int pkcs8_dir_open(void *_handle, const char *config) +{ + if (!_handle || !config) { + return DNSSEC_EINVAL; + } + + pkcs8_dir_handle_t *handle = _handle; + + char *path = realpath(config, NULL); + if (!path) { + return DNSSEC_NOT_FOUND; + } + + handle->dir_name = path; + + return DNSSEC_EOK; +} + +static int pkcs8_dir_close(void *_handle) +{ + if (!_handle) { + return DNSSEC_EINVAL; + } + + pkcs8_dir_handle_t *handle = _handle; + + free(handle->dir_name); + memset(handle, 0, sizeof(*handle)); + + return DNSSEC_EOK; +} + +static int pkcs8_dir_read(void *_handle, const char *id, dnssec_binary_t *pem) +{ + if (!_handle || !id || !pem) { + return DNSSEC_EINVAL; + } + + pkcs8_dir_handle_t *handle = _handle; + + // open file and get it's size + + _cleanup_close_ int file = 0; + int result = key_open_read(handle->dir_name, id, &file); + if (result != DNSSEC_EOK) { + return result; + } + + size_t size = 0; + result = file_size(file, &size); + if (result != DNSSEC_EOK) { + return result; + } + + if (size == 0) { + return DNSSEC_MALFORMED_DATA; + } + + // read the stored data + + dnssec_binary_t read_pem = { 0 }; + result = dnssec_binary_alloc(&read_pem, size); + if (result != DNSSEC_EOK) { + return result; + } + + ssize_t read_count = read(file, read_pem.data, read_pem.size); + if (read_count == -1) { + dnssec_binary_free(&read_pem); + return dnssec_errno_to_error(errno); + } + + assert(read_count == read_pem.size); + *pem = read_pem; + + return DNSSEC_EOK; +} + +static bool key_is_duplicate(int open_error, pkcs8_dir_handle_t *handle, + const char *id, const dnssec_binary_t *pem) +{ + assert(handle); + assert(id); + assert(pem); + + if (open_error != dnssec_errno_to_error(EEXIST)) { + return false; + } + + _cleanup_binary_ dnssec_binary_t old = { 0 }; + int r = pkcs8_dir_read(handle, id, &old); + if (r != DNSSEC_EOK) { + return false; + } + + return dnssec_binary_cmp(&old, pem) == 0; +} + +static int pkcs8_dir_write(void *_handle, const char *id, const dnssec_binary_t *pem) +{ + if (!_handle || !id || !pem) { + return DNSSEC_EINVAL; + } + + if (pem->size == 0 || pem->data == NULL) { + return DNSSEC_MALFORMED_DATA; + } + + pkcs8_dir_handle_t *handle = _handle; + + // create the file + + _cleanup_close_ int file = 0; + int result = key_open_write(handle->dir_name, id, &file); + if (result != DNSSEC_EOK) { + if (key_is_duplicate(result, handle, id, pem)) { + return DNSSEC_EOK; + } + return result; + } + + // write the data + + ssize_t wrote_count = write(file, pem->data, pem->size); + if (wrote_count == -1) { + return dnssec_errno_to_error(errno); + } + + assert(wrote_count == pem->size); + + return DNSSEC_EOK; +} + +static int pkcs8_dir_list(void *_handle, dnssec_list_t **list_ptr) +{ + if (!_handle || !list_ptr) { + return DNSSEC_EINVAL; + } + + pkcs8_dir_handle_t *handle = _handle; + + _cleanup_closedir_ DIR *dir = opendir(handle->dir_name); + if (!dir) { + return DNSSEC_NOT_FOUND; + } + + dnssec_list_t *list = dnssec_list_new(); + if (!list) { + return DNSSEC_ENOMEM; + } + + errno = 0; + struct dirent *result; + while ((result = readdir(dir)) != NULL) { + char *keyid = filename_to_keyid(result->d_name); + if (keyid) { + dnssec_list_append(list, keyid); + } + } + + if (errno != 0) { + dnssec_list_free_full(list, NULL, NULL); + return dnssec_errno_to_error(errno); + } + + *list_ptr = list; + + return DNSSEC_EOK; +} + +static int pkcs8_dir_remove(void *_handle, const char *id) +{ + if (!_handle || !id) { + return DNSSEC_EINVAL; + } + + pkcs8_dir_handle_t *handle = _handle; + + _cleanup_free_ char *filename = key_path(handle->dir_name, id); + if (!filename) { + return DNSSEC_ENOMEM; + } + + if (unlink(filename) == -1) { + return dnssec_errno_to_error(errno); + } + + return DNSSEC_EOK; +} + +const dnssec_keystore_pkcs8_functions_t PKCS8_DIR_FUNCTIONS = { + .handle_new = pkcs8_dir_handle_new, + .handle_free = pkcs8_dir_handle_free, + .init = pkcs8_dir_init, + .open = pkcs8_dir_open, + .close = pkcs8_dir_close, + .read = pkcs8_dir_read, + .write = pkcs8_dir_write, + .list = pkcs8_dir_list, + .remove = pkcs8_dir_remove, +}; + +/* -- public API ----------------------------------------------------------- */ + +_public_ +int dnssec_keystore_init_pkcs8_dir(dnssec_keystore_t **store_ptr) +{ + if (!store_ptr) { + return DNSSEC_EINVAL; + } + + return dnssec_keystore_init_pkcs8_custom(store_ptr, &PKCS8_DIR_FUNCTIONS); +} |