]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
common: Add Linux Keyring Secret Store Wrapper
authorMarcel Lauhoff <marcel.lauhoff@clyso.com>
Fri, 25 Apr 2025 14:27:57 +0000 (16:27 +0200)
committerMarcel Lauhoff <marcel.lauhoff@clyso.com>
Mon, 1 Jun 2026 16:43:29 +0000 (18:43 +0200)
Add RAII wrapper around the Linux Key Retention Service
add_key(2), keyctl_read(3), keyctl_invalidate(3)

Signed-off-by: Marcel Lauhoff <marcel.lauhoff@clyso.com>
On-behalf-of: SAP marcel.lauhoff@sap.com

src/common/CMakeLists.txt
src/common/keyring.cc [new file with mode: 0644]
src/common/keyring.h [new file with mode: 0644]
src/test/CMakeLists.txt
src/test/common/CMakeLists.txt
src/test/common/test_keyring.cc [new file with mode: 0644]

index fa6771c2357b7f14d7f6ad7df09c321b8db62ce7..8f8f1dd0ba0ffe00a3e893a0bb4b414cbe54ce3b 100644 (file)
@@ -289,4 +289,9 @@ if(HAVE_KEYUTILS)
   set(parse_secret_srcs
     secret.c)
   add_library(parse_secret_objs OBJECT ${parse_secret_srcs})
+
+  set(keyring_srcs
+    keyring.cc)
+  add_library(keyring STATIC ${keyring_srcs})
+  target_link_libraries(keyring keyutils::keyutils)
 endif()
diff --git a/src/common/keyring.cc b/src/common/keyring.cc
new file mode 100644 (file)
index 0000000..860ec91
--- /dev/null
@@ -0,0 +1,143 @@
+
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2025 Clyso GmbH
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation.  See file COPYING.
+ *
+ */
+
+#include "keyring.h"
+
+#ifndef _WIN32
+extern "C" {
+#include <keyutils.h>
+}
+#endif
+
+namespace ceph {
+
+#ifdef _WIN32
+
+LinuxKeyringSecret::LinuxKeyringSecret(key_serial_t serial, size_t len) noexcept
+    : _len(len), _serial(serial) {}
+
+LinuxKeyringSecret::~LinuxKeyringSecret() noexcept {}
+
+tl::expected<LinuxKeyringSecret, std::error_code> LinuxKeyringSecret::add(
+    const std::string& key, const std::string& secret) noexcept {
+  return tl::unexpected(std::error_code(-ENOSYS, std::system_category()));
+}
+
+bool LinuxKeyringSecret::supported() noexcept {
+  return false;
+}
+
+void LinuxKeyringSecret::initialize_process_keyring() noexcept {
+}
+
+[[nodiscard]] std::error_code LinuxKeyringSecret::read(std::string& out) const {
+  return {-ENOSYS, std::system_category()};
+}
+
+[[nodiscard]] std::error_code LinuxKeyringSecret::remove() const {
+  return {-ENOSYS, std::system_category()};
+}
+
+[[nodiscard]] std::error_code LinuxKeyringSecret::reset() {
+  return {-ENOSYS, std::system_category()};
+}
+
+#else
+
+LinuxKeyringSecret::LinuxKeyringSecret(key_serial_t serial, size_t len) noexcept
+    : _len(len), _serial(serial) {}
+
+LinuxKeyringSecret::~LinuxKeyringSecret() noexcept {
+  reset();
+}
+
+tl::expected<LinuxKeyringSecret, std::error_code> LinuxKeyringSecret::add(
+    const std::string& key, const std::string& secret) noexcept {
+  const auto serial = add_key(
+      "user", key.c_str(), secret.c_str(), secret.size(),
+      KEY_SPEC_PROCESS_KEYRING);
+  if (serial == -1) {
+    return tl::unexpected(std::error_code(errno, std::system_category()));
+  }
+  return LinuxKeyringSecret(serial, secret.size());
+}
+
+bool LinuxKeyringSecret::supported(std::error_code* ec) noexcept {
+  auto secret = LinuxKeyringSecret::add("ceph_test_keyring_support", "ceph");
+  if (!secret) {
+    if (ec != nullptr) {
+      *ec = secret.error();
+    }
+    return false;
+  }
+  std::string out;
+  if (auto err = secret.value().read(out); err) {
+    if (ec != nullptr) {
+      *ec = err;
+    }
+    return false;
+  }
+  if (auto err = secret.value().reset(); err) {
+    if (ec != nullptr) {
+      *ec = err;
+    }
+    return false;
+  }
+  return true;
+}
+
+void LinuxKeyringSecret::initialize_process_keyring() noexcept {
+  keyctl_get_keyring_ID(KEY_SPEC_PROCESS_KEYRING, 1);
+}
+
+[[nodiscard]] std::error_code LinuxKeyringSecret::read(std::string& out) const {
+  out.clear();
+  out.resize(_len);
+  const auto ret = keyctl_read(_serial, out.data(), _len);
+  if (ret == -1) {
+    return {errno, std::system_category()};
+  } else if (static_cast<size_t>(ret) != _len) {
+    return {-EINVAL, std::generic_category()};
+  }
+  return {};
+}
+
+[[nodiscard]] std::error_code LinuxKeyringSecret::remove() const {
+  const auto ret = keyctl_invalidate(_serial);
+  if (ret != 0) {
+    return {errno, std::system_category()};
+  }
+  return {};
+}
+
+[[nodiscard]] std::error_code LinuxKeyringSecret::reset() {
+  if (_serial == -1) {
+    return {-EINVAL, std::generic_category()};
+  }
+  if (const auto ret = remove(); !ret) {
+    return ret;
+  }
+  _serial = -1;
+  _len = 0;
+  return {};
+}
+
+[[nodiscard]] bool LinuxKeyringSecret::initialized() const {
+  return _len > 0 && _serial != -1;
+}
+
+#endif
+
+}  // namespace ceph
diff --git a/src/common/keyring.h b/src/common/keyring.h
new file mode 100644 (file)
index 0000000..63fe29d
--- /dev/null
@@ -0,0 +1,84 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2025 Clyso GmbH
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation.  See file COPYING.
+ *
+ */
+
+#pragma once
+
+#include <ostream>
+#include <system_error>
+#include <utility>
+
+#include "fmt/ostream.h"
+#include "include/expected.hpp"
+
+namespace ceph {
+
+/// RAII style wrapper around a secret stored in the Linux Key
+/// Retention Service.
+/// See: add_key(2), keyctl_read(3), keyctl_invalidate(3)
+class LinuxKeyringSecret {
+  size_t _len;
+  int _serial;
+
+  explicit LinuxKeyringSecret(int serial, size_t len) noexcept;
+ public:
+  LinuxKeyringSecret() = delete;
+  LinuxKeyringSecret(const LinuxKeyringSecret&) = delete;
+  LinuxKeyringSecret& operator=(const LinuxKeyringSecret&) = delete;
+  LinuxKeyringSecret(LinuxKeyringSecret&& other) noexcept
+      : _len(std::exchange(other._len, 0)),
+        _serial(std::exchange(other._serial, -1)) {};
+  LinuxKeyringSecret& operator=(LinuxKeyringSecret&& other) noexcept {
+    if (this != &other) {
+      if (this->initialized()) {
+       this->reset();
+      }
+      _len = std::exchange(other._len, 0);
+      _serial = std::exchange(other._serial, -1);
+    }
+    return *this;
+  };
+
+  ~LinuxKeyringSecret() noexcept;
+
+  // Add a secret to the kernel process keyring. Beware: calling this
+  // with with an existing key _updates_ the value and causes multiple
+  // instances to refer to the same key.
+  static tl::expected<LinuxKeyringSecret, std::error_code> add(
+      const std::string& key, const std::string& secret) noexcept;
+  static bool supported(std::error_code* ec = nullptr) noexcept;
+  // Initialize the process keyring. Do this before starting any
+  // threads that want to share possession of keys in the process
+  // keyring
+  static void initialize_process_keyring() noexcept;
+  [[nodiscard]] std::error_code read(std::string& out) const;
+  [[nodiscard]] std::error_code remove() const;
+  [[nodiscard]] bool initialized() const;
+ private:
+  std::error_code reset();
+
+  friend std::ostream& operator<<(
+      std::ostream& os, const LinuxKeyringSecret& secret) {
+    if (secret._serial == -1) {
+      return os << "LinuxKeyringSecret{}";
+    }
+    return os << "LinuxKeyringSecret{" << secret._serial << "}";
+  }
+  friend class LinuxKeyringTest_LifecycleMoveAssignResetsDestination_Test;
+};
+}  // namespace ceph
+
+#if FMT_VERSION >= 90000
+template <>
+struct fmt::formatter<ceph::LinuxKeyringSecret> : fmt::ostream_formatter {};
+#endif
index 91b488adea64b22211c41d1a8aaa3e4bc281686c..85d462c70c68e338fa1cfe906eb78ebbdeda42e3 100644 (file)
@@ -1095,7 +1095,7 @@ if (benchmark_FOUND)
   add_executable(bench_secmem
   bench_secmem.cc
   )
-  target_link_libraries(bench_secmem ceph-common global-static benchmark::benchmark keyutils)
+  target_link_libraries(bench_secmem ceph-common global-static benchmark::benchmark keyutils::keyutils)
 else()
   message(STATUS "The google/benchmark library was not found. Skipping micro benchmark tests")
 endif(benchmark_FOUND)
index 655bbe9a6de319c03459d0afd7fc04ed531b3132..0765f0c2efd628f88f26af60110e31d1653145b2 100644 (file)
@@ -121,6 +121,15 @@ add_executable(unittest_web_cache
 add_ceph_unittest(unittest_web_cache)
 target_link_libraries(unittest_web_cache ceph-common)
 
+if(HAVE_KEYUTILS)
+  # unittest_web_cache
+  add_executable(unittest_keyring
+    test_keyring.cc
+    )
+  add_ceph_unittest(unittest_keyring)
+  target_link_libraries(unittest_keyring global keyring)
+endif()
+
 # unittest_sloppy_crc_map
 add_executable(unittest_sloppy_crc_map
   test_sloppy_crc_map.cc
diff --git a/src/test/common/test_keyring.cc b/src/test/common/test_keyring.cc
new file mode 100644 (file)
index 0000000..ced857e
--- /dev/null
@@ -0,0 +1,65 @@
+#include <common/keyring.h>
+#include <gmock/gmock-matchers.h>
+#include <gtest/gtest.h>
+extern "C" {
+#include <keyutils.h>
+}
+
+namespace ceph {
+
+class LinuxKeyringTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    std::error_code ec;
+    if (!LinuxKeyringSecret::supported(&ec)) {
+      GTEST_SKIP() << "Linux Keyring is unsupported. " << ec
+                   << ". Skipping test";
+    }
+  }
+};
+
+TEST_F(LinuxKeyringTest, Basics) {
+  std::string secret("secret");
+  auto maybe_keyring_secret = LinuxKeyringSecret::add("testkey", secret);
+
+  ASSERT_TRUE(maybe_keyring_secret.has_value()) << maybe_keyring_secret.error();
+  auto keyring_secret = std::move(maybe_keyring_secret.value());
+
+  std::string out;
+  ASSERT_FALSE(keyring_secret.read(out));
+  ASSERT_EQ(secret, out);
+
+  ASSERT_FALSE(keyring_secret.remove());
+  ASSERT_TRUE(keyring_secret.read(out));
+}
+
+TEST_F(LinuxKeyringTest, Lifecycle) {
+  std::string secret("secret");
+  auto uut = LinuxKeyringSecret::add("testkey", secret);
+  ASSERT_TRUE(uut.has_value());
+  ASSERT_TRUE(uut->initialized());
+  auto next = std::move(uut);
+  ASSERT_FALSE(uut->initialized());
+  ASSERT_TRUE(next->initialized());
+}
+
+TEST_F(LinuxKeyringTest, LifecycleMoveAssignResetsDestination) {
+  std::string secret("secret");
+  auto dest = LinuxKeyringSecret::add("testkey", secret);
+  auto source = LinuxKeyringSecret::add("testkey2", secret);
+  ASSERT_TRUE(dest.has_value());
+  ASSERT_TRUE(source.has_value());
+
+  auto dest_serial = dest->_serial;
+  ASSERT_NE(-1, dest_serial);
+
+  dest = std::move(source);
+
+  std::array<char, 1024> buf = {0};
+  auto ret = keyctl_describe(dest_serial, buf.data(), buf.size());
+
+  EXPECT_EQ(-1, ret) << buf.data();
+  ASSERT_EQ(ENOKEY, errno);
+}
+
+}  // namespace ceph