diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:49:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:49:46 +0000 |
commit | 50b37d4a27d3295a29afca2286f1a5a086142cec (patch) | |
tree | 9212f763934ee090ef72d823f559f52ce387f268 /src/modules/rlm_pap | |
parent | Initial commit. (diff) | |
download | freeradius-50b37d4a27d3295a29afca2286f1a5a086142cec.tar.xz freeradius-50b37d4a27d3295a29afca2286f1a5a086142cec.zip |
Adding upstream version 3.2.1+dfsg.upstream/3.2.1+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/modules/rlm_pap')
-rw-r--r-- | src/modules/rlm_pap/all.mk | 2 | ||||
-rw-r--r-- | src/modules/rlm_pap/rlm_pap.c | 1415 |
2 files changed, 1417 insertions, 0 deletions
diff --git a/src/modules/rlm_pap/all.mk b/src/modules/rlm_pap/all.mk new file mode 100644 index 0000000..adec9cd --- /dev/null +++ b/src/modules/rlm_pap/all.mk @@ -0,0 +1,2 @@ +SOURCES := rlm_pap.c +TARGET := rlm_pap.a diff --git a/src/modules/rlm_pap/rlm_pap.c b/src/modules/rlm_pap/rlm_pap.c new file mode 100644 index 0000000..318e9a8 --- /dev/null +++ b/src/modules/rlm_pap/rlm_pap.c @@ -0,0 +1,1415 @@ +/* + * This program is is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * $Id$ + * @file rlm_pap.c + * @brief Hashes plaintext passwords to compare against a prehashed reference. + * + * @copyright 2001-2012 The FreeRADIUS server project. + * @copyright 2012 Matthew Newton <matthew@newtoncomputing.co.uk> + * @copyright 2001 Kostas Kalevras <kkalev@noc.ntua.gr> + */ +RCSID("$Id$") +USES_APPLE_DEPRECATED_API + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/base64.h> +#include <freeradius-devel/rad_assert.h> + +#include <ctype.h> + +#include "../../include/md5.h" +#include "../../include/sha1.h" + +#ifdef HAVE_OPENSSL_EVP_H +# include <openssl/evp.h> +#endif + +/* + * Define a structure for our module configuration. + * + * These variables do not need to be in a structure, but it's + * a lot cleaner to do so, and a pointer to the structure can + * be used as the instance handle. + */ +typedef struct rlm_pap_t { + char const *name; /* CONF_SECTION->name, not strdup'd */ + int auth_type; + bool normify; +} rlm_pap_t; + +/* + * A mapping of configuration file names to internal variables. + * + * Note that the string is dynamically allocated, so it MUST + * be freed. When the configuration file parse re-reads the string, + * it free's the old one, and strdup's the new one, placing the pointer + * to the strdup'd string into 'config.string'. This gets around + * buffer over-flows. + */ +static const CONF_PARSER module_config[] = { + { "normalise", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_pap_t, normify), "yes" }, + CONF_PARSER_TERMINATOR +}; + + +/* + * For auto-header discovery. + * + * @note Header comparison is case insensitive. + */ +static const FR_NAME_NUMBER header_names[] = { + { "{clear}", PW_CLEARTEXT_PASSWORD }, + { "{cleartext}", PW_CLEARTEXT_PASSWORD }, + { "{md5}", PW_MD5_PASSWORD }, + { "{base64_md5}", PW_MD5_PASSWORD }, + { "{smd5}", PW_SMD5_PASSWORD }, + { "{crypt}", PW_CRYPT_PASSWORD }, +#ifdef HAVE_OPENSSL_EVP_H + /* + * It'd make more sense for the headers to be + * ssha2-* with SHA3 coming soon but we're at + * the mercy of directory implementors. + */ + { "{sha2}", PW_SHA2_PASSWORD }, + { "{sha224}", PW_SHA2_PASSWORD }, + { "{sha256}", PW_SHA2_PASSWORD }, + { "{sha384}", PW_SHA2_PASSWORD }, + { "{sha512}", PW_SHA2_PASSWORD }, + { "{ssha224}", PW_SSHA2_224_PASSWORD }, + { "{ssha256}", PW_SSHA2_256_PASSWORD }, + { "{ssha384}", PW_SSHA2_384_PASSWORD }, + { "{ssha512}", PW_SSHA2_512_PASSWORD }, + { "{x-pbkdf2}", PW_PBKDF2_PASSWORD }, +#endif + { "{sha}", PW_SHA_PASSWORD }, + { "{ssha}", PW_SSHA_PASSWORD }, + { "{md4}", PW_NT_PASSWORD }, + { "{nt}", PW_NT_PASSWORD }, + { "{nthash}", PW_NT_PASSWORD }, + { "{x-nthash}", PW_NT_PASSWORD }, + { "{ns-mta-md5}", PW_NS_MTA_MD5_PASSWORD }, + { "{x- orcllmv}", PW_LM_PASSWORD }, + { "{X- orclntv}", PW_NT_PASSWORD }, + { NULL, 0 } +}; + +#ifdef HAVE_OPENSSL_EVP_H +static const FR_NAME_NUMBER pbkdf2_crypt_names[] = { + { "HMACSHA1", PW_SSHA1_PASSWORD }, + { "HMACSHA2+224", PW_SSHA2_224_PASSWORD }, + { "HMACSHA2+256", PW_SSHA2_256_PASSWORD }, + { "HMACSHA2+384", PW_SSHA2_384_PASSWORD }, + { "HMACSHA2+512", PW_SSHA2_512_PASSWORD }, +# if OPENSSL_VERSION_NUMBER >= 0x10101000L + { "HMACSHA3+224", PW_SSHA3_224_PASSWORD }, + { "HMACSHA3+256", PW_SSHA3_256_PASSWORD }, + { "HMACSHA3+384", PW_SSHA3_384_PASSWORD }, + { "HMACSHA3+512", PW_SSHA3_512_PASSWORD }, +# endif +}; + +static const FR_NAME_NUMBER pbkdf2_passlib_names[] = { + { "sha1", PW_SSHA1_PASSWORD }, + { "sha256", PW_SSHA2_256_PASSWORD }, + { "sha512", PW_SSHA2_512_PASSWORD } +}; +#endif + +static int mod_instantiate(CONF_SECTION *conf, void *instance) +{ + rlm_pap_t *inst = instance; + DICT_VALUE *dval; + + inst->name = cf_section_name2(conf); + if (!inst->name) { + inst->name = cf_section_name1(conf); + } + + dval = dict_valbyname(PW_AUTH_TYPE, 0, inst->name); + if (dval) { + inst->auth_type = dval->value; + } else { + inst->auth_type = 0; + } + + return 0; +} + +/** Hex or base64 or bin auto-discovery + * + * Here we try and autodiscover what encoding was used for the password/hash, and + * convert it back to binary or plaintext. + * + * @note Earlier versions used a 0x prefix as a hard indicator that the string was + * hex encoded, and would fail if the 0x was present but the string didn't + * consist of hexits. The base64 char set is a superset of hex, and it was + * observed in the wild, that occasionally base64 encoded data really could + * start with 0x. That's why min_len (and decodability) are used as the + * only heuristics now. + * + * @param[in] request Current request. + * @param[in,out] vp to normify. + * @param[in] min_len we expect the decoded version to be. + */ +static void normify(REQUEST *request, VALUE_PAIR *vp, size_t min_len) +{ + uint8_t buffer[256]; + + if (min_len >= sizeof(buffer)) return; /* paranoia */ + + rad_assert((vp->da->type == PW_TYPE_OCTETS) || (vp->da->type == PW_TYPE_STRING)); + + /* + * Hex encoding. Length is even, and it's greater than + * twice the minimum length. + */ + if (!(vp->vp_length & 0x01) && vp->vp_length >= (2 * min_len)) { + size_t decoded; + + decoded = fr_hex2bin(buffer, sizeof(buffer), vp->vp_strvalue, vp->vp_length); + if (decoded == (vp->vp_length >> 1)) { + RDEBUG2("Normalizing %s from hex encoding, %zu bytes -> %zu bytes", + vp->da->name, vp->vp_length, decoded); + fr_pair_value_memcpy(vp, buffer, decoded); + return; + } + } + + /* + * Base 64 encoding. It's at least 4/3 the original size, + * and we want to avoid division... + */ + if ((vp->vp_length * 3) >= ((min_len * 4))) { + ssize_t decoded; + decoded = fr_base64_decode(buffer, sizeof(buffer), vp->vp_strvalue, vp->vp_length); + if (decoded < 0) return; + if (decoded >= (ssize_t) min_len) { + RDEBUG2("Normalizing %s from base64 encoding, %zu bytes -> %zu bytes", + vp->da->name, vp->vp_length, decoded); + fr_pair_value_memcpy(vp, buffer, decoded); + return; + } + } + + /* + * Else unknown encoding, or already binary. Leave it. + */ +} + +/** Convert a Password-With-Header attribute to the correct type + * + * Attribute may be base64 encoded, in which case it will be decoded + * first, then evaluated. + * + * @note The buffer for octets types\ attributes is extended by one byte + * and '\0' terminated, to allow it to be used as a char buff. + * + * @param request Current request. + * @param vp Password-With-Header attribute to convert. + * @return a new VALUE_PAIR on success, NULL on error. + */ +static VALUE_PAIR *normify_with_header(REQUEST *request, VALUE_PAIR *vp) +{ + int attr; + char const *p, *q; + size_t len; + + uint8_t digest[257]; /* +1 for \0 */ + ssize_t decoded; + + char buffer[256]; + + VALUE_PAIR *new; + + VERIFY_VP(vp); + + /* + * Ensure this is only ever called with a + * string type attribute. + */ + rad_assert(vp->da->type == PW_TYPE_STRING); + +redo: + p = vp->vp_strvalue; + len = vp->vp_length; + + /* + * Has a header {...} prefix + */ + q = strchr(p, '}'); + if (q) { + size_t hlen; + + hlen = (q + 1) - p; + if (hlen >= sizeof(buffer)) { + REDEBUG("Password header too long. Got %zu bytes must be less than %zu bytes", + hlen, sizeof(buffer)); + return NULL; + } + + memcpy(buffer, p, hlen); + buffer[hlen] = '\0'; + + attr = fr_str2int(header_names, buffer, 0); + if (!attr) { + if (RDEBUG_ENABLED3) { + RDEBUG3("Unknown header %s in Password-With-Header = \"%s\", re-writing to " + "Cleartext-Password", buffer, vp->vp_strvalue); + } else { + RDEBUG("Unknown header %s in Password-With-Header, re-writing to " + "Cleartext-Password", buffer); + } + goto unknown_header; + } + + /* + * The data after the '}' may be binary, so we copy it via + * memcpy. BUT it might be a string (or used as one), so + * we ensure that there's a trailing zero, too. + */ + new = fr_pair_afrom_num(request, attr, 0); + if (new->da->type == PW_TYPE_OCTETS) { + fr_pair_value_memcpy(new, (uint8_t const *) q + 1, (len - hlen) + 1); + new->vp_length = (len - hlen); /* lie about the length */ + } else { + fr_pair_value_strcpy(new, q + 1); + } + + if (RDEBUG_ENABLED3) { + char *old_value, *new_value; + + old_value = vp_aprints_value(request, vp, '\''); + new_value = vp_aprints_value(request, new, '\''); + RDEBUG3("Converted: &control:%s = '%s' -> &control:%s = '%s'", + vp->da->name, old_value, new->da->name, new_value); + talloc_free(old_value); + talloc_free(new_value); + } else { + RDEBUG2("Converted: &control:%s -> &control:%s", vp->da->name, new->da->name); + } + + return new; + } + + /* + * Doesn't have a header {...} prefix + * + * See if it's base64, if it is, decode it and check again! + */ + decoded = fr_base64_decode(digest, sizeof(digest) - 1, vp->vp_strvalue, len); + if ((decoded > 0) && (digest[0] == '{') && (memchr(digest, '}', decoded) != NULL)) { + RDEBUG2("Normalizing %s from base64 encoding, %zu bytes -> %zu bytes", + vp->da->name, vp->vp_length, decoded); + /* + * Password-With-Header is a string attribute. + * Even though we're handling binary data, the buffer + * must be \0 terminated. + */ + digest[decoded] = '\0'; + fr_pair_value_memcpy(vp, digest, decoded + 1); + vp->vp_length = decoded; /* lie about the length */ + + goto redo; + } + + if (RDEBUG_ENABLED3) { + RDEBUG3("No {...} in Password-With-Header = \"%s\", re-writing to " + "Cleartext-Password", vp->vp_strvalue); + } else { + RDEBUG("No {...} in Password-With-Header, re-writing to Cleartext-Password"); + } + +unknown_header: + new = fr_pair_afrom_num(request, PW_CLEARTEXT_PASSWORD, 0); + fr_pair_value_strcpy(new, vp->vp_strvalue); + + return new; +} + +/* + * Authorize the user for PAP authentication. + * + * This isn't strictly necessary, but it does make the + * server simpler to configure. + */ +static rlm_rcode_t CC_HINT(nonnull) mod_authorize(void *instance, REQUEST *request) +{ + rlm_pap_t *inst = instance; + bool auth_type = false; + bool found_pw = false; + VALUE_PAIR *vp; + vp_cursor_t cursor; + + for (vp = fr_cursor_init(&cursor, &request->config); + vp; + vp = fr_cursor_next(&cursor)) { + VERIFY_VP(vp); + next: + switch (vp->da->attr) { + case PW_USER_PASSWORD: /* deprecated */ + RWDEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + RWDEBUG("!!! Ignoring control:User-Password. Update your !!!"); + RWDEBUG("!!! configuration so that the \"known good\" clear text !!!"); + RWDEBUG("!!! password is in Cleartext-Password and NOT in !!!"); + RWDEBUG("!!! User-Password. !!!"); + RWDEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + break; + + case PW_PASSWORD_WITH_HEADER: /* preferred */ + { + VALUE_PAIR *new; + + /* + * Password already exists: use that instead of this one. + */ + if (fr_pair_find_by_num(request->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY)) { + RWDEBUG("Config already contains a \"known good\" password " + "(&control:Cleartext-Password). Ignoring &control:Password-With-Header"); + break; + } + + new = normify_with_header(request, vp); + if (new) fr_cursor_insert(&cursor, new); /* inserts at the end of the list */ + + RDEBUG2("Removing &control:Password-With-Header"); + vp = fr_cursor_remove(&cursor); /* advances the cursor for us */ + talloc_free(vp); + + found_pw = true; + + vp = fr_cursor_current(&cursor); + if (vp) goto next; + } + break; + + case PW_CLEARTEXT_PASSWORD: + case PW_CRYPT_PASSWORD: + case PW_NS_MTA_MD5_PASSWORD: + found_pw = true; + break; /* don't touch these */ + + case PW_MD5_PASSWORD: + case PW_SMD5_PASSWORD: + case PW_NT_PASSWORD: + case PW_LM_PASSWORD: + if (inst->normify) { + normify(request, vp, 16); /* ensure it's in the right format */ + } + found_pw = true; + break; + +#ifdef HAVE_OPENSSL_EVP_H + case PW_SHA2_PASSWORD: + if (inst->normify) { + normify(request, vp, 28); /* ensure it's in the right format */ + } + found_pw = true; + break; + + case PW_SSHA2_224_PASSWORD: + if (inst->normify) { + normify(request, vp, 28); /* ensure it's in the right format */ + } + found_pw = true; + break; + + case PW_SSHA2_256_PASSWORD: + if (inst->normify) { + normify(request, vp, 32); /* ensure it's in the right format */ + } + found_pw = true; + break; + + case PW_SSHA2_384_PASSWORD: + if (inst->normify) { + normify(request, vp, 48); /* ensure it's in the right format */ + } + found_pw = true; + break; + + case PW_SSHA2_512_PASSWORD: + if (inst->normify) { + normify(request, vp, 64); /* ensure it's in the right format */ + } + found_pw = true; + break; +#endif + + case PW_SHA_PASSWORD: + case PW_SSHA_PASSWORD: + if (inst->normify) { + normify(request, vp, 20); /* ensure it's in the right format */ + } + found_pw = true; + break; + + /* + * If it's proxied somewhere, don't complain + * about not having passwords or Auth-Type. + */ + case PW_PROXY_TO_REALM: + { + REALM *realm = realm_find(vp->vp_strvalue); + if (realm && realm->auth_pool) { + return RLM_MODULE_NOOP; + } + break; + } + + case PW_AUTH_TYPE: + auth_type = true; + + /* + * Auth-Type := Accept + * Auth-Type := Reject + */ + if ((vp->vp_integer == 254) || + (vp->vp_integer == 4)) { + found_pw = true; + } + break; + + default: + break; /* ignore it */ + + } + } + + /* + * Print helpful warnings if there was no password. + */ + if (!found_pw) { + /* + * Likely going to be proxied. Avoid printing + * warning message. + */ + if (fr_pair_find_by_num(request->config, PW_REALM, 0, TAG_ANY) || + (fr_pair_find_by_num(request->config, PW_PROXY_TO_REALM, 0, TAG_ANY))) { + return RLM_MODULE_NOOP; + } + + /* + * The TLS types don't need passwords. + */ + vp = fr_pair_find_by_num(request->packet->vps, PW_EAP_TYPE, 0, TAG_ANY); + if (vp && + ((vp->vp_integer == 13) || /* EAP-TLS */ + (vp->vp_integer == 21) || /* EAP-TTLS */ + (vp->vp_integer == 25))) { /* PEAP */ + return RLM_MODULE_NOOP; + } + + if (auth_type) { + DEBUG("Not doing PAP as Auth-Type is already set."); + return RLM_MODULE_NOOP; + } + + RWDEBUG("No \"known good\" password found for the user. Not setting Auth-Type"); + RWDEBUG("Authentication will fail unless a \"known good\" password is available"); + return RLM_MODULE_NOOP; + } + + /* + * Don't touch existing Auth-Types. + */ + if (auth_type) { + if (auth_type != inst->auth_type) RWDEBUG2("Auth-Type already set. Not setting to PAP"); + return RLM_MODULE_NOOP; + } + + /* + * Can't do PAP if there's no password. + */ + if (!request->password || + (request->password->da->attr != PW_USER_PASSWORD)) { + RDEBUG2("No User-Password attribute in the request. Cannot do PAP"); + return RLM_MODULE_NOOP; + } + + if (inst->auth_type) { + vp = radius_pair_create(request, &request->config, + PW_AUTH_TYPE, 0); + vp->vp_integer = inst->auth_type; + } + + return RLM_MODULE_UPDATED; +} + +/* + * PAP authentication functions + */ + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_clear(UNUSED rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + if (RDEBUG_ENABLED3) { + RDEBUG3("Comparing with \"known good\" Cleartext-Password \"%s\" (%zd)", vp->vp_strvalue, vp->vp_length); + } else { + RDEBUG("Comparing with \"known good\" Cleartext-Password"); + } + + if ((vp->vp_length != request->password->vp_length) || + (rad_digest_cmp(vp->vp_octets, + request->password->vp_octets, + vp->vp_length) != 0)) { + REDEBUG("Cleartext password does not match \"known good\" password"); + return RLM_MODULE_REJECT; + } + return RLM_MODULE_OK; +} + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_crypt(UNUSED rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + if (RDEBUG_ENABLED3) { + RDEBUG3("Comparing with \"known good\" Crypt-Password \"%s\"", vp->vp_strvalue); + } else { + RDEBUG("Comparing with \"known-good\" Crypt-password"); + } + + if (fr_crypt_check(request->password->vp_strvalue, + vp->vp_strvalue) != 0) { + REDEBUG("Crypt digest does not match \"known good\" digest"); + return RLM_MODULE_REJECT; + } + return RLM_MODULE_OK; +} + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_md5(rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + FR_MD5_CTX md5_context; + uint8_t digest[128]; + + RDEBUG("Comparing with \"known-good\" MD5-Password"); + + if (inst->normify) { + normify(request, vp, 16); + } + if (vp->vp_length != 16) { + REDEBUG("\"known-good\" MD5 password has incorrect length"); + return RLM_MODULE_INVALID; + } + + fr_md5_init(&md5_context); + fr_md5_update(&md5_context, request->password->vp_octets, + request->password->vp_length); + fr_md5_final(digest, &md5_context); + + if (rad_digest_cmp(digest, vp->vp_octets, vp->vp_length) != 0) { + REDEBUG("MD5 digest does not match \"known good\" digest"); + return RLM_MODULE_REJECT; + } + + return RLM_MODULE_OK; +} + + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_smd5(rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + FR_MD5_CTX md5_context; + uint8_t digest[128]; + + RDEBUG("Comparing with \"known-good\" SMD5-Password"); + + if (inst->normify) { + normify(request, vp, 16); + } + if (vp->vp_length <= 16) { + REDEBUG("\"known-good\" SMD5-Password has incorrect length"); + return RLM_MODULE_INVALID; + } + + fr_md5_init(&md5_context); + fr_md5_update(&md5_context, request->password->vp_octets, + request->password->vp_length); + fr_md5_update(&md5_context, &vp->vp_octets[16], vp->vp_length - 16); + fr_md5_final(digest, &md5_context); + + /* + * Compare only the MD5 hash results, not the salt. + */ + if (rad_digest_cmp(digest, vp->vp_octets, 16) != 0) { + REDEBUG("SMD5 digest does not match \"known good\" digest"); + return RLM_MODULE_REJECT; + } + + return RLM_MODULE_OK; +} + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_sha(rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + fr_sha1_ctx sha1_context; + uint8_t digest[128]; + + RDEBUG("Comparing with \"known-good\" SHA-Password"); + + if (inst->normify) { + normify(request, vp, 20); + } + if (vp->vp_length != 20) { + REDEBUG("\"known-good\" SHA1-password has incorrect length"); + return RLM_MODULE_INVALID; + } + + fr_sha1_init(&sha1_context); + fr_sha1_update(&sha1_context, request->password->vp_octets, + request->password->vp_length); + fr_sha1_final(digest,&sha1_context); + + if (rad_digest_cmp(digest, vp->vp_octets, vp->vp_length) != 0) { + REDEBUG("SHA1 digest does not match \"known good\" digest"); + return RLM_MODULE_REJECT; + } + + return RLM_MODULE_OK; +} + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_ssha(rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + fr_sha1_ctx sha1_context; + uint8_t digest[128]; + + RDEBUG("Comparing with \"known-good\" SSHA-Password"); + + if (inst->normify) { + normify(request, vp, 20); + } + if (vp->vp_length <= 20) { + REDEBUG("\"known-good\" SSHA-Password has incorrect length"); + return RLM_MODULE_INVALID; + } + + fr_sha1_init(&sha1_context); + fr_sha1_update(&sha1_context, request->password->vp_octets, request->password->vp_length); + + fr_sha1_update(&sha1_context, &vp->vp_octets[20], vp->vp_length - 20); + fr_sha1_final(digest, &sha1_context); + + if (rad_digest_cmp(digest, vp->vp_octets, 20) != 0) { + REDEBUG("SSHA digest does not match \"known good\" digest"); + return RLM_MODULE_REJECT; + } + + return RLM_MODULE_OK; +} + +#ifdef HAVE_OPENSSL_EVP_H +static rlm_rcode_t CC_HINT(nonnull) pap_auth_sha2(rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + EVP_MD_CTX *ctx; + EVP_MD const *md; + char const *name; + uint8_t digest[EVP_MAX_MD_SIZE]; + unsigned int digest_len; + + RDEBUG("Comparing with \"known-good\" SHA2-Password"); + + if (inst->normify) normify(request, vp, 28); + + /* + * All the SHA-2 algorithms produce digests of different lengths, + * so it's trivial to determine which EVP_MD to use. + */ + switch (vp->vp_length) { + /* SHA-224 */ + case 28: + name = "SHA2-224"; + md = EVP_sha224(); + break; + + /* SHA-256 */ + case 32: + name = "SHA2-256"; + md = EVP_sha256(); + break; + + /* SHA-384 */ + case 48: + name = "SHA2-384"; + md = EVP_sha384(); + break; + + /* SHA-512 */ + case 64: + name = "SHA2-512"; + md = EVP_sha512(); + break; + + default: + REDEBUG("\"known good\" digest length (%zu) does not match output length of any SHA-2 digests", + vp->vp_length); + return RLM_MODULE_INVALID; + } + + ctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(ctx, md, NULL); + EVP_DigestUpdate(ctx, request->password->vp_octets, request->password->vp_length); + EVP_DigestFinal_ex(ctx, digest, &digest_len); + EVP_MD_CTX_destroy(ctx); + + rad_assert((size_t) digest_len == vp->vp_length); /* This would be an OpenSSL bug... */ + + if (rad_digest_cmp(digest, vp->vp_octets, vp->vp_length) != 0) { + REDEBUG("%s digest does not match \"known good\" digest", name); + return RLM_MODULE_REJECT; + } + + return RLM_MODULE_OK; +} + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_ssha2(rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + EVP_MD_CTX *ctx; + EVP_MD const *md = NULL; + char const *name = NULL; + uint8_t digest[EVP_MAX_MD_SIZE]; + unsigned int digest_len, min_len = 0; + + switch (vp->da->attr) { + case PW_SSHA2_224_PASSWORD: + name = "SSHA2-224"; + md = EVP_sha224(); + min_len = 28; + break; + + case PW_SSHA2_256_PASSWORD: + name = "SSHA2-256"; + md = EVP_sha256(); + min_len = 32; + break; + + case PW_SSHA2_384_PASSWORD: + name = "SSHA2-384"; + md = EVP_sha384(); + min_len = 48; + break; + + case PW_SSHA2_512_PASSWORD: + name = "SSHA2-512"; + min_len = 64; + md = EVP_sha512(); + break; + + default: + rad_assert(0); + } + + RDEBUG("Comparing with \"known-good\" %s-Password", name); + + /* + * Unlike plain SHA2 we already know what length + * to expect, so can be more specific with the + * minimum digest length. + */ + if (inst->normify) normify(request, vp, min_len + 1); + + if (vp->vp_length <= min_len) { + REDEBUG("\"known-good\" %s-Password has incorrect length, got %zu bytes, need at least %u bytes", + name, vp->vp_length, min_len + 1); + return RLM_MODULE_INVALID; + } + + ctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(ctx, md, NULL); + EVP_DigestUpdate(ctx, request->password->vp_octets, request->password->vp_length); + EVP_DigestUpdate(ctx, &vp->vp_octets[min_len], vp->vp_length - min_len); + EVP_DigestFinal_ex(ctx, digest, &digest_len); + EVP_MD_CTX_destroy(ctx); + + rad_assert((size_t) digest_len == min_len); /* This would be an OpenSSL bug... */ + + /* + * Only compare digest_len bytes, the rest is salt. + */ + if (rad_digest_cmp(digest, vp->vp_octets, (size_t)digest_len) != 0) { + REDEBUG("%s digest does not match \"known good\" digest", name); + return RLM_MODULE_REJECT; + } + + return RLM_MODULE_OK; +} + +/** Validates Crypt::PBKDF2 LDAP format strings + * + * @param[in] request The current request. + * @param[in] str Raw PBKDF2 string. + * @param[in] len Length of string. + * @return + * - RLM_MODULE_REJECT + * - RLM_MODULE_OK + */ +static inline rlm_rcode_t CC_HINT(nonnull) pap_auth_pbkdf2_parse(REQUEST *request, const uint8_t *str, size_t len, + const FR_NAME_NUMBER hash_names[], + char scheme_sep, char iter_sep, char salt_sep, + bool iter_is_base64, VALUE_PAIR const *password) +{ + rlm_rcode_t rcode = RLM_MODULE_INVALID; + + uint8_t const *p, *q, *end; + ssize_t slen; + + EVP_MD const *evp_md; + int digest_type; + size_t digest_len; + + uint32_t iterations; + + uint8_t *salt = NULL; + size_t salt_len; + uint8_t hash[EVP_MAX_MD_SIZE]; + uint8_t digest[EVP_MAX_MD_SIZE]; + + char hash_token[128]; + + RDEBUG2("Comparing with \"known-good\" PBKDF2-Password"); + + if (len <= 1) { + REDEBUG("PBKDF2-Password is too short"); + goto finish; + } + + /* + * Parse PBKDF string = {hash_algorithm}<scheme_sep><iterations><iter_sep>b64(<salt>)<salt_sep>b64(<hash>) + */ + p = str; + end = p + len; + + q = memchr(p, scheme_sep, end - p); + if (!q) { + REDEBUG("PBKDF2-Password has no component separators"); + goto finish; + } + + if ((q-p) >= (int)sizeof(hash_token)) { + REDEBUG("PBKDF2-Password has invalid hash token"); + goto finish; + } + + strlcpy(hash_token, (char const *)p, (q - p) + 1); + digest_type = fr_str2int(hash_names, hash_token, -1); + switch (digest_type) { + case PW_SSHA1_PASSWORD: + evp_md = EVP_sha1(); + digest_len = SHA1_DIGEST_LENGTH; + break; + + case PW_SSHA2_224_PASSWORD: + evp_md = EVP_sha224(); + digest_len = SHA224_DIGEST_LENGTH; + break; + + case PW_SSHA2_256_PASSWORD: + evp_md = EVP_sha256(); + digest_len = SHA256_DIGEST_LENGTH; + break; + + case PW_SSHA2_384_PASSWORD: + evp_md = EVP_sha384(); + digest_len = SHA384_DIGEST_LENGTH; + break; + + case PW_SSHA2_512_PASSWORD: + evp_md = EVP_sha512(); + digest_len = SHA512_DIGEST_LENGTH; + break; + +# if OPENSSL_VERSION_NUMBER >= 0x10101000L + case PW_SSHA3_224_PASSWORD: + evp_md = EVP_sha3_224(); + digest_len = SHA224_DIGEST_LENGTH; + break; + + case PW_SSHA3_256_PASSWORD: + evp_md = EVP_sha3_256(); + digest_len = SHA256_DIGEST_LENGTH; + break; + + case PW_SSHA3_384_PASSWORD: + evp_md = EVP_sha3_384(); + digest_len = SHA384_DIGEST_LENGTH; + break; + + case PW_SSHA3_512_PASSWORD: + evp_md = EVP_sha3_512(); + digest_len = SHA512_DIGEST_LENGTH; + break; +# endif + + default: + REDEBUG("Unknown PBKDF2 hash method \"%.*s\"", (int)(q - p), p); + goto finish; + } + + p = q + 1; + + if (((end - p) < 1) || !(q = memchr(p, iter_sep, end - p))) { + REDEBUG("PBKDF2-Password missing iterations component"); + goto finish; + } + + if ((q - p) == 0) { + REDEBUG("PBKDF2-Password iterations component too short"); + goto finish; + } + + /* + * If it's not base64 encoded, assume it's ascii + */ + if (!iter_is_base64) { + char iterations_buff[sizeof("4294967295") + 1]; + char *qq; + + strlcpy(iterations_buff, (char const *)p, (q - p) + 1); + + iterations = strtoul(iterations_buff, &qq, 10); + if (*qq != '\0') { + REMARKER(iterations_buff, qq - iterations_buff, + "PBKDF2-Password iterations field contains an invalid character"); + + goto finish; + } + p = q + 1; + /* + * base64 encoded and big endian + */ + } else { + (void)fr_strerror(); + slen = fr_base64_decode((uint8_t *)&iterations, sizeof(iterations), (char const *)p, q - p); + if (slen < 0) { + REDEBUG("Failed decoding PBKDF2-Password iterations component (%.*s): %s", + (int)(q - p), p, fr_strerror()); + goto finish; + } + if (slen != sizeof(iterations)) { + REDEBUG("Decoded PBKDF2-Password iterations component is wrong size"); + } + + iterations = ntohl(iterations); + + p = q + 1; + } + + /* + * Sanitise iterations. Seems OpenSSL 1.0 did this, but at least + * version 1.1 in RH8 does not, so safest to check ourselves. + */ + if (iterations == 0) { + RWDEBUG("PBKDF2 can not have zero iterations; increasing to 1"); + iterations = 1; + } + + if (((end - p) < 1) || !(q = memchr(p, salt_sep, end - p))) { + REDEBUG("PBKDF2-Password missing salt component"); + goto finish; + } + + if ((q - p) == 0) { + REDEBUG("PBKDF2-Password salt component too short"); + goto finish; + } + + MEM(salt = talloc_array(request, uint8_t, FR_BASE64_DEC_LENGTH(((size_t)(q - p))))); + slen = fr_base64_decode(salt, talloc_array_length(salt), (char const *) p, q - p); + if (slen < 0) { + REDEBUG("Failed decoding PBKDF2-Password salt component: %s", fr_strerror()); + goto finish; + } + salt_len = (size_t)slen; + + p = q + 1; + + if ((q - p) == 0) { + REDEBUG("PBKDF2-Password hash component too short"); + goto finish; + } + + slen = fr_base64_decode(hash, sizeof(hash), (char const *)p, end - p); + if (slen < 0) { + REDEBUG("Failed decoding PBKDF2-Password hash component: %s", fr_strerror()); + goto finish; + } + + if ((size_t)slen != digest_len) { + REDEBUG("PBKDF2-Password hash component length is incorrect for hash type, expected %zu, got %zd", + digest_len, slen); + goto finish; + } + + RDEBUG2("PBKDF2 %s: Iterations %d, salt length %zu, hash length %zd", + fr_int2str(pbkdf2_crypt_names, digest_type, "<UNKNOWN>"), + iterations, salt_len, slen); + + /* + * Hash and compare + */ + if (PKCS5_PBKDF2_HMAC((char const *)password->vp_octets, (int)password->vp_length, + (unsigned char const *)salt, (int)salt_len, + (int)iterations, + evp_md, + (int)digest_len, (unsigned char *)digest) == 0) { + REDEBUG("PBKDF2 digest failure"); + goto finish; + } + + if (rad_digest_cmp(digest, hash, (size_t)digest_len) != 0) { + REDEBUG("PBKDF2 digest does not match \"known good\" digest"); + rcode = RLM_MODULE_REJECT; + } else { + rcode = RLM_MODULE_OK; + } + +finish: + talloc_free(salt); + + return rcode; +} + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_pbkdf2(UNUSED rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *password) +{ + uint8_t const *p = password->vp_octets, *q, *end = p + password->vp_length; + + rad_assert(request->password != NULL); + rad_assert(request->password->da->attr == PW_USER_PASSWORD); + + if (end - p < 2) { + REDEBUG("PBKDF2-Password too short"); + return RLM_MODULE_INVALID; + } + + /* + * If it doesn't begin with a $ assume + * It's Crypt::PBKDF2 LDAP format + * + * {X-PBKDF2}<digest>:<b64 rounds>:<b64_salt>:<b64_hash> + */ + if (*p != '$') { + /* + * Strip the header if it's present + */ + if (*p == '{') { + q = memchr(p, '}', end - p); + p = q + 1; + } + return pap_auth_pbkdf2_parse(request, p, end - p, + pbkdf2_crypt_names, ':', ':', ':', true, request->password); + } + + /* + * Crypt::PBKDF2 Crypt format + * + * $PBKDF2$<digest>:<rounds>:<b64_salt>$<b64_hash> + */ + if ((size_t)(end - p) >= sizeof("$PBKDF2$") && (memcmp(p, "$PBKDF2$", sizeof("$PBKDF2$") - 1) == 0)) { + p += sizeof("$PBKDF2$") - 1; + return pap_auth_pbkdf2_parse(request, p, end - p, + pbkdf2_crypt_names, ':', ':', '$', false, request->password); + } + + /* + * Python's passlib format + * + * $pbkdf2-<digest>$<rounds>$<alt_b64_salt>$<alt_b64_hash> + * + * Note: Our base64 functions also work with alt_b64 + */ + if ((size_t)(end - p) >= sizeof("$pbkdf2-") && (memcmp(p, "$pbkdf2-", sizeof("$pbkdf2-") - 1) == 0)) { + p += sizeof("$pbkdf2-") - 1; + return pap_auth_pbkdf2_parse(request, p, end - p, + pbkdf2_passlib_names, '$', '$', '$', false, request->password); + } + + REDEBUG("Can't determine format of PBKDF2-Password"); + + return RLM_MODULE_INVALID; +} +#endif + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_nt(rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + ssize_t len; + uint8_t digest[16]; + uint8_t ucs2_password[512]; + + RDEBUG("Comparing with \"known-good\" NT-Password"); + + rad_assert(request->password != NULL); + rad_assert(request->password->da->attr == PW_USER_PASSWORD); + + if (inst->normify) { + normify(request, vp, 16); + } + + if (vp->vp_length != 16) { + REDEBUG("\"known good\" NT-Password has incorrect length"); + return RLM_MODULE_INVALID; + } + + len = fr_utf8_to_ucs2(ucs2_password, sizeof(ucs2_password), request->password->vp_strvalue, request->password->vp_length); + if (len < 0) { + REDEBUG("User-Password is not in UCS2 format"); + return RLM_MODULE_INVALID; + } + + fr_md4_calc(digest, (uint8_t *) ucs2_password, len); + + if (rad_digest_cmp(digest, vp->vp_octets, vp->vp_length) != 0) { + REDEBUG("NT digest does not match \"known good\" digest"); + return RLM_MODULE_REJECT; + } + + return RLM_MODULE_OK; +} + + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_lm(rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + uint8_t digest[16]; + char charbuf[32 + 1]; + ssize_t len; + + RDEBUG("Comparing with \"known-good\" LM-Password"); + + if (inst->normify) { + normify(request, vp, 16); + } + if (vp->vp_length != 16) { + REDEBUG("\"known good\" LM-Password has incorrect length"); + return RLM_MODULE_INVALID; + } + + len = radius_xlat(charbuf, sizeof(charbuf), request, "%{mschap:LM-Hash %{User-Password}}", NULL, NULL); + if (len < 0){ + return RLM_MODULE_FAIL; + } + + if ((fr_hex2bin(digest, sizeof(digest), charbuf, len) != vp->vp_length) || + (rad_digest_cmp(digest, vp->vp_octets, vp->vp_length) != 0)) { + REDEBUG("LM digest does not match \"known good\" digest"); + return RLM_MODULE_REJECT; + } + + return RLM_MODULE_OK; +} + +static rlm_rcode_t CC_HINT(nonnull) pap_auth_ns_mta_md5(UNUSED rlm_pap_t *inst, REQUEST *request, VALUE_PAIR *vp) +{ + FR_MD5_CTX md5_context; + uint8_t digest[128]; + uint8_t buff[MAX_STRING_LEN]; + uint8_t buff2[MAX_STRING_LEN + 50]; + + RDEBUG("Using NT-MTA-MD5-Password"); + + if (vp->vp_length != 64) { + REDEBUG("\"known good\" NS-MTA-MD5-Password has incorrect length"); + return RLM_MODULE_INVALID; + } + + /* + * Sanity check the value of NS-MTA-MD5-Password + */ + if (fr_hex2bin(digest, sizeof(digest), vp->vp_strvalue, vp->vp_length) != 16) { + REDEBUG("\"known good\" NS-MTA-MD5-Password has invalid value"); + return RLM_MODULE_INVALID; + } + + /* + * Ensure we don't have buffer overflows. + * + * This really: sizeof(buff) - 2 - 2*32 - strlen(passwd) + */ + if (request->password->vp_length >= (sizeof(buff) - 2 - 2 * 32)) { + REDEBUG("\"known good\" NS-MTA-MD5-Password is too long"); + return RLM_MODULE_INVALID; + } + + /* + * Set up the algorithm. + */ + { + uint8_t *p = buff2; + + memcpy(p, &vp->vp_octets[32], 32); + p += 32; + *(p++) = 89; + memcpy(p, (uint8_t const *)request->password->vp_strvalue, request->password->vp_length); + p += request->password->vp_length; + *(p++) = 247; + memcpy(p, &vp->vp_octets[32], 32); + p += 32; + + fr_md5_init(&md5_context); + fr_md5_update(&md5_context, buff2, p - buff2); + fr_md5_final(buff, &md5_context); + } + + if (rad_digest_cmp(digest, buff, 16) != 0) { + REDEBUG("NS-MTA-MD5 digest does not match \"known good\" digest"); + return RLM_MODULE_REJECT; + } + + return RLM_MODULE_OK; +} + +/* + * Authenticate the user via one of any well-known password. + */ +static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request) +{ + rlm_pap_t *inst = instance; + VALUE_PAIR *vp; + rlm_rcode_t rc = RLM_MODULE_INVALID; + vp_cursor_t cursor; + rlm_rcode_t (*auth_func)(rlm_pap_t *, REQUEST *, VALUE_PAIR *) = NULL; + + if (!request->password || + (request->password->da->vendor != 0) || + (request->password->da->attr != PW_USER_PASSWORD)) { + REDEBUG("You set 'Auth-Type = PAP' for a request that does not contain a User-Password attribute!"); + return RLM_MODULE_INVALID; + } + + /* + * The user MUST supply a non-zero-length password. + */ + if (request->password->vp_length == 0) { + REDEBUG("Password must not be empty"); + return RLM_MODULE_INVALID; + } + + if (RDEBUG_ENABLED3) { + RDEBUG3("Login attempt with password \"%s\" (%zd)", request->password->vp_strvalue, request->password->vp_length); + } else { + RDEBUG("Login attempt with password"); + } + + /* + * Auto-detect passwords, by attribute in the + * config items, to find out which authentication + * function to call. + */ + for (vp = fr_cursor_init(&cursor, &request->config); + vp; + vp = fr_cursor_next(&cursor)) { + if (!vp->da->vendor) switch (vp->da->attr) { + case PW_CLEARTEXT_PASSWORD: + auth_func = &pap_auth_clear; + break; + + case PW_CRYPT_PASSWORD: + auth_func = &pap_auth_crypt; + break; + + case PW_MD5_PASSWORD: + auth_func = &pap_auth_md5; + break; + + case PW_SMD5_PASSWORD: + auth_func = &pap_auth_smd5; + break; + +#ifdef HAVE_OPENSSL_EVP_H + case PW_SHA2_PASSWORD: + auth_func = &pap_auth_sha2; + break; + + case PW_SSHA2_224_PASSWORD: + case PW_SSHA2_256_PASSWORD: + case PW_SSHA2_384_PASSWORD: + case PW_SSHA2_512_PASSWORD: + auth_func = &pap_auth_ssha2; + break; + + case PW_PBKDF2_PASSWORD: + auth_func = &pap_auth_pbkdf2; + break; +#endif + + case PW_SHA_PASSWORD: + auth_func = &pap_auth_sha; + break; + + case PW_SSHA_PASSWORD: + auth_func = &pap_auth_ssha; + break; + + case PW_NT_PASSWORD: + auth_func = &pap_auth_nt; + break; + + case PW_LM_PASSWORD: + auth_func = &pap_auth_lm; + break; + + case PW_NS_MTA_MD5_PASSWORD: + auth_func = &pap_auth_ns_mta_md5; + break; + + default: + break; + } + + if (auth_func != NULL) break; + } + + /* + * No attribute was found that looked like a password to match. + */ + if (!auth_func) { + RDEBUG("No password configured for the user. Cannot do authentication"); + return RLM_MODULE_FAIL; + } + + /* + * Authenticate, and return. + */ + rc = auth_func(inst, request, vp); + + if (rc == RLM_MODULE_REJECT) { + RDEBUG("Passwords don't match"); + } + + if (rc == RLM_MODULE_OK) { + RDEBUG("User authenticated successfully"); + } + + return rc; +} + + +/* + * The module name should be the only globally exported symbol. + * That is, everything else should be 'static'. + * + * If the module needs to temporarily modify it's instantiation + * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE. + * The server will then take care of ensuring that the module + * is single-threaded. + */ +extern module_t rlm_pap; +module_t rlm_pap = { + .magic = RLM_MODULE_INIT, + .name = "pap", + .type = RLM_TYPE_HUP_SAFE, + .inst_size = sizeof(rlm_pap_t), + .config = module_config, + .instantiate = mod_instantiate, + .methods = { + [MOD_AUTHENTICATE] = mod_authenticate, + [MOD_AUTHORIZE] = mod_authorize + }, +}; |