···8080SASL
8181----
8282- SASLServ and its modules have been almost entirely re-written
8383-- Add support for SASL SCRAM-SHA logins (see `doc/SASL-SCRAM-SHA`)
8483- Advertise SASL mechanism list to UnrealIRCd servers
8584- Use a parameter vector to allow an arbitrary number of S2S arguments
8686-- Indicate whether the client is on a plaintext connection or not. This can
8787- be used by user_can_login hooks.
8585+- Indicate whether the client is on a plaintext connection or not.
8686+ - This can be used by user_can_login hooks.
8787+- Add support for SASL SCRAM-SHA logins (see `doc/SASL-SCRAM-SHA`)
8888+- Add support for Curve25519 ECDH-based challenge-response logins
8989+ - This is a private SASL mechanism that does not have widespread client
9090+ support yet, but it is expected to eventually replace the older
9191+ ECDSA-NIST256P-CHALLENGE mechanism, due to concerns within the
9292+ cryptographic community about the safety of the NIST curves.
9393+ - A complete mechanism documentation, including the protocol, and a design
9494+ rationale, is located in `modules/saslserv/ecdh-x25519-challenge.c`. This
9595+ will enable client authors to integrate the functionality into their IRC
9696+ clients.
9797+ - A tool, `atheme-ecdh-x25519-tool`, is provided (and is installed into
9898+ `bin/` by `make install`) to enable users to generate private keys, obtain
9999+ their public keys in base64 format (to pass to `NickServ SET`), encode
100100+ keypairs as a QR-Code (should mobile clients end up implementing this
101101+ functionality, and users wish to easily transfer their private keys), and
102102+ serve as a reference tool to generate server challenges, and responses to
103103+ server challenges, for client authors to verify their implementations
104104+ against. Note that the QR-Code printing support requires a UTF-8-capable
105105+ terminal emulator, with a monospace font supporting Unicode box-drawing
106106+ characters.
107107+ - If you wish to build and install the tool without building and installing
108108+ everything, simply execute the following commands in the source directory:
109109+ - ./configure --with-libmowgli=no
110110+ - make -C libmowgli-2/ install
111111+ - make -C libathemecore/ install
112112+ - make -C src/ecdh-x25519-tool/ install
113113+ - ~/atheme/bin/atheme-ecdh-x25519-tool -h
8811489115MemoServ
90116--------
···591591 *
592592 * Core components modules/saslserv/main
593593 * AUTHCOOKIE mechanism (for IRIS) modules/saslserv/authcookie
594594+ * ECDH-X25519-CHALLENGE mechanism modules/saslserv/ecdh-x25519-challenge
594595 * ECDSA-NIST256P-CHALLENGE mechanism modules/saslserv/ecdsa-nist256p-challenge
595596 * EXTERNAL mechanism (IRCv3.1+) modules/saslserv/external
596597 * PLAIN mechanism modules/saslserv/plain
597598 * SCRAM-SHA-* mechanisms modules/saslserv/scram-sha
598599 *
600600+ * ECDH-X25519-CHALLENGE support requires that Atheme be compiled against a
601601+ * cryptographic library that provides X25519 ECDH support (BoringSSL,
602602+ * LibreSSL, ARM mbedTLS, Nettle, Sodium). This will be checked while running
603603+ * ./configure.
604604+ *
599605 * ECDSA-NIST256P-CHALLENGE support requires that Atheme be compiled against
600606 * an OpenSSL with ECDSA support (not RHEL etc. unless you compile your own).
607607+ * This will be checked while running ./configure.
601608 *
602609 * You MUST read doc/SASL-SCRAM-SHA before loading modules/saslserv/scram-sha!
603610 */
604611loadmodule "modules/saslserv/main";
605612loadmodule "modules/saslserv/authcookie";
613613+#loadmodule "modules/saslserv/ecdh-x25519-challenge";
606614#loadmodule "modules/saslserv/ecdsa-nist256p-challenge";
607615#loadmodule "modules/saslserv/external";
608616loadmodule "modules/saslserv/plain";
···11+Help for SET X25519-PUBKEY:
22+33+SET X25519-PUBKEY allows you to change your
44+public key used for SASL ECDH-X25519-CHALLENGE
55+authentication.
66+77+If no pubkey is given, your public key will
88+be removed from the services database.
99+1010+Syntax: SET X25519-PUBKEY [<pubkey>]
+16-2
include/atheme/sysconf.h.in
···5050 language is requested. */
5151#undef ENABLE_NLS
52525353+/* Define to 1 if any library can provide ECDH-X25519-CHALLENGE */
5454+#undef HAVE_ANYLIB_ECDH_X25519
5555+5356/* Define to 1 if any password quality library appears to be usable */
5457#undef HAVE_ANY_PASSWORD_QUALITY_LIBRARY
5558···140143/* Define to 1 if libcrypto appears to be usable */
141144#undef HAVE_LIBCRYPTO
142145143143-/* Define to 1 if libcrypto has usable elliptic curve key construction and
144144- signature verification functions */
146146+/* Define to 1 if libcrypto can provide SASL ECDH-X25519-CHALLENGE */
147147+#undef HAVE_LIBCRYPTO_ECDH_X25519
148148+149149+/* Define to 1 if libcrypto can provide SASL ECDSA-NIST256P-CHALLENGE */
145150#undef HAVE_LIBCRYPTO_ECDSA
146151147152/* Define to 1 if libcrypto has HMAC_CTX_new() & HMAC_CTX_free() */
···166171/* Define to 1 if libmbedcrypto has usable CTR-DRBG functions */
167172#undef HAVE_LIBMBEDCRYPTO_CTR_DRBG
168173174174+/* Define to 1 if libmbedcrypto can provide SASL ECDH-X25519-CHALLENGE */
175175+#undef HAVE_LIBMBEDCRYPTO_ECDH_X25519
176176+169177/* Define to 1 if libmbedcrypto has usable HMAC-DRBG functions */
170178#undef HAVE_LIBMBEDCRYPTO_HMAC_DRBG
171179172180/* Define to 1 if libnettle appears to be usable */
173181#undef HAVE_LIBNETTLE
182182+183183+/* Define to 1 if libnettle can provide SASL ECDH-X25519-CHALLENGE */
184184+#undef HAVE_LIBNETTLE_ECDH_X25519
174185175186/* Define to 1 if libnettle has a usable constant-time memory comparison
176187 function */
···187198188199/* Define to 1 if libsodium appears to be usable */
189200#undef HAVE_LIBSODIUM
201201+202202+/* Define to 1 if libsodium can provide SASL ECDH-X25519-CHALLENGE */
203203+#undef HAVE_LIBSODIUM_ECDH_X25519
190204191205/* Define to 1 if libsodium has a usable constant-time memory comparison
192206 function */
+8
m4/atheme-conditional-compilation.m4
···44 AC_SUBST([CONTRIB_COND_D])
55])
6677+AC_DEFUN([ATHEME_COND_ECDH_X25519_TOOL_ENABLE], [
88+99+ ECDH_X25519_TOOL_COND_D="ecdh-x25519-tool"
1010+ AC_SUBST([ECDH_X25519_TOOL_COND_D])
1111+1212+ AC_DEFINE([HAVE_ANYLIB_ECDH_X25519], [1], [Define to 1 if any library can provide ECDH-X25519-CHALLENGE])
1313+])
1414+715AC_DEFUN([ATHEME_COND_ECDSA_TOOLS_ENABLE], [
816917 ECDSA_TOOLS_COND_D="ecdsadecode ecdsakeygen ecdsasign"
+22-2
m4/atheme-libtest-crypto.m4
···132132 AC_MSG_RESULT([no])
133133 ])
134134135135- AC_MSG_CHECKING([if libcrypto has usable elliptic curve key construction and signature verification functions])
135135+ AC_MSG_CHECKING([if libcrypto can provide SASL ECDH-X25519-CHALLENGE])
136136+ AC_LINK_IFELSE([
137137+ AC_LANG_PROGRAM([[
138138+ #ifdef HAVE_STDDEF_H
139139+ # include <stddef.h>
140140+ #endif
141141+ #include <openssl/curve25519.h>
142142+ ]], [[
143143+ (void) X25519_keypair(NULL, NULL);
144144+ (void) X25519(NULL, NULL, NULL);
145145+ ]])
146146+ ], [
147147+ AC_MSG_RESULT([yes])
148148+ AC_DEFINE([HAVE_LIBCRYPTO_ECDH_X25519], [1], [Define to 1 if libcrypto can provide SASL ECDH-X25519-CHALLENGE])
149149+ ATHEME_COND_ECDH_X25519_TOOL_ENABLE
150150+ LIBCRYPTO_USABLE="Yes"
151151+ ], [
152152+ AC_MSG_RESULT([no])
153153+ ])
154154+155155+ AC_MSG_CHECKING([if libcrypto can provide SASL ECDSA-NIST256P-CHALLENGE])
136156 AC_LINK_IFELSE([
137157 AC_LANG_PROGRAM([[
138158 #ifdef HAVE_STDDEF_H
···149169 ]])
150170 ], [
151171 AC_MSG_RESULT([yes])
152152- AC_DEFINE([HAVE_LIBCRYPTO_ECDSA], [1], [Define to 1 if libcrypto has usable elliptic curve key construction and signature verification functions])
172172+ AC_DEFINE([HAVE_LIBCRYPTO_ECDSA], [1], [Define to 1 if libcrypto can provide SASL ECDSA-NIST256P-CHALLENGE])
153173 ATHEME_COND_ECDSA_TOOLS_ENABLE
154174 LIBCRYPTO_USABLE="Yes"
155175 ], [
···11+/*
22+ * SPDX-License-Identifier: ISC
33+ * SPDX-URL: https://spdx.org/licenses/ISC.html
44+ *
55+ * Copyright (C) 2019 Aaron M. D. Jones <aaronmdjones@gmail.com>
66+ *
77+ * ECDH-X25519-CHALLENGE mechanism provider.
88+ *
99+ * This is a client-first SASL mechanism.
1010+ *
1111+ * The first message from the client to the server consists of an authcid (or,
1212+ * optionally, an authcid, followed by a null octet, followed by an authzid).
1313+ * The latter allows SASL impersonation. Clients MAY set both the authcid and
1414+ * the authzid to the same value.
1515+ *
1616+ * client_message = authcid [ 0x00 authzid ]
1717+ *
1818+ * The server then does the following:
1919+ *
2020+ * client_pubkey = fetch_configured_pubkey(authcid)
2121+ * ++ server_seckey = Curve25519_clamp(CSPRNG(32))
2222+ * ++ server_pubkey = Curve25519(server_seckey, basepoint)
2323+ * shared_secret = Curve25519(server_seckey, client_pubkey)
2424+ * session_salt = CSPRNG(32)
2525+ * better_secret = ECDH-X25519-KDF()
2626+ * session_challenge = CSPRNG(32)
2727+ * masked_challenge = (session_challenge XOR better_secret)
2828+ * server_response = server_pubkey || session_salt || masked_challenge
2929+ *
3030+ * The client then does the following:
3131+ *
3232+ * client_seckey = fetch_configured_seckey()
3333+ * client_pubkey = Curve25519(client_seckey, basepoint)
3434+ * shared_secret = Curve25519(client_seckey, server_pubkey)
3535+ * better_secret = ECDH-X25519-KDF()
3636+ * session_challenge = (masked_challenge XOR better_secret)
3737+ * client_response = session_challenge
3838+ *
3939+ * If the client possesses the correct private key, the secret derived on both
4040+ * sides will be identical, and thus the client response will be the correct
4141+ * session challenge, which is verified by the server against the session
4242+ * challenge it generated.
4343+ *
4444+ * If the challenges match, the client is logged in as the authzid, or, if an
4545+ * authzid was not provided, the authcid.
4646+ *
4747+ * For the definitions of Curve25519_clamp(), Curve25519(), and basepoint,
4848+ * please see [1][2].
4949+ *
5050+ * This mechanism is intended to eventually replace ECDSA-NIST256P-CHALLENGE,
5151+ * due to concerns within the cryptographic community about the continued
5252+ * safety and reliability of the NIST curves, and P-256 in particular [3][4].
5353+ *
5454+ * Curve25519 ECDH was chosen over Ed25519 EdDSA as the basis for this
5555+ * mechanism for three primary reasons:
5656+ *
5757+ * * Curve25519 ECDH was carefully-designed to allow all possible values as
5858+ * a valid public key, removing the need for the server to validate the
5959+ * keys, or to worry about operating with potentially-malicious key data.
6060+ *
6161+ * * Verification of the client's response is a simple constant-time memory
6262+ * comparison operation, avoiding exposing the server's cryptographic guts
6363+ * to potentially-malicious client data.
6464+ *
6565+ * * More crypto libraries implement Curve25519 ECDH than Ed25519 EdDSA as
6666+ * of this writing.
6767+ *
6868+ * Additionally, key agreement is faster than signature verification, but that
6969+ * does not turn out to be of any consequence here because of the HKDF below:
7070+ *
7171+ * The ECDH-X25519-KDF() function is defined as follows:
7272+ *
7373+ * ECDH-X25519-KDF() = {
7474+ * IKM = Hash(shared_secret || client_pubkey || server_pubkey)
7575+ * PRK = HKDF-Extract(session_salt, IKM)
7676+ * OKM = HKDF-Expand(PRK, "ECDH-X25519-CHALLENGE", L=32)
7777+ * // The string above is 21 ASCII octets
7878+ * return OKM
7979+ * }
8080+ *
8181+ * The digest algorithm used for Hash(), HKDF-Extract(), and HKDF-Expand(), is
8282+ * SHA2-256. The HKDF-Extract() and HKDF-Expand() functions, and the order and
8383+ * meaning of their arguments, are as described in Section 2 of RFC5869 [5].
8484+ *
8585+ * Note that both the client's and server's Curve25519 public keys are used in
8686+ * the key derivation process in, and the Curve25519 shared secret is derived
8787+ * in, and the server's public key is transmitted to the client in, the
8888+ * canonical Curve25519 key representation format of a string of 32 octets,
8989+ * representing a **little-endian** 256-bit integer. This diverges from the
9090+ * canonical network octet-order (big-endian), but it is specified as such in
9191+ * Section 5 of RFC7748 [6]. If your cryptographic library reads and writes
9292+ * secret keys, public keys, and shared secrets, in big-endian format, you
9393+ * will need to reverse the string's octets; swap the 1st and 32nd octets,
9494+ * swap the 2nd and 31st octets, swap the 3rd and 30th octets, ... etc. As of
9595+ * this writing, the only known cryptographic library with this octet-order
9696+ * requirement that supports Curve25519 ECDH key agreement is ARM mbedTLS [7].
9797+ *
9898+ * The first half of the shared secret derivation process is justified in [8].
9999+ * The second half of the shared secret derivation process is to ensure that
100100+ * the derived shared secret is cryptographically bound to this application
101101+ * (by using the mechanism name as an input), should the client end up reusing
102102+ * their private key for something else, and to ensure that it is safe for the
103103+ * server to reuse its private key across sessions (by using the session salt
104104+ * as an input -- this also ensures contributory behaviour on the part of the
105105+ * server), should it wish to do so for efficiency:
106106+ *
107107+ * ++ These steps are not necessary for each session; the server MAY perform
108108+ * them at a regular interval instead, if it wishes to forgo the expense of
109109+ * deriving a new key-pair for each and every session. Servers SHOULD NOT
110110+ * perform this only once (on startup, or as statically-configured); a
111111+ * RECOMMENDED keypair regeneration interval is once per hour.
112112+ *
113113+ *
114114+ *
115115+ * [1] <https://cr.yp.to/ecdh.html>
116116+ * [2] <https://cr.yp.to/ecdh/curve25519-20060209.pdf>
117117+ * [3] <https://safecurves.cr.yp.to/>
118118+ * [4] <https://cr.yp.to/talks/2013.05.31/slides-dan+tanja-20130531-4x3.pdf>
119119+ * [5] <https://tools.ietf.org/html/rfc5869#section-2>
120120+ * [6] <https://tools.ietf.org/html/rfc7748#section-5>
121121+ * [7] <https://github.com/ARMmbed/mbedTLS/issues/2490>
122122+ * [8] <https://download.libsodium.org/doc/advanced/scalar_multiplication>
123123+ *
124124+ * The number of possible keys is limited to the group size (~~ 2^252),
125125+ * which is smaller than the key space. For this reason, and to mitigate
126126+ * subtle attacks due to the fact many (p, n) pairs produce the same
127127+ * result, using the output of the multiplication q directly as a shared
128128+ * key is not recommended. A better way to compute a shared key is
129129+ * H(q || pk1 || pk2), with pk1 and pk2 being the public keys.
130130+ */
131131+132132+#include <atheme.h>
133133+134134+#ifdef HAVE_ANYLIB_ECDH_X25519
135135+136136+#include "ecdh-x25519-challenge-shared.c"
137137+138138+#define ATHEME_ECDH_X25519_KEY_REGEN_INTERVAL 3600 /* 1 hour */
139139+#define ATHEME_ECDH_X25519_PUBKEY_MDNAME "private:x25519pubkey"
140140+141141+static mowgli_patricia_t **ns_set_cmdtree = NULL;
142142+static const struct sasl_core_functions *sasl_core_functions = NULL;
143143+144144+static mowgli_eventloop_timer_t *ecdh_x25519_keypair_regen_timer = NULL;
145145+static unsigned char ecdh_x25519_server_seckey[ATHEME_ECDH_X25519_XKEY_LEN];
146146+static unsigned char ecdh_x25519_server_pubkey[ATHEME_ECDH_X25519_XKEY_LEN];
147147+148148+static const char ecdh_x25519_mkdf_salt[] = "ECDH-X25519-CHALLENGE SASL Mechanism HKDF Salt Generator";
149149+static const char ecdh_x25519_mkdf_challenge[] = "ECDH-X25519-CHALLENGE SASL Mechanism HKDF Challenge Generator";
150150+151151+static bool
152152+ecdh_x25519_keypair_regen(void)
153153+{
154154+ unsigned char seckey[ATHEME_ECDH_X25519_XKEY_LEN];
155155+ unsigned char pubkey[ATHEME_ECDH_X25519_XKEY_LEN];
156156+157157+ if (! ecdh_x25519_create_keypair(seckey, pubkey))
158158+ // This function logs messages on failure
159159+ return false;
160160+161161+ (void) memcpy(ecdh_x25519_server_seckey, seckey, sizeof seckey);
162162+ (void) memcpy(ecdh_x25519_server_pubkey, pubkey, sizeof pubkey);
163163+ (void) smemzero(seckey, sizeof seckey);
164164+ (void) smemzero(pubkey, sizeof pubkey);
165165+ return true;
166166+}
167167+168168+static void
169169+ecdh_x25519_keypair_regen_cb(void ATHEME_VATTR_UNUSED *const restrict vptr)
170170+{
171171+ // This function logs messages on failure
172172+ (void) ecdh_x25519_keypair_regen();
173173+}
174174+175175+static enum sasl_mechanism_result ATHEME_FATTR_WUR
176176+ecdh_x25519_sasl_step_account_names(struct sasl_session *const restrict p,
177177+ const struct sasl_input_buf *const restrict in,
178178+ struct sasl_output_buf *const restrict out)
179179+{
180180+ unsigned char client_pubkey[ATHEME_ECDH_X25519_XKEY_LEN];
181181+ unsigned char shared_secret[ATHEME_ECDH_X25519_XKEY_LEN];
182182+ unsigned char better_secret[ATHEME_ECDH_X25519_CHAL_LEN];
183183+ unsigned char challenge[ATHEME_ECDH_X25519_CHAL_LEN];
184184+ static union ecdh_x25519_server_response resp;
185185+186186+ enum sasl_mechanism_result retval = ASASL_MRESULT_ERROR;
187187+ struct metadata *md = NULL;
188188+ struct myuser *mu = NULL;
189189+190190+ const char *const end = memchr(in->buf, 0x00, in->len);
191191+192192+ if (! end)
193193+ {
194194+ if (in->len > NICKLEN)
195195+ {
196196+ (void) slog(LG_DEBUG, "%s: in->len (%zu) is unacceptable", MOWGLI_FUNC_NAME, in->len);
197197+ goto cleanup;
198198+ }
199199+ if (! sasl_core_functions->authcid_can_login(p, in->buf, &mu))
200200+ {
201201+ (void) slog(LG_DEBUG, "%s: authcid_can_login failed", MOWGLI_FUNC_NAME);
202202+ goto cleanup;
203203+ }
204204+ }
205205+ else
206206+ {
207207+ const char *const bufchrptr = in->buf;
208208+ const size_t authcid_length = (size_t) (end - bufchrptr);
209209+ const size_t authzid_length = in->len - 1 - authcid_length;
210210+211211+ if (! authcid_length || authcid_length > NICKLEN)
212212+ {
213213+ (void) slog(LG_DEBUG, "%s: authcid_length (%zu) is unacceptable",
214214+ MOWGLI_FUNC_NAME, authcid_length);
215215+ goto cleanup;
216216+ }
217217+ if (! authzid_length || authzid_length > NICKLEN)
218218+ {
219219+ (void) slog(LG_DEBUG, "%s: authzid_length (%zu) is unacceptable",
220220+ MOWGLI_FUNC_NAME, authzid_length);
221221+ goto cleanup;
222222+ }
223223+ if (! sasl_core_functions->authzid_can_login(p, end + 1, NULL))
224224+ {
225225+ (void) slog(LG_DEBUG, "%s: authzid_can_login failed", MOWGLI_FUNC_NAME);
226226+ goto cleanup;
227227+ }
228228+ if (! sasl_core_functions->authcid_can_login(p, in->buf, &mu))
229229+ {
230230+ (void) slog(LG_DEBUG, "%s: authcid_can_login failed", MOWGLI_FUNC_NAME);
231231+ goto cleanup;
232232+ }
233233+ }
234234+ if (! mu)
235235+ {
236236+ (void) slog(LG_ERROR, "%s: authcid_can_login did not set 'mu' (BUG)", MOWGLI_FUNC_NAME);
237237+ goto cleanup;
238238+ }
239239+ if (! (md = metadata_find(mu, ATHEME_ECDH_X25519_PUBKEY_MDNAME)))
240240+ {
241241+ (void) slog(LG_DEBUG, "%s: metadata_find() failed", MOWGLI_FUNC_NAME);
242242+ goto cleanup;
243243+ }
244244+ if (base64_decode(md->value, client_pubkey, sizeof client_pubkey) != sizeof client_pubkey)
245245+ {
246246+ (void) slog(LG_ERROR, "%s: base64_decode() failed (BUG!)", MOWGLI_FUNC_NAME);
247247+ goto cleanup;
248248+ }
249249+250250+ if (! ecdh_x25519_compute_shared(ecdh_x25519_server_seckey, client_pubkey, shared_secret))
251251+ // This function logs messages on failure
252252+ goto cleanup;
253253+254254+ (void) memcpy(resp.field.pubkey, ecdh_x25519_server_pubkey, sizeof ecdh_x25519_server_pubkey);
255255+ (void) atheme_random_buf(resp.field.salt, sizeof resp.field.salt);
256256+257257+ if (! ecdh_x25519_kdf(shared_secret, client_pubkey, resp.field.pubkey, resp.field.salt, better_secret))
258258+ // This function logs messages on failure
259259+ goto cleanup;
260260+261261+ (void) atheme_random_buf(challenge, sizeof challenge);
262262+263263+ p->mechdata = smemdup(challenge, sizeof challenge);
264264+265265+ for (size_t x = 0; x < sizeof resp.field.challenge; x++)
266266+ resp.field.challenge[x] = challenge[x] ^ better_secret[x];
267267+268268+ out->buf = resp.octets;
269269+ out->len = sizeof resp.octets;
270270+271271+ retval = ASASL_MRESULT_CONTINUE;
272272+273273+cleanup:
274274+ (void) smemzero(shared_secret, sizeof shared_secret);
275275+ (void) smemzero(better_secret, sizeof better_secret);
276276+ (void) smemzero(challenge, sizeof challenge);
277277+ return retval;
278278+}
279279+280280+static enum sasl_mechanism_result ATHEME_FATTR_WUR
281281+ecdh_x25519_sasl_step_verify_challenge(struct sasl_session *const restrict p,
282282+ const struct sasl_input_buf *const restrict in)
283283+{
284284+ if (in->len != ATHEME_ECDH_X25519_CHAL_LEN)
285285+ {
286286+ (void) slog(LG_DEBUG, "%s: in->len (%zu) is unacceptable", MOWGLI_FUNC_NAME, in->len);
287287+ return ASASL_MRESULT_ERROR;
288288+ }
289289+ if (smemcmp(p->mechdata, in->buf, in->len) != 0)
290290+ {
291291+ (void) slog(LG_DEBUG, "%s: challenge is incorrect", MOWGLI_FUNC_NAME);
292292+ return ASASL_MRESULT_FAILURE;
293293+ }
294294+295295+ (void) slog(LG_DEBUG, "%s: authentication successful", MOWGLI_FUNC_NAME);
296296+ return ASASL_MRESULT_SUCCESS;
297297+}
298298+299299+static enum sasl_mechanism_result ATHEME_FATTR_WUR
300300+ecdh_x25519_sasl_step(struct sasl_session *const restrict p,
301301+ const struct sasl_input_buf *const restrict in,
302302+ struct sasl_output_buf *const restrict out)
303303+{
304304+ if (! (in && in->buf && in->len))
305305+ {
306306+ (void) slog(LG_DEBUG, "%s: client did not send any data", MOWGLI_FUNC_NAME);
307307+ return ASASL_MRESULT_ERROR;
308308+ }
309309+310310+ if (p->mechdata == NULL)
311311+ return ecdh_x25519_sasl_step_account_names(p, in, out);
312312+ else
313313+ return ecdh_x25519_sasl_step_verify_challenge(p, in);
314314+}
315315+316316+static void
317317+ecdh_x25519_sasl_finish(struct sasl_session *const restrict p)
318318+{
319319+ if (! (p && p->mechdata))
320320+ return;
321321+322322+ (void) sfree(p->mechdata);
323323+324324+ p->mechdata = NULL;
325325+}
326326+327327+static void
328328+ns_cmd_set_x25519_pubkey_func(struct sourceinfo *const restrict si, const int parc, char **const restrict parv)
329329+{
330330+ struct metadata *const md = metadata_find(si->smu, ATHEME_ECDH_X25519_PUBKEY_MDNAME);
331331+332332+ if (! parc)
333333+ {
334334+ if (! md)
335335+ {
336336+ (void) command_fail(si, fault_nochange, _("Public key was not set"));
337337+ return;
338338+ }
339339+340340+ (void) metadata_delete(si->smu, ATHEME_ECDH_X25519_PUBKEY_MDNAME);
341341+ (void) logcommand(si, CMDLOG_SET, "SET:X25519-PUBKEY:REMOVE");
342342+ (void) command_success_nodata(si, _("Public key entry has been deleted."));
343343+ return;
344344+ }
345345+346346+ const char *const newkey = parv[0];
347347+ unsigned char client_pubkey[ATHEME_ECDH_X25519_XKEY_LEN];
348348+349349+ if (base64_decode(newkey, client_pubkey, sizeof client_pubkey) != sizeof client_pubkey)
350350+ {
351351+ (void) command_fail(si, fault_badparams, _("The public key specified is not valid."));
352352+ return;
353353+ }
354354+ if (md && strcmp(md->value, newkey) == 0)
355355+ {
356356+ (void) command_fail(si, fault_nochange, _("Your public key is already set to \2%s\2."), newkey);
357357+ return;
358358+ }
359359+360360+ (void) metadata_add(si->smu, ATHEME_ECDH_X25519_PUBKEY_MDNAME, newkey);
361361+ (void) logcommand(si, CMDLOG_SET, "SET:X25519-PUBKEY: \2%s\2", newkey);
362362+ (void) command_success_nodata(si, _("Your public key is now set to \2%s\2."), newkey);
363363+}
364364+365365+static const struct sasl_mechanism sasl_mech_ecdh_x25519_challenge = {
366366+ .name = "ECDH-X25519-CHALLENGE",
367367+ .mech_start = NULL,
368368+ .mech_step = &ecdh_x25519_sasl_step,
369369+ .mech_finish = &ecdh_x25519_sasl_finish,
370370+};
371371+372372+static struct command ns_cmd_set_x25519_pubkey = {
373373+ .name = "X25519-PUBKEY",
374374+ .desc = N_("Changes your ECDH-X25519-CHALLENGE public key."),
375375+ .access = AC_AUTHENTICATED,
376376+ .maxparc = 1,
377377+ .cmd = &ns_cmd_set_x25519_pubkey_func,
378378+ .help = { .path = "nickserv/set_x25519_pubkey" },
379379+};
380380+381381+static void
382382+mod_init(struct module *const restrict m)
383383+{
384384+ MODULE_TRY_REQUEST_SYMBOL(m, ns_set_cmdtree, "nickserv/set_core", "ns_set_cmdtree");
385385+ MODULE_TRY_REQUEST_SYMBOL(m, sasl_core_functions, "saslserv/main", "sasl_core_functions");
386386+387387+ if (! ecdh_x25519_selftest())
388388+ {
389389+ (void) slog(LG_ERROR, "%s: self-test failed (BUG?); refusing to load", m->name);
390390+ m->mflags |= MODFLAG_FAIL;
391391+ return;
392392+ }
393393+ if (! ecdh_x25519_keypair_regen())
394394+ {
395395+ (void) slog(LG_ERROR, "%s: keypair generation failed (BUG?); refusing to load", m->name);
396396+ m->mflags |= MODFLAG_FAIL;
397397+ return;
398398+ }
399399+ if (! (ecdh_x25519_keypair_regen_timer = mowgli_timer_add(base_eventloop, "ecdh_x25519_keypair_regen",
400400+ &ecdh_x25519_keypair_regen_cb, NULL, ATHEME_ECDH_X25519_KEY_REGEN_INTERVAL)))
401401+ {
402402+ (void) slog(LG_ERROR, "%s: mowgli_timer_add() failed (BUG?); refusing to load", m->name);
403403+ m->mflags |= MODFLAG_FAIL;
404404+ return;
405405+ }
406406+407407+ (void) sasl_core_functions->mech_register(&sasl_mech_ecdh_x25519_challenge);
408408+ (void) command_add(&ns_cmd_set_x25519_pubkey, *ns_set_cmdtree);
409409+}
410410+411411+static void
412412+mod_deinit(const enum module_unload_intent ATHEME_VATTR_UNUSED intent)
413413+{
414414+ (void) smemzero(ecdh_x25519_server_seckey, sizeof ecdh_x25519_server_seckey);
415415+ (void) mowgli_timer_destroy(base_eventloop, ecdh_x25519_keypair_regen_timer);
416416+ (void) sasl_core_functions->mech_unregister(&sasl_mech_ecdh_x25519_challenge);
417417+ (void) command_delete(&ns_cmd_set_x25519_pubkey, *ns_set_cmdtree);
418418+}
419419+420420+#else /* HAVE_ANYLIB_ECDH_X25519 */
421421+422422+static void
423423+mod_init(struct module *const restrict m)
424424+{
425425+ (void) slog(LG_ERROR, "Module %s requires a library with usable Curve25519 key generation "
426426+ "& ECDH key agreement functions, refusing to load.", m->name);
427427+428428+ m->mflags |= MODFLAG_FAIL;
429429+}
430430+431431+static void
432432+mod_deinit(const enum module_unload_intent ATHEME_VATTR_UNUSED intent)
433433+{
434434+ // Nothing To Do
435435+}
436436+437437+#endif /* !HAVE_ANYLIB_ECDH_X25519 */
438438+439439+SIMPLE_DECLARE_MODULE_V1("saslserv/ecdh-x25519-challenge", MODULE_UNLOAD_CAPABILITY_OK)