Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
441 lines (392 sloc)
16.1 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * SPDX-License-Identifier: ISC | |
| * SPDX-URL: https://spdx.org/licenses/ISC.html | |
| * | |
| * Copyright (C) 2019 Aaron M. D. Jones <me@aaronmdjones.net> | |
| * | |
| * ECDH-X25519-CHALLENGE mechanism provider. | |
| * | |
| * This is a client-first SASL mechanism. | |
| * | |
| * The first message from the client to the server consists of an authcid (or, | |
| * optionally, an authcid, followed by a null octet, followed by an authzid). | |
| * The latter allows SASL impersonation. Clients MAY set both the authcid and | |
| * the authzid to the same value. | |
| * | |
| * client_message = authcid [ 0x00 authzid ] | |
| * | |
| * The server then does the following: | |
| * | |
| * client_pubkey = fetch_configured_pubkey(authcid) | |
| * ++ server_seckey = Curve25519_clamp(CSPRNG(32)) | |
| * ++ server_pubkey = Curve25519(server_seckey, basepoint) | |
| * shared_secret = Curve25519(server_seckey, client_pubkey) | |
| * session_salt = CSPRNG(32) | |
| * better_secret = ECDH-X25519-KDF() | |
| * session_challenge = CSPRNG(32) | |
| * masked_challenge = (session_challenge XOR better_secret) | |
| * server_response = server_pubkey || session_salt || masked_challenge | |
| * | |
| * The client then does the following: | |
| * | |
| * client_seckey = fetch_configured_seckey() | |
| * client_pubkey = Curve25519(client_seckey, basepoint) | |
| * shared_secret = Curve25519(client_seckey, server_pubkey) | |
| * better_secret = ECDH-X25519-KDF() | |
| * session_challenge = (masked_challenge XOR better_secret) | |
| * client_response = session_challenge | |
| * | |
| * If the client possesses the correct private key, the secret derived on both | |
| * sides will be identical, and thus the client response will be the correct | |
| * session challenge, which is verified by the server against the session | |
| * challenge it generated. | |
| * | |
| * If the challenges match, the client is logged in as the authzid, or, if an | |
| * authzid was not provided, the authcid. | |
| * | |
| * For the definitions of Curve25519_clamp(), Curve25519(), and basepoint, | |
| * please see [1][2]. | |
| * | |
| * This mechanism is intended to eventually replace ECDSA-NIST256P-CHALLENGE, | |
| * due to concerns within the cryptographic community about the continued | |
| * safety and reliability of the NIST curves, and P-256 in particular [3][4]. | |
| * | |
| * Curve25519 ECDH was chosen over Ed25519 EdDSA as the basis for this | |
| * mechanism for three primary reasons: | |
| * | |
| * * Curve25519 ECDH was carefully-designed to allow all possible values as | |
| * a valid public key, removing the need for the server to validate the | |
| * keys, or to worry about operating with potentially-malicious key data. | |
| * | |
| * * Verification of the client's response is a simple constant-time memory | |
| * comparison operation, avoiding exposing the server's cryptographic guts | |
| * to potentially-malicious client data. | |
| * | |
| * * More crypto libraries implement Curve25519 ECDH than Ed25519 EdDSA as | |
| * of this writing. | |
| * | |
| * Additionally, key agreement is faster than signature verification, but that | |
| * does not turn out to be of any consequence here because of the HKDF below: | |
| * | |
| * The ECDH-X25519-KDF() function is defined as follows: | |
| * | |
| * ECDH-X25519-KDF() = { | |
| * IKM = Hash(shared_secret || client_pubkey || server_pubkey) | |
| * PRK = HKDF-Extract(session_salt, IKM) | |
| * OKM = HKDF-Expand(PRK, "ECDH-X25519-CHALLENGE", L=32) | |
| * // The string above is 21 ASCII octets | |
| * return OKM | |
| * } | |
| * | |
| * The digest algorithm used for Hash(), HKDF-Extract(), and HKDF-Expand(), is | |
| * SHA2-256. The HKDF-Extract() and HKDF-Expand() functions, and the order and | |
| * meaning of their arguments, are as described in Section 2 of RFC5869 [5]. | |
| * | |
| * Note that both the client's and server's Curve25519 public keys are used in | |
| * the key derivation process in, and the Curve25519 shared secret is derived | |
| * in, and the server's public key is transmitted to the client in, the | |
| * canonical Curve25519 key representation format of a string of 32 octets, | |
| * representing a **little-endian** 256-bit integer. This diverges from the | |
| * canonical network octet-order (big-endian), but it is specified as such in | |
| * Section 5 of RFC7748 [6]. If your cryptographic library reads and writes | |
| * secret keys, public keys, and shared secrets, in big-endian format, you | |
| * will need to reverse the string's octets; swap the 1st and 32nd octets, | |
| * swap the 2nd and 31st octets, swap the 3rd and 30th octets, ... etc. As of | |
| * this writing, the only known cryptographic library with this octet-order | |
| * requirement that supports Curve25519 ECDH key agreement is ARM mbedTLS [7]. | |
| * | |
| * The first half of the shared secret derivation process is justified in [8]. | |
| * The second half of the shared secret derivation process is to ensure that | |
| * the derived shared secret is cryptographically bound to this application | |
| * (by using the mechanism name as an input), should the client end up reusing | |
| * their private key for something else, and to ensure that it is safe for the | |
| * server to reuse its private key across sessions (by using the session salt | |
| * as an input -- this also ensures contributory behaviour on the part of the | |
| * server), should it wish to do so for efficiency: | |
| * | |
| * ++ These steps are not necessary for each session; the server MAY perform | |
| * them at a regular interval instead, if it wishes to forgo the expense of | |
| * deriving a new key-pair for each and every session. Servers SHOULD NOT | |
| * perform this only once (on startup, or as statically-configured); a | |
| * RECOMMENDED keypair regeneration interval is once per hour. | |
| * | |
| * | |
| * | |
| * [1] <https://cr.yp.to/ecdh.html> | |
| * [2] <https://cr.yp.to/ecdh/curve25519-20060209.pdf> | |
| * [3] <https://safecurves.cr.yp.to/> | |
| * [4] <https://cr.yp.to/talks/2013.05.31/slides-dan+tanja-20130531-4x3.pdf> | |
| * [5] <https://tools.ietf.org/html/rfc5869#section-2> | |
| * [6] <https://tools.ietf.org/html/rfc7748#section-5> | |
| * [7] <https://github.com/ARMmbed/mbedTLS/issues/2490> | |
| * [8] <https://download.libsodium.org/doc/advanced/scalar_multiplication> | |
| * | |
| * The number of possible keys is limited to the group size (~~ 2^252), | |
| * which is smaller than the key space. For this reason, and to mitigate | |
| * subtle attacks due to the fact many (p, n) pairs produce the same | |
| * result, using the output of the multiplication q directly as a shared | |
| * key is not recommended. A better way to compute a shared key is | |
| * H(q || pk1 || pk2), with pk1 and pk2 being the public keys. | |
| */ | |
| #include <atheme.h> | |
| #ifdef HAVE_ANYLIB_ECDH_X25519 | |
| #include "ecdh-x25519-challenge-shared.c" | |
| #define ATHEME_ECDH_X25519_KEY_REGEN_INTERVAL SECONDS_PER_HOUR | |
| #define ATHEME_ECDH_X25519_PUBKEY_MDNAME "private:x25519pubkey" | |
| static mowgli_patricia_t **ns_set_cmdtree = NULL; | |
| static const struct sasl_core_functions *sasl_core_functions = NULL; | |
| static mowgli_eventloop_timer_t *ecdh_x25519_keypair_regen_timer = NULL; | |
| static unsigned char ecdh_x25519_server_seckey[ATHEME_ECDH_X25519_XKEY_LEN]; | |
| static unsigned char ecdh_x25519_server_pubkey[ATHEME_ECDH_X25519_XKEY_LEN]; | |
| static bool | |
| ecdh_x25519_keypair_regen(void) | |
| { | |
| unsigned char seckey[ATHEME_ECDH_X25519_XKEY_LEN]; | |
| unsigned char pubkey[ATHEME_ECDH_X25519_XKEY_LEN]; | |
| if (! ecdh_x25519_create_keypair(seckey, pubkey)) | |
| // This function logs messages on failure | |
| return false; | |
| (void) memcpy(ecdh_x25519_server_seckey, seckey, sizeof seckey); | |
| (void) memcpy(ecdh_x25519_server_pubkey, pubkey, sizeof pubkey); | |
| (void) smemzero(seckey, sizeof seckey); | |
| return true; | |
| } | |
| static void | |
| ecdh_x25519_keypair_regen_cb(void ATHEME_VATTR_UNUSED *const restrict vptr) | |
| { | |
| // This function logs messages on failure | |
| (void) ecdh_x25519_keypair_regen(); | |
| } | |
| static enum sasl_mechanism_result ATHEME_FATTR_WUR | |
| ecdh_x25519_sasl_step_account_names(struct sasl_session *const restrict p, | |
| const struct sasl_input_buf *const restrict in, | |
| struct sasl_output_buf *const restrict out) | |
| { | |
| unsigned char client_pubkey[ATHEME_ECDH_X25519_XKEY_LEN]; | |
| unsigned char shared_secret[ATHEME_ECDH_X25519_XKEY_LEN]; | |
| unsigned char better_secret[ATHEME_ECDH_X25519_CHAL_LEN]; | |
| unsigned char challenge[ATHEME_ECDH_X25519_CHAL_LEN]; | |
| static union ecdh_x25519_server_response resp; | |
| enum sasl_mechanism_result retval = ASASL_MRESULT_ERROR; | |
| struct metadata *md = NULL; | |
| struct myuser *mu = NULL; | |
| const char *const end = memchr(in->buf, 0x00, in->len); | |
| if (! end) | |
| { | |
| if (in->len > NICKLEN) | |
| { | |
| (void) slog(LG_DEBUG, "%s: in->len (%zu) is unacceptable", MOWGLI_FUNC_NAME, in->len); | |
| goto cleanup; | |
| } | |
| if (! sasl_core_functions->authcid_can_login(p, in->buf, &mu)) | |
| { | |
| (void) slog(LG_DEBUG, "%s: authcid_can_login failed", MOWGLI_FUNC_NAME); | |
| goto cleanup; | |
| } | |
| } | |
| else | |
| { | |
| const char *const bufchrptr = in->buf; | |
| const size_t authcid_length = (size_t) (end - bufchrptr); | |
| const size_t authzid_length = in->len - 1 - authcid_length; | |
| if (! authcid_length || authcid_length > NICKLEN) | |
| { | |
| (void) slog(LG_DEBUG, "%s: authcid_length (%zu) is unacceptable", | |
| MOWGLI_FUNC_NAME, authcid_length); | |
| goto cleanup; | |
| } | |
| if (! authzid_length || authzid_length > NICKLEN) | |
| { | |
| (void) slog(LG_DEBUG, "%s: authzid_length (%zu) is unacceptable", | |
| MOWGLI_FUNC_NAME, authzid_length); | |
| goto cleanup; | |
| } | |
| if (! sasl_core_functions->authzid_can_login(p, end + 1, NULL)) | |
| { | |
| (void) slog(LG_DEBUG, "%s: authzid_can_login failed", MOWGLI_FUNC_NAME); | |
| goto cleanup; | |
| } | |
| if (! sasl_core_functions->authcid_can_login(p, in->buf, &mu)) | |
| { | |
| (void) slog(LG_DEBUG, "%s: authcid_can_login failed", MOWGLI_FUNC_NAME); | |
| goto cleanup; | |
| } | |
| } | |
| if (! mu) | |
| { | |
| (void) slog(LG_ERROR, "%s: authcid_can_login did not set 'mu' (BUG)", MOWGLI_FUNC_NAME); | |
| goto cleanup; | |
| } | |
| if (! (md = metadata_find(mu, ATHEME_ECDH_X25519_PUBKEY_MDNAME))) | |
| { | |
| (void) slog(LG_DEBUG, "%s: metadata_find() failed", MOWGLI_FUNC_NAME); | |
| goto cleanup; | |
| } | |
| if (base64_decode(md->value, client_pubkey, sizeof client_pubkey) != sizeof client_pubkey) | |
| { | |
| (void) slog(LG_ERROR, "%s: base64_decode() failed (BUG!)", MOWGLI_FUNC_NAME); | |
| goto cleanup; | |
| } | |
| if (! ecdh_x25519_compute_shared(ecdh_x25519_server_seckey, client_pubkey, shared_secret)) | |
| // This function logs messages on failure | |
| goto cleanup; | |
| (void) memcpy(resp.field.pubkey, ecdh_x25519_server_pubkey, sizeof resp.field.pubkey); | |
| (void) atheme_random_buf(resp.field.salt, sizeof resp.field.salt); | |
| if (! ecdh_x25519_kdf(shared_secret, client_pubkey, resp.field.pubkey, resp.field.salt, better_secret)) | |
| // This function logs messages on failure | |
| goto cleanup; | |
| (void) atheme_random_buf(challenge, sizeof challenge); | |
| p->mechdata = smemdup(challenge, sizeof challenge); | |
| for (size_t x = 0; x < sizeof resp.field.challenge; x++) | |
| resp.field.challenge[x] = challenge[x] ^ better_secret[x]; | |
| out->buf = resp.octets; | |
| out->len = sizeof resp.octets; | |
| retval = ASASL_MRESULT_CONTINUE; | |
| cleanup: | |
| (void) smemzero(shared_secret, sizeof shared_secret); | |
| (void) smemzero(better_secret, sizeof better_secret); | |
| (void) smemzero(challenge, sizeof challenge); | |
| return retval; | |
| } | |
| static enum sasl_mechanism_result ATHEME_FATTR_WUR | |
| ecdh_x25519_sasl_step_verify_challenge(struct sasl_session *const restrict p, | |
| const struct sasl_input_buf *const restrict in) | |
| { | |
| if (in->len != ATHEME_ECDH_X25519_CHAL_LEN) | |
| { | |
| (void) slog(LG_DEBUG, "%s: in->len (%zu) is unacceptable", MOWGLI_FUNC_NAME, in->len); | |
| return ASASL_MRESULT_ERROR; | |
| } | |
| if (smemcmp(p->mechdata, in->buf, in->len) != 0) | |
| { | |
| (void) slog(LG_DEBUG, "%s: challenge is incorrect", MOWGLI_FUNC_NAME); | |
| return ASASL_MRESULT_FAILURE; | |
| } | |
| (void) slog(LG_DEBUG, "%s: authentication successful", MOWGLI_FUNC_NAME); | |
| return ASASL_MRESULT_SUCCESS; | |
| } | |
| static enum sasl_mechanism_result ATHEME_FATTR_WUR | |
| ecdh_x25519_sasl_step(struct sasl_session *const restrict p, | |
| const struct sasl_input_buf *const restrict in, | |
| struct sasl_output_buf *const restrict out) | |
| { | |
| if (! (in && in->buf && in->len)) | |
| { | |
| (void) slog(LG_DEBUG, "%s: client did not send any data", MOWGLI_FUNC_NAME); | |
| return ASASL_MRESULT_ERROR; | |
| } | |
| if (p->mechdata == NULL) | |
| return ecdh_x25519_sasl_step_account_names(p, in, out); | |
| else | |
| return ecdh_x25519_sasl_step_verify_challenge(p, in); | |
| } | |
| static void | |
| ecdh_x25519_sasl_finish(struct sasl_session *const restrict p) | |
| { | |
| if (! (p && p->mechdata)) | |
| return; | |
| (void) sfree(p->mechdata); | |
| p->mechdata = NULL; | |
| } | |
| static void | |
| ns_cmd_set_x25519_pubkey_func(struct sourceinfo *const restrict si, const int parc, char **const restrict parv) | |
| { | |
| struct metadata *const md = metadata_find(si->smu, ATHEME_ECDH_X25519_PUBKEY_MDNAME); | |
| if (! parc) | |
| { | |
| if (! md) | |
| { | |
| (void) command_fail(si, fault_nochange, _("Your public key was not set.")); | |
| return; | |
| } | |
| (void) metadata_delete(si->smu, ATHEME_ECDH_X25519_PUBKEY_MDNAME); | |
| (void) logcommand(si, CMDLOG_SET, "SET:X25519-PUBKEY:REMOVE"); | |
| (void) command_success_nodata(si, _("Your public key entry has been deleted.")); | |
| return; | |
| } | |
| unsigned char pubkey[ATHEME_ECDH_X25519_XKEY_LEN]; | |
| char pubkey_b64[BASE64_SIZE_STR(sizeof pubkey)]; | |
| if (base64_decode(parv[0], pubkey, sizeof pubkey) != sizeof pubkey) | |
| { | |
| (void) command_fail(si, fault_badparams, _("The public key specified is not valid.")); | |
| return; | |
| } | |
| if (base64_encode(pubkey, sizeof pubkey, pubkey_b64, sizeof pubkey_b64) != BASE64_SIZE_RAW(sizeof pubkey)) | |
| { | |
| (void) command_fail(si, fault_badparams, _("The public key specified is not valid.")); | |
| return; | |
| } | |
| if (md && strcmp(md->value, pubkey_b64) == 0) | |
| { | |
| (void) command_fail(si, fault_nochange, _("Your public key is already set to \2%s\2."), pubkey_b64); | |
| return; | |
| } | |
| (void) metadata_add(si->smu, ATHEME_ECDH_X25519_PUBKEY_MDNAME, pubkey_b64); | |
| (void) logcommand(si, CMDLOG_SET, "SET:X25519-PUBKEY: \2%s\2", pubkey_b64); | |
| (void) command_success_nodata(si, _("Your public key is now set to \2%s\2."), pubkey_b64); | |
| } | |
| static const struct sasl_mechanism sasl_mech_ecdh_x25519_challenge = { | |
| .name = "ECDH-X25519-CHALLENGE", | |
| .mech_start = NULL, | |
| .mech_step = &ecdh_x25519_sasl_step, | |
| .mech_finish = &ecdh_x25519_sasl_finish, | |
| .password_based = false, | |
| }; | |
| static struct command ns_cmd_set_x25519_pubkey = { | |
| .name = "X25519-PUBKEY", | |
| .desc = N_("Changes your ECDH-X25519-CHALLENGE public key."), | |
| .access = AC_AUTHENTICATED, | |
| .maxparc = 1, | |
| .cmd = &ns_cmd_set_x25519_pubkey_func, | |
| .help = { .path = "nickserv/set_x25519_pubkey" }, | |
| }; | |
| static void | |
| mod_init(struct module *const restrict m) | |
| { | |
| MODULE_TRY_REQUEST_SYMBOL(m, ns_set_cmdtree, "nickserv/set_core", "ns_set_cmdtree") | |
| MODULE_TRY_REQUEST_SYMBOL(m, sasl_core_functions, "saslserv/main", "sasl_core_functions") | |
| if (! ecdh_x25519_selftest()) | |
| { | |
| (void) slog(LG_ERROR, "%s: self-test failed (BUG?); refusing to load", m->name); | |
| m->mflags |= MODFLAG_FAIL; | |
| return; | |
| } | |
| if (! ecdh_x25519_keypair_regen()) | |
| { | |
| (void) slog(LG_ERROR, "%s: keypair generation failed (BUG?); refusing to load", m->name); | |
| m->mflags |= MODFLAG_FAIL; | |
| return; | |
| } | |
| if (! (ecdh_x25519_keypair_regen_timer = mowgli_timer_add(base_eventloop, "ecdh_x25519_keypair_regen", | |
| &ecdh_x25519_keypair_regen_cb, NULL, ATHEME_ECDH_X25519_KEY_REGEN_INTERVAL))) | |
| { | |
| (void) slog(LG_ERROR, "%s: mowgli_timer_add() failed (BUG?); refusing to load", m->name); | |
| m->mflags |= MODFLAG_FAIL; | |
| return; | |
| } | |
| (void) sasl_core_functions->mech_register(&sasl_mech_ecdh_x25519_challenge); | |
| (void) command_add(&ns_cmd_set_x25519_pubkey, *ns_set_cmdtree); | |
| } | |
| static void | |
| mod_deinit(const enum module_unload_intent ATHEME_VATTR_UNUSED intent) | |
| { | |
| (void) smemzero(ecdh_x25519_server_seckey, sizeof ecdh_x25519_server_seckey); | |
| (void) mowgli_timer_destroy(base_eventloop, ecdh_x25519_keypair_regen_timer); | |
| (void) sasl_core_functions->mech_unregister(&sasl_mech_ecdh_x25519_challenge); | |
| (void) command_delete(&ns_cmd_set_x25519_pubkey, *ns_set_cmdtree); | |
| } | |
| #else /* HAVE_ANYLIB_ECDH_X25519 */ | |
| static void | |
| mod_init(struct module *const restrict m) | |
| { | |
| (void) slog(LG_ERROR, "Module %s requires a library with usable Curve25519 key generation " | |
| "& ECDH key agreement functions, refusing to load.", m->name); | |
| m->mflags |= MODFLAG_FAIL; | |
| } | |
| static void | |
| mod_deinit(const enum module_unload_intent ATHEME_VATTR_UNUSED intent) | |
| { | |
| // Nothing To Do | |
| } | |
| #endif /* !HAVE_ANYLIB_ECDH_X25519 */ | |
| SIMPLE_DECLARE_MODULE_V1("saslserv/ecdh-x25519-challenge", MODULE_UNLOAD_CAPABILITY_OK) |