summaryrefslogtreecommitdiffstats
path: root/src/msg/async/crypto_onwire.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/msg/async/crypto_onwire.cc')
-rw-r--r--src/msg/async/crypto_onwire.cc311
1 files changed, 311 insertions, 0 deletions
diff --git a/src/msg/async/crypto_onwire.cc b/src/msg/async/crypto_onwire.cc
new file mode 100644
index 00000000..4e423406
--- /dev/null
+++ b/src/msg/async/crypto_onwire.cc
@@ -0,0 +1,311 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include <array>
+#include <openssl/evp.h>
+
+#include "crypto_onwire.h"
+
+#include "common/debug.h"
+#include "common/ceph_crypto.h"
+#include "include/types.h"
+
+#define dout_subsys ceph_subsys_ms
+
+namespace ceph::crypto::onwire {
+
+static constexpr const std::size_t AESGCM_KEY_LEN{16};
+static constexpr const std::size_t AESGCM_IV_LEN{12};
+static constexpr const std::size_t AESGCM_TAG_LEN{16};
+static constexpr const std::size_t AESGCM_BLOCK_LEN{16};
+
+struct nonce_t {
+ ceph_le32 fixed;
+ ceph_le64 counter;
+
+ bool operator==(const nonce_t& rhs) const {
+ return !memcmp(this, &rhs, sizeof(*this));
+ }
+} __attribute__((packed));
+static_assert(sizeof(nonce_t) == AESGCM_IV_LEN);
+
+using key_t = std::array<std::uint8_t, AESGCM_KEY_LEN>;
+
+// http://www.mindspring.com/~dmcgrew/gcm-nist-6.pdf
+// https://www.openssl.org/docs/man1.0.2/crypto/EVP_aes_128_gcm.html#GCM-mode
+// https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption
+// https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
+class AES128GCM_OnWireTxHandler : public ceph::crypto::onwire::TxHandler {
+ CephContext* const cct;
+ std::unique_ptr<EVP_CIPHER_CTX, decltype(&::EVP_CIPHER_CTX_free)> ectx;
+ ceph::bufferlist buffer;
+ nonce_t nonce, initial_nonce;
+ bool used_initial_nonce;
+ bool new_nonce_format; // 64-bit counter?
+ static_assert(sizeof(nonce) == AESGCM_IV_LEN);
+
+public:
+ AES128GCM_OnWireTxHandler(CephContext* const cct,
+ const key_t& key,
+ const nonce_t& nonce,
+ bool new_nonce_format)
+ : cct(cct),
+ ectx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free),
+ nonce(nonce), initial_nonce(nonce), used_initial_nonce(false),
+ new_nonce_format(new_nonce_format) {
+ ceph_assert_always(ectx);
+ ceph_assert_always(key.size() * CHAR_BIT == 128);
+
+ if (1 != EVP_EncryptInit_ex(ectx.get(), EVP_aes_128_gcm(),
+ nullptr, nullptr, nullptr)) {
+ throw std::runtime_error("EVP_EncryptInit_ex failed");
+ }
+
+ if(1 != EVP_EncryptInit_ex(ectx.get(), nullptr, nullptr,
+ key.data(), nullptr)) {
+ throw std::runtime_error("EVP_EncryptInit_ex failed");
+ }
+ }
+
+ ~AES128GCM_OnWireTxHandler() override {
+ ::ceph::crypto::zeroize_for_security(&nonce, sizeof(nonce));
+ ::ceph::crypto::zeroize_for_security(&initial_nonce, sizeof(initial_nonce));
+ }
+
+ void reset_tx_handler(const uint32_t* first, const uint32_t* last) override;
+
+ void authenticated_encrypt_update(const ceph::bufferlist& plaintext) override;
+ ceph::bufferlist authenticated_encrypt_final() override;
+};
+
+void AES128GCM_OnWireTxHandler::reset_tx_handler(const uint32_t* first,
+ const uint32_t* last)
+{
+ if (nonce == initial_nonce) {
+ if (used_initial_nonce) {
+ throw ceph::crypto::onwire::TxHandlerError("out of nonces");
+ }
+ used_initial_nonce = true;
+ }
+
+ if(1 != EVP_EncryptInit_ex(ectx.get(), nullptr, nullptr, nullptr,
+ reinterpret_cast<const unsigned char*>(&nonce))) {
+ throw std::runtime_error("EVP_EncryptInit_ex failed");
+ }
+
+ ceph_assert(buffer.get_append_buffer_unused_tail_length() == 0);
+ buffer.reserve(std::accumulate(first, last, AESGCM_TAG_LEN));
+
+ if (!new_nonce_format) {
+ // msgr2.0: 32-bit counter followed by 64-bit fixed field,
+ // susceptible to overflow!
+ nonce.fixed = nonce.fixed + 1;
+ } else {
+ nonce.counter = nonce.counter + 1;
+ }
+}
+
+void AES128GCM_OnWireTxHandler::authenticated_encrypt_update(
+ const ceph::bufferlist& plaintext)
+{
+ ceph_assert(buffer.get_append_buffer_unused_tail_length() >=
+ plaintext.length());
+ auto filler = buffer.append_hole(plaintext.length());
+
+ for (const auto& plainbuf : plaintext.buffers()) {
+ int update_len = 0;
+
+ if(1 != EVP_EncryptUpdate(ectx.get(),
+ reinterpret_cast<unsigned char*>(filler.c_str()),
+ &update_len,
+ reinterpret_cast<const unsigned char*>(plainbuf.c_str()),
+ plainbuf.length())) {
+ throw std::runtime_error("EVP_EncryptUpdate failed");
+ }
+ ceph_assert_always(update_len >= 0);
+ ceph_assert(static_cast<unsigned>(update_len) == plainbuf.length());
+ filler.advance(update_len);
+ }
+
+ ldout(cct, 15) << __func__
+ << " plaintext.length()=" << plaintext.length()
+ << " buffer.length()=" << buffer.length()
+ << dendl;
+}
+
+ceph::bufferlist AES128GCM_OnWireTxHandler::authenticated_encrypt_final()
+{
+ int final_len = 0;
+ ceph_assert(buffer.get_append_buffer_unused_tail_length() ==
+ AESGCM_BLOCK_LEN);
+ auto filler = buffer.append_hole(AESGCM_BLOCK_LEN);
+ if(1 != EVP_EncryptFinal_ex(ectx.get(),
+ reinterpret_cast<unsigned char*>(filler.c_str()),
+ &final_len)) {
+ throw std::runtime_error("EVP_EncryptFinal_ex failed");
+ }
+ ceph_assert_always(final_len == 0);
+
+ static_assert(AESGCM_BLOCK_LEN == AESGCM_TAG_LEN);
+ if(1 != EVP_CIPHER_CTX_ctrl(ectx.get(),
+ EVP_CTRL_GCM_GET_TAG, AESGCM_TAG_LEN,
+ filler.c_str())) {
+ throw std::runtime_error("EVP_CIPHER_CTX_ctrl failed");
+ }
+
+ ldout(cct, 15) << __func__
+ << " buffer.length()=" << buffer.length()
+ << " final_len=" << final_len
+ << dendl;
+ return std::move(buffer);
+}
+
+// RX PART
+class AES128GCM_OnWireRxHandler : public ceph::crypto::onwire::RxHandler {
+ CephContext* const cct;
+ std::unique_ptr<EVP_CIPHER_CTX, decltype(&::EVP_CIPHER_CTX_free)> ectx;
+ nonce_t nonce;
+ bool new_nonce_format; // 64-bit counter?
+ static_assert(sizeof(nonce) == AESGCM_IV_LEN);
+
+public:
+ AES128GCM_OnWireRxHandler(CephContext* const cct,
+ const key_t& key,
+ const nonce_t& nonce,
+ bool new_nonce_format)
+ : cct(cct),
+ ectx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free),
+ nonce(nonce), new_nonce_format(new_nonce_format) {
+ ceph_assert_always(ectx);
+ ceph_assert_always(key.size() * CHAR_BIT == 128);
+
+ if (1 != EVP_DecryptInit_ex(ectx.get(), EVP_aes_128_gcm(),
+ nullptr, nullptr, nullptr)) {
+ throw std::runtime_error("EVP_DecryptInit_ex failed");
+ }
+
+ if(1 != EVP_DecryptInit_ex(ectx.get(), nullptr, nullptr,
+ key.data(), nullptr)) {
+ throw std::runtime_error("EVP_DecryptInit_ex failed");
+ }
+ }
+
+ ~AES128GCM_OnWireRxHandler() override {
+ ::ceph::crypto::zeroize_for_security(&nonce, sizeof(nonce));
+ }
+
+ std::uint32_t get_extra_size_at_final() override {
+ return AESGCM_TAG_LEN;
+ }
+ void reset_rx_handler() override;
+ void authenticated_decrypt_update(ceph::bufferlist& bl) override;
+ void authenticated_decrypt_update_final(ceph::bufferlist& bl) override;
+};
+
+void AES128GCM_OnWireRxHandler::reset_rx_handler()
+{
+ if(1 != EVP_DecryptInit_ex(ectx.get(), nullptr, nullptr, nullptr,
+ reinterpret_cast<const unsigned char*>(&nonce))) {
+ throw std::runtime_error("EVP_DecryptInit_ex failed");
+ }
+
+ if (!new_nonce_format) {
+ // msgr2.0: 32-bit counter followed by 64-bit fixed field,
+ // susceptible to overflow!
+ nonce.fixed = nonce.fixed + 1;
+ } else {
+ nonce.counter = nonce.counter + 1;
+ }
+}
+
+void AES128GCM_OnWireRxHandler::authenticated_decrypt_update(
+ ceph::bufferlist& bl)
+{
+ // discard cached crcs as we will be writing through c_str()
+ bl.invalidate_crc();
+ for (auto& buf : bl.buffers()) {
+ auto p = reinterpret_cast<unsigned char*>(const_cast<char*>(buf.c_str()));
+ int update_len = 0;
+
+ if (1 != EVP_DecryptUpdate(ectx.get(), p, &update_len, p, buf.length())) {
+ throw std::runtime_error("EVP_DecryptUpdate failed");
+ }
+ ceph_assert_always(update_len >= 0);
+ ceph_assert(static_cast<unsigned>(update_len) == buf.length());
+ }
+}
+
+void AES128GCM_OnWireRxHandler::authenticated_decrypt_update_final(
+ ceph::bufferlist& bl)
+{
+ unsigned orig_len = bl.length();
+ ceph_assert(orig_len >= AESGCM_TAG_LEN);
+
+ // decrypt optional data. Caller is obliged to provide only signature but it
+ // may supply ciphertext as well. Combining the update + final is reflected
+ // combined together.
+ ceph::bufferlist auth_tag;
+ bl.splice(orig_len - AESGCM_TAG_LEN, AESGCM_TAG_LEN, &auth_tag);
+ if (bl.length() > 0) {
+ authenticated_decrypt_update(bl);
+ }
+
+ // we need to ensure the tag is stored in continuous memory.
+ if (1 != EVP_CIPHER_CTX_ctrl(ectx.get(), EVP_CTRL_GCM_SET_TAG,
+ AESGCM_TAG_LEN, auth_tag.c_str())) {
+ throw std::runtime_error("EVP_CIPHER_CTX_ctrl failed");
+ }
+
+ // I expect that 0 bytes will be appended. The call is supposed solely to
+ // authenticate the message.
+ {
+ int final_len = 0;
+ if (0 >= EVP_DecryptFinal_ex(ectx.get(), nullptr, &final_len)) {
+ throw MsgAuthError();
+ }
+ ceph_assert_always(final_len == 0);
+ ceph_assert(bl.length() + AESGCM_TAG_LEN == orig_len);
+ }
+}
+
+ceph::crypto::onwire::rxtx_t ceph::crypto::onwire::rxtx_t::create_handler_pair(
+ CephContext* cct,
+ const AuthConnectionMeta& auth_meta,
+ bool new_nonce_format,
+ bool crossed)
+{
+ if (auth_meta.is_mode_secure()) {
+ ceph_assert_always(auth_meta.connection_secret.length() >= \
+ sizeof(key_t) + 2 * sizeof(nonce_t));
+ const char* secbuf = auth_meta.connection_secret.c_str();
+
+ key_t key;
+ {
+ ::memcpy(key.data(), secbuf, sizeof(key));
+ secbuf += sizeof(key);
+ }
+
+ nonce_t rx_nonce;
+ {
+ ::memcpy(&rx_nonce, secbuf, sizeof(rx_nonce));
+ secbuf += sizeof(rx_nonce);
+ }
+
+ nonce_t tx_nonce;
+ {
+ ::memcpy(&tx_nonce, secbuf, sizeof(tx_nonce));
+ secbuf += sizeof(tx_nonce);
+ }
+
+ return {
+ std::make_unique<AES128GCM_OnWireRxHandler>(
+ cct, key, crossed ? tx_nonce : rx_nonce, new_nonce_format),
+ std::make_unique<AES128GCM_OnWireTxHandler>(
+ cct, key, crossed ? rx_nonce : tx_nonce, new_nonce_format)
+ };
+ } else {
+ return { nullptr, nullptr };
+ }
+}
+
+} // namespace ceph::crypto::onwire