/* * Copyright (C) 2018 ScyllaDB * */ /* * SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0 */ #ifdef HAVE_KMIP #include #include #include #include #include #include #include #include #include #include #include #include // workaround cryptsoft sdk issue: #define strcasestr kmip_strcasestr #include #include #undef strcasestr #include "kmip_host.hh" #include "encryption.hh" #include "encryption_exceptions.hh" #include "symmetric_key.hh" #include "utils/hash.hh" #include "utils/loading_cache.hh" #include "utils/UUID.hh" #include "utils/UUID_gen.hh" #include "utils/http.hh" #include "marshal_exception.hh" #include "db/config.hh" using namespace std::chrono_literals; static logger kmip_log("kmip"); static constexpr uint16_t kmip_port = 5696u; // default for command execution/failover retry. static constexpr int default_num_cmd_retry = 5; static constexpr int min_num_cmd_retry = 2; static constexpr auto base_backoff_time = 100ms; std::ostream& operator<<(std::ostream& os, KMIP* kmip) { auto* s = KMIP_dump_str(kmip, KMIP_DUMP_FORMAT_DEFAULT); os << s; free(s); return os; } static void kmip_logger(void *cb_arg, unsigned char *str, unsigned long len) { // kmipc likes to write a log of white space and newlines. Skip these. std::string_view v(reinterpret_cast(str), len); if (std::find_if(v.begin(), v.end(), [](char c) { return !::isspace(c); }) == v.end()) { return; } kmip_log.trace("kmipcmd: {}", v); } namespace encryption { bool operator==(const kmip_host::key_options& l, const kmip_host::key_options& r) { return std::tie(l.template_name, l.key_namespace) == std::tie(r.template_name, r.key_namespace); } class kmip_error_category : public std::error_category { public: constexpr kmip_error_category() noexcept : std::error_category{} {} const char * name() const noexcept { return "KMIP"; } std::string message(int error) const { return KMIP_error2string(error); } }; static const kmip_error_category kmip_errorc; class kmip_error : public std::system_error { public: kmip_error(int res) : system_error(res, kmip_errorc) {} kmip_error(int res, const std::string& msg) : system_error(res, kmip_errorc, msg) {} }; // Checks a gnutls return value. // < 0 -> error. static void kmip_chk(int res, KMIP_CMD * cmd = nullptr) { if (res != KMIP_ERROR_NONE) { int status=0, reason=0; char* message = nullptr; if (KMIP_CMD_get_result(cmd, &status, &reason, &message) == KMIP_ERROR_NONE) { auto* ctxt = cmd != nullptr ? KMIP_CMD_get_ctx(cmd) : "(unknown cmd)"; auto s = fmt::format("{}: status={}, reason={}, message={}", ctxt, KMIP_RESULT_STATUS_to_string(status, 0, nullptr), KMIP_RESULT_REASON_to_string(reason, 0, nullptr), message ? message : "" ); throw kmip_error(res, s); } throw kmip_error(res); } } class kmip_host::impl { public: struct kmip_key_info { key_info info; key_options options; bool operator==(const kmip_key_info& i) const { return info == i.info && options == i.options; } friend std::ostream& operator<<(std::ostream& os, const kmip_key_info& info) { return os << info.info << ":" << info.options; } }; struct kmip_key_info_hash { size_t operator()(const kmip_key_info& i) const { return utils::tuple_hash()( std::tie(i.info.alg, i.info.len, i.options.template_name, i.options.key_namespace)); } }; using key_and_id_type = std::tuple, id_type>; inline static constexpr std::chrono::milliseconds default_expiry = 30s; inline static constexpr std::chrono::milliseconds default_refresh = 100s; inline static constexpr uintptr_t max_hosts = 1<<8; inline static constexpr size_t def_max_pooled_connections_per_host = 8; impl(encryption_context& ctxt, const sstring& name, const host_options& options) : _ctxt(ctxt), _name(name), _options(options), _attr_cache( utils::loading_cache_config{ .max_size = std::numeric_limits::max(), .expiry = options.key_cache_expiry.value_or( default_expiry), .refresh = options.key_cache_refresh.value_or(default_refresh)}, kmip_log, std::bind(&impl::create_key, this, std::placeholders::_1)), _id_cache( utils::loading_cache_config{ .max_size = std::numeric_limits::max(), .expiry = options.key_cache_expiry.value_or( default_expiry), .refresh = options.key_cache_refresh.value_or(default_refresh), }, kmip_log, std::bind(&impl::find_key, this, std::placeholders::_1)), _max_retry(std::max(size_t(min_num_cmd_retry), options.max_command_retries.value_or(default_num_cmd_retry))) { if (_options.hosts.size() > max_hosts) { throw std::invalid_argument("Too many hosts"); } KMIP_CMD_set_default_logfile(nullptr, nullptr); // disable logfile KMIP_CMD_set_default_logger(kmip_logger, nullptr); // send logs to us instead } future<> connect(); future<> disconnect(); future, id_type>> get_or_create_key(const key_info&, const key_options& = {}); future> get_key_by_id(const id_type&, const std::optional& = {}); id_type kmip_id_to_id(const sstring&) const; sstring id_to_kmip_string(const id_type&) const; private: future create_key(const kmip_key_info&); future> find_key(const id_type&); future> find_matching_keys(const kmip_key_info&, std::optional max = {}); static shared_ptr ensure_compatible_key(shared_ptr, const key_info&); template class kmip_handle; class kmip_cmd; class kmip_data_list; class connection; std::tuple make_attributes(const kmip_key_info&, bool include_template = true) const; union userdata { void * ptr; const char* host; }; friend std::ostream& operator<<(std::ostream& os, const impl& me) { fmt::print(os, "{}", me._name); return os; } using con_ptr = ::shared_ptr; using opt_int = std::optional; template future do_cmd(kmip_cmd, Func &&); template future do_cmd(KMIP_CMD*, con_ptr, Func&, bool retain_connection_after_command = false); future get_connection(KMIP_CMD*); future get_connection(const sstring&); future<> clear_connections(const sstring& host); future<> release(KMIP_CMD*, con_ptr, bool retain_connection = false); size_t max_pooled_connections_per_host() const { return _options.max_pooled_connections_per_host.value_or(def_max_pooled_connections_per_host); } bool is_current_host(const sstring& host) { return host == _options.hosts.at(_index % _options.hosts.size()); } encryption_context& _ctxt; sstring _name; host_options _options; utils::loading_cache, kmip_key_info_hash> _attr_cache; utils::loading_cache, 2, utils::loading_cache_reload_enabled::yes, utils::simple_entry_size<::shared_ptr>> _id_cache; using connections = std::deque; using host_to_connections = std::unordered_map; host_to_connections _host_connections; // current default host. If a host fails, incremented and // we try another in the host ip list. size_t _index = 0; size_t _max_retry = default_num_cmd_retry; }; } template <> struct fmt::formatter : fmt::ostream_formatter {}; template <> struct fmt::formatter : fmt::ostream_formatter {}; namespace encryption { class kmip_host::impl::connection { public: connection(const sstring& host, host_options& options) : _host(host) , _options(options) {} ~connection() {} const sstring& host() const { return _host; } void attach(KMIP_CMD*); future<> connect(); future<> wait_for_io(); future<> close(); private: static int io_callback(KMIP*, void*, int, void*, unsigned int, unsigned int*); int send(void*, unsigned int, unsigned int*); int recv(void*, unsigned int, unsigned int*); friend std::ostream& operator<<(std::ostream& os, const connection& me) { return os << me._host; } sstring _host; host_options& _options; output_stream _output; input_stream _input; seastar::connected_socket _socket; std::optional> _in_buffer; std::optional> _pending; }; } template <> struct fmt::formatter : fmt::ostream_formatter {}; namespace encryption { future<> kmip_host::impl::connection::connect() { auto cred = ::make_shared(); auto f = make_ready_future(); kmip_log.debug("connecting {}", _host); if (!_options.priority_string.empty()) { cred->set_priority_string(_options.priority_string); } else { cred->set_priority_string(db::config::default_tls_priority); } if (!_options.certfile.empty()) { f = f.then([this, cred] { return cred->set_x509_key_file(_options.certfile, _options.keyfile, seastar::tls::x509_crt_format::PEM); }); } if (!_options.truststore.empty()) { f = f.then([this, cred] { return cred->set_x509_trust_file(_options.truststore, seastar::tls::x509_crt_format::PEM); }); } else { f = f.then([cred] { return cred->set_system_trust(); }); } return f.then([this, cred] { // TODO, find if we should do hostname verification // TODO: connect all failovers already? // Use the URL parser to handle ipv6 etc proper. // Turn host arg into a URL. auto info = utils::http::parse_simple_url("kmip://" + _host); auto name = info.host; auto port = info.port != 80 ? info.port : kmip_port; return seastar::net::dns::resolve_name(name).then([this, cred, port, name](seastar::net::inet_address addr) { kmip_log.debug("Try connect {}:{}", addr, port); // TODO: should we verify non-numeric hosts here? (opts.server_name) // Adding this might break existing users with half-baked certs. return seastar::tls::connect(cred, seastar::socket_address{addr, uint16_t(port)}).then([this](seastar::connected_socket s) { kmip_log.debug("Successfully connected {}", _host); // #998 Set keepalive to try avoiding connection going stale in between commands. s.set_keepalive_parameters(net::tcp_keepalive_params{60s, 60s, 10}); s.set_keepalive(true); _input = s.input(); _output = s.output(); }); }); }); } future<> kmip_host::impl::connection::wait_for_io() { kmip_log.trace("{}: Waiting...", *this); auto o = std::exchange(_pending, std::nullopt); return o ? std::move(*o) : make_ready_future(); } int kmip_host::impl::connection::send(void* data, unsigned int len, unsigned int*) { if (_pending) { kmip_log.trace("{}: operation pending...", *this); return KMIP_ERROR_RETRY; } kmip_log.trace("{}: Sending {} bytes", *this, len); auto f = _output.write(reinterpret_cast(data), len).then([this] { kmip_log.trace("{}: send done. flushing...", *this); return _output.flush(); }); // if the call failed already, we still want to // drop back to "wait_for_io()", because we cannot throw // exceptions through the kmipc code frames. if (!f.available() || f.failed()) { _pending.emplace(std::move(f)); } return KMIP_ERROR_NONE; } int kmip_host::impl::connection::recv(void* data, unsigned int len, unsigned int* outlen) { kmip_log.trace("{}: Waiting for data ({})", *this, len); for (;;) { if (_in_buffer) { auto n = std::min(unsigned(_in_buffer->size()), len); *outlen = n; kmip_log.trace("{}: returning {} ({}) bytes", *this, n, _in_buffer->size()); std::copy(_in_buffer->begin(), _in_buffer->begin() + n, reinterpret_cast(data)); _in_buffer->trim_front(n); if (_in_buffer->empty()) { _in_buffer = std::nullopt; } // #998 cryptsoft example returns error on EOF. if (n == 0) { return KMIP_ERROR_IO; } break; } if (_pending) { kmip_log.trace("{}: operation pending...", *this); return KMIP_ERROR_RETRY; } kmip_log.trace("{}: issue read", *this); auto f = _input.read().then([this](temporary_buffer buf) { kmip_log.trace("{}: got {} bytes", *this, buf.size()); _in_buffer = std::move(buf); }); // if the call failed already, we still want to // drop back to "wait_for_io()", because we cannot throw // exceptions through the kmipc code frames. if (!f.available() || f.failed()) { _pending.emplace(std::move(f)); } } return KMIP_ERROR_NONE; } int kmip_host::impl::connection::io_callback(KMIP *kmip, void *cb_arg, int op, void *data, unsigned int len, unsigned int *outlen) { auto* conn = reinterpret_cast(cb_arg); try { switch(op) { default: return KMIP_ERROR_NOT_SUPPORTED; case KMIP_IO_CMD_SEND: return conn->send(data, len, outlen); case KMIP_IO_CMD_RECV: return conn->recv(data, len, outlen); } } catch (...) { kmip_log.warn("Error in KMIP IO: {}", std::current_exception()); return KMIP_ERROR_IO; } } void kmip_host::impl::connection::attach(KMIP_CMD* cmd) { kmip_log.trace("{} Attach: {}", *this, reinterpret_cast(cmd)); if (cmd == nullptr) { return; } if (!_options.username.empty()) { kmip_chk( KMIP_CMD_set_credential_username(cmd, const_cast(_options.username.c_str()), const_cast(_options.password.c_str()))); } /* because we haven't passed in anything to the KMIP_CMD layer * that would provide it with the protocol version details we * have to separately indicate that here */ kmip_chk(KMIP_CMD_set_lib_protocol(cmd, KMIP_LIB_PROTOCOL_KMIP1)); /* handle all IO via the callback */ kmip_chk( KMIP_CMD_set_io_cb(cmd, &connection::io_callback, reinterpret_cast(this))); } future<> kmip_host::impl::connection::close() { return _output.close().finally([this] { return _input.close(); }); } template class kmip_host::impl::kmip_handle { public: kmip_handle(T * ptr) : _ptr(ptr, FreeFunc) {} kmip_handle(kmip_handle&&) = default; kmip_handle& operator=(kmip_handle&&) = default; T* get() const { return _ptr.get(); } operator T*() const { return _ptr.get(); } explicit operator bool() const { return _ptr != nullptr; } private: using ptr_type = std::unique_ptr; ptr_type _ptr; }; class kmip_host::impl::kmip_cmd : public kmip_handle { public: kmip_cmd(int flags = KMIP_CMD_FLAGS_DEFAULT|KMIP_CMD_FLAGS_LOG|KMIP_CMD_FLAGS_LOG_XML) : kmip_handle([flags] { KMIP_CMD* cmd; kmip_chk(KMIP_CMD_new_ex(flags, nullptr, &cmd)); return cmd; }()) {} kmip_cmd(kmip_cmd&&) = default; kmip_cmd& operator=(kmip_cmd&&) = default; friend std::ostream& operator<<(std::ostream& os, const kmip_cmd& cmd) { return os << KMIP_CMD_get_request(cmd); } }; } template <> struct fmt::formatter : fmt::ostream_formatter {}; namespace encryption { class kmip_host::impl::kmip_data_list : public kmip_handle { public: kmip_data_list(int flags = KMIP_DATA_LIST_FLAGS_DEFAULT) : kmip_handle([flags] { KMIP_DATA_LIST* kdl; kmip_chk(KMIP_DATA_LIST_new(flags, &kdl)); return kdl; }()) {} kmip_data_list(kmip_data_list&&) = default; kmip_data_list& operator=(kmip_data_list&&) = default; }; /** * Clears and releases a connection cp. Release connection after. * If retain_connection is true, the connection is only cleared of command data and * can be reused by caller, otherwise it is either added to the connection pool * or dropped. */ future<> kmip_host::impl::release(KMIP_CMD* cmd, con_ptr cp, bool retain_connection) { auto i = _host_connections.find(cp->host()); userdata u; u.host = i->first.c_str(); if (cmd) { KMIP_CMD_set_userdata(cmd, u.ptr); } if (!retain_connection && is_current_host(i->first) && max_pooled_connections_per_host() > i->second.size()) { i->second.emplace_back(std::move(cp)); } // If we neither retain nor cache the connection, do proper close now. Ensures // TLS goodbye etc are sent and streams are flushed. The latter comes into // play if we did background flush that have not yet actually happened... // Should not happen since we send and wait for response, but lets be careful if (cp && !retain_connection) { co_await cp->close(); } } /** * Run a function on a KMIP command using connection cp. Release connection after. * If retain_connection_after_command is true, the connection is only cleared of command data and * can be reused by caller. */ template future kmip_host::impl::do_cmd(KMIP_CMD* cmd, con_ptr cp, Func& f, bool retain_connection_after_command) { cp->attach(cmd); return repeat_until_value([this, cmd, &f, cp, retain_connection_after_command] { int res = f(cmd); switch (res) { case KMIP_ERROR_RETRY: return cp->wait_for_io().then([] { return opt_int(); }).handle_exception([cp](auto ep) { // get here if we had any wire exceptions below. // make sure to force flush and stuff here as well. return cp->close().then_wrapped([ep = std::move(ep)](auto f) mutable { try { f.get(); } catch (...) { } return make_exception_future(std::move(ep)); }); }); case 0: return release(cmd, cp, retain_connection_after_command).then([res] { return make_ready_future(res); }); default: // error. connection is discarded. close it. return cp->close().then_wrapped([cp, res](auto f) { // ignore any exception thrown from the close. // ensure we provide the kmip error instead. try { f.get(); } catch (...) { } return make_ready_future(res); }); } }).finally([cp] {}); } template future kmip_host::impl::do_cmd(kmip_cmd cmd_in, Func && f) { kmip_log.trace("{}: begin do_cmd", *this, cmd_in); KMIP_CMD* cmd = cmd_in; // #998 Need to do retry loop, because we can have either timed out connection, // lost it (connected server went down) or some other network error. return do_with(std::move(f), [this, cmd](Func& f) { return repeat_until_value([this, cmd, &f, retry = _max_retry]() mutable { --retry; return get_connection(cmd).handle_exception([this, cmd, retry](std::exception_ptr ep) { if (retry) { // failed to connect. do more serious backing off. // we only retry this once, since get_connection // will either give back cached connections, // or explicitly try all avail hosts. // In the first case, we will do the lower retry // loop if something is stale/borked, the latter is // more or less dead. auto sleeptime = base_backoff_time * (_max_retry - retry); kmip_log.debug("{}: Connection failed. backoff {}", *this, std::chrono::duration_cast(sleeptime).count()); return seastar::sleep(sleeptime).then([this, cmd] { kmip_log.debug("{}: retrying...", *this); return get_connection(cmd); }); } return make_exception_future(std::move(ep)); }).then([this, cmd, &f, retry](con_ptr cp) mutable { auto host = cp->host(); auto res = do_cmd(cmd, std::move(cp), f); kmip_log.trace("{}: request {}", *this, fmt::ptr(KMIP_CMD_get_request(cmd))); return res.then([this, retry, host = std::move(host)](int res) { if (res == KMIP_ERROR_IO) { kmip_log.debug("{}: request error {}", *this, kmip_errorc.message(res)); if (retry) { // do some backing off unless this is the first retry, which // might be a stale connection. Clear out all caches for the // current host first, then retry. auto f = clear_connections(host); if (retry != (_max_retry - 1)) { f = f.then([this] { auto sleeptime = base_backoff_time; kmip_log.debug("{}: backoff {}ms", *this, std::chrono::duration_cast(sleeptime).count()); return seastar::sleep(sleeptime); }); } return f.then([this] { kmip_log.debug("{}: retrying...", *this); return opt_int{}; }); } } return make_ready_future(res); }); }); }); }).then([this, cmd = std::move(cmd_in)](int res) mutable { kmip_chk(res, cmd); kmip_log.trace("{}: result {}", *this, fmt::ptr(KMIP_CMD_get_response(cmd))); return std::move(cmd); }); } future kmip_host::impl::get_connection(const sstring& host) { // TODO: if a pooled connection is stale, the command run will fail, // and the connection will be discarded. Would be good if we could detect this case // and re-run command with a new connection. Maybe always verify connection, even if // it is old? auto& q = _host_connections[host]; if (!q.empty()) { auto cp = q.front(); q.pop_front(); return make_ready_future<::shared_ptr>(cp); } auto cp = ::make_shared(host, _options); kmip_log.trace("{}: connecting to {}", *this, host); return cp->connect().then([this, cp, host] { kmip_log.trace("{}: verifying {}", *this, host); kmip_cmd cmd; static auto connection_query = [](KMIP_CMD* cmd) { static const std::array query_options = { KMIP_QUERY_FUNCTION_QUERY_OPERATIONS, KMIP_QUERY_FUNCTION_QUERY_OBJECTS, }; return KMIP_CMD_query(cmd, const_cast(query_options.data()), unsigned(query_options.size())); }; // when/if this succeeds, it will push the connection onto the available stack auto f = do_cmd(cmd, cp, connection_query, true /* keep cp */); return f.then([this, host, cmd = std::move(cmd), cp](int res) { kmip_chk(res, cmd); kmip_log.trace("{}: connected {}", *this, host); return cp; }); }); } future kmip_host::impl::get_connection(KMIP_CMD* cmd) { userdata u{ KMIP_CMD_get_userdata(cmd) }; if (u.host != nullptr) { return get_connection(u.host).then([](con_ptr cp) { return cp; }); } using con_ptr = ::shared_ptr; using con_opt = std::optional; return repeat_until_value([this, i = size_t(0)]() mutable { if (i++ == _options.hosts.size()) { throw missing_resource_error("Could not connect to any server"); } auto& host = _options.hosts[_index % _options.hosts.size()]; return get_connection(host).then([](con_ptr cp) { return con_opt(std::move(cp)); }).handle_exception([this, host](auto) { ++_index; // if we fail one host, clear out any // caches for it just in case. return clear_connections(host).then([] { return con_opt(); }); }); }); } future<> kmip_host::impl::clear_connections(const sstring& host) { auto q = std::exchange(_host_connections[host], {}); return parallel_for_each(q.begin(), q.end(), [](con_ptr c) { return c->close().handle_exception([c](auto ep) { // ignore exceptions }); }); } future<> kmip_host::impl::connect() { return do_for_each(_options.hosts, [this](const sstring& host) { return get_connection(host).then([this](auto cp) { return release(nullptr, cp); }); }); } future<> kmip_host::impl::disconnect() { co_await do_for_each(_options.hosts, [this](const sstring& host) { return clear_connections(host); }); co_await _attr_cache.stop(); co_await _id_cache.stop(); } static unsigned from_str(unsigned (*f)(char*, int, int*), const sstring& s, const sstring& what) { int found = 0; auto res = f(const_cast(s.c_str()), CODE2STR_FLAG_STR_CASE, &found); if (!found) { throw std::invalid_argument(format("Unsupported {}: {}", what, s)); } return res; } std::tuple kmip_host::impl::make_attributes(const kmip_key_info& info, bool include_template) const { kmip_data_list kdl_attrs; if (!info.options.template_name.empty()) { kmip_chk(KMIP_DATA_LIST_add_attr_str_by_tag(kdl_attrs, KMIP_TAG_TEMPLATE, const_cast(info.options.template_name.c_str())) ); } if (!info.options.key_namespace.empty()) { kmip_chk(KMIP_DATA_LIST_add_attr_str(kdl_attrs, const_cast("x-key-namespace"), const_cast(info.options.key_namespace.c_str())) ); } auto [type, mode, padding] = parse_key_spec_and_validate_defaults(info.info.alg); try { auto crypt_alg = from_str(&KMIP_string_to_CRYPTOGRAPHIC_ALGORITHM, type, "cryptographic algorithm"); return std::make_tuple(std::move(kdl_attrs), crypt_alg); } catch (std::invalid_argument& e) { std::throw_with_nested(std::invalid_argument("Invalid algorithm: " + info.info.alg)); } } kmip_host::id_type kmip_host::impl::kmip_id_to_id(const sstring& s) const { try { // #2205 - we previously made all ID:s into uuids (because the literal functions // are called KMIP_CMD_get_uuid etc). This has issues with Keysecure which apparently // does _not_ give back UUID format strings, but "other" things. // Could just always store ascii as bytes instead, but that would now // break existing installations, so we check for UUID, and if it does not // match we encode it. utils::UUID uuid(s); return uuid.serialize(); } catch (marshal_exception&) { // very simple exncoding scheme: add a "len" byte at the end. // iff byte size of id + 1 (len) equals 16 (length of UUID), // add a padding byte. size_t len = s.size() + 1; if (len == 16) { ++len; } bytes res(len, 0); std::copy(s.begin(), s.end(), res.begin()); res.back() = int8_t(len - s.size()); return res; } } sstring kmip_host::impl::id_to_kmip_string(const id_type& id) const { // see comment above for encoding scheme. if (id.size() == 16) { // if byte size is UUID it must be a UUID. No "old" id:s are // not, and we never encode non-uuid as 16 bytes. auto uuid = utils::UUID_gen::get_UUID(id); return fmt::format("{}", uuid); } auto len = id.size() - id.back(); return sstring(id.begin(), id.begin() + len); } future kmip_host::impl::create_key(const kmip_key_info& info) { if (this_shard_id() == 0) { // #1039 First try looking for existing keys on server return find_matching_keys(info, 1).then([this, info](std::vector ids) { if (!ids.empty()) { // got it return get_key_by_id(ids.front(), info.info).then([id = ids.front()](shared_ptr k) { return key_and_id_type(std::move(k), id); }); } kmip_log.debug("{}: Creating key {}", _name, info); auto kdl_attrs_crypt_alg = make_attributes(info); auto&& kdl_attrs = std::get<0>(kdl_attrs_crypt_alg); auto&& crypt_alg = std::get<1>(kdl_attrs_crypt_alg); // TODO: this is inefficient. We can probably put this in a single batch. kmip_cmd cmd; KMIP_CMD_set_ctx(cmd, const_cast("Create key")); return do_cmd(std::move(cmd), [info, kdl_attrs = std::move(kdl_attrs), crypt_alg](KMIP_CMD* cmd) { return KMIP_CMD_create_smpl(cmd, KMIP_OBJECT_TYPE_SYMMETRIC_KEY, crypt_alg, KMIP_CRYPTOGRAPHIC_USAGE_ENCRYPT|KMIP_CRYPTOGRAPHIC_USAGE_DECRYPT, int(info.info.len), KMIP_DATA_LIST_attrs(kdl_attrs), KMIP_DATA_LIST_n_attrs(kdl_attrs) ); }).then([this, info](kmip_cmd cmd) { /* now get the details (the value of the key) */ char* new_id; kmip_chk(KMIP_CMD_get_uuid(cmd, 0, &new_id), cmd); sstring uuid(new_id); kmip_log.debug("{}: Created {}:{}", _name, info, uuid); KMIP_CMD_set_ctx(cmd, const_cast("activate")); return do_cmd(std::move(cmd), [new_id](KMIP_CMD* cmd) { return KMIP_CMD_activate(cmd, new_id); }).then([this, info, uuid](kmip_cmd cmd) { auto id = kmip_id_to_id(uuid); kmip_log.debug("{}: Activated {}", _name, uuid); return get_key_by_id(id, info.info).then([id](auto k) { return key_and_id_type(k, id); }); }); }); }); } return smp::submit_to(0, [this, info] { return _ctxt.get_kmip_host(_name)->get_or_create_key(info.info, info.options).then([](std::tuple, id_type> k_id) { auto&& [k, id] = k_id; return make_ready_future>(std::tuple(k->info(), k->key(), id)); }); }).then([](std::tuple info_b_id) { auto&& [info, b, id] = info_b_id; return make_ready_future(key_and_id_type(make_shared(info, b), id)); }); } future> kmip_host::impl::find_matching_keys(const kmip_key_info& info, std::optional max) { kmip_log.debug("{}: Finding matching key {}", _name, info); auto [kdl_attrs, crypt_alg] = make_attributes(info, false); static const char kmip_tag_cryptographic_length[] = KMIP_TAGSTR_CRYPTOGRAPHIC_LENGTH; static const char kmip_tag_cryptographic_usage_mask[] = KMIP_TAGSTR_CRYPTOGRAPHIC_USAGE_MASK; // #1079. Query mask apparently ignores things like cryptographic // attribute set of options, instead we must specify the query // as a list of attributes. kmip_chk(KMIP_DATA_LIST_add_attr_enum_by_tag(kdl_attrs, KMIP_TAG_OBJECT_TYPE, KMIP_OBJECT_TYPE_SYMMETRIC_KEY) ); kmip_chk(KMIP_DATA_LIST_add_attr_enum_by_tag(kdl_attrs, KMIP_TAG_CRYPTOGRAPHIC_ALGORITHM, int(crypt_alg)) ); kmip_chk(KMIP_DATA_LIST_add_attr_int(kdl_attrs, // our kmip sdk is broken/const-challenged const_cast(kmip_tag_cryptographic_length), int(info.info.len)) ); kmip_chk(KMIP_DATA_LIST_add_attr_enum_by_tag(kdl_attrs, KMIP_TAG_STATE, KMIP_STATE_ACTIVE) ); kmip_chk(KMIP_DATA_LIST_add_attr_int(kdl_attrs, const_cast(kmip_tag_cryptographic_usage_mask), KMIP_CRYPTOGRAPHIC_USAGE_ENCRYPT|KMIP_CRYPTOGRAPHIC_USAGE_DECRYPT) ); kmip_cmd cmd; KMIP_CMD_set_ctx(cmd, const_cast("Find matching key")); std::unique_ptr mp; int* maxp = nullptr; if (max) { mp = std::make_unique(*max); maxp = mp.get(); } return do_cmd(std::move(cmd), [kdl_attrs = std::move(kdl_attrs), maxp](KMIP_CMD* cmd) { return KMIP_CMD_locate(cmd, maxp, nullptr, KMIP_DATA_LIST_attrs(kdl_attrs), KMIP_DATA_LIST_n_attrs(kdl_attrs)); }).then([this, info, mp = std::move(mp)](kmip_cmd cmd) { std::vector result; for (int i = 0; ; ++i) { char* new_id; auto err = KMIP_CMD_get_uuid(cmd, i, &new_id); if (err == KMIP_ERROR_NOT_FOUND) { break; } kmip_chk(err, cmd); result.emplace_back(kmip_id_to_id(new_id)); } kmip_log.debug("{}: Found {} matching keys {}", _name, result.size(), info); return result; }); } future> kmip_host::impl::find_key(const id_type& id) { if (this_shard_id() == 0) { kmip_cmd cmd; KMIP_CMD_set_ctx(cmd, const_cast("Find key")); auto uuid = id_to_kmip_string(id); kmip_log.debug("{}: Finding {}", _name, uuid); // Batch operation. Nothing is sent/received until xmit below kmip_chk(KMIP_CMD_batch_start(cmd)); kmip_chk(KMIP_CMD_set_batch_order(cmd, 1)); { int key_format_type = KMIP_KEY_FORMAT_TYPE_RAW; kmip_chk(KMIP_CMD_get(cmd, const_cast(uuid.c_str()), &key_format_type, nullptr, nullptr)); } kmip_chk(KMIP_CMD_get_attributes(cmd, const_cast(uuid.c_str()), nullptr, 0)); return do_cmd(std::move(cmd), [](KMIP_CMD* cmd) { return KMIP_CMD_batch_xmit(cmd); }).then([this, uuid](kmip_cmd cmd) { auto nb = KMIP_CMD_get_batch_count(cmd); if (nb != 2) { throw malformed_response_error("Invalid batch count in response: " + std::to_string(nb)); } sstring alg; sstring mode; sstring padding; // "Get" result auto kdl_res = KMIP_CMD_get_batch(cmd, 0); /* get a reference to the key material (the actual key value) */ unsigned char* key; unsigned int keylen; kmip_chk(KMIP_DATA_LIST_get_data(kdl_res, KMIP_TAG_KEY_MATERIAL, 0, &key, &keylen)); auto tag_to_string = [](auto f, auto val) { int found; auto p = f(val, CODE2STR_FLAG_STR_CASE, &found); if (!found) { throw malformed_response_error("Invalid tag: " + std::to_string(val)); } return sstring(p); }; int crypto_alg; kmip_chk(KMIP_DATA_LIST_get_32(kdl_res, KMIP_TAG_CRYPTOGRAPHIC_ALGORITHM, 0, &crypto_alg)); alg = tag_to_string(&KMIP_CRYPTOGRAPHIC_ALGORITHM_to_string, crypto_alg); // "Attribute list" result // This will apparently most of the time _not_ contain the info we want, // depending on server, but we record as much as we can anyway. // The actual resulting keys used will be based on external config. Only // key data and verifying that it is compatible with said info is // important for us. auto kdl_attr = KMIP_CMD_get_batch(cmd, 1); unsigned int attr_count = 0; kmip_chk(KMIP_DATA_LIST_get_count(kdl_attr, KMIP_TAG_ATTRIBUTE, &attr_count)); for (unsigned int i = 0; i < attr_count; i++) { KMIP_DATA *attr = nullptr; int n_attr = 0; kmip_chk(KMIP_DATA_LIST_get_struct(kdl_attr, KMIP_TAG_ATTRIBUTE, i, &attr, &n_attr, NULL)); KMIP_DATA *attr_val = nullptr; kmip_chk(KMIP_DATA_get(attr, n_attr,KMIP_TAG_ATTRIBUTE_VALUE, 0, &attr_val)); switch (attr_val->tag) { case KMIP_TAG_BLOCK_CIPHER_MODE: mode = tag_to_string(&KMIP_BLOCK_CIPHER_MODE_to_string, attr_val->data32); break; case KMIP_TAG_PADDING_METHOD: padding = tag_to_string(&KMIP_PADDING_METHOD_to_string, attr_val->data32); break; default: break; } } if (alg.empty()) { throw configuration_error("Could not find algorithm"); } if (mode.empty() != padding.empty()) { throw configuration_error("Invalid block mode/padding"); } auto str = mode.empty() || padding.empty() ? alg : alg + "/" + mode + "/" + padding; key_info derived_info{ str, keylen*8}; kmip_log.trace("{}: Found {}:{} {}", _name, uuid, derived_info.alg, derived_info.len); return make_shared(derived_info, bytes(key, key + keylen)); }); } return smp::submit_to(0, [this, id] { return _ctxt.get_kmip_host(_name)->get_key_by_id(id).then([](shared_ptr k) { return make_ready_future>(std::tuple(k->info(), k->key())); }); }).then([](std::tuple info_b) { auto&& [info, b] = info_b; return make_shared(info, b); }); } shared_ptr kmip_host::impl::ensure_compatible_key(shared_ptr k, const key_info& info) { // keys we get back are typically void // of block mode/padding info (because this is meaningless // from the standpoint of the kmip server). // Check and re-init the actual key used based // on what the user wants so we adhere to block mode etc. if (!info.compatible(k->info())) { throw malformed_response_error(fmt::format("Incompatible key: {}", k->info())); } if (k->info() != info) { k = ::make_shared(info, k->key()); } return k; } [[noreturn]] static void translate_kmip_error(const kmip_error& e) { switch (e.code().value()) { case KMIP_ERROR_BAD_CONNECT: case KMIP_ERROR_IO: std::throw_with_nested(network_error(e.what())); case KMIP_ERROR_BAD_PROTOCOL: std::throw_with_nested(configuration_error(e.what())); case KMIP_ERROR_NOT_FOUND: std::throw_with_nested(missing_resource_error(e.what())); case KMIP_ERROR_AUTH_FAILED: case KMIP_ERROR_CERT_AUTH_FAILED: std::throw_with_nested(permission_error(e.what())); default: std::throw_with_nested(service_error(e.what())); } } future, kmip_host::id_type>> kmip_host::impl::get_or_create_key(const key_info& info, const key_options& opts) { kmip_log.debug("{}: Lookup key {}:{}", _name, info, opts); try { auto linfo = info; auto kinfo = co_await _attr_cache.get(kmip_key_info{info, opts}); co_return std::tuple(ensure_compatible_key(std::get<0>(kinfo), linfo), std::get<1>(kinfo)); } catch (kmip_error& e) { translate_kmip_error(e); } catch (base_error&) { throw; } catch (std::invalid_argument& e) { std::throw_with_nested(configuration_error(fmt::format("get_or_create_key: {}", e.what()))); } catch (...) { std::throw_with_nested(service_error(fmt::format("get_or_create_key: {}", std::current_exception()))); } } future> kmip_host::impl::get_key_by_id(const id_type& id, const std::optional& info) { try { auto linfo = info; // maintain on stack auto k = co_await _id_cache.get(id); if (linfo) { k = ensure_compatible_key(k, *linfo); } co_return k; } catch (kmip_error& e) { translate_kmip_error(e); } catch (base_error&) { throw; } catch (std::invalid_argument& e) { std::throw_with_nested(configuration_error(fmt::format("get_key_by_id: {}", e.what()))); } catch (...) { std::throw_with_nested(service_error(fmt::format("get_key_by_id: {}", std::current_exception()))); } } kmip_host::kmip_host(encryption_context& ctxt, const sstring& name, const std::unordered_map& map) : kmip_host(ctxt, name, [&ctxt, &map] { host_options opts; map_wrapper> m(map); try { static const std::regex wsc("\\s*,\\s*"); // comma+whitespace std::string hosts = m("hosts").value(); auto i = std::sregex_token_iterator(hosts.begin(), hosts.end(), wsc, -1); auto e = std::sregex_token_iterator(); std::for_each(i, e, [&](const std::string & s) { opts.hosts.emplace_back(s); }); } catch (std::bad_optional_access&) { throw std::invalid_argument("No KMIP host names provided"); } opts.certfile = m("certificate").value_or(""); opts.keyfile = m("keyfile").value_or(""); opts.truststore = m("truststore").value_or(""); opts.priority_string = m("priority_string").value_or(""); opts.username = m("username").value_or(""); opts.password = ctxt.maybe_decrypt_config_value(m("password").value_or("")); if (m("max_command_retries")) { opts.max_command_retries = std::stoul(*m("max_command_retries")); } opts.key_cache_expiry = parse_expiry(m("key_cache_expiry")); opts.key_cache_refresh = parse_expiry(m("key_cache_refresh")); return opts; }()) {} kmip_host::kmip_host(encryption_context& ctxt, const sstring& name, const host_options& opts) : _impl(std::make_unique(ctxt, name, opts)) {} kmip_host::~kmip_host() = default; future<> kmip_host::connect() { return _impl->connect(); } future<> kmip_host::disconnect() { return _impl->disconnect(); } future, kmip_host::id_type>> kmip_host::get_or_create_key(const key_info& info, const key_options& opts) { return _impl->get_or_create_key(info, opts); } future> kmip_host::get_key_by_id(const id_type& id, std::optional info) { return _impl->get_key_by_id(id, info); } future> kmip_host::get_key_by_name(const sstring& name) { return _impl->get_key_by_id(_impl->kmip_id_to_id(name)); } std::ostream& operator<<(std::ostream& os, const kmip_host::key_options& opts) { return os << opts.template_name << ":" << opts.key_namespace; } } #else #include "kmip_host.hh" namespace encryption { class kmip_host::impl { }; kmip_host::kmip_host(encryption_context& ctxt, const sstring& name, const std::unordered_map& map) { throw std::runtime_error("KMIP support not enabled"); } kmip_host::kmip_host(encryption_context& ctxt, const sstring& name, const host_options& opts) { throw std::runtime_error("KMIP support not enabled"); } kmip_host::~kmip_host() = default; future<> kmip_host::connect() { throw std::runtime_error("KMIP support not enabled"); } future<> kmip_host::disconnect() { throw std::runtime_error("KMIP support not enabled"); } future, kmip_host::id_type>> kmip_host::get_or_create_key(const key_info& info, const key_options& opts) { throw std::runtime_error("KMIP support not enabled"); } future> kmip_host::get_key_by_id(const id_type& id, std::optional info) { throw std::runtime_error("KMIP support not enabled"); } future> kmip_host::get_key_by_name(const sstring& name) { throw std::runtime_error("KMIP support not enabled"); } std::ostream& operator<<(std::ostream& os, const kmip_host::key_options& opts) { return os << opts.template_name << ":" << opts.key_namespace; } } #endif