David Uhden Collado wrote:
Stuart Henderson wrote:
On 2026/10/16 22:01, David Uhden Collado wrote:
Please find attached a patch updating net/i2pd from 2.59.0 to
2.61.0, including fixes for regressions introduced by the upstream
pledge/unveil integration.
I've just committed my version (which disables pledge) as the change
in it to certs are packaged will avoid a lot of broken update diffs.
The OpenBSD-specific changes include:
- Move sandbox initialization until the effective configuration
paths and PID file are resolved, preserving custom paths and user
configurations.
- Restrict pledge/unveil permissions to those actually required,
checking errors instead of silently continuing.
- Restore SSU2 interface MTU detection using AF_LINK data from
getifaddrs(), avoiding SIOCGIFMTU, which is restricted by pledge.
- Handle SIGHUP synchronously through sigwait(), preventing unsafe
reload operations and thread self-join failures.
- Fix UDP cleanup timer ownership during tunnel reloads.
These more complicated changes should go upstream, it's too much for
patches in ports really, merging with any upstream changes in the
same area will be tricky, and this port seems to attract unskilled
update diffs so $deity knows how tgey'll get mangled.
https://github.com/PurpleI2P/i2pd/pull/2600
Tests are welcome...
Hello,
Please find attached a revised patch for net/i2pd 2.61.0, with
REVISION=0.
I reported the underlying problems upstream, but the proposed fixes
were dismissed as "neoroslop" rather than accepted. Since some of
these issues affect security and correctness, I would prefer to have
them reviewed on their technical merits instead of leaving them
unresolved in the OpenBSD package.
The revised patch includes:
- Correct pledge/unveil initialization, effective path handling and
privilege restrictions, without disabling either mechanism.
- Restore SSU2 MTU detection using AF_LINK instead of ioctls restricted
by pledge.
- Fix SIGHUP handling using sigwait(), avoiding asynchronous reload
operations and thread self-join failures.
- Fix UDP timer ownership during configuration reloads.
- Correct OpenSSL/LibreSSL feature detection and compatibility.
- Harden ML-KEM key management, memory cleanup, buffer validation and
error propagation in NTCP2, SSU2 and ECIES handshakes.
- Improve AEAD and signature error handling, including Ed25519ph.
The cryptographic changes are particularly relevant because i2pd
2.61.0 introduced post-quantum support with LibreSSL. Failed
cryptographic operations should not silently leave handshake state
inconsistent or allow uninitialized output to be used.
I would appreciate review, particularly of the LibreSSL and
pledge/unveil changes.
Thank you for your time and consideration.
Best regards,
David.
Index: net/i2pd/Makefile
===================================================================
RCS file: /cvs/ports/net/i2pd/Makefile,v
diff -u -p -r1.34 Makefile
--- Makefile 17 Oct 2026 08:47:19 -0000 1.34
+++ net/i2pd/Makefile 17 Oct 2026 18:42:52 -0000
@@ -3,6 +3,7 @@ COMMENT = client for the I2P anonymous n
GH_ACCOUNT = PurpleI2P
GH_PROJECT = i2pd
GH_TAGNAME = 2.61.0
+REVISION = 0
CATEGORIES = net
HOMEPAGE = https://i2pd.website
@@ -12,9 +13,10 @@ MAINTAINER = SystemFailure <openbsd@syst
# BSD
PERMIT_PACKAGE = Yes
+# uses pledge() and unveil()
WANTLIB += ${COMPILER_LIBCXX}
-WANTLIB += boost_atomic-mt boost_container-mt boost_filesystem-mt
-WANTLIB += boost_program_options-mt c crypto m miniupnpc ssl z
+WANTLIB += boost_filesystem-mt boost_program_options-mt
+WANTLIB += boost_atomic-mt boost_container-mt c crypto m miniupnpc ssl z
COMPILER = base-clang ports-gcc
MODULES = devel/cmake
@@ -29,16 +31,15 @@ CONFIGURE_ARGS = -DWITH_UPNP=ON
WRKSRC = ${WRKDIST}/build
post-install:
- ${INSTALL_DATA_DIR} ${PREFIX}/include/i2pd \
- ${PREFIX}/share/examples/i2pd
+ ${INSTALL_DATA_DIR} ${PREFIX}/include/i2pd
${INSTALL_DATA} ${WRKDIST}/libi2pd{,_client}/*.h \
- ${PREFIX}/include/i2pd
+ ${PREFIX}/include/i2pd
.for dir in family reseed
- ${INSTALL_DATA_DIR} ${WRKINST}/var/i2pd/certificates/${dir}
+ ${INSTALL_DATA_DIR} ${PREFIX}/share/examples/i2pd/certificates/${dir}
${INSTALL_DATA} ${WRKDIST}/contrib/certificates/${dir}/* \
- ${WRKINST}/var/i2pd/certificates/${dir}
+ ${PREFIX}/share/examples/i2pd/certificates/${dir}
.endfor
- ${SUBST_DATA} ${WRKDIST}/contrib/i2pd.conf \
+ ${INSTALL_DATA} ${WRKDIST}/contrib/i2pd.conf \
${PREFIX}/share/examples/i2pd/i2pd.conf
${INSTALL_DATA} ${WRKDIST}/contrib/tunnels.conf \
${PREFIX}/share/examples/i2pd/tunnels.conf
Index: net/i2pd/patches/patch-contrib_i2pd_conf
===================================================================
RCS file: /cvs/ports/net/i2pd/patches/patch-contrib_i2pd_conf,v
diff -u -p -r1.1 patch-contrib_i2pd_conf
--- patches/patch-contrib_i2pd_conf 17 Oct 2026 08:47:19 -0000 1.1
+++ net/i2pd/patches/patch-contrib_i2pd_conf 17 Oct 2026 18:42:52 -0000
@@ -1,23 +1,27 @@
Index: contrib/i2pd.conf
--- contrib/i2pd.conf.orig
+++ contrib/i2pd.conf
-@@ -8,16 +8,16 @@
+@@ -7,17 +7,17 @@
+ ## by removing the "#" symbol.
## Tunnels config file
- ## Default: ~/.i2pd/tunnels.conf or /var/lib/i2pd/tunnels.conf
+-## Default: ~/.i2pd/tunnels.conf or /var/lib/i2pd/tunnels.conf
-# tunconf = /var/lib/i2pd/tunnels.conf
-+tunconf = ${SYSCONFDIR}/i2pd/tunnels.conf
++## Default: ~/.i2pd/tunnels.conf or /var/i2pd/tunnels.conf
++tunconf = /etc/i2pd/tunnels.conf
## Tunnels config files path
## Use that path to store separated tunnels in different config files.
- ## Default: ~/.i2pd/tunnels.d or /var/lib/i2pd/tunnels.d
+-## Default: ~/.i2pd/tunnels.d or /var/lib/i2pd/tunnels.d
-# tunnelsdir = /var/lib/i2pd/tunnels.d
-+tunnelsdir = ${SYSCONFDIR}/i2pd/tunnels.d
++## Default: ~/.i2pd/tunnels.d or /var/i2pd/tunnels.d
++tunnelsdir = /etc/i2pd/tunnels.d
## Path to certificates used for verifying .su3, families
- ## Default: ~/.i2pd/certificates or /var/lib/i2pd/certificates
+-## Default: ~/.i2pd/certificates or /var/lib/i2pd/certificates
-# certsdir = /var/lib/i2pd/certificates
-+certsdir = ${SYSCONFDIR}/i2pd/certificates
++## Default: ~/.i2pd/certificates or /var/i2pd/certificates
++certsdir = /etc/i2pd/certificates
## Where to write pidfile (default: /run/i2pd.pid, not used in Windows)
# pidfile = /run/i2pd.pid
Index: net/i2pd/patches/patch-daemon_Daemon_cpp
===================================================================
RCS file: /cvs/ports/net/i2pd/patches/patch-daemon_Daemon_cpp,v
diff -u -p -r1.1 patch-daemon_Daemon_cpp
--- patches/patch-daemon_Daemon_cpp 17 Oct 2026 08:47:19 -0000 1.1
+++ net/i2pd/patches/patch-daemon_Daemon_cpp 17 Oct 2026 18:42:52 -0000
@@ -1,14 +1,184 @@
-neuter broken pledge/unveil
-
Index: daemon/Daemon.cpp
--- daemon/Daemon.cpp.orig
+++ daemon/Daemon.cpp
-@@ -107,7 +107,7 @@ namespace util
- i2p::config::ParseConfig(config);
+@@ -11,7 +11,12 @@
+ #include <regex>
+
+ #ifdef __OpenBSD__
+-# include<unistd.h>
++#include <unistd.h>
++#include <limits.h>
++#include <stdexcept>
++#include <cerrno>
++#include <cstring>
++#include <cstdlib>
+ #endif
+
+ #include "Daemon.h"
+@@ -43,6 +48,19 @@ namespace i2p
+ {
+ namespace util
+ {
++#ifdef __OpenBSD__
++ // Keep paths stable across DaemonUnix::start()'s chdir(). The leaf need
++ // not exist yet (log files, for example), so realpath() is unsuitable.
++ static std::string AbsolutePath (const std::string& path)
++ {
++ if (path.empty () || path.front () == '/') return path;
++ char cwd[PATH_MAX];
++ if (!getcwd (cwd, sizeof (cwd)))
++ throw std::runtime_error ("Daemon: Can't resolve working directory");
++ return std::string (cwd) + "/" + path;
++ }
++#endif
++
+ class Daemon_Singleton::Daemon_Singleton_Private
+ {
+ public:
+@@ -91,7 +109,11 @@ namespace util
+ }
+
+ i2p::fs::DetectDataDir(datadir, IsService());
++#ifdef __OpenBSD__
++ i2p::fs::DetectDataDir (AbsolutePath (i2p::fs::GetDataDir ()), IsService ());
++#else
+ i2p::fs::Init();
++#endif
+
+ datadir = i2p::fs::GetDataDir();
+
+@@ -108,81 +130,23 @@ namespace util
i2p::config::Finalize();
--#ifdef __OpenBSD__
-+#if 0
- auto init_pledge = []() {
- std::string pledge_file; i2p::config::GetOption("openbsd.pledge_file", pledge_file);
- if (pledge_file == "")
+ #ifdef __OpenBSD__
+- auto init_pledge = []() {
+- std::string pledge_file; i2p::config::GetOption("openbsd.pledge_file", pledge_file);
+- if (pledge_file == "")
+- {
+- LogPrint(eLogDebug, "Use default pledge values");
+- // TODO: remove that not need
+- pledge("stdio rpath wpath cpath inet dns unix recvfd sendfd proc error mcast chown flock",nullptr);
+- } else {
+- std::ifstream f(pledge_file);
+- if(!f) {
+- std::cerr << "Can't open pledge file " << pledge_file<<std::endl;
+- exit(1);
+- }
+- std::string line;
+- std::vector<std::string> rules;
+- while(std::getline(f, line)){
+- rules.push_back(line);
+- }
+- if(f.bad()) {
+- std::cerr << "IO error with pledge file" << std::endl;
+- }
+- std::ostringstream out;
+- for(auto r : rules)
+- out << r << " ";
+- pledge(out.str().c_str(), nullptr);
+- }
+-
+-
+- };
+- auto init_unevil = []() {
+- unveil("/usr/lib", "r");
+- unveil("/usr/local/lib", "r");
+- unveil("/usr/libexec/ld.so", "r");
+- unveil("/dev/urandom", "r");
+- unveil("/tmp", "rw");
+- unveil("/etc/i2pd", "r"); // ваще не нужно вроде на весь прям каталог
+-
+- #define UNVEIL_DIR(dir) unveil(dir.c_str(), "rwc")
+-
+- std::string unevil_file; i2p::config::GetOption("openbsd.unevil_file",unevil_file);
+- UNVEIL_DIR(unevil_file);
+- std::string tunnelsdir, certsdir, logfile, datadir, reseed_file, openbsd_pledge_file;
+- i2p::config::GetOption("tunnelsdir", tunnelsdir);
+- UNVEIL_DIR(tunnelsdir);
+- i2p::config::GetOption("certsdir", certsdir);
+- UNVEIL_DIR(certsdir);
+- i2p::config::GetOption("datadir", datadir);
+- UNVEIL_DIR(datadir);
+- i2p::config::GetOption("reseed.file", reseed_file);
+- unveil(reseed_file.c_str(), "r");
+- i2p::config::GetOption("openbsd.pledge_file", openbsd_pledge_file);
+- unveil(openbsd_pledge_file.c_str(), "r");
+- std::string tunconf ;i2p::config::GetOption("tunconf", tunconf); unveil(tunconf.c_str(), "r");
+- std::string conf ;i2p::config::GetOption("tunconf", conf); unveil(conf.c_str(), "r");
+- std::string pidfile ;i2p::config::GetOption("pidfile", pidfile); unveil(pidfile.c_str(), "rwc");
+- i2p::config::GetOption("logfile", logfile); unveil(logfile.c_str(), "rwc");
+- if(unevil_file != "")
+- {
+- std::ifstream f(unevil_file);
+- if (!f) {
+- std::cerr << "Can't open unevil file" << std::endl;
+- exit(1);
+- }
+- std::string line;
+- while(std::getline(f, line)){
+- UNVEIL_DIR(line);
+- }
+- }
+- #undef UNVEIL_DIR
+- unveil(NULL, NULL);
+- };
+- bool openbsd_unevil_enabled; i2p::config::GetOption("openbsd.unevil_enabled", openbsd_unevil_enabled);
+- bool openbsd_pledge_enabled; i2p::config::GetOption("openbsd.pledge_enabled", openbsd_pledge_enabled);
+- if(openbsd_unevil_enabled) init_unevil();
+- if(openbsd_pledge_enabled) init_pledge();
++ // A datadir in the configuration is not available during config lookup.
++ std::string configuredDataDir = DaemonDataDir;
++ if (configuredDataDir.empty ())
++ i2p::config::GetOption ("datadir", configuredDataDir);
++ i2p::fs::DetectDataDir (configuredDataDir, IsService ());
++ i2p::fs::DetectDataDir (AbsolutePath (i2p::fs::GetDataDir ()), IsService ());
++ datadir = i2p::fs::GetDataDir ();
++ i2p::fs::Init ();
++ i2p::config::SetOption ("conf", AbsolutePath (config));
++ for (const char * option : {"tunconf", "tunnelsdir", "certsdir", "logfile", "pidfile",
++ "reseed.file", "reseed.zipfile", "addressbook.hostsfile",
++ "openbsd.pledge_file", "openbsd.unevil_file"})
++ {
++ std::string path;
++ i2p::config::GetOption (option, path);
++ if (!path.empty ()) i2p::config::SetOption (option, AbsolutePath (path));
++ }
+ #endif
+
+ i2p::config::GetOption("daemon", isDaemon);
+@@ -216,6 +180,9 @@ namespace util
+ } else if (logs == "file") {
+ if (logfile == "")
+ logfile = i2p::fs::DataDirPath("i2pd.log");
++#ifdef __OpenBSD__
++ i2p::config::SetOption ("logfile", logfile);
++#endif
+ LogPrint(eLogInfo, "Log: Sending messages to ", logfile);
+ i2p::log::Logger().SendTo (logfile);
+ #ifndef _WIN32
+@@ -236,6 +203,25 @@ namespace util
+ bool ssu; i2p::config::GetOption("ssu", ssu);
+ if (!ssu && i2p::config::IsDefault ("precomputation.elgamal"))
+ precomputation = false; // we don't elgamal table if no ssu, unless it's specified explicitly
++#ifdef __OpenBSD__
++ // Keep startup-only promises until fork/setsid/setrlimit and unveil.
++ // Explicit pledge policies retain their existing override semantics.
++ bool usePledge; i2p::config::GetOption ("openbsd.pledge_enabled", usePledge);
++ std::string policy; i2p::config::GetOption ("openbsd.pledge_file", policy);
++ if (usePledge && policy.empty ())
++ {
++ std::string promises = "stdio rpath wpath cpath fattr inet dns unix flock proc";
++ bool useUnveil; i2p::config::GetOption ("openbsd.unevil_enabled", useUnveil);
++ if (useUnveil) promises += " unveil";
++ bool upnp; i2p::config::GetOption ("upnp.enabled", upnp);
++ if (upnp) promises += " mcast";
++ if (pledge (promises.c_str (), nullptr) == -1)
++ {
++ std::cerr << "Daemon: startup pledge: " << strerror (errno) << std::endl;
++ std::exit (EXIT_FAILURE);
++ }
++ }
++#endif
+ i2p::crypto::InitCrypto (precomputation);
+
+ i2p::transport::InitAddressFromIface (); // get address4/6 from interfaces
Index: net/i2pd/patches/patch-daemon_I2PControl_cpp
===================================================================
RCS file: net/i2pd/patches/patch-daemon_I2PControl_cpp
diff -N net/i2pd/patches/patch-daemon_I2PControl_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-daemon_I2PControl_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,12 @@
+Index: daemon/I2PControl.cpp
+--- daemon/I2PControl.cpp.orig
++++ daemon/I2PControl.cpp
+@@ -441,7 +441,7 @@ namespace client
+ void I2PControlService::CreateCertificate (const char *crt_path, const char *key_path)
+ {
+ FILE *f = NULL;
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ EVP_PKEY * pkey = EVP_RSA_gen(4096); // e = 65537
+ #else
+ EVP_PKEY * pkey = EVP_PKEY_new ();
Index: net/i2pd/patches/patch-daemon_UnixDaemon_cpp
===================================================================
RCS file: net/i2pd/patches/patch-daemon_UnixDaemon_cpp
diff -N net/i2pd/patches/patch-daemon_UnixDaemon_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-daemon_UnixDaemon_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,272 @@
+Index: daemon/UnixDaemon.cpp
+--- daemon/UnixDaemon.cpp.orig
++++ daemon/UnixDaemon.cpp
+@@ -18,6 +18,18 @@
+ #include <sys/stat.h>
+ #include <sys/resource.h>
+
++#ifdef __OpenBSD__
++#include <fstream>
++#include <vector>
++#include <cerrno>
++#include <cstring>
++#include <cstdlib>
++#include <limits.h>
++#include <map>
++#include <syslog.h>
++#include <pthread.h>
++#endif
++
+ #include "Config.h"
+ #include "FS.h"
+ #include "Log.h"
+@@ -27,6 +39,18 @@
+ #include "Transports.h"
+ #include "util.h"
+
++#ifdef __OpenBSD__
++// Blocked before service threads are created; consumed only by run().
++static sigset_t daemonSignals;
++
++static bool SignalError (const char * operation, int error)
++{
++ std::cerr << "Daemon: " << operation << ": " << strerror (error) << std::endl;
++ syslog (LOG_DAEMON | LOG_ERR, "Daemon: %s: %s", operation, strerror (error));
++ return false;
++}
++#endif
++
+ void handle_signal(int sig)
+ {
+ switch (sig)
+@@ -148,7 +172,10 @@ namespace i2p
+
+ // Pidfile
+ // this code is c-styled and a bit ugly, but we need fd for locking pidfile
+- std::string pidfile; i2p::config::GetOption("pidfile", pidfile);
++#ifndef __OpenBSD__
++ std::string pidfile;
++#endif
++ i2p::config::GetOption("pidfile", pidfile);
+ if (pidfile == "") {
+ pidfile = i2p::fs::DataDirPath("i2pd.pid");
+ }
+@@ -191,8 +218,29 @@ namespace i2p
+ gracefulShutdownInterval = 0; // not specified
+
+ // handle signal TSTP
+- bool handleTSTP; i2p::config::GetOption("unix.handle_sigtstp", handleTSTP);
++ bool handleTSTP = false; i2p::config::GetOption("unix.handle_sigtstp", handleTSTP);
+
++#ifdef __OpenBSD__
++ // ReloadConfig() can stop and join destination threads. Never run
++ // it from an asynchronous handler on an arbitrary service thread.
++ sigemptyset (&daemonSignals);
++ for (int sig : {SIGHUP, SIGUSR1, SIGABRT, SIGTERM, SIGINT, SIGPIPE})
++ sigaddset (&daemonSignals, sig);
++ if (handleTSTP)
++ {
++ sigaddset (&daemonSignals, SIGTSTP);
++ sigaddset (&daemonSignals, SIGCONT);
++ }
++ int error = pthread_sigmask (SIG_BLOCK, &daemonSignals, nullptr);
++ if (error) return SignalError ("pthread_sigmask", error);
++ // Inherited SIG_IGN dispositions could discard a blocked signal.
++ struct sigaction sa{};
++ sa.sa_handler = SIG_DFL;
++ sigemptyset (&sa.sa_mask);
++ for (int sig = 1; sig < NSIG; ++sig)
++ if (sigismember (&daemonSignals, sig) == 1 && sigaction (sig, &sa, nullptr) == -1)
++ return SignalError ("sigaction", errno);
++#else
+ // Signal handler
+ struct sigaction sa;
+ sa.sa_handler = handle_signal;
+@@ -210,13 +258,150 @@ namespace i2p
+ sigaction(SIGCONT, &sa, 0);
+ }
+
++#endif
++
++#ifdef __OpenBSD__
++ // Resolve/read sandbox policy before the first unveil hides it.
++ bool useUnveil, usePledge;
++ i2p::config::GetOption ("openbsd.unevil_enabled", useUnveil);
++ i2p::config::GetOption ("openbsd.pledge_enabled", usePledge);
++ // CreatePrivateFile() uses chmod when saving router and destination keys.
++ std::string promises = "stdio rpath wpath cpath fattr inet dns unix flock";
++ bool upnp; i2p::config::GetOption ("upnp.enabled", upnp);
++ if (upnp) promises += " mcast";
++ std::vector<std::string> extraPaths;
++ auto fail = [](const std::string& message) {
++ std::cerr << message << std::endl;
++ // The logger thread has not started, and daemon stderr is /dev/null.
++ syslog (LOG_DAEMON | LOG_ERR, "%s", message.c_str ());
++ return false;
++ };
++ auto readPolicy = [&fail](const char * option, std::vector<std::string>& lines) {
++ std::string path; i2p::config::GetOption (option, path);
++ if (path.empty ()) return true;
++ std::ifstream f (path);
++ if (!f)
++ {
++ return fail (std::string ("Daemon: Can't open ") + option + ": " + path);
++ }
++ std::string line;
++ while (std::getline (f, line))
++ if (!line.empty ()) lines.push_back (line);
++ if (f.bad ())
++ {
++ return fail (std::string ("Daemon: Can't read ") + option + ": " + path);
++ }
++ return true;
++ };
++ if (usePledge)
++ {
++ std::string policy; i2p::config::GetOption ("openbsd.pledge_file", policy);
++ if (!policy.empty ())
++ {
++ std::vector<std::string> lines;
++ if (!readPolicy ("openbsd.pledge_file", lines)) return false;
++ promises.clear ();
++ for (const auto& line : lines) promises += line + " ";
++ }
++ }
++ if (useUnveil && !readPolicy ("openbsd.unevil_file", extraPaths)) return false;
++ if (useUnveil)
++ {
++ // Coalesce aliases before unveil: repeated rules may only reduce
++ // permissions. For example, certsdir may equal datadir.
++ std::map<std::string, std::string> rules;
++ auto allow = [&rules, &fail](std::string path, const char * permissions) {
++ if (path.empty ()) return true;
++ while (path.size () > 1 && path.back () == '/') path.pop_back ();
++ char resolved[PATH_MAX];
++ if (realpath (path.c_str (), resolved)) path = resolved;
++ else
++ {
++ if (errno != ENOENT)
++ return fail ("Daemon: resolve " + path + ": " + strerror (errno));
++ // A missing leaf is remembered in its existing parent by
++ // unveil, without giving access to sibling files.
++ auto slash = path.find_last_of ('/');
++ std::string parent = slash == std::string::npos ? "." : path.substr (0, slash + 1);
++ if (!realpath (parent.c_str (), resolved))
++ return fail ("Daemon: resolve parent of " + path + ": " + strerror (errno));
++ path = std::string (resolved) + "/" + path.substr (slash == std::string::npos ? 0 : slash + 1);
++ }
++ auto& mask = rules[path];
++ for (const char * p = permissions; *p; ++p)
++ if (mask.find (*p) == std::string::npos) mask += *p;
++ return true;
++ };
++ if (!allow (i2p::fs::GetDataDir (), "rwc") ||
++ !allow (i2p::fs::GetCertsDir (), "r") || !allow (pidfile, "rwc")) return false;
++ for (const char * option : {"conf", "tunconf", "tunnelsdir", "reseed.file", "reseed.zipfile"})
++ {
++ std::string path; i2p::config::GetOption (option, path);
++ if (!allow (path, "r")) return false;
++ }
++ for (const char * option : {"logfile", "addressbook.hostsfile"})
++ {
++ std::string path; i2p::config::GetOption (option, path);
++ if (!allow (path, "rwc")) return false;
++ }
++ bool control; i2p::config::GetOption ("i2pcontrol.enabled", control);
++ if (control)
++ {
++ for (const char * option : {"i2pcontrol.cert", "i2pcontrol.key"})
++ {
++ std::string path; i2p::config::GetOption (option, path);
++ if (!path.empty () && path.front () != '/') path = i2p::fs::DataDirPath (path);
++ if (!allow (path, "rwc")) return false;
++ }
++ uint16_t port; i2p::config::GetOption ("i2pcontrol.port", port);
++ if (!port)
++ {
++ std::string path; i2p::config::GetOption ("i2pcontrol.address", path);
++ if (!allow (path, "rwc")) return false;
++ }
++ }
++ bool outproxy; i2p::config::GetOption ("socksproxy.outproxy.enabled", outproxy);
++ uint16_t port; i2p::config::GetOption ("socksproxy.outproxyport", port);
++ if (outproxy && !port)
++ {
++ std::string path; i2p::config::GetOption ("socksproxy.outproxy", path);
++ if (!allow (path, "w")) return false;
++ }
++ // miniupnpc consults MiniSSDPD before multicast discovery.
++ if (upnp && !allow ("/var/run/minissdpd.sock", "w")) return false;
++ for (const auto& path : extraPaths)
++ if (!allow (path, "rwc")) return false;
++ for (const auto& rule : rules)
++ if (unveil (rule.first.c_str (), rule.second.c_str ()) == -1)
++ return fail ("Daemon: unveil " + rule.first + ": " + strerror (errno));
++ if (unveil (nullptr, nullptr) == -1)
++ {
++ return fail (std::string ("Daemon: unveil lock: ") + strerror (errno));
++ }
++ }
++ // fork/setsid/setrlimit and PID locking are complete; no proc,
++ // id, chown, descriptor passing or error promise is needed.
++ if (usePledge && pledge (promises.c_str (), nullptr) == -1)
++ {
++ return fail (std::string ("Daemon: pledge: ") + strerror (errno));
++ }
++#endif
++
+ return Daemon_Singleton::start();
+ }
+
+ bool DaemonUnix::stop()
+ {
++#ifdef __OpenBSD__
++ // Keep the PID lock while router data is still being saved.
++ bool stopped = Daemon_Singleton::stop();
+ i2p::fs::Remove(pidfile);
++ close (pidFH);
++ return stopped;
++#else
++ i2p::fs::Remove(pidfile);
+ return Daemon_Singleton::stop();
++#endif
+ }
+
+ void DaemonUnix::run ()
+@@ -225,6 +410,36 @@ namespace i2p
+ while (running)
+ {
+ std::this_thread::sleep_for (std::chrono::seconds(1));
++#ifdef __OpenBSD__
++ // Poll at the existing one-second shutdown tick. Only this thread
++ // consumes these signals, so a pending signal cannot be stolen
++ // between sigpending() and sigwait().
++ sigset_t pending;
++ if (sigpending (&pending) == -1)
++ {
++ SignalError ("sigpending", errno);
++ running = false;
++ break;
++ }
++ for (int sig = 1; sig < NSIG; ++sig)
++ if (sigismember (&daemonSignals, sig) == 1 && sigismember (&pending, sig) == 1)
++ {
++ sigset_t one;
++ sigemptyset (&one);
++ sigaddset (&one, sig);
++ int received;
++ int error = sigwait (&one, &received);
++ if (error)
++ {
++ SignalError ("sigwait", error);
++ running = false;
++ break;
++ }
++ handle_signal (received);
++ if (!running) break;
++ }
++ if (!running) break;
++#endif
+ if (gracefulShutdownInterval)
+ {
+ gracefulShutdownInterval--; // - 1 second
Index: net/i2pd/patches/patch-libi2pd_Config_cpp
===================================================================
RCS file: /cvs/ports/net/i2pd/patches/patch-libi2pd_Config_cpp,v
diff -u -p -N -r1.1 patch-libi2pd_Config_cpp
--- net/i2pd/patches/patch-libi2pd_Config_cpp 17 Oct 2026 08:47:19 -0000 1.1
+++ /dev/null 1 Jan 1970 00:00:00 -0000
@@ -1,23 +0,0 @@
-neuter broken pledge/unveil
-
-Index: libi2pd/Config.cpp
---- libi2pd/Config.cpp.orig
-+++ libi2pd/Config.cpp
-@@ -396,7 +396,7 @@ namespace config {
- ;
- #endif
-
--#ifdef __OpenBSD__
-+#if 0
- options_description openbsd_specific("OpenBSD specific options");
- openbsd_specific.add_options()
- ("openbsd.pledge_file", value<std::string>()->default_value(""), "OpenbSD file with pledge rules")
-@@ -433,7 +433,7 @@ namespace config {
- #ifdef __linux__
- .add(unix_specific)
- #endif
--#ifdef __OpenBSD__
-+#if 0
- .add(openbsd_specific)
- #endif
- ;
Index: net/i2pd/patches/patch-libi2pd_Crypto_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Crypto_cpp
diff -N net/i2pd/patches/patch-libi2pd_Crypto_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Crypto_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,97 @@
+Index: libi2pd/Crypto.cpp
+--- libi2pd/Crypto.cpp.orig
++++ libi2pd/Crypto.cpp
+@@ -11,13 +11,14 @@
+ #include <vector>
+ #include <mutex>
+ #include <memory>
++#include <limits>
+ #include <openssl/dh.h>
+ #include <openssl/md5.h>
+ #include <openssl/crypto.h>
+ #include "TunnelBase.h"
+ #include <openssl/ssl.h>
+ #include <openssl/kdf.h>
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ #include <openssl/param_build.h>
+ #include <openssl/core_names.h>
+ #endif
+@@ -148,7 +149,7 @@ namespace crypto
+ #define dsap GetCryptoConstants ().dsap
+ #define dsaq GetCryptoConstants ().dsaq
+ #define dsag GetCryptoConstants ().dsag
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ EVP_PKEY * CreateDSA (BIGNUM * pubKey, BIGNUM * privKey)
+ {
+ EVP_PKEY * pkey = nullptr;
+@@ -649,40 +650,39 @@ namespace crypto
+ static bool AEADChaCha20Poly1305 (EVP_CIPHER_CTX * ctx, const uint8_t * msg, size_t msgLen,
+ const uint8_t * ad, size_t adLen, const uint8_t * key, const uint8_t * nonce, uint8_t * buf, size_t len, bool encrypt)
+ {
+- if (!ctx || len < msgLen) return false;
+- if (encrypt && len < msgLen + 16) return false;
+- bool ret = true;
+- int outlen = 0;
++ if (!ctx || len < msgLen || (encrypt && len - msgLen < 16)) return false;
++ // EVP update lengths are int in both OpenSSL and LibreSSL.
++ if (msgLen > (size_t)std::numeric_limits<int>::max () ||
++ adLen > (size_t)std::numeric_limits<int>::max ()) return false;
++ int outlen = 0, finalLen = 0;
+ if (encrypt)
+ {
+- EVP_EncryptInit_ex(ctx, EVP_chacha20_poly1305(), 0, 0, 0);
+- EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, 0);
+- EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce);
+- EVP_EncryptUpdate(ctx, NULL, &outlen, ad, adLen);
+- EVP_EncryptUpdate(ctx, buf, &outlen, msg, msgLen);
+- EVP_EncryptFinal_ex(ctx, buf + outlen, &outlen);
+- EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, buf + msgLen);
++ if (EVP_EncryptInit_ex (ctx, EVP_chacha20_poly1305 (), nullptr, nullptr, nullptr) != 1 ||
++ EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, nullptr) != 1 ||
++ EVP_EncryptInit_ex (ctx, nullptr, nullptr, key, nonce) != 1) return false;
++ if (adLen && EVP_EncryptUpdate (ctx, nullptr, &outlen, ad, (int)adLen) != 1) return false;
++ outlen = 0;
++ if (msgLen && EVP_EncryptUpdate (ctx, buf, &outlen, msg, (int)msgLen) != 1) return false;
++ if (outlen != (int)msgLen || EVP_EncryptFinal_ex (ctx, buf + outlen, &finalLen) != 1 || finalLen != 0) return false;
++ return EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_AEAD_GET_TAG, 16, buf + msgLen) == 1;
+ }
+- else
+- {
+ #if defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x4000000fL
+- std::vector<uint8_t> m(msgLen + 16);
+- if (msg == buf)
+- {
+- // we have to use different buffers otherwise verification fails
+- memcpy (m.data (), msg, msgLen + 16);
+- msg = m.data ();
+- }
+-#endif
+- EVP_DecryptInit_ex(ctx, EVP_chacha20_poly1305(), 0, 0, 0);
+- EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, 0);
+- EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, (uint8_t *)(msg + msgLen));
+- EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce);
+- EVP_DecryptUpdate(ctx, NULL, &outlen, ad, adLen);
+- EVP_DecryptUpdate(ctx, buf, &outlen, msg, msgLen);
+- ret = EVP_DecryptFinal_ex(ctx, buf + outlen, &outlen) > 0;
++ std::vector<uint8_t> m;
++ if (msg == buf)
++ {
++ // Older LibreSSL cannot authenticate in-place decryption.
++ m.assign (msg, msg + msgLen + 16);
++ msg = m.data ();
+ }
+- return ret;
++#endif
++ if (EVP_DecryptInit_ex (ctx, EVP_chacha20_poly1305 (), nullptr, nullptr, nullptr) != 1 ||
++ EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, nullptr) != 1 ||
++ EVP_DecryptInit_ex (ctx, nullptr, nullptr, key, nonce) != 1 ||
++ EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_AEAD_SET_TAG, 16, (void *)(msg + msgLen)) != 1) return false;
++ if (adLen && EVP_DecryptUpdate (ctx, nullptr, &outlen, ad, (int)adLen) != 1) return false;
++ outlen = 0;
++ if (msgLen && EVP_DecryptUpdate (ctx, buf, &outlen, msg, (int)msgLen) != 1) return false;
++ return outlen == (int)msgLen && EVP_DecryptFinal_ex (ctx, buf + outlen, &finalLen) == 1 && finalLen == 0;
+ }
+
+ bool AEADChaCha20Poly1305 (const uint8_t * msg, size_t msgLen, const uint8_t * ad, size_t adLen,
Index: net/i2pd/patches/patch-libi2pd_Crypto_h
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Crypto_h
diff -N net/i2pd/patches/patch-libi2pd_Crypto_h
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Crypto_h 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,29 @@
+Index: libi2pd/Crypto.h
+--- libi2pd/Crypto.h.orig
++++ libi2pd/Crypto.h
+@@ -31,10 +31,15 @@
+ #if (!defined(LIBRESSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER != 0x030000000)) // 3.0.0, regression in SipHash, not implemented in LibreSSL
+ # define OPENSSL_SIPHASH 1
+ #endif
+-#if (OPENSSL_VERSION_NUMBER >= 0x030500000) || ( (defined(LIBRESSL_VERSION_NUMBER) && (LIBRESSL_VERSION_NUMBER >= 0x4030000fL)) ) // 3.5.0 openssl, 4.3.0 libressl
++#if (!defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x030500000) || ( (defined(LIBRESSL_VERSION_NUMBER) && (LIBRESSL_VERSION_NUMBER >= 0x4030000fL)) ) // 3.5.0 openssl, 4.3.0 libressl
+ # define OPENSSL_PQ 1
+ #endif
+
++// The Ed25519ph instance parameter first appeared in OpenSSL 3.2.
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30200000L
++# define OPENSSL_ED25519PH 1
++#endif
++
+ namespace i2p
+ {
+ namespace crypto
+@@ -42,7 +47,7 @@ namespace crypto
+ bool bn2buf (const BIGNUM * bn, uint8_t * buf, size_t len);
+
+ // DSA
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ EVP_PKEY * CreateDSA (BIGNUM * pubKey = nullptr, BIGNUM * privKey = nullptr);
+ #else
+ DSA * CreateDSA ();
Index: net/i2pd/patches/patch-libi2pd_Destination_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Destination_cpp
diff -N net/i2pd/patches/patch-libi2pd_Destination_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Destination_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,33 @@
+Index: libi2pd/Destination.cpp
+--- libi2pd/Destination.cpp.orig
++++ libi2pd/Destination.cpp
+@@ -14,6 +14,7 @@
+ #include <charconv>
+ #include <boost/algorithm/string.hpp>
+ #include "Crypto.h"
++#include "PostQuantum.h"
+ #include "ECIESX25519AEADRatchetSession.h"
+ #include "Log.h"
+ #include "FS.h"
+@@ -1013,7 +1014,10 @@ namespace client
+ try
+ {
+ i2p::data::CryptoKeyType cryptoType = std::stoi(it1);
+-#if !OPENSSL_PQ
++#if OPENSSL_PQ
++ if (cryptoType <= i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD ||
++ i2p::crypto::IsMLKEMSupported (cryptoType))
++#else
+ if (cryptoType <= i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD) // skip PQ keys if not supported
+ #endif
+ {
+@@ -1047,6 +1051,9 @@ namespace client
+
+ for (auto& it: encryptionKeyTypes)
+ {
++#if OPENSSL_PQ
++ if (it > i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD && !i2p::crypto::IsMLKEMSupported (it)) continue;
++#endif
+ auto encryptionKey = std::make_shared<i2p::crypto::LocalEncryptionKey> (it);
+ if (IsPublic ())
+ PersistTemporaryKeys (encryptionKey);
Index: net/i2pd/patches/patch-libi2pd_ECIESX25519AEADRatchetSession_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_ECIESX25519AEADRatchetSession_cpp
diff -N net/i2pd/patches/patch-libi2pd_ECIESX25519AEADRatchetSession_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_ECIESX25519AEADRatchetSession_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,57 @@
+Index: libi2pd/ECIESX25519AEADRatchetSession.cpp
+--- libi2pd/ECIESX25519AEADRatchetSession.cpp.orig
++++ libi2pd/ECIESX25519AEADRatchetSession.cpp
+@@ -308,7 +308,7 @@ namespace garlic
+ len -= keyLen + 16;
+
+ m_PQKeys = i2p::crypto::CreateMLKEMKeys (cryptoType);
+- m_PQKeys->SetPublicKey (encapsKey.data ());
++ if (!m_PQKeys || !m_PQKeys->SetPublicKey (encapsKey.data ())) return false;
+ }
+ }
+ // if PQ failed it's valid situation meaning regular ECIES_X25519(type 4)
+@@ -569,7 +569,7 @@ namespace garlic
+ {
+ i2p::crypto::InitNoiseIKStateMLKEM (GetNoiseState (), m_RemoteStaticKeyType, m_RemoteStaticKey); // bpk
+ m_PQKeys = i2p::crypto::CreateMLKEMKeys (m_RemoteStaticKeyType);
+- m_PQKeys->GenerateKeys ();
++ if (!m_PQKeys || !m_PQKeys->GenerateKeys ()) return false;
+ }
+ else
+ #endif
+@@ -587,7 +587,7 @@ namespace garlic
+ {
+ auto keyLen = i2p::crypto::GetMLKEMPublicKeyLen (m_RemoteStaticKeyType);
+ std::vector<uint8_t> encapsKey(keyLen);
+- m_PQKeys->GetPublicKey (encapsKey.data ());
++ if (!m_PQKeys || !m_PQKeys->GetPublicKey (encapsKey.data ())) return false;
+ // encrypt encapsKey
+ if (!Encrypt (encapsKey.data (), out + offset, keyLen))
+ {
+@@ -678,7 +678,7 @@ namespace garlic
+ memcpy (m_NSRCK->data (), m_CK, 64);
+ size_t cipherTextLen = i2p::crypto::GetMLKEMCipherTextLen (m_RemoteStaticKeyType);
+ std::vector<uint8_t> kemCiphertext(cipherTextLen);
+- m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret);
++ if (!m_PQKeys || !m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret)) return false;
+
+ if (!Encrypt (kemCiphertext.data (), out + offset, cipherTextLen))
+ {
+@@ -750,7 +750,7 @@ namespace garlic
+ size_t cipherTextLen = i2p::crypto::GetMLKEMCipherTextLen (m_RemoteStaticKeyType);
+ std::vector<uint8_t> kemCiphertext(cipherTextLen);
+ uint8_t sharedSecret[32];
+- m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret);
++ if (!m_PQKeys || !m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret)) return false;
+
+ memcpy (m_CK, m_NSRCK->data (), 64); // restore key
+ if (!Encrypt (kemCiphertext.data (), out + offset, cipherTextLen))
+@@ -829,7 +829,7 @@ namespace garlic
+ buf += cipherTextLen + 16;
+ len -= cipherTextLen + 16;
+ // decaps
+- m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret);
++ if (!m_PQKeys || !m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret)) return false;
+ MixKey (sharedSecret);
+ }
+ #endif
Index: net/i2pd/patches/patch-libi2pd_FS_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_FS_cpp
diff -N net/i2pd/patches/patch-libi2pd_FS_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_FS_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,12 @@
+Index: libi2pd/FS.cpp
+--- libi2pd/FS.cpp.orig
++++ libi2pd/FS.cpp
+@@ -113,6 +113,8 @@ namespace fs {
+ dataDir = fs_lib::wpath(commonAppData).string() + "\\" + appName;
+ #endif
+ }
++#elif defined(__OpenBSD__)
++ dataDir = "/var/" + appName;
+ #else
+ dataDir = "/var/lib/" + appName;
+ #endif
Index: net/i2pd/patches/patch-libi2pd_Family_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Family_cpp
diff -N net/i2pd/patches/patch-libi2pd_Family_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Family_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,71 @@
+Index: libi2pd/Family.cpp
+--- libi2pd/Family.cpp.orig
++++ libi2pd/Family.cpp
+@@ -18,6 +18,20 @@ namespace i2p
+ {
+ namespace data
+ {
++ static int GetFamilyKeyCurve (EVP_PKEY * pkey)
++ {
++ if (!pkey || EVP_PKEY_base_id (pkey) != EVP_PKEY_EC) return -1;
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L
++ char groupName[80];
++ if (EVP_PKEY_get_group_name (pkey, groupName, sizeof (groupName), nullptr) != 1) return -1;
++ return OBJ_txt2nid (groupName);
++#else
++ const EC_KEY * ec = EVP_PKEY_get0_EC_KEY (pkey);
++ const EC_GROUP * group = ec ? EC_KEY_get0_group (ec) : nullptr;
++ return group ? EC_GROUP_get_curve_name (group) : -1;
++#endif
++ }
++
+ Families::Families ()
+ {
+ }
+@@ -51,15 +65,8 @@ namespace data
+ auto pkey = X509_get_pubkey (cert);
+ if (pkey)
+ {
+- int curve = 0;
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
+- char groupName[20];
+- if (EVP_PKEY_get_group_name(pkey, groupName, sizeof(groupName), NULL) == 1)
+- curve = OBJ_txt2nid (groupName);
+- else
+- curve = -1;
+-#endif
+- if (!curve || curve == NID_X9_62_prime256v1)
++ int curve = GetFamilyKeyCurve (pkey);
++ if (curve == NID_X9_62_prime256v1)
+ {
+ if (!m_SigningKeys.emplace (cn, std::make_pair(pkey, (int)m_SigningKeys.size () + 1)).second)
+ {
+@@ -68,7 +75,10 @@ namespace data
+ }
+ }
+ else
++ {
++ EVP_PKEY_free (pkey);
+ LogPrint (eLogWarning, "Family: elliptic curve ", curve, " is not supported");
++ }
+ }
+ }
+ }
+@@ -156,15 +166,8 @@ namespace data
+ {
+ SSL * ssl = SSL_new (ctx);
+ EVP_PKEY * pkey = SSL_get_privatekey (ssl);
+- int curve = 0;
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
+- char groupName[20];
+- if (EVP_PKEY_get_group_name(pkey, groupName, sizeof(groupName), NULL) == 1)
+- curve = OBJ_txt2nid (groupName);
+- else
+- curve = -1;
+-#endif
+- if (!curve || curve == NID_X9_62_prime256v1)
++ int curve = GetFamilyKeyCurve (pkey);
++ if (curve == NID_X9_62_prime256v1 && family.length () + 32 <= 100)
+ {
+ uint8_t buf[100], sign[72], signature[64];
+ size_t len = family.length ();
Index: net/i2pd/patches/patch-libi2pd_LeaseSet_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_LeaseSet_cpp
diff -N net/i2pd/patches/patch-libi2pd_LeaseSet_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_LeaseSet_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,23 @@
+Index: libi2pd/LeaseSet.cpp
+--- libi2pd/LeaseSet.cpp.orig
++++ libi2pd/LeaseSet.cpp
+@@ -9,6 +9,7 @@
+ #include <string.h>
+ #include "I2PEndian.h"
+ #include "Crypto.h"
++#include "PostQuantum.h"
+ #include "Log.h"
+ #include "Tag.h"
+ #include "Timestamp.h"
+@@ -425,7 +426,10 @@ namespace data
+ if (IsStoreLeases () && !preferredKeyFound) // create encryptor with leases only
+ {
+ // we pick max key type if preferred not found
+-#if !OPENSSL_PQ
++#if OPENSSL_PQ
++ if (keyType <= i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD ||
++ i2p::crypto::IsMLKEMSupported (keyType))
++#else
+ if (keyType <= i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD) // skip PQ keys if not supported
+ #endif
+ {
Index: net/i2pd/patches/patch-libi2pd_NTCP2_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_NTCP2_cpp
diff -N net/i2pd/patches/patch-libi2pd_NTCP2_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_NTCP2_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,88 @@
+Index: libi2pd/NTCP2.cpp
+--- libi2pd/NTCP2.cpp.orig
++++ libi2pd/NTCP2.cpp
+@@ -49,9 +49,11 @@ namespace transport
+ #if OPENSSL_PQ
+ switch (version)
+ {
++#ifndef LIBRESSL_VERSION_NUMBER
+ case 3:
+ m_CryptoType = i2p::data::CRYPTO_KEY_TYPE_ECIES_MLKEM512_X25519_AEAD;
+ break;
++#endif
+ case 4:
+ m_CryptoType = i2p::data::CRYPTO_KEY_TYPE_ECIES_MLKEM768_X25519_AEAD;
+ break;
+@@ -158,7 +160,7 @@ namespace transport
+ encryption.Encrypt (pub, 32, m_IV, m_Buffer); // X
+ // ML-KEM encap_key
+ m_PQKeys = i2p::crypto::CreateMLKEMKeys (m_CryptoType);
+- m_PQKeys->GenerateKeys ();
++ if (!m_PQKeys || !m_PQKeys->GenerateKeys ()) return false;
+ }
+ else
+ encryption.Encrypt (GetPub (), 32, m_IV, m_Buffer); // X
+@@ -178,7 +180,7 @@ namespace transport
+ // ML-KEM frame
+ auto keyLen = i2p::crypto::GetMLKEMPublicKeyLen (m_CryptoType);
+ std::vector<uint8_t> encapsKey(keyLen);
+- m_PQKeys->GetPublicKey (encapsKey.data ());
++ if (!m_PQKeys || !m_PQKeys->GetPublicKey (encapsKey.data ())) return false;
+ // encrypt encapsKey
+ if (!Encrypt (encapsKey.data (), m_Buffer + offset, keyLen))
+ {
+@@ -250,12 +252,13 @@ namespace transport
+ maxPaddingLength -= 64;
+ size_t maxMsgSize = m_MaxMsgSize;
+ #if OPENSSL_PQ
+- if (m_PQKeys)
++ if (m_CryptoType > i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD)
+ {
++ if (!m_PQKeys) return false;
+ size_t cipherTextLen = i2p::crypto::GetMLKEMCipherTextLen (m_CryptoType);
+ std::vector<uint8_t> kemCiphertext(cipherTextLen);
+ uint8_t sharedSecret[32];
+- m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret);
++ if (!m_PQKeys || !m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret)) return false;
+ if (!Encrypt (kemCiphertext.data (), m_Buffer + offset, cipherTextLen))
+ {
+ LogPrint (eLogWarning, "NTCP2: SessionCreated ML-KEM ciphertext section AEAD encryption failed");
+@@ -373,7 +376,7 @@ namespace transport
+ MixHash (m_Buffer + offset, keyLen + 16);
+ offset += keyLen + 16;
+ m_PQKeys = i2p::crypto::CreateMLKEMKeys (m_CryptoType);
+- m_PQKeys->SetPublicKey (encapsKey.data ());
++ if (!m_PQKeys || !m_PQKeys->SetPublicKey (encapsKey.data ())) return false;
+ }
+ }
+ #endif
+@@ -440,8 +443,9 @@ namespace transport
+ return false;
+ }
+ #if OPENSSL_PQ
+- if (m_CryptoType > i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD && m_PQKeys)
++ if (m_CryptoType > i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD)
+ {
++ if (!m_PQKeys) return false;
+ // decrypt kem_ciphertext frame
+ size_t cipherTextLen = i2p::crypto::GetMLKEMCipherTextLen (m_CryptoType);
+ std::vector<uint8_t> kemCiphertext(cipherTextLen);
+@@ -455,7 +459,7 @@ namespace transport
+ offset += cipherTextLen + 16;
+ // decaps
+ uint8_t sharedSecret[32];
+- m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret);
++ if (!m_PQKeys || !m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret)) return false;
+ MixKey (sharedSecret);
+ }
+ #endif
+@@ -2285,6 +2289,9 @@ namespace transport
+ void NTCP2Server::SetVersion (int version)
+ {
+ #if OPENSSL_PQ
++#ifdef LIBRESSL_VERSION_NUMBER
++ if (version == 3) version = 2; // ML-KEM-512 is not supported by LibreSSL
++#endif
+ m_Version = version;
+ #endif
+ }
Index: net/i2pd/patches/patch-libi2pd_PostQuantum_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_PostQuantum_cpp
diff -N net/i2pd/patches/patch-libi2pd_PostQuantum_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_PostQuantum_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,347 @@
+Index: libi2pd/PostQuantum.cpp
+--- libi2pd/PostQuantum.cpp.orig
++++ libi2pd/PostQuantum.cpp
+@@ -6,6 +6,8 @@
+ * See full license text in LICENSE file at top of project tree
+ */
+
++#include <cstring>
++#include <openssl/crypto.h>
+ #include "Log.h"
+ #include "PostQuantum.h"
+
+@@ -14,227 +16,173 @@
+ #ifndef LIBRESSL_VERSION_NUMBER
+ #include <openssl/param_build.h>
+ #include <openssl/core_names.h>
+-#else
+-#include<openssl/mlkem.h>
+ #endif
+
+ namespace i2p
+ {
+ namespace crypto
+ {
++#ifdef LIBRESSL_VERSION_NUMBER
++ // LibreSSL allocates operation outputs. Always wipe and release them,
++ // including when an operation or a subsequent length check fails.
++ struct MLKEMBuffer
++ {
++ MLKEMBuffer () = default;
++ MLKEMBuffer (const MLKEMBuffer&) = delete;
++ MLKEMBuffer& operator= (const MLKEMBuffer&) = delete;
++ uint8_t * data = nullptr;
++ size_t len = 0;
++ ~MLKEMBuffer ()
++ {
++ if (data) OPENSSL_cleanse (data, len);
++ OPENSSL_free (data);
++ }
++ };
++#endif
++
+ MLKEMKeys::MLKEMKeys (MLKEMTypes type):
+-#ifndef LIBRESSL_VERSION_NUMBER
+- m_Name (std::get<0>(MLKEMS[type])), m_KeyLen (std::get<1>(MLKEMS[type])),
+- m_CTLen (std::get<2>(MLKEMS[type])),
+-#else
+- m_Name (std::get<0>(MLKEMS[eMLKEM768])), m_KeyLen (std::get<1>(MLKEMS[eMLKEM768])),
+- m_CTLen (std::get<2>(MLKEMS[eMLKEM768])),
++ m_Name (std::get<0>(MLKEMS.at (type))), m_KeyLen (std::get<1>(MLKEMS.at (type))),
++ m_CTLen (std::get<2>(MLKEMS.at (type))), m_Pkey (nullptr)
++#ifdef LIBRESSL_VERSION_NUMBER
++ , m_PublicKey (nullptr),
++ m_Rank (type == eMLKEM768 ? MLKEM768_RANK : type == eMLKEM1024 ? MLKEM1024_RANK : 0)
+ #endif
+- m_Pkey (nullptr)
+ {
+ }
+
+ MLKEMKeys::~MLKEMKeys ()
+ {
+- FreeKeys();
++ FreeKeys ();
+ }
+
+ void MLKEMKeys::FreeKeys ()
+ {
+ #ifndef LIBRESSL_VERSION_NUMBER
+- if (m_Pkey) EVP_PKEY_free (m_Pkey);
++ EVP_PKEY_free (m_Pkey);
+ #else
+- if (m_Pkey) MLKEM_private_key_free (m_Pkey);
++ MLKEM_private_key_free (m_Pkey);
++ MLKEM_public_key_free (m_PublicKey);
++ m_PublicKey = nullptr;
+ #endif
+- if (m_Pkey) m_Pkey = nullptr;
++ m_Pkey = nullptr;
+ }
+
+- void MLKEMKeys::GenerateKeys ()
++ bool MLKEMKeys::GenerateKeys ()
+ {
+- FreeKeys();
++ FreeKeys ();
+ #ifndef LIBRESSL_VERSION_NUMBER
+- m_Pkey = EVP_PKEY_Q_keygen(NULL, NULL, m_Name.c_str ());
+- LogPrint(eLogDebug, "MLKEM: GenerateKeys [ openssl ]");
++ m_Pkey = EVP_PKEY_Q_keygen (nullptr, nullptr, m_Name.c_str ());
++ if (m_Pkey) return true;
+ #else
+- m_Pkey = MLKEM_private_key_new( MLKEM768_RANK);
+-
+- uint8_t * pub_key = nullptr;
+- size_t pub_key_len = 0;
+- uint8_t * seed = nullptr;
+- size_t seed_len = 0;
+-
+- if (MLKEM_generate_key(m_Pkey, &pub_key, &pub_key_len, &seed, &seed_len) == 1)
+- {
+- LogPrint(eLogDebug, "MLKEM: GenerateKeys [ libressl ] success");
+- if (pub_key_len <= sizeof(m_CachedPub))
+- {
+- memcpy(m_CachedPub, pub_key, pub_key_len);
+- m_IsPubCached = true;
+- LogPrint(eLogDebug, "MLKEM [libressl] cache the pub succes");
+- } else
+- {
+- LogPrint(eLogError, "MLKEM: can't cache private key [libressl]");
+- }
+- OPENSSL_free(pub_key);
+- if (seed) OPENSSL_free(seed);
+- } else
+- {
+- LogPrint(eLogError, "MLKEM: GenerateKeys [ libressl ] failed");
+- MLKEM_private_key_free(m_Pkey);
+- m_Pkey = nullptr;
+- }
++ MLKEMBuffer pub;
++ // No seed is needed: LibreSSL wipes its temporary seed internally.
++ m_Pkey = MLKEM_private_key_new (m_Rank);
++ m_PublicKey = MLKEM_public_key_new (m_Rank);
++ if (m_Pkey && m_PublicKey &&
++ MLKEM_generate_key (m_Pkey, &pub.data, &pub.len, nullptr, nullptr) == 1 &&
++ pub.len == m_KeyLen &&
++ MLKEM_public_from_private (m_Pkey, m_PublicKey) == 1) return true;
+ #endif
+- } // end method
++ FreeKeys ();
++ LogPrint (eLogError, "MLKEM: Failed to generate keys");
++ return false;
++ }
+
+- void MLKEMKeys::GetPublicKey (uint8_t * pub) const
++ bool MLKEMKeys::GetPublicKey (uint8_t * pub) const
+ {
+- if (m_Pkey)
+- {
+ #ifndef LIBRESSL_VERSION_NUMBER
+- size_t len = m_KeyLen;
+- EVP_PKEY_get_octet_string_param (m_Pkey, OSSL_PKEY_PARAM_PUB_KEY, pub, m_KeyLen, &len);
++ size_t len = 0;
++ if (m_Pkey && EVP_PKEY_get_octet_string_param (m_Pkey,
++ OSSL_PKEY_PARAM_PUB_KEY, pub, m_KeyLen, &len) == 1 && len == m_KeyLen) return true;
+ #else
+- if (!m_Pkey) return;
+-
+- LogPrint(eLogDebug, "MLKEM: GetPublicKey [ libressl ]");
+-
+- if (m_IsPubCached)
+- {
+- memcpy(pub, m_CachedPub, MLKEM768_KEY_LENGTH);
+- LogPrint(eLogDebug,"MLKEM [libressl]: copy pubkey");
+- }
+- else
+- LogPrint(eLogError, "MLKEM: Public key not cached!");
+-#endif
++ MLKEMBuffer encoded;
++ if (m_PublicKey && MLKEM_marshal_public_key (m_PublicKey, &encoded.data, &encoded.len) == 1 &&
++ encoded.len == m_KeyLen)
++ {
++ memcpy (pub, encoded.data, m_KeyLen);
++ return true;
+ }
++#endif
++ LogPrint (eLogError, "MLKEM: Failed to export public key");
++ return false;
+ }
+
+-
+- void MLKEMKeys::SetPublicKey (const uint8_t * pub)
++ bool MLKEMKeys::SetPublicKey (const uint8_t * pub)
+ {
++ // A failed import must not leave a previous key available for use.
++ FreeKeys ();
+ #ifndef LIBRESSL_VERSION_NUMBER
+- if (m_Pkey)
+- {
+- EVP_PKEY_free (m_Pkey);
+- m_Pkey = nullptr;
+- }
+ OSSL_PARAM params[] =
+ {
+- OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, (void*)pub, m_KeyLen),
++ OSSL_PARAM_octet_string (OSSL_PKEY_PARAM_PUB_KEY, (void *)pub, m_KeyLen),
+ OSSL_PARAM_END
+ };
+-
+- EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_from_name(NULL, m_Name.c_str(), NULL);
+- if (ctx)
+- {
+- EVP_PKEY_fromdata_init(ctx);
+- if (EVP_PKEY_fromdata(ctx, &m_Pkey, OSSL_KEYMGMT_SELECT_PUBLIC_KEY, params) <= 0)
+- LogPrint(eLogError, "MLKEM: Failed to set public key data");
+- EVP_PKEY_CTX_free(ctx);
+- }
+- else
+- LogPrint(eLogError, "MLKEM: can't create PKEY context");
++ auto ctx = EVP_PKEY_CTX_new_from_name (nullptr, m_Name.c_str (), nullptr);
++ bool ok = ctx && EVP_PKEY_fromdata_init (ctx) == 1 &&
++ EVP_PKEY_fromdata (ctx, &m_Pkey, OSSL_KEYMGMT_SELECT_PUBLIC_KEY, params) == 1;
++ EVP_PKEY_CTX_free (ctx);
++ if (ok) return true;
+ #else
+- MLKEM_public_key * pub_key = MLKEM_public_key_new(MLKEM768_RANK);
+- if (MLKEM_parse_public_key(pub_key, pub, m_KeyLen))
+- {
+- memcpy(m_CachedPub, pub, m_KeyLen);
+- m_IsPubCached = true;
+- LogPrint(eLogDebug, "MLKEM: SetPublicKey [ libressl ] success");
+- }
+- else
+- LogPrint(eLogError, "MLKEM: failed to parse public key");
+- if (pub_key) MLKEM_public_key_free(pub_key);
++ m_PublicKey = MLKEM_public_key_new (m_Rank);
++ if (m_PublicKey && MLKEM_parse_public_key (m_PublicKey, pub, m_KeyLen) == 1) return true;
+ #endif
++ FreeKeys ();
++ LogPrint (eLogError, "MLKEM: Failed to parse public key");
++ return false;
+ }
+
+- void MLKEMKeys::Encaps (uint8_t * ciphertext, uint8_t * shared)
++ bool MLKEMKeys::Encaps (uint8_t * ciphertext, uint8_t * shared)
+ {
+ #ifndef LIBRESSL_VERSION_NUMBER
+- if (!m_Pkey)
+- {
+- LogPrint(eLogError, "MLKEM: No public key for Encaps");
+- return;
+- }
+- auto ctx = EVP_PKEY_CTX_new_from_pkey (NULL, m_Pkey, NULL);
+- if (ctx)
+- {
+- EVP_PKEY_encapsulate_init (ctx, NULL);
+- size_t len = m_CTLen, sharedLen = 32;
+- EVP_PKEY_encapsulate (ctx, ciphertext, &len, shared, &sharedLen);
+- EVP_PKEY_CTX_free (ctx);
+- }
+- else
+- LogPrint (eLogError, "MLKEM can't create PKEY context");
++ auto ctx = m_Pkey ? EVP_PKEY_CTX_new_from_pkey (nullptr, m_Pkey, nullptr) : nullptr;
++ size_t len = m_CTLen, sharedLen = 32;
++ bool ok = ctx && EVP_PKEY_encapsulate_init (ctx, nullptr) == 1 &&
++ EVP_PKEY_encapsulate (ctx, ciphertext, &len, shared, &sharedLen) == 1 &&
++ len == m_CTLen && sharedLen == 32;
++ EVP_PKEY_CTX_free (ctx);
++ if (ok) return true;
+ #else
+- if (!m_IsPubCached)
++ MLKEMBuffer ct, secret;
++ if (m_PublicKey && MLKEM_encap (m_PublicKey, &ct.data, &ct.len, &secret.data, &secret.len) == 1 &&
++ ct.len == m_CTLen && secret.len == 32)
+ {
+- LogPrint(eLogError, "MLKEM: No public key for Encaps");
+- return;
++ memcpy (ciphertext, ct.data, m_CTLen);
++ memcpy (shared, secret.data, 32);
++ return true;
+ }
+- auto pub_key = MLKEM_public_key_new(MLKEM768_RANK);
+- if (MLKEM_parse_public_key (pub_key, m_CachedPub, m_KeyLen) != 1)
+- {
+- LogPrint(eLogError, "MLKEM can't parse cached public key");
+- return;
+- }
+- uint8_t * out_ct = nullptr;
+- size_t out_ct_len = 0;
+- uint8_t * out_ss = nullptr;
+- size_t out_ss_len = 0;
+- if (MLKEM_encap(pub_key, &out_ct, &out_ct_len, &out_ss, &out_ss_len) == 1)
+- {
+- memcpy(ciphertext, out_ct, out_ct_len);
+- memcpy(shared, out_ss, out_ss_len);
+- OPENSSL_cleanse(out_ct, out_ct_len);
+- OPENSSL_cleanse(out_ss, out_ss_len);
+- OPENSSL_free(out_ct);
+- OPENSSL_free(out_ss);
+- LogPrint(eLogDebug, "MLKEM [libressl] succesfully encaps");
+- }
+- else
+- LogPrint(eLogError, "MLKEM [libressl]: encapsulation failed");
+- MLKEM_public_key_free(pub_key);
+ #endif
++ OPENSSL_cleanse (shared, 32);
++ LogPrint (eLogError, "MLKEM: Encapsulation failed");
++ return false;
+ }
+
+- void MLKEMKeys::Decaps (const uint8_t * ciphertext, uint8_t * shared)
++ bool MLKEMKeys::Decaps (const uint8_t * ciphertext, uint8_t * shared)
+ {
+- if (!m_Pkey) return;
+ #ifndef LIBRESSL_VERSION_NUMBER
+- auto ctx = EVP_PKEY_CTX_new_from_pkey (NULL, m_Pkey, NULL);
+- if (ctx)
++ auto ctx = m_Pkey ? EVP_PKEY_CTX_new_from_pkey (nullptr, m_Pkey, nullptr) : nullptr;
++ size_t sharedLen = 32;
++ bool ok = ctx && EVP_PKEY_decapsulate_init (ctx, nullptr) == 1 &&
++ EVP_PKEY_decapsulate (ctx, shared, &sharedLen, ciphertext, m_CTLen) == 1 && sharedLen == 32;
++ EVP_PKEY_CTX_free (ctx);
++ if (ok) return true;
++#else
++ MLKEMBuffer secret;
++ // Success includes implicit rejection. The following Noise AEAD
++ // authentication detects an invalid, correctly sized ciphertext.
++ if (m_Pkey && MLKEM_decap (m_Pkey, ciphertext, m_CTLen, &secret.data, &secret.len) == 1 && secret.len == 32)
+ {
+- EVP_PKEY_decapsulate_init (ctx, NULL);
+- size_t sharedLen = 32;
+- EVP_PKEY_decapsulate (ctx, shared, &sharedLen, ciphertext, m_CTLen);
+- EVP_PKEY_CTX_free (ctx);
++ memcpy (shared, secret.data, 32);
++ return true;
+ }
+- else
+- LogPrint (eLogError, "MLKEM can't create PKEY context");
+-#else
+- uint8_t * out_shared_secret = nullptr;
+- size_t out_shared_secret_len = 0;
+- if (MLKEM_decap(m_Pkey, ciphertext, m_CTLen, &out_shared_secret, &out_shared_secret_len) == 1)
+- {
+- memcpy(shared, out_shared_secret, out_shared_secret_len);
+-
+- OPENSSL_cleanse(out_shared_secret, out_shared_secret_len);
+-
+- OPENSSL_free(out_shared_secret);
+- LogPrint(eLogDebug, "MLKEM [libressl] succesfully decrypt");
+- }
+- else
+- {
+- LogPrint(eLogError, "MLKEM [libressl]: decapsulation failed");
+- }
+ #endif
++ OPENSSL_cleanse (shared, 32);
++ LogPrint (eLogError, "MLKEM: Decapsulation failed");
++ return false;
+ }
+
+ std::unique_ptr<MLKEMKeys> CreateMLKEMKeys (i2p::data::CryptoKeyType type)
+ {
+- if (type <= i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD ||
+- type - i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD > (int)MLKEMS.size ()) return nullptr;
++ if (!IsMLKEMSupported (type)) return nullptr;
+ return std::make_unique<MLKEMKeys>((MLKEMTypes)(type - i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD - 1));
+ }
+
Index: net/i2pd/patches/patch-libi2pd_PostQuantum_h
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_PostQuantum_h
diff -N net/i2pd/patches/patch-libi2pd_PostQuantum_h
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_PostQuantum_h 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,64 @@
+Index: libi2pd/PostQuantum.h
+--- libi2pd/PostQuantum.h.orig
++++ libi2pd/PostQuantum.h
+@@ -15,7 +15,7 @@
+ #include <tuple>
+ #include "Crypto.h"
+ #include "Identity.h"
+-#ifdef LIBRESSL_VERSION_NUMBER
++#if OPENSSL_PQ && defined(LIBRESSL_VERSION_NUMBER)
+ # include <openssl/mlkem.h>
+ #endif
+
+@@ -60,6 +60,16 @@ namespace crypto
+ return std::get<2>(MLKEMS[type - i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD - 1]);
+ }
+
++ constexpr bool IsMLKEMSupported (i2p::data::CryptoKeyType type)
++ {
++ if (!GetMLKEMPublicKeyLen (type)) return false;
++#ifdef LIBRESSL_VERSION_NUMBER
++ return type != i2p::data::CRYPTO_KEY_TYPE_ECIES_MLKEM512_X25519_AEAD;
++#else
++ return true;
++#endif
++ }
++
+ class MLKEMKeys
+ {
+ public:
+@@ -67,13 +77,18 @@ namespace crypto
+ MLKEMKeys (MLKEMTypes type);
+ ~MLKEMKeys ();
+
+- void GenerateKeys ();
+- void GetPublicKey (uint8_t * pub) const;
+- void SetPublicKey (const uint8_t * pub);
++ MLKEMKeys (const MLKEMKeys&) = delete;
++ MLKEMKeys& operator= (const MLKEMKeys&) = delete;
++
++ // Buffers must hold GetKeyLen(), GetCTLen(), or 32 bytes as appropriate.
++ // On false, callers must abort the handshake and discard outputs.
++ [[nodiscard]] bool GenerateKeys ();
++ [[nodiscard]] bool GetPublicKey (uint8_t * pub) const;
++ [[nodiscard]] bool SetPublicKey (const uint8_t * pub);
+ size_t GetKeyLen () const { return m_KeyLen; };
+ size_t GetCTLen () const { return m_CTLen; };
+- void Encaps (uint8_t * ciphertext, uint8_t * shared);
+- void Decaps (const uint8_t * ciphertext, uint8_t * shared);
++ [[nodiscard]] bool Encaps (uint8_t * ciphertext, uint8_t * shared);
++ [[nodiscard]] bool Decaps (const uint8_t * ciphertext, uint8_t * shared);
+
+
+ private:
+@@ -88,8 +103,8 @@ namespace crypto
+ EVP_PKEY * m_Pkey;
+ #else
+ MLKEM_private_key * m_Pkey;
+- uint8_t m_CachedPub[MLKEM768_KEY_LENGTH];
+- bool m_IsPubCached = false;
++ MLKEM_public_key * m_PublicKey;
++ const int m_Rank;
+ #endif
+ };
+
Index: net/i2pd/patches/patch-libi2pd_Reseed_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Reseed_cpp
diff -N net/i2pd/patches/patch-libi2pd_Reseed_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Reseed_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,30 @@
+Index: libi2pd/Reseed.cpp
+--- libi2pd/Reseed.cpp.orig
++++ libi2pd/Reseed.cpp
+@@ -15,7 +15,7 @@
+ #include <boost/algorithm/string.hpp>
+ #include <openssl/ssl.h>
+ #include <openssl/err.h>
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ #include <openssl/core_names.h>
+ #endif
+ #include <zlib.h>
+@@ -511,7 +511,7 @@ namespace data
+ // extract RSA key (we need n only, e = 65537)
+ EVP_PKEY * pubKey = X509_get_pubkey (cert);
+ const BIGNUM * n = nullptr;
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ BIGNUM * n1 = BN_new ();
+ if (EVP_PKEY_get_bn_param (pubKey, OSSL_PKEY_PARAM_RSA_N, &n1) > 0)
+ n = n1;
+@@ -531,7 +531,7 @@ namespace data
+ }
+ else
+ LogPrint (eLogError, "Reseed: Can't extract RSA key from ", filename);
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ BN_free (n1);
+ #endif
+ }
Index: net/i2pd/patches/patch-libi2pd_RouterContext_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_RouterContext_cpp
diff -N net/i2pd/patches/patch-libi2pd_RouterContext_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_RouterContext_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,17 @@
+Index: libi2pd/RouterContext.cpp
+--- libi2pd/RouterContext.cpp.orig
++++ libi2pd/RouterContext.cpp
+@@ -585,7 +585,13 @@ namespace i2p
+ if (addr && (addr->host != host || !addr->ssu->mtu))
+ {
+ addr->host = host;
++#ifdef __OpenBSD__
++ // Address updates must not replace an explicitly configured MTU.
++ uint16_t configuredMTU; i2p::config::GetOption ("ssu2.mtu6", configuredMTU);
++ if (!configuredMTU && m_StatusV6 != eRouterStatusProxy)
++#else
+ if (m_StatusV6 != eRouterStatusProxy)
++#endif
+ {
+ // update MTU
+ auto mtu = i2p::util::net::GetMTU (host);
Index: net/i2pd/patches/patch-libi2pd_SSU2Session_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_SSU2Session_cpp
diff -N net/i2pd/patches/patch-libi2pd_SSU2Session_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_SSU2Session_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,101 @@
+Index: libi2pd/SSU2Session.cpp
+--- libi2pd/SSU2Session.cpp.orig
++++ libi2pd/SSU2Session.cpp
+@@ -741,7 +741,7 @@ namespace transport
+ {
+ i2p::data::CryptoKeyType cryptoType = (i2p::data::CryptoKeyType)(m_Version + 2);
+ m_PQKeys = i2p::crypto::CreateMLKEMKeys (cryptoType);
+- m_PQKeys->GenerateKeys ();
++ if (!m_PQKeys || !m_PQKeys->GenerateKeys ()) return false;
+ offset = m_PQKeys->GetKeyLen () + 16;
+ payloadSize += offset;
+ }
+@@ -791,7 +791,7 @@ namespace transport
+ {
+ size_t keyLen = m_PQKeys->GetKeyLen ();
+ std::vector<uint8_t> encapsKey(keyLen);
+- m_PQKeys->GetPublicKey (encapsKey.data ());
++ if (!m_PQKeys || !m_PQKeys->GetPublicKey (encapsKey.data ())) return false;
+ if (!m_NoiseState->Encrypt (encapsKey.data (), payload, keyLen))
+ {
+ LogPrint (eLogWarning, "SSU2: SessionRequest ML-KEM encap_key frame AEAD encryption failed ");
+@@ -904,6 +904,7 @@ namespace transport
+ {
+ auto cryptoType = (i2p::data::CryptoKeyType)(m_Version + 2);
+ auto keyLen = i2p::crypto::GetMLKEMPublicKeyLen (cryptoType);
++ if (!i2p::crypto::IsMLKEMSupported (cryptoType) || offset + keyLen + 16 > len) return false;
+ std::vector<uint8_t> encapsKey(keyLen);
+ if (!m_NoiseState->Decrypt (buf + offset, encapsKey.data (), keyLen))
+ {
+@@ -913,7 +914,7 @@ namespace transport
+ m_NoiseState->MixHash (buf + offset, keyLen + 16);
+ offset += keyLen + 16;
+ m_PQKeys = i2p::crypto::CreateMLKEMKeys (cryptoType);
+- m_PQKeys->SetPublicKey (encapsKey.data ());
++ if (!m_PQKeys || !m_PQKeys->SetPublicKey (encapsKey.data ())) return false;
+ }
+ #endif
+ if (offset + 16 > len)
+@@ -976,11 +977,12 @@ namespace transport
+ size_t maxPayloadSize = m_MaxPayloadSize - 48;
+ size_t payloadSize = 0, offset = 0;
+ #if OPENSSL_PQ
+- if (m_Version > 2 && m_PQKeys)
++ if (m_Version > 2)
+ {
++ if (!m_PQKeys) return;
+ size_t cipherTextLen = m_PQKeys->GetCTLen ();
+ std::vector<uint8_t> kemCiphertext(cipherTextLen);
+- m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret);
++ if (!m_PQKeys || !m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret)) return;
+ if (!m_NoiseState->Encrypt (kemCiphertext.data (), payload, cipherTextLen))
+ {
+ LogPrint (eLogWarning, "SSU2: SessionCreated ML-KEM ciphertext section AEAD encryption failed");
+@@ -1047,6 +1049,11 @@ namespace transport
+ LogPrint (eLogWarning, "SSU2: Unexpected SessionCreated message");
+ return false;
+ }
++ if (len < 80)
++ {
++ LogPrint (eLogWarning, "SSU2: SessionCreated message too short ", len);
++ return false;
++ }
+ Header header;
+ memcpy (header.buf, buf, 16);
+ header.ll[0] ^= CreateHeaderMask (m_Address->i, buf + (len - 24));
+@@ -1056,11 +1063,7 @@ namespace transport
+ if (header.h.type != eSSU2SessionCreated)
+ // this situation is valid, because it might be Retry with different encryption
+ return false;
+- if (len < 80)
+- {
+- LogPrint (eLogWarning, "SSU2: SessionCreated message too short ", len);
+- return false;
+- }
++
+ m_HandshakeInterval = i2p::util::GetMillisecondsSinceEpoch () - m_HandshakeInterval;
+ const uint8_t nonce[12] = {0};
+ uint8_t headerX[48];
+@@ -1073,10 +1076,12 @@ namespace transport
+ m_NoiseState->MixKey (sharedSecret);
+ size_t offset = 64;
+ #if OPENSSL_PQ
+- if (m_Version > 2 && m_PQKeys)
++ if (m_Version > 2)
+ {
++ if (!m_PQKeys) return false;
+ i2p::data::CryptoKeyType cryptoType = (i2p::data::CryptoKeyType)(m_Version + 2);
+ size_t cipherTextLen = i2p::crypto::GetMLKEMCipherTextLen (cryptoType);
++ if (!i2p::crypto::IsMLKEMSupported (cryptoType) || offset + cipherTextLen + 16 > len) return false;
+ std::vector<uint8_t> kemCiphertext(cipherTextLen);
+ if (!m_NoiseState->Decrypt (buf + offset, kemCiphertext.data (), cipherTextLen))
+ {
+@@ -1085,7 +1090,7 @@ namespace transport
+ }
+ m_NoiseState->MixHash (buf + offset, cipherTextLen + 16);
+ offset += cipherTextLen + 16;
+- m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret);
++ if (!m_PQKeys || !m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret)) return false;
+ m_NoiseState->MixKey (sharedSecret);
+ }
+ #endif
Index: net/i2pd/patches/patch-libi2pd_Signature_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Signature_cpp
diff -N net/i2pd/patches/patch-libi2pd_Signature_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Signature_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,102 @@
+Index: libi2pd/Signature.cpp
+--- libi2pd/Signature.cpp.orig
++++ libi2pd/Signature.cpp
+@@ -8,7 +8,8 @@
+
+ #include <memory>
+ #include <openssl/evp.h>
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#include <openssl/crypto.h>
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ #include <openssl/core_names.h>
+ #include <openssl/param_build.h>
+ #endif
+@@ -19,7 +20,7 @@ namespace i2p
+ {
+ namespace crypto
+ {
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ DSAVerifier::DSAVerifier ():
+ m_PublicKey (nullptr)
+ {
+@@ -172,7 +173,7 @@ namespace crypto
+ }
+ #endif
+
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ ECDSAVerifier::ECDSAVerifier (int curve, size_t keyLen, const EVP_MD * hash):
+ m_Curve(curve), m_KeyLen (keyLen), m_Hash (hash), m_PublicKey (nullptr)
+ {
+@@ -405,7 +406,7 @@ namespace crypto
+ LogPrint (eLogError, "EdDSA signing key is not set");
+ }
+
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000)
++#if OPENSSL_ED25519PH
+ static const OSSL_PARAM EDDSA25519phParams[] =
+ {
+ OSSL_PARAM_utf8_string ("instance", (char *)"Ed25519ph", 9),
+@@ -415,19 +416,13 @@ namespace crypto
+ bool EDDSA25519phVerifier::Verify (const uint8_t * buf, size_t len, const uint8_t * signature) const
+ {
+ auto pkey = GetPkey ();
+- if (pkey)
+- {
+- uint8_t digest[64];
+- SHA512 (buf, len, digest);
+- EVP_MD_CTX * ctx = EVP_MD_CTX_create ();
+- EVP_DigestVerifyInit_ex (ctx, NULL, NULL, NULL, NULL, pkey, EDDSA25519phParams);
+- auto ret = EVP_DigestVerify (ctx, signature, 64, digest, 64);
+- EVP_MD_CTX_destroy (ctx);
+- return ret;
+- }
+- else
+- LogPrint (eLogError, "EdDSA verification key is not set");
+- return false;
++ EVP_MD_CTX * ctx = EVP_MD_CTX_new ();
++ // The provider applies SHA-512 for the Ed25519ph instance.
++ bool ret = pkey && ctx &&
++ EVP_DigestVerifyInit_ex (ctx, nullptr, nullptr, nullptr, nullptr, pkey, EDDSA25519phParams) == 1 &&
++ EVP_DigestVerify (ctx, signature, 64, buf, len) == 1;
++ EVP_MD_CTX_free (ctx);
++ return ret;
+ }
+
+ EDDSA25519phSigner::EDDSA25519phSigner (const uint8_t * signingPrivateKey):
+@@ -438,21 +433,21 @@ namespace crypto
+ void EDDSA25519phSigner::Sign (const uint8_t * buf, int len, uint8_t * signature) const
+ {
+ auto pkey = GetPkey ();
+- if (pkey)
++ EVP_MD_CTX * ctx = EVP_MD_CTX_new ();
++ size_t l = 64;
++ uint8_t sig[64];
++ bool ok = pkey && ctx && len >= 0 &&
++ EVP_DigestSignInit_ex (ctx, nullptr, nullptr, nullptr, nullptr, pkey, EDDSA25519phParams) == 1 &&
++ EVP_DigestSign (ctx, sig, &l, buf, len) == 1 && l == sizeof (sig);
++ if (ok)
++ memcpy (signature, sig, sizeof (sig));
++ else
+ {
+- uint8_t digest[64];
+- SHA512 (buf, len, digest);
+- EVP_MD_CTX * ctx = EVP_MD_CTX_create ();
+- size_t l = 64;
+- uint8_t sig[64];
+- EVP_DigestSignInit_ex (ctx, NULL, NULL, NULL, NULL, pkey, EDDSA25519phParams);
+- if (!EVP_DigestSign (ctx, sig, &l, digest, 64))
+- LogPrint (eLogError, "EdDSA signing failed");
+- memcpy (signature, sig, 64);
+- EVP_MD_CTX_destroy (ctx);
++ memset (signature, 0, sizeof (sig));
++ LogPrint (eLogError, "Ed25519ph signing failed");
+ }
+- else
+- LogPrint (eLogError, "EdDSA signing key is not set");
++ OPENSSL_cleanse (sig, sizeof (sig));
++ EVP_MD_CTX_free (ctx);
+ }
+ #endif
+ }
Index: net/i2pd/patches/patch-libi2pd_Signature_h
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Signature_h
diff -N net/i2pd/patches/patch-libi2pd_Signature_h
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Signature_h 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,48 @@
+Index: libi2pd/Signature.h
+--- libi2pd/Signature.h.orig
++++ libi2pd/Signature.h
+@@ -62,7 +62,7 @@ namespace crypto
+
+ private:
+
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ EVP_PKEY * m_PublicKey;
+ #else
+ DSA * m_PublicKey;
+@@ -82,7 +82,7 @@ namespace crypto
+
+ private:
+
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ EVP_PKEY * m_PrivateKey;
+ #else
+ DSA * m_PrivateKey;
+@@ -96,7 +96,7 @@ namespace crypto
+ constexpr size_t ECDSAP384_KEY_LENGTH = 96;
+ constexpr size_t ECDSAP521_KEY_LENGTH = 132;
+
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ class ECDSAVerifier: public Verifier
+ {
+ public:
+@@ -377,7 +377,7 @@ namespace crypto
+ EVP_PKEY * GetPkey () const { return m_Pkey; };
+ };
+
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if OPENSSL_ED25519PH
+ class EDDSA25519phVerifier: public EDDSA25519Verifier
+ {
+ public:
+@@ -423,7 +423,7 @@ namespace crypto
+ EDDSA25519SignerCompat * m_Fallback;
+ };
+
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if OPENSSL_ED25519PH
+ class EDDSA25519phSigner: public EDDSA25519Signer
+ {
+ public:
Index: net/i2pd/patches/patch-libi2pd_client_ClientContext_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_client_ClientContext_cpp
diff -N net/i2pd/patches/patch-libi2pd_client_ClientContext_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_client_ClientContext_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,44 @@
+Index: libi2pd_client/ClientContext.cpp
+--- libi2pd_client/ClientContext.cpp.orig
++++ libi2pd_client/ClientContext.cpp
+@@ -190,10 +190,15 @@ namespace client
+ }
+
+ LogPrint(eLogInfo, "Clients: Stopping UDP Tunnels timers");
+- if (m_CleanupUDPTimer)
+ {
+- m_CleanupUDPTimer->cancel ();
+- m_CleanupUDPTimer = nullptr;
++#ifdef __OpenBSD__
++ std::lock_guard<std::mutex> lock(m_ForwardsMutex);
++#endif
++ if (m_CleanupUDPTimer)
++ {
++ m_CleanupUDPTimer->cancel ();
++ m_CleanupUDPTimer = nullptr;
++ }
+ }
+
+ {
+@@ -219,6 +224,21 @@ namespace client
+ // change shared local destination
+ m_SharedLocalDestination->Release ();
+ CreateNewSharedLocalDestination ();
++
++#ifdef __OpenBSD__
++ // The old destination (still in m_Destinations) owns the timer's
++ // io_context. Rebind before it is stopped and removed below.
++ // CleanupUDP() uses the same mutex when touching/rearming the timer.
++ {
++ std::lock_guard<std::mutex> lock(m_ForwardsMutex);
++ if (m_CleanupUDPTimer)
++ {
++ m_CleanupUDPTimer->cancel ();
++ m_CleanupUDPTimer = std::make_unique<boost::asio::steady_timer>(m_SharedLocalDestination->GetService ());
++ ScheduleCleanupUDP ();
++ }
++ }
++#endif
+
+ // recreate HTTP proxy
+ if (m_HttpProxy)
Index: net/i2pd/patches/patch-libi2pd_util_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_util_cpp
diff -N net/i2pd/patches/patch-libi2pd_util_cpp
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_util_cpp 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,40 @@
+Index: libi2pd/util.cpp
+--- libi2pd/util.cpp.orig
++++ libi2pd/util.cpp
+@@ -24,6 +24,10 @@
+ #include <pthread_np.h>
+ #endif
+
++#ifdef __OpenBSD__
++#include <net/if.h>
++#endif
++
+ #if defined(__APPLE__)
+ # include <AvailabilityMacros.h>
+ #endif
+@@ -556,6 +560,17 @@ namespace net
+ int mtu = fallback;
+ if (ifa && family)
+ { // interface found?
++#ifdef __OpenBSD__
++ // getifaddrs supplies if_data for AF_LINK entries. NET_RT_IFLIST
++ // is permitted by pledge("inet"), unlike SIOCGIFMTU.
++ for (auto link = ifaddr; link; link = link->ifa_next)
++ if (link->ifa_addr && link->ifa_addr->sa_family == AF_LINK &&
++ link->ifa_data && !strcmp (link->ifa_name, ifa->ifa_name))
++ {
++ mtu = static_cast<const struct if_data *> (link->ifa_data)->ifi_mtu;
++ break;
++ }
++#else
+ int fd = socket(family, SOCK_DGRAM, 0);
+ if (fd > 0)
+ {
+@@ -569,6 +584,7 @@ namespace net
+ }
+ else
+ LogPrint(eLogError, "NetIface: Failed to create datagram socket");
++#endif
+ }
+ else
+ LogPrint(eLogWarning, "NetIface: Interface for local address", localAddress.to_string(), " not found");
Index: net/i2pd/patches/patch-test_Makefile
===================================================================
RCS file: /cvs/ports/net/i2pd/patches/patch-test_Makefile,v
diff -u -p -N -r1.1 patch-test_Makefile
--- net/i2pd/patches/patch-test_Makefile 17 Oct 2026 08:47:19 -0000 1.1
+++ /dev/null 1 Jan 1970 00:00:00 -0000
@@ -1,11 +0,0 @@
-Index: tests/Makefile
---- tests/Makefile.orig
-+++ tests/Makefile
-@@ -18,7 +18,6 @@ ifneq (, $(findstring mingw, $(SYS))$(findstring windo
- endif
-
- LDLIBS = \
-- -lboost_system$(BOOST_SUFFIX) \
- -lboost_program_options$(BOOST_SUFFIX) \
- -lssl \
- -lcrypto \
Index: net/i2pd/patches/patch-tests_Makefile
===================================================================
RCS file: net/i2pd/patches/patch-tests_Makefile
diff -N net/i2pd/patches/patch-tests_Makefile
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-tests_Makefile 17 Oct 2026 18:42:53 -0000
@@ -0,0 +1,11 @@
+Index: tests/Makefile
+--- tests/Makefile.orig
++++ tests/Makefile
+@@ -18,7 +18,6 @@ ifneq (, $(findstring mingw, $(SYS))$(findstring windo
+ endif
+
+ LDLIBS = \
+- -lboost_system$(BOOST_SUFFIX) \
+ -lboost_program_options$(BOOST_SUFFIX) \
+ -lssl \
+ -lcrypto \