From e78a95f5da0d13bc0e206424f096434ba4d2f7ca Mon Sep 17 00:00:00 2001 From: Radoslaw Zarzynski Date: Thu, 24 Jan 2019 01:34:31 +0100 Subject: [PATCH] crypto: AES128GCM_StreamHandler brings authenticated encryption with AES-GCM. Signed-off-by: Radoslaw Zarzynski --- src/auth/AuthSessionHandler.cc | 230 ++++++++++++++++++++++++++++++++- src/msg/async/ProtocolV2.cc | 3 + 2 files changed, 229 insertions(+), 4 deletions(-) diff --git a/src/auth/AuthSessionHandler.cc b/src/auth/AuthSessionHandler.cc index 28e466898c6..479171f77b0 100644 --- a/src/auth/AuthSessionHandler.cc +++ b/src/auth/AuthSessionHandler.cc @@ -240,12 +240,234 @@ public: }; +#ifdef USE_OPENSSL +# include +#endif + +// 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 +class AES128GCM_StreamHandler : public AuthStreamHandler { + CephContext* const cct; + std::unique_ptr ectx; + + static constexpr const std::size_t AES_KEY_LEN{16}; + static constexpr const std::size_t STREAM_AES_IV_LEN{16}; + static constexpr const std::size_t STREAM_GCM_TAG_LEN{16}; + static constexpr const std::size_t AES_BLOCK_LEN{16}; + + std::array connection_secret; + + // using GCC's "Tetra Integer" mode here + ceph::uint128_t nonce; + static_assert(sizeof(nonce) == STREAM_AES_IV_LEN); + +public: + AES128GCM_StreamHandler(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() >= AES_KEY_LEN); + ceph_assert_always(ectx); + + std::copy_n(std::cbegin(auth_meta.connection_secret), AES_KEY_LEN, + std::begin(connection_secret)); + //EVP_CIPHER_CTX_reset(ectx.get()); + } + + ~AES128GCM_StreamHandler() override { + memset(&nonce, 0, sizeof(nonce)); + //connection_secret.zero(); + } + + std::size_t calculate_padded_size(const std::size_t length) { + // we need to take into account the PKCS#7 padding. There *always* will + // be at least one byte of padding. This stays even to input aligned to + // AES_BLOCK_LEN. Otherwise we would face ambiguities during decryption. + // To exemplify: + // length := 10 -> p2align(10, 16) + 16 == 16 (06 bytes for padding), + // length := 16 -> p2align(16, 16) + 16 == 32 (16 bytes for padding). + // + // Technically padding will be added by OpenSSL but its format and even + // presence is a part of the public interface, I believe. + return p2align(length, AES_BLOCK_LEN) + AES_BLOCK_LEN; + } + + std::size_t calculate_payload_size(const std::size_t length) override { + // as we're doing *authenticated encryption* additional space is needed + // to store Auth Tag. OpenSSL defaults this to 96 bits (12 bytes). + const std::size_t authenticated_and_encrypted_size = \ + calculate_padded_size(length) + STREAM_GCM_TAG_LEN; + return authenticated_and_encrypted_size; + } + + void authenticated_encrypt(ceph::bufferlist& payload) override; + void authenticated_decrypt(char* payload, uint32_t& length) override; +}; + +void AES128GCM_StreamHandler::authenticated_encrypt(ceph::bufferlist& payload) +{ + 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_CIPHER_CTX_ctrl(ectx.get(), EVP_CTRL_GCM_SET_IVLEN, + STREAM_AES_IV_LEN, nullptr) ) { + throw std::runtime_error("EVP_CIPHER_CTX_ctrl failed"); + } + + if(1 != EVP_EncryptInit_ex(ectx.get(), nullptr, nullptr, + reinterpret_cast(connection_secret.data()), + reinterpret_cast(&nonce))) { + throw std::runtime_error("EVP_EncryptInit_ex failed"); + } + + // TODO: consider in-place transformata + auto out_tmp = \ + ceph::buffer::ptr_node::create(calculate_payload_size(payload.length())); + + // append necessary padding accordingly to PKCS#7 + { + const auto padding_size = \ + calculate_padded_size(payload.length()) - payload.length(); + auto filler = payload.append_hole(padding_size); + ::memset(filler.c_str(), static_cast(padding_size), padding_size); + } + + int update_len = 0; + if(1 != EVP_EncryptUpdate(ectx.get(), + reinterpret_cast(out_tmp->c_str()), + &update_len, + reinterpret_cast(payload.c_str()), + payload.length())) { + throw std::runtime_error("EVP_EncryptUpdate failed"); + } + + int final_len = 0; + if(1 != EVP_EncryptFinal_ex(ectx.get(), + reinterpret_cast(out_tmp->c_str() + update_len), + &final_len)) { + throw std::runtime_error("EVP_EncryptFinal_ex failed"); + } + + if(1 != EVP_CIPHER_CTX_ctrl(ectx.get(), + EVP_CTRL_GCM_GET_TAG, STREAM_GCM_TAG_LEN, + out_tmp->c_str() + update_len + final_len)) { + throw std::runtime_error("EVP_CIPHER_CTX_ctrl failed"); + } + +#if 0 + if (1 != EVP_CIPHER_CTX_cleanup(ectx.get())) { + throw std::runtime_error("EVP_CIPHER_CTX_cleanup failed"); + } +#endif + + ldout(cct, 15) << __func__ + << " payload.length()=" << payload.length() + << " out_tmp->length()=" << out_tmp->length() + << " update_len=" << update_len + << " final_len=" << final_len + << dendl; + ceph_assert(update_len + final_len + STREAM_GCM_TAG_LEN == out_tmp->length()); + + payload.clear(); + payload.push_back(std::move(out_tmp)); + nonce++; +} + +void AES128GCM_StreamHandler::authenticated_decrypt( + char* payload, + uint32_t& length) +{ + ceph_assert(length > 0); + ceph_assert(length % AES_BLOCK_LEN == 0); + 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_CIPHER_CTX_ctrl(ectx.get(), EVP_CTRL_GCM_SET_IVLEN, + STREAM_AES_IV_LEN, nullptr) ) { + throw std::runtime_error("EVP_CIPHER_CTX_ctrl failed"); + } + + if(1 != EVP_DecryptInit_ex(ectx.get(), nullptr, nullptr, + reinterpret_cast(connection_secret.data()), + reinterpret_cast(&nonce))) { + throw std::runtime_error("EVP_DecryptInit_ex failed"); + } + + // TODO: consider in-place transformata + auto out_tmp = \ + ceph::buffer::ptr_node::create(length - STREAM_GCM_TAG_LEN); + + int update_len = 0; + if (1 != EVP_DecryptUpdate(ectx.get(), + reinterpret_cast(out_tmp->c_str()), + &update_len, + reinterpret_cast(payload), + length - STREAM_GCM_TAG_LEN)) { + throw std::runtime_error("EVP_DecryptUpdate failed"); + } + + if (1 != EVP_CIPHER_CTX_ctrl(ectx.get(), EVP_CTRL_GCM_SET_TAG, + STREAM_GCM_TAG_LEN, + payload + length - STREAM_GCM_TAG_LEN)) { + throw std::runtime_error("EVP_CIPHER_CTX_ctrl failed"); + } + + int final_len = 0; + if (0 >= EVP_DecryptFinal_ex(ectx.get(), + reinterpret_cast(out_tmp->c_str() + update_len), + &final_len)) { + ldout(cct, 15) << __func__ + << " length=" << length + << " out_tmp->length()=" << out_tmp->length() + << " update_len=" << update_len + << " final_len=" << final_len + << dendl; + throw std::runtime_error("EVP_DecryptFinal_ex failed"); + } else { + ceph_assert_always(update_len + final_len + STREAM_GCM_TAG_LEN == length); + ceph_assert_always((update_len + final_len) % AES_BLOCK_LEN == 0); + + // BE CAREFUL: we cannot expose any single bit of information about + // the cause of failure. Otherwise we'll face padding oracle attack. + // See: https://en.wikipedia.org/wiki/Padding_oracle_attack. + const auto pad_len = \ + std::min((*out_tmp)[update_len + final_len - 1], AES_BLOCK_LEN); + + // TODO: move to a new interface after dropping AES-CBC-HMAC-SHA256 + length = update_len + final_len - pad_len; + memcpy(payload, out_tmp->c_str(), length); + nonce++; + } +} + + AuthStreamHandler::rxtx_t AuthStreamHandler::create_stream_handler_pair( CephContext* cct, const class AuthConnectionMeta& auth_meta) { - return { - std::make_shared(cct, auth_meta), - std::make_shared(cct, auth_meta) - }; + 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_shared(cct, auth_meta, rx_nonce), + std::make_shared(cct, auth_meta, tx_nonce) + }; + } else { + return { nullptr, nullptr }; + } } diff --git a/src/msg/async/ProtocolV2.cc b/src/msg/async/ProtocolV2.cc index 03bd9e941be..e17b7938838 100644 --- a/src/msg/async/ProtocolV2.cc +++ b/src/msg/async/ProtocolV2.cc @@ -1078,6 +1078,8 @@ void ProtocolV2::authencrypt_payload(bufferlist &payload) { // using tx if (session_security.tx) { session_security.tx->authenticated_encrypt(payload); + // padding will be always present + ceph_assert_always(payload.length() > 0); } } @@ -1791,6 +1793,7 @@ CtPtr ProtocolV2::handle_message_complete() { msg_payload.claim_append(extra); uint32_t payload_len = msg_payload.length(); + ceph_assert_always(payload_len > 0); authdecrypt_payload(msg_payload.c_str(), payload_len); front.clear(); -- 2.39.5