Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume. Co-authored-by: Cursor <cursoragent@cursor.com>
378 lines
13 KiB
C++
378 lines
13 KiB
C++
/// @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 <algorithm>
|
|
#include <array>
|
|
#include <cerrno>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#ifndef DPF_HAS_OPENSSL
|
|
#if defined(__has_include)
|
|
#if __has_include(<openssl/ssl.h>)
|
|
#define DPF_HAS_OPENSSL 1
|
|
#endif
|
|
#endif
|
|
#endif
|
|
#ifndef DPF_HAS_OPENSSL
|
|
#define DPF_HAS_OPENSSL 0
|
|
#endif
|
|
|
|
#if DPF_HAS_OPENSSL
|
|
#include <openssl/err.h>
|
|
#include <openssl/evp.h>
|
|
#include <openssl/rand.h>
|
|
#include <openssl/x509.h>
|
|
#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<std::uint32_t>(v[0]) << 18)
|
|
| (static_cast<std::uint32_t>(v[1]) << 12)
|
|
| (static_cast<std::uint32_t>(v[2] < 0 ? 0 : v[2]) << 6)
|
|
| static_cast<std::uint32_t>(v[3] < 0 ? 0 : v[3]);
|
|
out += static_cast<char>((x >> 16) & 0xff);
|
|
if (v[2] >= 0)
|
|
out += static_cast<char>((x >> 8) & 0xff);
|
|
if (v[3] >= 0)
|
|
out += static_cast<char>(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<std::uint8_t, size> 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<const std::uint8_t *>(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<ssize_t>(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<X509> self_signed(EVP_PKEY * k, const char * cn)
|
|
{
|
|
std::shared_ptr<X509> 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<const unsigned char *>(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<EVP_PKEY> key_;
|
|
std::shared_ptr<X509> cert_;
|
|
#else
|
|
std::shared_ptr<void> key_;
|
|
#endif
|
|
public_key pub_{};
|
|
bool development_ = false;
|
|
};
|
|
|
|
} // namespace net
|
|
} // namespace dpf
|
|
|
|
#endif // LIBDPF_INCLUDE_DPF_NET_IDENTITY_HPP__
|