From b2b06ea7b3372ea8123beb22e465f33fc1971b98 Mon Sep 17 00:00:00 2001 From: Radoslaw Zarzynski Date: Tue, 5 Feb 2019 23:39:05 +0100 Subject: [PATCH] auth: implement ceph::crypto::onwire with OpenSSL EVP. Signed-off-by: Radoslaw Zarzynski --- src/include/buffer.h | 5 +- src/msg/CMakeLists.txt | 1 + src/msg/async/crypto_onwire.cc | 311 +++++++++++++++++++++++++++++++++ src/msg/async/crypto_onwire.h | 15 +- 4 files changed, 326 insertions(+), 6 deletions(-) create mode 100644 src/msg/async/crypto_onwire.cc diff --git a/src/include/buffer.h b/src/include/buffer.h index a0faaeee03546..e5a581ca5b0b5 100644 --- a/src/include/buffer.h +++ b/src/include/buffer.h @@ -883,9 +883,12 @@ inline namespace v14_2_0 { contiguous_filler(char* const pos) : pos(pos) {} public: + void advance(const unsigned len) { + pos += len; + } void copy_in(const unsigned len, const char* const src) { memcpy(pos, src, len); - pos += len; + advance(len); } char* c_str() { return pos; diff --git a/src/msg/CMakeLists.txt b/src/msg/CMakeLists.txt index 136980303986b..647bbb80e8361 100644 --- a/src/msg/CMakeLists.txt +++ b/src/msg/CMakeLists.txt @@ -28,6 +28,7 @@ list(APPEND msg_srcs async/EventSelect.cc async/PosixStack.cc async/Stack.cc + async/crypto_onwire.cc async/net_handler.cc) if(LINUX) diff --git a/src/msg/async/crypto_onwire.cc b/src/msg/async/crypto_onwire.cc new file mode 100644 index 0000000000000..a664a39912960 --- /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 + +#include "crypto_onwire.h" + +#include "common/debug.h" +#include "include/types.h" + +#define dout_subsys ceph_subsys_ms + +// 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 { + 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}; + + using nonce_t = std::array; + + CephContext* const cct; + std::unique_ptr ectx; + ceph::bufferlist buffer; + // using GCC's "Tetra Integer" mode here + ceph::uint128_t nonce; + +public: + AES128GCM_OnWireTxHandler(CephContext* const cct, + const AuthConnectionMeta& auth_meta, + const ceph::uint128_t& nonce) + : cct(cct), + ectx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free), + nonce(nonce) { + ceph_assert_always(auth_meta.connection_secret.length() >= AESGCM_KEY_LEN); + ceph_assert_always(ectx); + + 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, + reinterpret_cast(auth_meta.connection_secret.c_str()), + nullptr)) { + //reinterpret_cast(&nonce))) { + throw std::runtime_error("EVP_EncryptInit_ex failed"); + } + } + + ~AES128GCM_OnWireTxHandler() override { + memset(&nonce, 0, sizeof(nonce)); + } + + std::uint32_t calculate_segment_size(std::uint32_t size) override + { + return size; + } + + void reset_tx_handler( + std::initializer_list update_size_sequence) override; + + ceph::bufferlist::contiguous_filler reserve(const std::uint32_t size) override { + return buffer.append_hole(size); + } + + void authenticated_encrypt_update(const ceph::bufferlist& plaintext) override; + ceph::bufferlist authenticated_encrypt_final() override; +}; + +void AES128GCM_OnWireTxHandler::reset_tx_handler( + std::initializer_list update_size_sequence) +{ + if(1 != EVP_EncryptInit_ex(ectx.get(), nullptr, nullptr, nullptr, + reinterpret_cast(&nonce))) { + throw std::runtime_error("EVP_EncryptInit_ex failed"); + } + + buffer.reserve(std::accumulate(std::begin(update_size_sequence), + std::end(update_size_sequence), AESGCM_TAG_LEN)); + + // transplantate the nonce update management from OpenVPN's AES-GCM + nonce += 1ULL << 32; +} + +void AES128GCM_OnWireTxHandler::authenticated_encrypt_update( + const ceph::bufferlist& plaintext) +{ + 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(filler.c_str()), + &update_len, + reinterpret_cast(plainbuf.c_str()), + plainbuf.length())) { + throw std::runtime_error("EVP_EncryptUpdate failed"); + } + ceph_assert_always(update_len >= 0); + ceph_assert(static_cast(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; + auto filler = buffer.append_hole(AESGCM_BLOCK_LEN); + if(1 != EVP_EncryptFinal_ex(ectx.get(), + reinterpret_cast(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 { + 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}; + + using nonce_t = std::array; + + CephContext* const cct; + std::unique_ptr ectx; + // using GCC's "Tetra Integer" mode here + ceph::uint128_t nonce; + +public: + AES128GCM_OnWireRxHandler(CephContext* const cct, + const AuthConnectionMeta& auth_meta, + const ceph::uint128_t& nonce) + : cct(cct), + ectx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free), + nonce(nonce) + { + ceph_assert_always(auth_meta.connection_secret.length() >= AESGCM_KEY_LEN); + ceph_assert_always(ectx); + + 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, + reinterpret_cast(auth_meta.connection_secret.c_str()), + nullptr)) { + throw std::runtime_error("EVP_DecryptInit_ex failed"); + } + } + + ~AES128GCM_OnWireRxHandler() override { + memset(&nonce, 0, sizeof(nonce)); + } + + void reset_rx_handler() override; + ceph::bufferlist authenticated_decrypt_update( + ceph::bufferlist&& ciphertext, + std::uint32_t alignment) override; + ceph::bufferlist authenticated_decrypt_update_final( + ceph::bufferlist&& ciphertext, + std::uint32_t alignment) override; +}; + +void AES128GCM_OnWireRxHandler::reset_rx_handler() +{ + if(1 != EVP_DecryptInit_ex(ectx.get(), nullptr, nullptr, nullptr, + reinterpret_cast(&nonce))) { + throw std::runtime_error("EVP_DecryptInit_ex failed"); + } + nonce += 1ULL << 32; +} + +ceph::bufferlist AES128GCM_OnWireRxHandler::authenticated_decrypt_update( + ceph::bufferlist&& ciphertext, + std::uint32_t alignment) +{ + ceph_assert(ciphertext.length() > 0); + //ceph_assert(ciphertext.length() % AESGCM_BLOCK_LEN == 0); + + // NOTE: we might consider in-place transformations in the future. AFAIK + // OpenSSL's might sustain that but lack of clear confirmation postpones. + auto plainnode = ceph::buffer::ptr_node::create(buffer::create_aligned( + ciphertext.length(), alignment)); + auto* plainbuf = reinterpret_cast(plainnode->c_str()); + + for (const auto& cipherbuf : ciphertext.buffers()) { + // XXX: Why int? + int update_len = 0; + + if (1 != EVP_DecryptUpdate(ectx.get(), + plainbuf, + &update_len, + reinterpret_cast(cipherbuf.c_str()), + cipherbuf.length())) { + throw std::runtime_error("EVP_DecryptUpdate failed"); + } + ceph_assert_always(update_len >= 0); + ceph_assert(cipherbuf.length() == static_cast(update_len)); + + plainbuf += update_len; + } + + ceph::bufferlist outbl; + outbl.push_back(std::move(plainnode)); + return std::move(outbl); +} + + +ceph::bufferlist AES128GCM_OnWireRxHandler::authenticated_decrypt_update_final( + ceph::bufferlist&& ciphertext_and_tag, + std::uint32_t alignment) +{ + const auto cnt_len = ciphertext_and_tag.length(); + ceph_assert(cnt_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 plainbl; + ceph::bufferlist auth_tag; + { + const auto tag_off = cnt_len - AESGCM_TAG_LEN; + ceph::bufferlist ciphertext; + ciphertext_and_tag.splice(0, tag_off, &ciphertext); + + // the rest is the signature (a.k.a auth tag) + auth_tag = std::move(ciphertext_and_tag); + + if (ciphertext.length()) { + plainbl = authenticated_decrypt_update(std::move(ciphertext), alignment); + } + } + + // 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)) { + ldout(cct, 15) << __func__ + << " plainbl.length()=" << plainbl.length() + << " final_len=" << final_len + << dendl; + throw std::runtime_error("EVP_DecryptFinal_ex failed"); + } else { + ceph_assert_always(final_len == 0); + ceph_assert_always(plainbl.length() + final_len + AESGCM_TAG_LEN == cnt_len); + } + } + + return plainbl; +} + +ceph::crypto::onwire::rxtx_t ceph::crypto::onwire::rxtx_t::create_handler_pair( + CephContext* cct, + const AuthConnectionMeta& auth_meta, + bool crossed) +{ + if (auth_meta.is_mode_secure()) { + // CLEANME, CLEANME CLEANME + ceph_assert_always( + auth_meta.connection_secret.length() >= 16 + 2*sizeof(ceph::uint128_t)); + + ceph::uint128_t rx_nonce; + ::memcpy(&rx_nonce, auth_meta.connection_secret.c_str() + 16, sizeof(rx_nonce)); + + ceph::uint128_t tx_nonce; + ::memcpy(&tx_nonce, auth_meta.connection_secret.c_str() + 16 + sizeof(rx_nonce), + sizeof(tx_nonce)); + return { + std::make_unique( + cct, auth_meta, crossed ? tx_nonce : rx_nonce), + std::make_unique( + cct, auth_meta, crossed ? rx_nonce : tx_nonce) + }; + } else { + return { nullptr, nullptr }; + } +} diff --git a/src/msg/async/crypto_onwire.h b/src/msg/async/crypto_onwire.h index d65a85f1c2a8d..64e1db99d24ae 100644 --- a/src/msg/async/crypto_onwire.h +++ b/src/msg/async/crypto_onwire.h @@ -16,8 +16,11 @@ #ifndef CEPH_CRYPTO_ONWIRE_H #define CEPH_CRYPTO_ONWIRE_H -#include "include/types.h" +#include +#include +#include "auth/Auth.h" +#include "include/buffer.h" namespace ceph::math { @@ -64,7 +67,7 @@ struct TxHandler { // bufferlist. The method MUST NOT be called after _final() if there // was no call to _reset(). virtual void authenticated_encrypt_update( - ceph::bufferlist&& plaintext) = 0; + const ceph::bufferlist& plaintext) = 0; // Generates authentication signature and returns bufferlist crafted // basing on plaintext from preceding call to _update(). @@ -100,11 +103,13 @@ struct rxtx_t { // Hmm, isn't that too much flexbility? std::unique_ptr rx; std::unique_ptr tx; + + static rxtx_t create_handler_pair( + CephContext* ctx, + const class AuthConnectionMeta& auth_meta, + bool crossed); }; -static rxtx_t create_stream_handler_pair( - CephContext* ctx, - const class AuthConnectionMeta& auth_meta); } // namespace ceph::crypto::onwire -- 2.39.5