Files
scylladb/ent/encryption/kmip_host.cc
Calle Wilund d000fa3335 ear::kmip_host: Handle ipv6 hosts + use system trust when not specified
Fixes #27362

The KMIP host connector should handle ipv4 connections (named or numeric).
It also should fall back to system trust when truststore is not specified.
2025-12-04 11:38:41 +00:00

1242 lines
46 KiB
C++

/*
* Copyright (C) 2018 ScyllaDB
*
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
*/
#ifdef HAVE_KMIP
#include <deque>
#include <unordered_map>
#include <regex>
#include <algorithm>
#include <seastar/net/dns.hh>
#include <seastar/net/api.hh>
#include <seastar/net/tls.hh>
#include <seastar/core/thread.hh>
#include <seastar/core/sleep.hh>
#include <seastar/core/reactor.hh>
#include <fmt/core.h>
#include <fmt/ostream.h>
// workaround cryptsoft sdk issue:
#define strcasestr kmip_strcasestr
#include <kmip_os.h>
#include <kmip.h>
#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<const char*>(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 : "<none>"
);
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<shared_ptr<symmetric_key>, 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<size_t>::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<size_t>::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<std::tuple<shared_ptr<symmetric_key>, id_type>> get_or_create_key(const key_info&, const key_options& = {});
future<shared_ptr<symmetric_key>> get_key_by_id(const id_type&, const std::optional<key_info>& = {});
id_type kmip_id_to_id(const sstring&) const;
sstring id_to_kmip_string(const id_type&) const;
private:
future<key_and_id_type> create_key(const kmip_key_info&);
future<shared_ptr<symmetric_key>> find_key(const id_type&);
future<std::vector<id_type>> find_matching_keys(const kmip_key_info&, std::optional<int> max = {});
static shared_ptr<symmetric_key> ensure_compatible_key(shared_ptr<symmetric_key>, const key_info&);
template<typename T, int(*)(T *)>
class kmip_handle;
class kmip_cmd;
class kmip_data_list;
class connection;
std::tuple<kmip_data_list, unsigned int> 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<connection>;
using opt_int = std::optional<int>;
template<typename Func>
future<kmip_cmd> do_cmd(kmip_cmd, Func &&);
template<typename Func>
future<int> do_cmd(KMIP_CMD*, con_ptr, Func&, bool retain_connection_after_command = false);
future<con_ptr> get_connection(KMIP_CMD*);
future<con_ptr> 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, key_and_id_type, 2,
utils::loading_cache_reload_enabled::yes,
utils::simple_entry_size<key_and_id_type>,
kmip_key_info_hash> _attr_cache;
utils::loading_cache<id_type, ::shared_ptr<symmetric_key>, 2,
utils::loading_cache_reload_enabled::yes,
utils::simple_entry_size<::shared_ptr<symmetric_key>>> _id_cache;
using connections = std::deque<con_ptr>;
using host_to_connections = std::unordered_map<sstring, connections>;
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<encryption::kmip_host::impl> : fmt::ostream_formatter {};
template <> struct fmt::formatter<encryption::kmip_host::impl::kmip_key_info> : 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<char> _output;
input_stream<char> _input;
seastar::connected_socket _socket;
std::optional<temporary_buffer<char>> _in_buffer;
std::optional<future<>> _pending;
};
}
template <> struct fmt::formatter<encryption::kmip_host::impl::connection> : fmt::ostream_formatter {};
namespace encryption {
future<> kmip_host::impl::connection::connect() {
auto cred = ::make_shared<seastar::tls::certificate_credentials>();
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<char *>(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<char *>(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<char> 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<kmip_host::impl::connection *>(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<void*>(cmd));
if (cmd == nullptr) {
return;
}
if (!_options.username.empty()) {
kmip_chk(
KMIP_CMD_set_credential_username(cmd,
const_cast<char *>(_options.username.c_str()),
const_cast<char *>(_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<void *>(this)));
}
future<> kmip_host::impl::connection::close() {
return _output.close().finally([this] {
return _input.close();
});
}
template<typename T, int(*FreeFunc)(T *)>
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<T, int(*)(T *)>;
ptr_type _ptr;
};
class kmip_host::impl::kmip_cmd : public kmip_handle<KMIP_CMD, &KMIP_CMD_free> {
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<encryption::kmip_host::impl::kmip_cmd> : fmt::ostream_formatter {};
namespace encryption {
class kmip_host::impl::kmip_data_list : public kmip_handle<KMIP_DATA_LIST, &KMIP_DATA_LIST_free> {
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<typename Func>
future<int> 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<opt_int>(std::move(ep));
});
});
case 0:
return release(cmd, cp, retain_connection_after_command).then([res] {
return make_ready_future<opt_int>(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<opt_int>(res);
});
}
}).finally([cp] {});
}
template<typename Func>
future<kmip_host::impl::kmip_cmd> 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<std::chrono::milliseconds>(sleeptime).count());
return seastar::sleep(sleeptime).then([this, cmd] {
kmip_log.debug("{}: retrying...", *this);
return get_connection(cmd);
});
}
return make_exception_future<con_ptr>(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<std::chrono::milliseconds>(sleeptime).count());
return seastar::sleep(sleeptime);
});
}
return f.then([this] {
kmip_log.debug("{}: retrying...", *this);
return opt_int{};
});
}
}
return make_ready_future<opt_int>(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::con_ptr> 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<connection>>(cp);
}
auto cp = ::make_shared<connection>(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<int, 2> query_options = {
KMIP_QUERY_FUNCTION_QUERY_OPERATIONS,
KMIP_QUERY_FUNCTION_QUERY_OBJECTS,
};
return KMIP_CMD_query(cmd, const_cast<int *>(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::con_ptr> 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<kmip_host::impl::connection>;
using con_opt = std::optional<con_ptr>;
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<char *>(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::kmip_data_list, unsigned int> 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<char*>(info.options.template_name.c_str()))
);
}
if (!info.options.key_namespace.empty()) {
kmip_chk(KMIP_DATA_LIST_add_attr_str(kdl_attrs,
const_cast<char *>("x-key-namespace"),
const_cast<char*>(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::key_and_id_type> 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<id_type> ids) {
if (!ids.empty()) {
// got it
return get_key_by_id(ids.front(), info.info).then([id = ids.front()](shared_ptr<symmetric_key> 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<char *>("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<char *>("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<shared_ptr<symmetric_key>, id_type> k_id) {
auto&& [k, id] = k_id;
return make_ready_future<std::tuple<key_info, bytes, id_type>>(std::tuple(k->info(), k->key(), id));
});
}).then([](std::tuple<key_info, bytes, id_type> info_b_id) {
auto&& [info, b, id] = info_b_id;
return make_ready_future<key_and_id_type>(key_and_id_type(make_shared<symmetric_key>(info, b), id));
});
}
future<std::vector<kmip_host::id_type>> kmip_host::impl::find_matching_keys(const kmip_key_info& info, std::optional<int> 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<char*>(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<char*>(kmip_tag_cryptographic_usage_mask),
KMIP_CRYPTOGRAPHIC_USAGE_ENCRYPT|KMIP_CRYPTOGRAPHIC_USAGE_DECRYPT)
);
kmip_cmd cmd;
KMIP_CMD_set_ctx(cmd, const_cast<char *>("Find matching key"));
std::unique_ptr<int> mp;
int* maxp = nullptr;
if (max) {
mp = std::make_unique<int>(*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<id_type> 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<shared_ptr<symmetric_key>> kmip_host::impl::find_key(const id_type& id) {
if (this_shard_id() == 0) {
kmip_cmd cmd;
KMIP_CMD_set_ctx(cmd, const_cast<char *>("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<char *>(uuid.c_str()), &key_format_type, nullptr, nullptr));
}
kmip_chk(KMIP_CMD_get_attributes(cmd, const_cast<char *>(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<symmetric_key>(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<symmetric_key> k) {
return make_ready_future<std::tuple<key_info, bytes>>(std::tuple(k->info(), k->key()));
});
}).then([](std::tuple<key_info, bytes> info_b) {
auto&& [info, b] = info_b;
return make_shared<symmetric_key>(info, b);
});
}
shared_ptr<symmetric_key> kmip_host::impl::ensure_compatible_key(shared_ptr<symmetric_key> 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<symmetric_key>(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<std::tuple<shared_ptr<symmetric_key>, 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<shared_ptr<symmetric_key>> kmip_host::impl::get_key_by_id(const id_type& id, const std::optional<key_info>& 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<sstring, sstring>& map)
: kmip_host(ctxt, name, [&ctxt, &map] {
host_options opts;
map_wrapper<std::unordered_map<sstring, sstring>> 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<impl>(ctxt, name, opts))
{}
kmip_host::~kmip_host() = default;
future<> kmip_host::connect() {
return _impl->connect();
}
future<> kmip_host::disconnect() {
return _impl->disconnect();
}
future<std::tuple<shared_ptr<symmetric_key>, 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<shared_ptr<symmetric_key>> kmip_host::get_key_by_id(const id_type& id, std::optional<key_info> info) {
return _impl->get_key_by_id(id, info);
}
future<shared_ptr<symmetric_key>> 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<sstring, sstring>& 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<std::tuple<shared_ptr<symmetric_key>, 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<shared_ptr<symmetric_key>> kmip_host::get_key_by_id(const id_type& id, std::optional<key_info> info) {
throw std::runtime_error("KMIP support not enabled");
}
future<shared_ptr<symmetric_key>> 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