/// @file dpf/net/identity.hpp /// @brief Party identity keys (Ed25519) and their text and file forms. /// @details A party is identified by a raw 32-byte Ed25519 public key, written /// as 44 characters of base64 (the same shape as a WireGuard key). A /// key file holds the 32-byte private seed as one base64 line and is /// created mode 0600. TLS needs a certificate, so `identity` also /// carries a self-signed certificate generated in memory from the /// key; peers check the key inside it, never the certificate fields. /// `development()` is a fixed, publicly known identity: it keeps the /// client path working with no configuration and provides no security. #ifndef LIBDPF_INCLUDE_DPF_NET_IDENTITY_HPP__ #define LIBDPF_INCLUDE_DPF_NET_IDENTITY_HPP__ #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef DPF_HAS_OPENSSL #if defined(__has_include) #if __has_include() #define DPF_HAS_OPENSSL 1 #endif #endif #endif #ifndef DPF_HAS_OPENSSL #define DPF_HAS_OPENSSL 0 #endif #if DPF_HAS_OPENSSL #include #include #include #include #endif #include "dpf/log.hpp" namespace dpf { namespace net { namespace detail { inline std::string base64_encode(const std::uint8_t * p, std::size_t n) { static const char tab[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::string out; out.reserve((n + 2) / 3 * 4); for (std::size_t i = 0; i < n; i += 3) { const std::uint32_t a = p[i]; const std::uint32_t b = i + 1 < n ? p[i + 1] : 0; const std::uint32_t c = i + 2 < n ? p[i + 2] : 0; const std::uint32_t v = (a << 16) | (b << 8) | c; out += tab[(v >> 18) & 63]; out += tab[(v >> 12) & 63]; out += i + 1 < n ? tab[(v >> 6) & 63] : '='; out += i + 2 < n ? tab[v & 63] : '='; } return out; } /// @brief Strict base64 (standard alphabet, padded). Returns false on any /// malformed input. inline bool base64_decode(const std::string & s, std::string & out) { auto val = [](char ch) -> int { if (ch >= 'A' && ch <= 'Z') return ch - 'A'; if (ch >= 'a' && ch <= 'z') return ch - 'a' + 26; if (ch >= '0' && ch <= '9') return ch - '0' + 52; if (ch == '+') return 62; if (ch == '/') return 63; return -1; }; out.clear(); if (s.size() % 4 != 0) return false; for (std::size_t i = 0; i < s.size(); i += 4) { const bool last = i + 4 == s.size(); int v[4]; for (int k = 0; k < 4; ++k) { const char ch = s[i + k]; if (ch == '=' && last && k >= 2) v[k] = -2; else v[k] = val(ch); if (v[k] == -1) return false; } if (v[0] < 0 || v[1] < 0 || (v[2] == -2 && v[3] != -2)) return false; const std::uint32_t x = (static_cast(v[0]) << 18) | (static_cast(v[1]) << 12) | (static_cast(v[2] < 0 ? 0 : v[2]) << 6) | static_cast(v[3] < 0 ? 0 : v[3]); out += static_cast((x >> 16) & 0xff); if (v[2] >= 0) out += static_cast((x >> 8) & 0xff); if (v[3] >= 0) out += static_cast(x & 0xff); } return true; } /// @brief First line of `path` that is neither blank nor a `#` comment. inline std::string first_key_line(const std::string & path, const char * what) { std::ifstream in(path); if (!in) throw std::runtime_error(std::string(what) + ": cannot read '" + path + "'"); std::string line; while (std::getline(in, line)) { while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) line.pop_back(); std::size_t b = 0; while (b < line.size() && (line[b] == ' ' || line[b] == '\t')) ++b; line.erase(0, b); if (!line.empty() && line[0] != '#') return line; } throw std::runtime_error(std::string(what) + ": '" + path + "' holds no key"); } #if DPF_HAS_OPENSSL inline std::string openssl_error(const char * what) { std::string out = what; unsigned long e = 0; bool first = true; while ((e = ERR_get_error()) != 0) { char buf[256]; ERR_error_string_n(e, buf, sizeof(buf)); out += first ? ": " : "; "; out += buf; first = false; } return out; } struct pkey_free { void operator()(EVP_PKEY * k) const noexcept { EVP_PKEY_free(k); } }; struct x509_free { void operator()(X509 * x) const noexcept { X509_free(x); } }; #endif } // namespace detail /// @brief A party's raw Ed25519 public key. struct public_key { static constexpr std::size_t size = 32; std::array bytes{}; /// @brief 44 characters of base64. std::string base64() const { return detail::base64_encode(bytes.data(), size); } /// @brief Parse base64, or read a public-key file given as `file:PATH`. static public_key parse(const std::string & text) { std::string s = text; if (s.rfind("file:", 0) == 0) s = detail::first_key_line(s.substr(5), "public key"); std::string raw; if (!detail::base64_decode(s, raw) || raw.size() != size) throw std::invalid_argument("public key must be 32 bytes of base64 " "(44 characters), got '" + s + "'"); public_key k; std::memcpy(k.bytes.data(), raw.data(), size); return k; } friend bool operator==(const public_key & a, const public_key & b) noexcept { return a.bytes == b.bytes; } friend bool operator!=(const public_key & a, const public_key & b) noexcept { return !(a == b); } }; /// @brief A party's private key plus the self-signed certificate TLS presents. /// @details Copies share the key. Without OpenSSL every constructor throws. class identity { public: /// @brief A fresh random key. static identity generate() { #if DPF_HAS_OPENSSL std::uint8_t seed[32]; if (RAND_bytes(seed, sizeof(seed)) != 1) throw std::runtime_error(detail::openssl_error("identity: RAND_bytes")); auto id = from_seed(seed); OPENSSL_cleanse(seed, sizeof(seed)); return id; #else throw std::logic_error("identity: built without OpenSSL"); #endif } /// @brief The key whose private seed is `seed`. static identity from_seed(const std::uint8_t * seed, const char * cn = "libdpf party") { #if DPF_HAS_OPENSSL identity id; EVP_PKEY * k = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, seed, 32); if (k == nullptr) throw std::runtime_error(detail::openssl_error("identity: bad Ed25519 seed")); id.key_.reset(k, detail::pkey_free{}); std::size_t n = public_key::size; if (EVP_PKEY_get_raw_public_key(k, id.pub_.bytes.data(), &n) != 1 || n != public_key::size) throw std::runtime_error(detail::openssl_error("identity: public key")); id.cert_ = self_signed(k, cn); return id; #else (void)seed; (void)cn; throw std::logic_error("identity: built without OpenSSL"); #endif } /// @brief Read a key file (one base64 line holding the 32-byte seed). /// @details Warns when the file is readable by group or others. static identity load(const std::string & path) { std::string raw; const auto line = detail::first_key_line(path, "identity"); if (!detail::base64_decode(line, raw) || raw.size() != 32) throw std::invalid_argument("identity: '" + path + "' is not a key file (expected 32 bytes of base64)"); struct stat st{}; if (::stat(path.c_str(), &st) == 0 && (st.st_mode & 077) != 0) DPF_LOG(warning, "security.key_file_mode").kv("path", path) .kv("detail", "identity key file is readable by group or others; " "chmod 600 it"); auto id = from_seed(reinterpret_cast(raw.data())); std::fill(raw.begin(), raw.end(), '\0'); return id; } /// @brief The fixed development identity. Its private key is in this /// source file, so it authenticates nothing. static const identity & development() { static const identity dev = [] { #if DPF_HAS_OPENSSL static const char phrase[] = "libdpf development identity (public; provides no security)"; std::uint8_t seed[32]; unsigned int n = sizeof(seed); if (EVP_Digest(phrase, sizeof(phrase) - 1, seed, &n, EVP_sha256(), nullptr) != 1) throw std::runtime_error(detail::openssl_error("identity: digest")); auto id = from_seed(seed, "libdpf development certificate (no security)"); id.development_ = true; return id; #else return identity(); #endif }(); if (!dev.key_) throw std::logic_error("identity: built without OpenSSL"); return dev; } /// @brief Write the private seed to a new file with mode 0600. Refuses to /// replace an existing file unless `overwrite`. void save(const std::string & path, bool overwrite = false) const { #if DPF_HAS_OPENSSL std::uint8_t seed[32]; std::size_t n = sizeof(seed); if (!key_ || EVP_PKEY_get_raw_private_key(key_.get(), seed, &n) != 1 || n != 32) throw std::runtime_error(detail::openssl_error("identity: private key")); const std::string text = "# libdpf identity key (private; keep mode 600)\n" + detail::base64_encode(seed, sizeof(seed)) + "\n"; OPENSSL_cleanse(seed, sizeof(seed)); const int flags = O_WRONLY | O_CREAT | O_CLOEXEC | (overwrite ? O_TRUNC : O_EXCL); const int fd = ::open(path.c_str(), flags, 0600); if (fd < 0) throw std::runtime_error("identity: cannot create '" + path + "': " + std::strerror(errno)); const bool ok = ::fchmod(fd, 0600) == 0 && ::write(fd, text.data(), text.size()) == static_cast(text.size()); ::close(fd); if (!ok) throw std::runtime_error("identity: cannot write '" + path + "'"); #else (void)path; (void)overwrite; throw std::logic_error("identity: built without OpenSSL"); #endif } const public_key & key() const noexcept { return pub_; } bool is_development() const noexcept { return development_; } #if DPF_HAS_OPENSSL EVP_PKEY * pkey() const noexcept { return key_.get(); } X509 * cert() const noexcept { return cert_.get(); } #endif private: identity() = default; #if DPF_HAS_OPENSSL static std::shared_ptr self_signed(EVP_PKEY * k, const char * cn) { std::shared_ptr x(X509_new(), detail::x509_free{}); if (!x) throw std::runtime_error(detail::openssl_error("identity: X509_new")); std::uint8_t serial[8]; if (RAND_bytes(serial, sizeof(serial)) != 1) throw std::runtime_error(detail::openssl_error("identity: serial")); std::uint64_t s = 0; for (auto b : serial) s = (s << 8) | b; s &= 0x7fffffffffffffffull; bool ok = X509_set_version(x.get(), 2) == 1 && ASN1_INTEGER_set_uint64(X509_get_serialNumber(x.get()), s) == 1 && X509_gmtime_adj(X509_getm_notBefore(x.get()), -86400) != nullptr && X509_time_adj_ex(X509_getm_notAfter(x.get()), 36500, 0, nullptr) != nullptr && X509_set_pubkey(x.get(), k) == 1; X509_NAME * name = X509_get_subject_name(x.get()); ok = ok && name != nullptr && X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast(cn), -1, -1, 0) == 1 && X509_set_issuer_name(x.get(), name) == 1 && X509_sign(x.get(), k, nullptr) > 0; if (!ok) throw std::runtime_error(detail::openssl_error("identity: certificate")); return x; } std::shared_ptr key_; std::shared_ptr cert_; #else std::shared_ptr key_; #endif public_key pub_{}; bool development_ = false; }; } // namespace net } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_NET_IDENTITY_HPP__