/** * @brief Convert base64 to hex * * Example: "%{base64tohex:Zm9v}" == "666f6f" */ static size_t base64_to_hex_xlat(UNUSED void *instance, REQUEST *request, char *fmt, char *out, size_t outlen, UNUSED RADIUS_ESCAPE_STRING func) { char buffer[1024]; char decbuf[1024]; size_t declen = sizeof(decbuf); size_t len; while (isspace((int) *fmt)) fmt++; len = radius_xlat(buffer, sizeof(buffer), fmt, request, func); if (!len) { radlog(L_ERR, "rlm_expr: xlat failed."); *out = '\0'; return 0; } if (!fr_base64_decode(buffer, len, decbuf, &declen)) { radlog(L_ERR, "rlm_expr: base64 string invalid"); *out = '\0'; return 0; } if ((size_t)((declen * 2) + 1) > outlen) { radlog(L_ERR, "rlm_expr: Base64 conversion failed, " "output buffer exhausted, needed %zd bytes, " "have %zd bytes", (declen * 2) + 1, outlen); } fr_bin2hex(decbuf, out, declen); return declen * 2; }
static ssize_t dhcp_xlat(UNUSED void *instance, REQUEST *request, char const *fmt, char **out, size_t freespace) { vp_cursor_t cursor; VALUE_PAIR *vp; uint8_t binbuf[255]; ssize_t len; while (isspace((int) *fmt)) fmt++; if ((radius_copy_vp(request, &vp, request, fmt) < 0) || !vp) return 0; fr_cursor_init(&cursor, &vp); len = fr_dhcp_encode_option(request, binbuf, sizeof(binbuf), &cursor); talloc_free(vp); if (len <= 0) { REDEBUG("DHCP option encoding failed: %s", fr_strerror()); return -1; } if ((size_t)((len * 2) + 1) > freespace) { REDEBUG("DHCP option encoding failed: Output buffer exhausted, needed %zd bytes, have %zd bytes", (len * 2) + 1, freespace); return -1; } return fr_bin2hex(*out, binbuf, len); }
static int cbtls_new_session(UNUSED SSL *s, SSL_SESSION *sess) { size_t size; char buffer[2 * MAX_SESSION_SIZE + 1]; size = sess->session_id_length; if (size > MAX_SESSION_SIZE) size = MAX_SESSION_SIZE; fr_bin2hex(sess->session_id, buffer, size); DEBUG2(" SSL: adding session %s to cache", buffer); return 1; }
static void cbtls_remove_session(UNUSED SSL_CTX *ctx, SSL_SESSION *sess) { size_t size; char buffer[2 * MAX_SESSION_SIZE + 1]; size = sess->session_id_length; if (size > MAX_SESSION_SIZE) size = MAX_SESSION_SIZE; fr_bin2hex(sess->session_id, buffer, size); DEBUG2(" SSL: Removing session %s from the cache", buffer); SSL_SESSION_free(sess); return; }
static SSL_SESSION *cbtls_get_session(UNUSED SSL *s, unsigned char *data, int len, UNUSED int *copy) { size_t size; char buffer[2 * MAX_SESSION_SIZE + 1]; size = len; if (size > MAX_SESSION_SIZE) size = MAX_SESSION_SIZE; fr_bin2hex(data, buffer, size); DEBUG2(" SSL: Client requested nonexistent cached session %s", buffer); return NULL; }
/** Generate the HMAC-SHA1 of a string or attribute * * Example: "%{hmacsha1:foo bar}" == "Zm9v" */ static ssize_t hmac_sha1_xlat(UNUSED void *instance, UNUSED REQUEST *request, char const *fmt, char *out, size_t outlen) { uint8_t const *data, *key; char const *p; ssize_t data_len, key_len; uint8_t digest[SHA1_DIGEST_LENGTH]; char data_ref[256]; if (outlen <= (sizeof(digest) * 2)) { REDEBUG("Insufficient space to write digest, needed %zu bytes, have %zu bytes", (sizeof(digest) * 2) + 1, outlen); return -1; } p = strchr(fmt, ' '); if (!p) { REDEBUG("HMAC requires exactly two arguments (&data &key)"); return -1; } if ((size_t)(p - fmt) >= sizeof(data_ref)) { REDEBUG("Insufficient space to store HMAC input data, needed %zu bytes, have %zu bytes", (p - fmt) + 1, sizeof(data_ref)); return -1; } strlcpy(data_ref, fmt, (p - fmt) + 1); data_len = xlat_fmt_to_ref(&data, request, data_ref); if (data_len < 0) return -1; while (isspace(*p) && p++); key_len = xlat_fmt_to_ref(&key, request, p); if (key_len < 0) return -1; fr_hmac_sha1(digest, data, data_len, key, key_len); return fr_bin2hex(out, digest, sizeof(digest)); }
/* * Find the named user in this modules database. Create the set * of attribute-value pairs to check and reply with for this user * from the database. The authentication code only needs to check * the password, the rest is done here. */ static int wimax_authorize(void *instance, REQUEST *request) { VALUE_PAIR *vp; /* quiet the compiler */ instance = instance; request = request; /* * Fix Calling-Station-Id. Damn you, WiMAX! */ vp = pairfind(request->packet->vps, PW_CALLING_STATION_ID); if (vp && (vp->length == 6)) { int i; uint8_t buffer[6]; memcpy(buffer, vp->vp_octets, 6); /* * RFC 3580 Section 3.20 says this is the preferred * format. Everyone *SANE* is using this format, * so we fix it here. */ for (i = 0; i < 6; i++) { fr_bin2hex(&buffer[i], &vp->vp_strvalue[i * 3], 1); vp->vp_strvalue[(i * 3) + 2] = '-'; } vp->vp_strvalue[(5*3)+2] = '\0'; vp->length = (5*3)+2; DEBUG2("rlm_wimax: Fixing WiMAX binary Calling-Station-Id to %s", vp->vp_strvalue); } return RLM_MODULE_OK; }
/** Convert base64 to hex * * Example: "%{base64tohex:Zm9v}" == "666f6f" */ static ssize_t base64_to_hex_xlat(UNUSED void *instance, UNUSED REQUEST *request, char const *fmt, char *out, size_t outlen) { uint8_t decbuf[1024]; ssize_t declen; ssize_t len = strlen(fmt); *out = '\0'; declen = fr_base64_decode(decbuf, sizeof(decbuf), fmt, len); if (declen < 0) { REDEBUG("Base64 string invalid"); return -1; } if ((size_t)((declen * 2) + 1) > outlen) { REDEBUG("Base64 conversion failed, output buffer exhausted, needed %zd bytes, have %zd bytes", (declen * 2) + 1, outlen); return -1; } return fr_bin2hex(out, decbuf, declen); }
/* * Find the named user in this modules database. Create the set * of attribute-value pairs to check and reply with for this user * from the database. The authentication code only needs to check * the password, the rest is done here. */ static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, REQUEST *request) { VALUE_PAIR *passwd_item, *chap; uint8_t pass_str[MAX_STRING_LEN]; if (!request->username) { RWDEBUG("Attribute 'User-Name' is required for authentication.\n"); return RLM_MODULE_INVALID; } chap = pairfind(request->packet->vps, PW_CHAP_PASSWORD, 0, TAG_ANY); if (!chap) { REDEBUG("You set 'Auth-Type = CHAP' for a request that does not contain a CHAP-Password attribute!"); return RLM_MODULE_INVALID; } if (chap->length == 0) { REDEBUG("CHAP-Password is empty"); return RLM_MODULE_INVALID; } if (chap->length != CHAP_VALUE_LENGTH + 1) { REDEBUG("CHAP-Password has invalid length"); return RLM_MODULE_INVALID; } /* * Don't print out the CHAP password here. It's binary crap. */ RDEBUG("Login attempt by \"%s\" with CHAP password", request->username->vp_strvalue); if ((passwd_item = pairfind(request->config_items, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY)) == NULL){ if (pairfind(request->config_items, PW_USER_PASSWORD, 0, TAG_ANY) != NULL){ REDEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); REDEBUG("!!! Please update your configuration so that the \"known !!!"); REDEBUG("!!! good\" cleartext password is in Cleartext-Password, !!!"); REDEBUG("!!! and NOT in User-Password. !!!"); REDEBUG("!!! !!!"); REDEBUG("!!! Authentication will fail because of this. !!!"); REDEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); } REDEBUG("Cleartext password is required for authentication"); return RLM_MODULE_INVALID; } rad_chap_encode(request->packet, pass_str, chap->vp_octets[0], passwd_item); if (RDEBUG_ENABLED3) { uint8_t const *p; size_t length; VALUE_PAIR *vp; char buffer[CHAP_VALUE_LENGTH * 2 + 1]; RDEBUG3("Comparing with \"known good\" Cleartext-Password \"%s\"", passwd_item->vp_strvalue); vp = pairfind(request->packet->vps, PW_CHAP_CHALLENGE, 0, TAG_ANY); if (vp) { p = vp->vp_octets; length = vp->length; } else { p = request->packet->vector; length = sizeof(request->packet->vector); } fr_bin2hex(buffer, p, length); RINDENT(); RDEBUG3("CHAP challenge : %s", buffer); fr_bin2hex(buffer, chap->vp_octets + 1, CHAP_VALUE_LENGTH); RDEBUG3("Client sent : %s", buffer); fr_bin2hex(buffer, pass_str + 1, CHAP_VALUE_LENGTH); RDEBUG3("We calculated : %s", buffer); REXDENT(); } else { RDEBUG2("Comparing with \"known good\" Cleartext-Password"); } if (rad_digest_cmp(pass_str + 1, chap->vp_octets + 1, CHAP_VALUE_LENGTH) != 0) { REDEBUG("Password is comparison failed: password is incorrect"); return RLM_MODULE_REJECT; } RDEBUG("CHAP user \"%s\" authenticated successfully", request->username->vp_strvalue); return RLM_MODULE_OK; }
/* * Print one value into a string. * delimitst will define if strings and dates will be delimited by '"' */ int vp_prints_value(char * out, size_t outlen, VALUE_PAIR *vp, int delimitst) { DICT_VALUE *v; char buf[1024]; const char *a = NULL; size_t len; time_t t; struct tm s_tm; out[0] = '\0'; if (!vp) return 0; switch (vp->type) { case PW_TYPE_STRING: if ((delimitst == 1) && vp->flags.has_tag) { /* Tagged attribute: print delimter and ignore tag */ buf[0] = '"'; fr_print_string(vp->vp_strvalue, vp->length, buf + 1, sizeof(buf) - 2); strcat(buf, "\""); } else if (delimitst == 1) { /* Non-tagged attribute: print delimter */ buf[0] = '"'; fr_print_string(vp->vp_strvalue, vp->length, buf + 1, sizeof(buf) - 2); strcat(buf, "\""); } else if (delimitst < 0) { /* xlat.c */ strlcpy(out, vp->vp_strvalue, outlen); return strlen(out); } else { /* Non-tagged attribute: no delimiter */ fr_print_string(vp->vp_strvalue, vp->length, buf, sizeof(buf)); } a = buf; break; case PW_TYPE_INTEGER: if ( vp->flags.has_tag ) { /* Attribute value has a tag, need to ignore it */ if ((v = dict_valbyattr(vp->attribute, (vp->vp_integer & 0xffffff))) != NULL) a = v->name; else { snprintf(buf, sizeof(buf), "%u", (vp->vp_integer & 0xffffff)); a = buf; } } else { case PW_TYPE_BYTE: case PW_TYPE_SHORT: /* Normal, non-tagged attribute */ if ((v = dict_valbyattr(vp->attribute, vp->vp_integer)) != NULL) a = v->name; else { snprintf(buf, sizeof(buf), "%u", vp->vp_integer); a = buf; } } break; case PW_TYPE_DATE: t = vp->vp_date; if (delimitst == 1) { len = strftime(buf, sizeof(buf), "\"%b %e %Y %H:%M:%S %Z\"", localtime_r(&t, &s_tm)); } else { len = strftime(buf, sizeof(buf), "%b %e %Y %H:%M:%S %Z", localtime_r(&t, &s_tm)); } if (len > 0) a = buf; break; case PW_TYPE_SIGNED: /* Damned code for 1 WiMAX attribute */ snprintf(buf, sizeof(buf), "%d", vp->vp_signed); a = buf; break; case PW_TYPE_IPADDR: a = inet_ntop(AF_INET, &(vp->vp_ipaddr), buf, sizeof(buf)); break; case PW_TYPE_ABINARY: #ifdef ASCEND_BINARY a = buf; print_abinary(vp, buf, sizeof(buf)); break; #else /* FALL THROUGH */ #endif case PW_TYPE_OCTETS: if (outlen <= (2 * (vp->length + 1))) return 0; strcpy(buf, "0x"); fr_bin2hex(vp->vp_octets, buf + 2, vp->length); a = buf; break; case PW_TYPE_IFID: a = ifid_ntoa(buf, sizeof(buf), vp->vp_octets); break; case PW_TYPE_IPV6ADDR: a = inet_ntop(AF_INET6, (const struct in6_addr *) vp->vp_strvalue, buf, sizeof(buf)); break; case PW_TYPE_IPV6PREFIX: { struct in6_addr addr; /* * Alignment issues. */ memcpy(&addr, vp->vp_strvalue + 2, sizeof(addr)); a = inet_ntop(AF_INET6, &addr, buf, sizeof(buf)); if (a) { char *p = buf + strlen(buf); snprintf(p, buf + sizeof(buf) - p - 1, "/%u", (unsigned int) vp->vp_octets[1]); } } break; case PW_TYPE_ETHERNET: snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", vp->vp_ether[0], vp->vp_ether[1], vp->vp_ether[2], vp->vp_ether[3], vp->vp_ether[4], vp->vp_ether[5]); a = buf; break; case PW_TYPE_TLV: if (outlen <= (2 * (vp->length + 1))) return 0; strcpy(buf, "0x"); fr_bin2hex(vp->vp_tlv, buf + 2, vp->length); a = buf; break; default: a = "UNKNOWN-TYPE"; break; } if (a != NULL) strlcpy(out, a, outlen); return strlen(out); }
/* * Process an EAP request */ fr_tls_status_t eaptls_process(eap_handler_t *handler) { tls_session_t *tls_session = (tls_session_t *) handler->opaque; EAPTLS_PACKET *tlspacket; fr_tls_status_t status; REQUEST *request = handler->request; if (!request) return FR_TLS_FAIL; RDEBUG2("Continuing EAP-TLS"); SSL_set_ex_data(tls_session->ssl, FR_TLS_EX_INDEX_REQUEST, request); if (handler->certs) fr_pair_add(&request->packet->vps, fr_pair_list_copy(request->packet, handler->certs)); /* * This case is when SSL generates Alert then we * send that alert to the client and then send the EAP-Failure */ status = eaptls_verify(handler); if ((status == FR_TLS_INVALID) || (status == FR_TLS_FAIL)) { REDEBUG("[eaptls verify] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>")); } else { RDEBUG2("[eaptls verify] = %s", fr_int2str(fr_tls_status_table, status, "<INVALID>")); } switch (status) { default: case FR_TLS_INVALID: case FR_TLS_FAIL: /* * Success means that we're done the initial * handshake. For TTLS, this means send stuff * back to the client, and the client sends us * more tunneled data. */ case FR_TLS_SUCCESS: goto done; /* * Normal TLS request, continue with the "get rest * of fragments" phase. */ case FR_TLS_REQUEST: eaptls_request(handler->eap_ds, tls_session); status = FR_TLS_HANDLED; goto done; /* * The handshake is done, and we're in the "tunnel * data" phase. */ case FR_TLS_OK: RDEBUG2("Done initial handshake"); /* * Get the rest of the fragments. */ case FR_TLS_FIRST_FRAGMENT: case FR_TLS_MORE_FRAGMENTS: case FR_TLS_LENGTH_INCLUDED: break; } /* * Extract the TLS packet from the buffer. */ if ((tlspacket = eaptls_extract(request, handler->eap_ds, status)) == NULL) { status = FR_TLS_FAIL; goto done; } /* * Get the session struct from the handler * * update the dirty_in buffer * * NOTE: This buffer will contain partial data when M bit is set. * * CAUTION while reinitializing this buffer, it should be * reinitialized only when this M bit is NOT set. */ if (tlspacket->dlen != (tls_session->record_plus)(&tls_session->dirty_in, tlspacket->data, tlspacket->dlen)) { talloc_free(tlspacket); REDEBUG("Exceeded maximum record size"); status = FR_TLS_FAIL; goto done; } /* * No longer needed. */ talloc_free(tlspacket); /* * SSL initalization is done. Return. * * The TLS data will be in the tls_session structure. */ if (SSL_is_init_finished(tls_session->ssl)) { /* * The initialization may be finished, but if * there more fragments coming, then send ACK, * and get the caller to continue the * conversation. */ if ((status == FR_TLS_MORE_FRAGMENTS) || (status == FR_TLS_FIRST_FRAGMENT)) { /* * Send the ACK. */ eaptls_send_ack(handler, tls_session->peap_flag); RDEBUG2("Init is done, but tunneled data is fragmented"); status = FR_TLS_HANDLED; goto done; } status = tls_application_data(tls_session, request); goto done; } /* * Continue the handshake. */ status = eaptls_operation(status, handler); if (status == FR_TLS_SUCCESS) { #define MAX_SESSION_SIZE (256) size_t size; VALUE_PAIR *vps; char buffer[2 * MAX_SESSION_SIZE + 1]; /* * Restore the cached VPs before processing the * application data. */ size = tls_session->ssl->session->session_id_length; if (size > MAX_SESSION_SIZE) size = MAX_SESSION_SIZE; fr_bin2hex(buffer, tls_session->ssl->session->session_id, size); vps = SSL_SESSION_get_ex_data(tls_session->ssl->session, fr_tls_ex_index_vps); if (!vps) { RWDEBUG("No information in cached session %s", buffer); } else { vp_cursor_t cursor; VALUE_PAIR *vp; RDEBUG("Adding cached attributes from session %s", buffer); /* * The cbtls_get_session() function doesn't have * access to sock->certs or handler->certs, which * is where the certificates normally live. So * the certs are all in the VPS list here, and * have to be manually extracted. */ RINDENT(); for (vp = fr_cursor_init(&cursor, &vps); vp; vp = fr_cursor_next(&cursor)) { /* * TLS-* attrs get added back to * the request list. */ if ((vp->da->vendor == 0) && (vp->da->attr >= PW_TLS_CERT_SERIAL) && (vp->da->attr <= PW_TLS_CLIENT_CERT_SUBJECT_ALT_NAME_UPN)) { /* * Certs already exist. Don't re-add them. */ if (!handler->certs) { rdebug_pair(L_DBG_LVL_2, request, vp, "request:"); fr_pair_add(&request->packet->vps, fr_pair_copy(request->packet, vp)); } } else { rdebug_pair(L_DBG_LVL_2, request, vp, "reply:"); fr_pair_add(&request->reply->vps, fr_pair_copy(request->reply, vp)); } } REXDENT(); } } done: SSL_set_ex_data(tls_session->ssl, FR_TLS_EX_INDEX_REQUEST, NULL); return status; }
/** Allocate a new IP address from a pool * */ static ippool_rcode_t redis_ippool_allocate(rlm_redis_ippool_t const *inst, REQUEST *request, uint8_t const *key_prefix, size_t key_prefix_len, uint8_t const *device_id, size_t device_id_len, uint8_t const *gateway_id, size_t gateway_id_len, uint32_t expires) { struct timeval now; redisReply *reply = NULL; fr_redis_rcode_t status; ippool_rcode_t ret = IPPOOL_RCODE_SUCCESS; rad_assert(key_prefix); rad_assert(device_id); gettimeofday(&now, NULL); /* * hiredis doesn't deal well with NULL string pointers */ if (!gateway_id) gateway_id = (uint8_t const *)""; status = ippool_script(&reply, request, inst->cluster, key_prefix, key_prefix_len, inst->wait_num, FR_TIMEVAL_TO_MS(&inst->wait_timeout), lua_alloc_digest, lua_alloc_cmd, "EVALSHA %s 1 %b %u %u %b %b", lua_alloc_digest, key_prefix, key_prefix_len, (unsigned int)now.tv_sec, expires, device_id, device_id_len, gateway_id, gateway_id_len); if (status != REDIS_RCODE_SUCCESS) { ret = IPPOOL_RCODE_FAIL; goto finish; } rad_assert(reply); if (reply->type != REDIS_REPLY_ARRAY) { REDEBUG("Expected result to be array got \"%s\"", fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>")); ret = IPPOOL_RCODE_FAIL; goto finish; } if (reply->elements == 0) { REDEBUG("Got empty result array"); ret = IPPOOL_RCODE_FAIL; goto finish; } /* * Process return code */ if (reply->element[0]->type != REDIS_REPLY_INTEGER) { REDEBUG("Server returned unexpected type \"%s\" for rcode element (result[0])", fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>")); ret = IPPOOL_RCODE_FAIL; goto finish; } ret = reply->element[0]->integer; if (ret < 0) goto finish; /* * Process IP address */ if (reply->elements > 1) { vp_tmpl_t ip_rhs = { .type = TMPL_TYPE_DATA, .tmpl_value_type = FR_TYPE_STRING }; vp_map_t ip_map = { .lhs = inst->allocated_address_attr, .op = T_OP_SET, .rhs = &ip_rhs }; switch (reply->element[1]->type) { /* * Destination attribute may not be IPv4, in which case * we want to pre-convert the integer value to an IPv4 * address before casting it once more to the type of * the destination attribute. */ case REDIS_REPLY_INTEGER: { if (ip_map.lhs->tmpl_da->type != FR_TYPE_IPV4_ADDR) { fr_value_box_t tmp; memset(&tmp, 0, sizeof(tmp)); tmp.vb_uint32 = ntohl((uint32_t)reply->element[1]->integer); tmp.type = FR_TYPE_UINT32; if (fr_value_box_cast(NULL, &ip_map.rhs->tmpl_value, FR_TYPE_IPV4_ADDR, NULL, &tmp)) { RPEDEBUG("Failed converting integer to IPv4 address"); ret = IPPOOL_RCODE_FAIL; goto finish; } } else { ip_map.rhs->tmpl_value.vb_uint32 = ntohl((uint32_t)reply->element[1]->integer); ip_map.rhs->tmpl_value_type = FR_TYPE_UINT32; } } goto do_ip_map; case REDIS_REPLY_STRING: ip_map.rhs->tmpl_value.vb_strvalue = reply->element[1]->str; ip_map.rhs->tmpl_value_length = reply->element[1]->len; ip_map.rhs->tmpl_value_type = FR_TYPE_STRING; do_ip_map: if (map_to_request(request, &ip_map, map_to_vp, NULL) < 0) { ret = IPPOOL_RCODE_FAIL; goto finish; } break; default: REDEBUG("Server returned unexpected type \"%s\" for IP element (result[1])", fr_int2str(redis_reply_types, reply->element[1]->type, "<UNKNOWN>")); ret = IPPOOL_RCODE_FAIL; goto finish; } } /* * Process Range identifier */ if (reply->elements > 2) { switch (reply->element[2]->type) { /* * Add range ID to request */ case REDIS_REPLY_STRING: { vp_tmpl_t range_rhs = { .name = "", .type = TMPL_TYPE_DATA, .tmpl_value_type = FR_TYPE_STRING, .quote = T_DOUBLE_QUOTED_STRING }; vp_map_t range_map = { .lhs = inst->range_attr, .op = T_OP_SET, .rhs = &range_rhs }; range_map.rhs->tmpl_value.vb_strvalue = reply->element[2]->str; range_map.rhs->tmpl_value_length = reply->element[2]->len; range_map.rhs->tmpl_value_type = FR_TYPE_STRING; if (map_to_request(request, &range_map, map_to_vp, NULL) < 0) { ret = IPPOOL_RCODE_FAIL; goto finish; } } break; case REDIS_REPLY_NIL: break; default: REDEBUG("Server returned unexpected type \"%s\" for range element (result[2])", fr_int2str(redis_reply_types, reply->element[2]->type, "<UNKNOWN>")); ret = IPPOOL_RCODE_FAIL; goto finish; } } /* * Process Expiry time */ if (inst->expiry_attr && (reply->elements > 3)) { vp_tmpl_t expiry_rhs = { .name = "", .type = TMPL_TYPE_DATA, .tmpl_value_type = FR_TYPE_STRING, .quote = T_DOUBLE_QUOTED_STRING }; vp_map_t expiry_map = { .lhs = inst->expiry_attr, .op = T_OP_SET, .rhs = &expiry_rhs }; if (reply->element[3]->type != REDIS_REPLY_INTEGER) { REDEBUG("Server returned unexpected type \"%s\" for expiry element (result[3])", fr_int2str(redis_reply_types, reply->element[3]->type, "<UNKNOWN>")); ret = IPPOOL_RCODE_FAIL; goto finish; } expiry_map.rhs->tmpl_value.vb_uint32 = reply->element[3]->integer; expiry_map.rhs->tmpl_value_type = FR_TYPE_UINT32; if (map_to_request(request, &expiry_map, map_to_vp, NULL) < 0) { ret = IPPOOL_RCODE_FAIL; goto finish; } } finish: fr_redis_reply_free(&reply); return ret; } /** Update an existing IP address in a pool * */ static ippool_rcode_t redis_ippool_update(rlm_redis_ippool_t const *inst, REQUEST *request, uint8_t const *key_prefix, size_t key_prefix_len, fr_ipaddr_t *ip, uint8_t const *device_id, size_t device_id_len, uint8_t const *gateway_id, size_t gateway_id_len, uint32_t expires) { struct timeval now; redisReply *reply = NULL; fr_redis_rcode_t status; ippool_rcode_t ret = IPPOOL_RCODE_SUCCESS; vp_tmpl_t range_rhs = { .name = "", .type = TMPL_TYPE_DATA, .tmpl_value_type = FR_TYPE_STRING, .quote = T_DOUBLE_QUOTED_STRING }; vp_map_t range_map = { .lhs = inst->range_attr, .op = T_OP_SET, .rhs = &range_rhs }; gettimeofday(&now, NULL); /* * hiredis doesn't deal well with NULL string pointers */ if (!device_id) device_id = (uint8_t const *)""; if (!gateway_id) gateway_id = (uint8_t const *)""; if ((ip->af == AF_INET) && inst->ipv4_integer) { status = ippool_script(&reply, request, inst->cluster, key_prefix, key_prefix_len, inst->wait_num, FR_TIMEVAL_TO_MS(&inst->wait_timeout), lua_update_digest, lua_update_cmd, "EVALSHA %s 1 %b %u %u %u %b %b", lua_update_digest, key_prefix, key_prefix_len, (unsigned int)now.tv_sec, expires, htonl(ip->addr.v4.s_addr), device_id, device_id_len, gateway_id, gateway_id_len); } else { char ip_buff[FR_IPADDR_PREFIX_STRLEN]; IPPOOL_SPRINT_IP(ip_buff, ip, ip->prefix); status = ippool_script(&reply, request, inst->cluster, key_prefix, key_prefix_len, inst->wait_num, FR_TIMEVAL_TO_MS(&inst->wait_timeout), lua_update_digest, lua_update_cmd, "EVALSHA %s 1 %b %u %u %s %b %b", lua_update_digest, key_prefix, key_prefix_len, (unsigned int)now.tv_sec, expires, ip_buff, device_id, device_id_len, gateway_id, gateway_id_len); } if (status != REDIS_RCODE_SUCCESS) { ret = IPPOOL_RCODE_FAIL; goto finish; } if (reply->type != REDIS_REPLY_ARRAY) { REDEBUG("Expected result to be array got \"%s\"", fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>")); ret = IPPOOL_RCODE_FAIL; goto finish; } if (reply->elements == 0) { REDEBUG("Got empty result array"); ret = IPPOOL_RCODE_FAIL; goto finish; } /* * Process return code */ if (reply->element[0]->type != REDIS_REPLY_INTEGER) { REDEBUG("Server returned unexpected type \"%s\" for rcode element (result[0])", fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>")); ret = IPPOOL_RCODE_FAIL; goto finish; } ret = reply->element[0]->integer; if (ret < 0) goto finish; /* * Process Range identifier */ if (reply->elements > 1) { switch (reply->element[1]->type) { /* * Add range ID to request */ case REDIS_REPLY_STRING: range_map.rhs->tmpl_value.vb_strvalue = reply->element[1]->str; range_map.rhs->tmpl_value_length = reply->element[1]->len; range_map.rhs->tmpl_value_type = FR_TYPE_STRING; if (map_to_request(request, &range_map, map_to_vp, NULL) < 0) { ret = IPPOOL_RCODE_FAIL; goto finish; } break; case REDIS_REPLY_NIL: break; default: REDEBUG("Server returned unexpected type \"%s\" for range element (result[1])", fr_int2str(redis_reply_types, reply->element[0]->type, "<UNKNOWN>")); ret = IPPOOL_RCODE_FAIL; goto finish; } } /* * Copy expiry time to expires attribute (if set) */ if (inst->expiry_attr) { vp_tmpl_t expiry_rhs = { .name = "", .type = TMPL_TYPE_DATA, .tmpl_value_type = FR_TYPE_STRING, .quote = T_DOUBLE_QUOTED_STRING }; vp_map_t expiry_map = { .lhs = inst->expiry_attr, .op = T_OP_SET, .rhs = &expiry_rhs }; expiry_map.rhs->tmpl_value.vb_uint32 = expires; expiry_map.rhs->tmpl_value_type = FR_TYPE_UINT32; if (map_to_request(request, &expiry_map, map_to_vp, NULL) < 0) { ret = IPPOOL_RCODE_FAIL; goto finish; } } finish: fr_redis_reply_free(&reply); return ret; } /** Release an existing IP address in a pool * */ static ippool_rcode_t redis_ippool_release(rlm_redis_ippool_t const *inst, REQUEST *request, uint8_t const *key_prefix, size_t key_prefix_len, fr_ipaddr_t *ip, uint8_t const *device_id, size_t device_id_len) { struct timeval now; redisReply *reply = NULL; fr_redis_rcode_t status; ippool_rcode_t ret = IPPOOL_RCODE_SUCCESS; gettimeofday(&now, NULL); /* * hiredis doesn't deal well with NULL string pointers */ if (!device_id) device_id = (uint8_t const *)""; if ((ip->af == AF_INET) && inst->ipv4_integer) { status = ippool_script(&reply, request, inst->cluster, key_prefix, key_prefix_len, inst->wait_num, FR_TIMEVAL_TO_MS(&inst->wait_timeout), lua_release_digest, lua_release_cmd, "EVALSHA %s 1 %b %u %u %b", lua_release_digest, key_prefix, key_prefix_len, (unsigned int)now.tv_sec, htonl(ip->addr.v4.s_addr), device_id, device_id_len); } else { char ip_buff[FR_IPADDR_PREFIX_STRLEN]; IPPOOL_SPRINT_IP(ip_buff, ip, ip->prefix); status = ippool_script(&reply, request, inst->cluster, key_prefix, key_prefix_len, inst->wait_num, FR_TIMEVAL_TO_MS(&inst->wait_timeout), lua_release_digest, lua_release_cmd, "EVALSHA %s 1 %b %u %s %b", lua_release_digest, key_prefix, key_prefix_len, (unsigned int)now.tv_sec, ip_buff, device_id, device_id_len); } if (status != REDIS_RCODE_SUCCESS) { ret = IPPOOL_RCODE_FAIL; goto finish; } if (reply->type != REDIS_REPLY_ARRAY) { REDEBUG("Expected result to be array got \"%s\"", fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>")); ret = IPPOOL_RCODE_FAIL; goto finish; } if (reply->elements == 0) { REDEBUG("Got empty result array"); ret = IPPOOL_RCODE_FAIL; goto finish; } /* * Process return code */ if (reply->element[0]->type != REDIS_REPLY_INTEGER) { REDEBUG("Server returned unexpected type \"%s\" for rcode element (result[0])", fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>")); ret = IPPOOL_RCODE_FAIL; goto finish; } ret = reply->element[0]->integer; if (ret < 0) goto finish; finish: fr_redis_reply_free(&reply); return ret; } /** Find the pool name we'll be allocating from * * @param[out] out Where to write the pool name. * @param[out] buff Where to write the pool name (in the case of an expansion). * @param[in] bufflen Size of the output buffer. * @param[in] inst This instance of the rlm_redis_ippool module. * @param[in] request The current request. * @return * - < 0 on error. * - 0 if no pool attribute exists, or the pool name is a zero length string. * - > 0 on success (length of data written to out). */ static inline ssize_t ippool_pool_name(uint8_t const **out, uint8_t buff[], size_t bufflen, rlm_redis_ippool_t const *inst, REQUEST *request) { ssize_t slen; slen = tmpl_expand(out, (char *)buff, bufflen, request, inst->pool_name, NULL, NULL); if (slen < 0) { if (inst->pool_name->type == TMPL_TYPE_ATTR) { RDEBUG2("Pool attribute not present in request. Doing nothing"); return 0; } REDEBUG("Failed expanding pool name"); return -1; } if (slen == 0) { RDEBUG2("Empty pool name. Doing nothing"); return 0; } if ((*out == buff) && is_truncated((size_t)slen, bufflen)) { REDEBUG("Pool name too long. Expected %zu bytes, got %zu bytes", bufflen, (size_t)slen); return -1; } return slen; } static rlm_rcode_t mod_action(rlm_redis_ippool_t const *inst, REQUEST *request, ippool_action_t action) { uint8_t key_prefix_buff[IPPOOL_MAX_KEY_PREFIX_SIZE], device_id_buff[256], gateway_id_buff[256]; uint8_t const *key_prefix, *device_id = NULL, *gateway_id = NULL; size_t key_prefix_len, device_id_len = 0, gateway_id_len = 0; ssize_t slen; fr_ipaddr_t ip; char expires_buff[20]; char const *expires_str; unsigned long expires = 0; char *q; slen = ippool_pool_name(&key_prefix, (uint8_t *)&key_prefix_buff, sizeof(key_prefix_len), inst, request); if (slen < 0) return RLM_MODULE_FAIL; if (slen == 0) return RLM_MODULE_NOOP; key_prefix_len = (size_t)slen; if (inst->device_id) { slen = tmpl_expand((char const **)&device_id, (char *)&device_id_buff, sizeof(device_id_buff), request, inst->device_id, NULL, NULL); if (slen < 0) { REDEBUG("Failed expanding device (%s)", inst->device_id->name); return RLM_MODULE_FAIL; } device_id_len = (size_t)slen; } if (inst->gateway_id) { slen = tmpl_expand((char const **)&gateway_id, (char *)&gateway_id_buff, sizeof(gateway_id_buff), request, inst->gateway_id, NULL, NULL); if (slen < 0) { REDEBUG("Failed expanding gateway (%s)", inst->gateway_id->name); return RLM_MODULE_FAIL; } gateway_id_len = (size_t)slen; } switch (action) { case POOL_ACTION_ALLOCATE: if (tmpl_expand(&expires_str, expires_buff, sizeof(expires_buff), request, inst->offer_time, NULL, NULL) < 0) { REDEBUG("Failed expanding offer_time (%s)", inst->offer_time->name); return RLM_MODULE_FAIL; } expires = strtoul(expires_str, &q, 10); if (q != (expires_str + strlen(expires_str))) { REDEBUG("Invalid offer_time. Must be an integer value"); return RLM_MODULE_FAIL; } ippool_action_print(request, action, L_DBG_LVL_2, key_prefix, key_prefix_len, NULL, device_id, device_id_len, gateway_id, gateway_id_len, expires); switch (redis_ippool_allocate(inst, request, key_prefix, key_prefix_len, device_id, device_id_len, gateway_id, gateway_id_len, (uint32_t)expires)) { case IPPOOL_RCODE_SUCCESS: RDEBUG2("IP address lease allocated"); return RLM_MODULE_UPDATED; case IPPOOL_RCODE_POOL_EMPTY: RWDEBUG("Pool contains no free addresses"); return RLM_MODULE_NOTFOUND; default: return RLM_MODULE_FAIL; } case POOL_ACTION_UPDATE: { char ip_buff[INET6_ADDRSTRLEN + 4]; char const *ip_str; if (tmpl_expand(&expires_str, expires_buff, sizeof(expires_buff), request, inst->lease_time, NULL, NULL) < 0) { REDEBUG("Failed expanding lease_time (%s)", inst->lease_time->name); return RLM_MODULE_FAIL; } expires = strtoul(expires_str, &q, 10); if (q != (expires_str + strlen(expires_str))) { REDEBUG("Invalid expires. Must be an integer value"); return RLM_MODULE_FAIL; } if (tmpl_expand(&ip_str, ip_buff, sizeof(ip_buff), request, inst->requested_address, NULL, NULL) < 0) { REDEBUG("Failed expanding requested_address (%s)", inst->requested_address->name); return RLM_MODULE_FAIL; } if (fr_inet_pton(&ip, ip_str, -1, AF_UNSPEC, false, true) < 0) { RPEDEBUG("Failed parsing address"); return RLM_MODULE_FAIL; } ippool_action_print(request, action, L_DBG_LVL_2, key_prefix, key_prefix_len, ip_str, device_id, device_id_len, gateway_id, gateway_id_len, expires); switch (redis_ippool_update(inst, request, key_prefix, key_prefix_len, &ip, device_id, device_id_len, gateway_id, gateway_id_len, (uint32_t)expires)) { case IPPOOL_RCODE_SUCCESS: RDEBUG2("Requested IP address' \"%s\" lease updated", ip_str); /* * Copy over the input IP address to the reply attribute */ if (inst->copy_on_update) { vp_tmpl_t ip_rhs = { .name = "", .type = TMPL_TYPE_DATA, .quote = T_BARE_WORD, }; vp_map_t ip_map = { .lhs = inst->allocated_address_attr, .op = T_OP_SET, .rhs = &ip_rhs }; ip_rhs.tmpl_value_length = strlen(ip_str); ip_rhs.tmpl_value.vb_strvalue = ip_str; ip_rhs.tmpl_value_type = FR_TYPE_STRING; if (map_to_request(request, &ip_map, map_to_vp, NULL) < 0) return RLM_MODULE_FAIL; } return RLM_MODULE_UPDATED; /* * It's useful to be able to identify the 'not found' case * as we can relay to a server where the IP address might * be found. This extremely useful for migrations. */ case IPPOOL_RCODE_NOT_FOUND: REDEBUG("Requested IP address \"%s\" is not a member of the specified pool", ip_str); return RLM_MODULE_NOTFOUND; case IPPOOL_RCODE_EXPIRED: REDEBUG("Requested IP address' \"%s\" lease already expired at time of renewal", ip_str); return RLM_MODULE_INVALID; case IPPOOL_RCODE_DEVICE_MISMATCH: REDEBUG("Requested IP address' \"%s\" lease allocated to another device", ip_str); return RLM_MODULE_INVALID; default: return RLM_MODULE_FAIL; } } case POOL_ACTION_RELEASE: { char ip_buff[INET6_ADDRSTRLEN + 4]; char const *ip_str; if (tmpl_expand(&ip_str, ip_buff, sizeof(ip_buff), request, inst->requested_address, NULL, NULL) < 0) { REDEBUG("Failed expanding requested_address (%s)", inst->requested_address->name); return RLM_MODULE_FAIL; } if (fr_inet_pton(&ip, ip_str, -1, AF_UNSPEC, false, true) < 0) { RPEDEBUG("Failed parsing address"); return RLM_MODULE_FAIL; } ippool_action_print(request, action, L_DBG_LVL_2, key_prefix, key_prefix_len, ip_str, device_id, device_id_len, gateway_id, gateway_id_len, 0); switch (redis_ippool_release(inst, request, key_prefix, key_prefix_len, &ip, device_id, device_id_len)) { case IPPOOL_RCODE_SUCCESS: RDEBUG2("IP address \"%s\" released", ip_str); return RLM_MODULE_UPDATED; /* * It's useful to be able to identify the 'not found' case * as we can relay to a server where the IP address might * be found. This extremely useful for migrations. */ case IPPOOL_RCODE_NOT_FOUND: REDEBUG("Requested IP address \"%s\" is not a member of the specified pool", ip_str); return RLM_MODULE_NOTFOUND; case IPPOOL_RCODE_DEVICE_MISMATCH: REDEBUG("Requested IP address' \"%s\" lease allocated to another device", ip_str); return RLM_MODULE_INVALID; default: return RLM_MODULE_FAIL; } } case POOL_ACTION_BULK_RELEASE: RDEBUG2("Bulk release not yet implemented"); return RLM_MODULE_NOOP; default: rad_assert(0); return RLM_MODULE_FAIL; } } static rlm_rcode_t mod_accounting(void *instance, UNUSED void *thread, REQUEST *request) CC_HINT(nonnull); static rlm_rcode_t mod_accounting(void *instance, UNUSED void *thread, REQUEST *request) { rlm_redis_ippool_t const *inst = instance; VALUE_PAIR *vp; /* * Pool-Action override */ vp = fr_pair_find_by_da(request->control, attr_pool_action, TAG_ANY); if (vp) return mod_action(inst, request, vp->vp_uint32); /* * Otherwise, guess the action by Acct-Status-Type */ vp = fr_pair_find_by_da(request->packet->vps, attr_acct_status_type, TAG_ANY); if (!vp) { RDEBUG2("Couldn't find &request:Acct-Status-Type or &control:Pool-Action, doing nothing..."); return RLM_MODULE_NOOP; } switch (vp->vp_uint32) { case FR_STATUS_START: case FR_STATUS_ALIVE: return mod_action(inst, request, POOL_ACTION_UPDATE); case FR_STATUS_STOP: return mod_action(inst, request, POOL_ACTION_RELEASE); case FR_STATUS_ACCOUNTING_OFF: case FR_STATUS_ACCOUNTING_ON: return mod_action(inst, request, POOL_ACTION_BULK_RELEASE); default: return RLM_MODULE_NOOP; } } static rlm_rcode_t mod_authorize(void *instance, UNUSED void *thread, REQUEST *request) CC_HINT(nonnull); static rlm_rcode_t mod_authorize(void *instance, UNUSED void *thread, REQUEST *request) { rlm_redis_ippool_t const *inst = instance; VALUE_PAIR *vp; /* * Unless it's overridden the default action is to allocate * when called in Post-Auth. */ vp = fr_pair_find_by_da(request->control, attr_pool_action, TAG_ANY); return mod_action(inst, request, vp ? vp->vp_uint32 : POOL_ACTION_ALLOCATE); } static rlm_rcode_t mod_post_auth(void *instance, UNUSED void *thread, REQUEST *request) CC_HINT(nonnull); static rlm_rcode_t mod_post_auth(void *instance, UNUSED void *thread, REQUEST *request) { rlm_redis_ippool_t const *inst = instance; VALUE_PAIR *vp; ippool_action_t action = POOL_ACTION_ALLOCATE; /* * Unless it's overridden the default action is to allocate * when called in Post-Auth. */ vp = fr_pair_find_by_da(request->control, attr_pool_action, TAG_ANY); if (vp) { if ((vp->vp_uint32 > 0) && (vp->vp_uint32 <= POOL_ACTION_BULK_RELEASE)) { action = vp->vp_uint32; } else { RWDEBUG("Ignoring invalid action %d", vp->vp_uint32); return RLM_MODULE_NOOP; } #ifdef WITH_DHCP } else if (request->dict == dict_dhcpv4) { vp = fr_pair_find_by_da(request->control, attr_message_type, TAG_ANY); if (!vp) goto run; if (vp->vp_uint8 == FR_DHCP_REQUEST) action = POOL_ACTION_UPDATE; #endif } run: return mod_action(inst, request, action); } static int mod_instantiate(void *instance, CONF_SECTION *conf) { static bool done_hash = false; CONF_SECTION *subcs = cf_section_find(conf, "redis", NULL); rlm_redis_ippool_t *inst = instance; rad_assert(inst->allocated_address_attr->type == TMPL_TYPE_ATTR); rad_assert(subcs); inst->cluster = fr_redis_cluster_alloc(inst, subcs, &inst->conf, true, NULL, NULL, NULL); if (!inst->cluster) return -1; if (!fr_redis_cluster_min_version(inst->cluster, "3.0.2")) { PERROR("Cluster error"); return -1; } /* * Pre-Compute the SHA1 hashes of the Lua scripts */ if (!done_hash) { fr_sha1_ctx sha1_ctx; uint8_t digest[SHA1_DIGEST_LENGTH]; fr_sha1_init(&sha1_ctx); fr_sha1_update(&sha1_ctx, (uint8_t const *)lua_alloc_cmd, sizeof(lua_alloc_cmd) - 1); fr_sha1_final(digest, &sha1_ctx); fr_bin2hex(lua_alloc_digest, digest, sizeof(digest)); fr_sha1_init(&sha1_ctx); fr_sha1_update(&sha1_ctx, (uint8_t const *)lua_update_cmd, sizeof(lua_update_cmd) - 1); fr_sha1_final(digest, &sha1_ctx); fr_bin2hex(lua_update_digest, digest, sizeof(digest)); fr_sha1_init(&sha1_ctx); fr_sha1_update(&sha1_ctx, (uint8_t const *)lua_release_cmd, sizeof(lua_release_cmd) - 1); fr_sha1_final(digest, &sha1_ctx); fr_bin2hex(lua_release_digest, digest, sizeof(digest)); } /* * If we don't have a separate time specifically for offers * just use the lease time. */ if (!inst->offer_time) inst->offer_time = inst->lease_time; return 0; } static int mod_load(void) { fr_redis_version_print(); return 0; } extern module_t rlm_redis_ippool; module_t rlm_redis_ippool = { .magic = RLM_MODULE_INIT, .name = "redis", .type = RLM_TYPE_THREAD_SAFE, .inst_size = sizeof(rlm_redis_ippool_t), .config = module_config, .onload = mod_load, .instantiate = mod_instantiate, .methods = { [MOD_ACCOUNTING] = mod_accounting, [MOD_AUTHORIZE] = mod_authorize, [MOD_POST_AUTH] = mod_post_auth, }, };
USES_APPLE_DEPRECATED_API /* OpenSSL API has been deprecated by Apple */ /* avoid inclusion of these FR headers which conflict w/ OpenSSL */ #define _FR_MD4_H #define _FR_SHA1_H #include "extern.h" #include <string.h> #include <openssl/des.h> /* des_cblock */ #include <openssl/md5.h> #include <openssl/hmac.h> /* * Generate the State attribute, suitable for passing to fr_pair_make(). * 'challenge' must be a null terminated string, and be sized at least * as large as indicated in the function definition. * * Returns 0 on success, non-zero otherwise. For successful returns, * 'rad_state' (suitable for passing to fr_pair_make()) and 'raw_state', * if non-NULL, will be filled in. * * In the simplest implementation, we would just use the challenge as state. * Unfortunately, the RADIUS secret protects only the User-Password * attribute; an attacker that can remove packets from the wire and insert * new ones can simply insert a replayed state without having to know * the secret. If not for an attacker that can remove packets from the * network, I believe trivial state to be secure. * * So, we have to make up for that deficiency by signing our state with * data unique to this specific request. A NAS would use the Request * Authenticator, but we don't know what that will be when the State is * returned to us, so we'll use the time. So our replay prevention * is limited to a time interval (inst->challenge_delay). We could keep * track of all challenges issued over that time interval for * better protection. * * Our state, then, is * (challenge + flags + time + hmac(challenge + resync + time, key)), * where '+' denotes concatentation, 'challenge' is ... the challenge, * 'flags' is a 32-bit value that can be used to record additional info, * 'time' is the 32-bit time (LSB if time_t is 64 bits), and 'key' is a * random key, generated in mod_instantiate(). 'flags' and 'time' are * in network byte order. * * As the signing key is unique to each server, only the server which * generates a challenge can verify it; this should be OK if your NAS's * load balance across RADIUS servers using a "first available" algorithm. * If your NAS's round-robin and don't "stick" to the same server if they * see a State attribute (ugh), you could use the RADIUS secret instead, * but read RFC 2104 first, and make very sure you really want to do this. * * Since only the "same server" can verify State, 'flags' and 'time' doesn't * really need to be in network byte order, but we do it anyway. * * The State attribute is an octet string, however some versions of Cisco * IOS and Catalyst OS (at least IOS 12.1(26)E4 and CatOS 7.6.12) treat it * as an ASCII string (they only return data up to the first NUL byte). * So we must handle state as an ASCII string (0x00 -> 0x3030). */ /* * OTP_MAX_RADSTATE_LEN is composed of: * * clen * 2 + challenge * 8 + flags * 8 + time * sizeof(hmac) * 2 + hmac * 1 \0' */ /** Generate an OTP state value * * Generates an OTP state value (an string of ASCII hexits in an opaque binary * string). * * @param[out] state buffer in which to write the generated state value. * @param[in] challenge The challenge value. * @param[in] clen The length of the challenge data. * @param[in] flags to remember. * @param[in] when the challenge was originally generated. * @param[in] key HMAC key. * @return the amount of data written into the state buffer. */ size_t otp_gen_state(char state[OTP_MAX_RADSTATE_LEN], char const challenge[OTP_MAX_CHALLENGE_LEN], size_t clen, int32_t flags, int32_t when, uint8_t const key[16]) { HMAC_CTX hmac_ctx; uint8_t hmac[MD5_DIGEST_LENGTH]; char *p; /* * Generate the hmac. We already have a dependency on openssl for * DES, so we'll use it's hmac functionality also -- saves us from * having to collect the data to be signed into one * contiguous piece. */ HMAC_Init(&hmac_ctx, key, sizeof(key[0]) * 16, EVP_md5()); HMAC_Update(&hmac_ctx, (uint8_t const *) challenge, clen); HMAC_Update(&hmac_ctx, (uint8_t *) &flags, 4); HMAC_Update(&hmac_ctx, (uint8_t *) &when, 4); HMAC_Final(&hmac_ctx, hmac, NULL); HMAC_cleanup(&hmac_ctx); /* * Generate the state. */ p = state; /* * Add the challenge (which is already ASCII encoded) */ p += fr_bin2hex(p, (uint8_t const *) challenge, clen); /* Add the flags and time. */ p += fr_bin2hex(p, (uint8_t *) &flags, 4); p += fr_bin2hex(p, (uint8_t *) &when, 4); /* Add the hmac. */ p += fr_bin2hex(p, hmac, 16); return p - state; }
/* * Find the named user in this modules database. Create the set * of attribute-value pairs to check and reply with for this user * from the database. The authentication code only needs to check * the password, the rest is done here. */ static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(UNUSED void *instance, REQUEST *request) { VALUE_PAIR *password, *chap; uint8_t pass_str[MAX_STRING_LEN]; if (!request->username) { REDEBUG("&request:User-Name attribute is required for authentication"); return RLM_MODULE_INVALID; } chap = fr_pair_find_by_num(request->packet->vps, PW_CHAP_PASSWORD, 0, TAG_ANY); if (!chap) { REDEBUG("You set '&control:Auth-Type = CHAP' for a request that " "does not contain a CHAP-Password attribute!"); return RLM_MODULE_INVALID; } if (chap->vp_length == 0) { REDEBUG("&request:CHAP-Password is empty"); return RLM_MODULE_INVALID; } if (chap->vp_length != CHAP_VALUE_LENGTH + 1) { REDEBUG("&request:CHAP-Password has invalid length"); return RLM_MODULE_INVALID; } password = fr_pair_find_by_num(request->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY); if (password == NULL) { if (fr_pair_find_by_num(request->config, PW_USER_PASSWORD, 0, TAG_ANY) != NULL){ REDEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); REDEBUG("!!! Please update your configuration so that the \"known !!!"); REDEBUG("!!! good\" cleartext password is in Cleartext-Password, !!!"); REDEBUG("!!! and NOT in User-Password. !!!"); REDEBUG("!!! !!!"); REDEBUG("!!! Authentication will fail because of this. !!!"); REDEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); } REDEBUG("&control:Cleartext-Password is required for authentication"); return RLM_MODULE_FAIL; } rad_chap_encode(request->packet, pass_str, chap->vp_octets[0], password); if (RDEBUG_ENABLED3) { uint8_t const *p; size_t length; VALUE_PAIR *vp; char buffer[MAX_STRING_LEN * 2 + 1]; RDEBUG3("Comparing with \"known good\" &control:Cleartext-Password value \"%s\"", password->vp_strvalue); vp = fr_pair_find_by_num(request->packet->vps, PW_CHAP_CHALLENGE, 0, TAG_ANY); if (vp) { RDEBUG2("Using challenge from &request:CHAP-Challenge"); p = vp->vp_octets; length = vp->vp_length; } else { RDEBUG2("Using challenge from authenticator field"); p = request->packet->vector; length = sizeof(request->packet->vector); } fr_bin2hex(buffer, p, length); RINDENT(); RDEBUG3("CHAP challenge : %s", buffer); fr_bin2hex(buffer, chap->vp_octets + 1, CHAP_VALUE_LENGTH); RDEBUG3("Client sent : %s", buffer); fr_bin2hex(buffer, pass_str + 1, CHAP_VALUE_LENGTH); RDEBUG3("We calculated : %s", buffer); REXDENT(); } else { RDEBUG2("Comparing with \"known good\" Cleartext-Password"); } if (rad_digest_cmp(pass_str + 1, chap->vp_octets + 1, CHAP_VALUE_LENGTH) != 0) { REDEBUG("Password comparison failed: password is incorrect"); return RLM_MODULE_REJECT; } RDEBUG("CHAP user \"%s\" authenticated successfully", request->username->vp_strvalue); return RLM_MODULE_OK; }
/* * Does dynamic translation of strings. * * Pulls NT-Response, LM-Response, or Challenge from MSCHAP * attributes. */ static size_t mschap_xlat(void *instance, REQUEST *request, char *fmt, char *out, size_t outlen, RADIUS_ESCAPE_STRING func) { size_t i, data_len; uint8_t *data = NULL; uint8_t buffer[32]; VALUE_PAIR *user_name; VALUE_PAIR *chap_challenge, *response; rlm_mschap_t *inst = instance; response = NULL; func = func; /* -Wunused */ /* * Challenge means MS-CHAPv1 challenge, or * hash of MS-CHAPv2 challenge, and peer challenge. */ if (strncasecmp(fmt, "Challenge", 9) == 0) { chap_challenge = pairfind(request->packet->vps, PW_MSCHAP_CHALLENGE); if (!chap_challenge) { RDEBUG2("No MS-CHAP-Challenge in the request."); return 0; } /* * MS-CHAP-Challenges are 8 octets, * for MS-CHAPv2 */ if (chap_challenge->length == 8) { RDEBUG2(" mschap1: %02x", chap_challenge->vp_octets[0]); data = chap_challenge->vp_octets; data_len = 8; /* * MS-CHAP-Challenges are 16 octets, * for MS-CHAPv2. */ } else if (chap_challenge->length == 16) { char *username_string; RDEBUG2(" mschap2: %02x", chap_challenge->vp_octets[0]); response = pairfind(request->packet->vps, PW_MSCHAP2_RESPONSE); if (!response) { RDEBUG2("MS-CHAP2-Response is required to calculate MS-CHAPv1 challenge."); return 0; } /* * Responses are 50 octets. */ if (response->length < 50) { radlog_request(L_AUTH, 0, request, "MS-CHAP-Response has the wrong format."); return 0; } user_name = pairfind(request->packet->vps, PW_USER_NAME); if (!user_name) { RDEBUG2("User-Name is required to calculateMS-CHAPv1 Challenge."); return 0; } /* * with_ntdomain_hack moved here, too. */ if ((username_string = strchr(user_name->vp_strvalue, '\\')) != NULL) { if (inst->with_ntdomain_hack) { username_string++; } else { RDEBUG2("NT Domain delimeter found, should we have enabled with_ntdomain_hack?"); username_string = user_name->vp_strvalue; } } else { username_string = user_name->vp_strvalue; } /* * Get the MS-CHAPv1 challenge * from the MS-CHAPv2 peer challenge, * our challenge, and the user name. */ challenge_hash(response->vp_octets + 2, chap_challenge->vp_octets, username_string, buffer); data = buffer; data_len = 8; } else { RDEBUG2("Invalid MS-CHAP challenge length"); return 0; } /* * Get the MS-CHAPv1 response, or the MS-CHAPv2 * response. */ } else if (strncasecmp(fmt, "NT-Response", 11) == 0) { response = pairfind(request->packet->vps, PW_MSCHAP_RESPONSE); if (!response) response = pairfind(request->packet->vps, PW_MSCHAP2_RESPONSE); if (!response) { RDEBUG2("No MS-CHAP-Response or MS-CHAP2-Response was found in the request."); return 0; } /* * For MS-CHAPv1, the NT-Response exists only * if the second octet says so. */ if ((response->attribute == PW_MSCHAP_RESPONSE) && ((response->vp_octets[1] & 0x01) == 0)) { RDEBUG2("No NT-Response in MS-CHAP-Response"); return 0; } /* * MS-CHAP-Response and MS-CHAP2-Response have * the NT-Response at the same offset, and are * the same length. */ data = response->vp_octets + 26; data_len = 24; /* * LM-Response is deprecated, and exists only * in MS-CHAPv1, and not often there. */ } else if (strncasecmp(fmt, "LM-Response", 11) == 0) { response = pairfind(request->packet->vps, PW_MSCHAP_RESPONSE); if (!response) { RDEBUG2("No MS-CHAP-Response was found in the request."); return 0; } /* * For MS-CHAPv1, the NT-Response exists only * if the second octet says so. */ if ((response->vp_octets[1] & 0x01) != 0) { RDEBUG2("No LM-Response in MS-CHAP-Response"); return 0; } data = response->vp_octets + 2; data_len = 24; /* * Pull the NT-Domain out of the User-Name, if it exists. */ } else if (strncasecmp(fmt, "NT-Domain", 9) == 0) { char *p, *q; user_name = pairfind(request->packet->vps, PW_USER_NAME); if (!user_name) { RDEBUG2("No User-Name was found in the request."); return 0; } /* * First check to see if this is a host/ style User-Name * (a la Kerberos host principal) */ if (strncmp(user_name->vp_strvalue, "host/", 5) == 0) { /* * If we're getting a User-Name formatted in this way, * it's likely due to PEAP. The Windows Domain will be * the first domain component following the hostname, * or the machine name itself if only a hostname is supplied */ p = strchr(user_name->vp_strvalue, '.'); if (!p) { RDEBUG2("setting NT-Domain to same as machine name"); strlcpy(out, user_name->vp_strvalue + 5, outlen); } else { p++; /* skip the period */ q = strchr(p, '.'); /* * use the same hack as below * only if another period was found */ if (q) *q = '\0'; strlcpy(out, p, outlen); if (q) *q = '.'; } } else { p = strchr(user_name->vp_strvalue, '\\'); if (!p) { RDEBUG2("No NT-Domain was found in the User-Name."); return 0; } /* * Hack. This is simpler than the alternatives. */ *p = '\0'; strlcpy(out, user_name->vp_strvalue, outlen); *p = '\\'; } return strlen(out); /* * Pull the User-Name out of the User-Name... */ } else if (strncasecmp(fmt, "User-Name", 9) == 0) { char *p; user_name = pairfind(request->packet->vps, PW_USER_NAME); if (!user_name) { RDEBUG2("No User-Name was found in the request."); return 0; } /* * First check to see if this is a host/ style User-Name * (a la Kerberos host principal) */ if (strncmp(user_name->vp_strvalue, "host/", 5) == 0) { /* * If we're getting a User-Name formatted in this way, * it's likely due to PEAP. When authenticating this against * a Domain, Windows will expect the User-Name to be in the * format of hostname$, the SAM version of the name, so we * have to convert it to that here. We do so by stripping * off the first 5 characters (host/), and copying everything * from that point to the first period into a string and appending * a $ to the end. */ p = strchr(user_name->vp_strvalue, '.'); /* * use the same hack as above * only if a period was found */ if (p) *p = '\0'; snprintf(out, outlen, "%s$", user_name->vp_strvalue + 5); if (p) *p = '.'; } else { p = strchr(user_name->vp_strvalue, '\\'); if (p) { p++; /* skip the backslash */ } else { p = user_name->vp_strvalue; /* use the whole User-Name */ } strlcpy(out, p, outlen); } return strlen(out); /* * Return the NT-Hash of the passed string */ } else if (strncasecmp(fmt, "NT-Hash ", 8) == 0) { char *p; p = fmt + 8; /* 7 is the length of 'NT-Hash' */ if ((p == '\0') || (outlen <= 32)) return 0; RDEBUG("rlm_mschap: NT-Hash: %s",p); ntpwdhash(buffer,p); fr_bin2hex(buffer, out, 16); out[32] = '\0'; RDEBUG("rlm_mschap: NT-Hash: Result: %s",out); return 32; /* * Return the LM-Hash of the passed string */ } else if (strncasecmp(fmt, "LM-Hash ", 8) == 0) { char *p; p = fmt + 8; /* 7 is the length of 'LM-Hash' */ if ((p == '\0') || (outlen <= 32)) return 0; RDEBUG("rlm_mschap: LM-Hash: %s",p); smbdes_lmpwdhash(p, buffer); fr_bin2hex(buffer, out, 16); out[32] = '\0'; RDEBUG("rlm_mschap: LM-Hash: Result: %s",out); return 32; } else { RDEBUG2("Unknown expansion string \"%s\"", fmt); return 0; } if (outlen == 0) return 0; /* nowhere to go, don't do anything */ /* * Didn't set anything: this is bad. */ if (!data) { RDEBUG2("Failed to do anything intelligent"); return 0; } /* * Check the output length. */ if (outlen < ((data_len * 2) + 1)) { data_len = (outlen - 1) / 2; } /* * */ for (i = 0; i < data_len; i++) { sprintf(out + (2 * i), "%02x", data[i]); } out[data_len * 2] = '\0'; return data_len * 2; }
/** Decrypt a Yubikey OTP AES block * * @param inst Module configuration. * @param passcode string to decrypt. * @return one of the RLM_RCODE_* constants. */ rlm_rcode_t rlm_yubikey_decrypt(rlm_yubikey_t *inst, REQUEST *request, char const *passcode) { uint32_t counter; yubikey_token_st token; DICT_ATTR const *da; char private_id[(YUBIKEY_UID_SIZE * 2) + 1]; VALUE_PAIR *key, *vp; da = dict_attrbyname("Yubikey-Key"); if (!da) { REDEBUG("Dictionary missing entry for 'Yubikey-Key'"); return RLM_MODULE_FAIL; } key = pairfind(request->config_items, da->attr, da->vendor, TAG_ANY); if (!key) { REDEBUG("Yubikey-Key attribute not found in control list, can't decrypt OTP data"); return RLM_MODULE_INVALID; } if (key->length != YUBIKEY_KEY_SIZE) { REDEBUG("Yubikey-Key length incorrect, expected %u got %zu", YUBIKEY_KEY_SIZE, key->length); return RLM_MODULE_INVALID; } yubikey_parse((uint8_t const *) passcode + inst->id_len, key->vp_octets, &token); /* * Apparently this just uses byte offsets... */ if (!yubikey_crc_ok_p((uint8_t *) &token)) { REDEBUG("Decrypting OTP token data failed, rejecting"); return RLM_MODULE_REJECT; } RDEBUG("Token data decrypted successfully"); if (request->log.lvl && request->log.func) { (void) fr_bin2hex((char *) &private_id, (uint8_t*) &token.uid, YUBIKEY_UID_SIZE); RDEBUG2("Private ID : 0x%s", private_id); RDEBUG2("Session counter : %u", yubikey_counter(token.ctr)); RDEBUG2("# used in session : %u", token.use); RDEBUG2("Token timestamp : %u", (token.tstph << 16) | token.tstpl); RDEBUG2("Random data : %u", token.rnd); RDEBUG2("CRC data : 0x%x", token.crc); } /* * Private ID used for validation purposes */ vp = pairmake(request, &request->packet->vps, "Yubikey-Private-ID", NULL, T_OP_SET); if (!vp) { REDEBUG("Failed creating Yubikey-Private-ID"); return RLM_MODULE_FAIL; } pairmemcpy(vp, token.uid, YUBIKEY_UID_SIZE); /* * Token timestamp */ vp = pairmake(request, &request->packet->vps, "Yubikey-Timestamp", NULL, T_OP_SET); if (!vp) { REDEBUG("Failed creating Yubikey-Timestamp"); return RLM_MODULE_FAIL; } vp->vp_integer = (token.tstph << 16) | token.tstpl; vp->length = 4; /* * Token random */ vp = pairmake(request, &request->packet->vps, "Yubikey-Random", NULL, T_OP_SET); if (!vp) { REDEBUG("Failed creating Yubikey-Random"); return RLM_MODULE_FAIL; } vp->vp_integer = token.rnd; vp->length = 4; /* * Combine the two counter fields together so we can do * replay attack checks. */ counter = (yubikey_counter(token.ctr) << 16) | token.use; vp = pairmake(request, &request->packet->vps, "Yubikey-Counter", NULL, T_OP_SET); if (!vp) { REDEBUG("Failed creating Yubikey-Counter"); return RLM_MODULE_FAIL; } vp->vp_integer = counter; vp->length = 4; /* * Now we check for replay attacks */ vp = pairfind(request->config_items, vp->da->attr, vp->da->vendor, TAG_ANY); if (!vp) { RWDEBUG("Yubikey-Counter not found in control list, skipping replay attack checks"); return RLM_MODULE_OK; } if (counter <= vp->vp_integer) { REDEBUG("Replay attack detected! Counter value %u, is lt or eq to last known counter value %u", counter, vp->vp_integer); return RLM_MODULE_REJECT; } return RLM_MODULE_OK; }
/** Escapes the raw string such that it should be safe to use as part of a file path * * This function is designed to produce a string that's still readable but portable * across the majority of file systems. * * For security reasons it cannot remove characters from the name, and must not allow * collisions to occur between different strings. * * With that in mind '-' has been chosen as the escape character, and will be double * escaped '-' -> '--' to avoid collisions. * * Escaping should be reversible if the original string needs to be extracted. * * @note function takes additional arguments so that it may be used as an xlat escape * function but it's fine to call it directly. * * @note OSX/Unix/NTFS/VFAT have a max filename size of 255 bytes. * * @param request Current request (may be NULL). * @param out Output buffer. * @param outlen Size of the output buffer. * @param in string to escape. * @param arg Context arguments (unused, should be NULL). */ size_t rad_filename_escape(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, UNUSED void *arg) { size_t freespace = outlen; while (*in != '\0') { size_t utf8_len; /* * Encode multibyte UTF8 chars */ utf8_len = fr_utf8_char((uint8_t const *) in, -1); if (utf8_len > 1) { if (freespace <= (utf8_len * 3)) break; switch (utf8_len) { case 2: snprintf(out, freespace, "-%x-%x", in[0], in[1]); break; case 3: snprintf(out, freespace, "-%x-%x-%x", in[0], in[1], in[2]); break; case 4: snprintf(out, freespace, "-%x-%x-%x-%x", in[0], in[1], in[2], in[3]); break; } freespace -= (utf8_len * 3); out += (utf8_len * 3); in += utf8_len; continue; } /* * Safe chars */ if (((*in >= 'A') && (*in <= 'Z')) || ((*in >= 'a') && (*in <= 'z')) || ((*in >= '0') && (*in <= '9')) || (*in == '_')) { if (freespace <= 1) break; *out++ = *in++; freespace--; continue; } if (freespace <= 2) break; /* * Double escape '-' (like \\) */ if (*in == '-') { *out++ = '-'; *out++ = '-'; freespace -= 2; in++; continue; } /* * Unsafe chars */ *out++ = '-'; fr_bin2hex(out, (uint8_t const *)in++, 1); out += 2; freespace -= 3; } *out = '\0'; return outlen - freespace; }
/** Unpack data * * Example: %{unpack:&Class 0 integer} * * Expands Class, treating octet at offset 0 (bytes 0-3) as an "integer". */ static ssize_t unpack_xlat(UNUSED void *instance, REQUEST *request, char const *fmt, char *out, size_t outlen) { char *data_name, *data_size, *data_type; char *p; size_t len, input_len; int offset; PW_TYPE type; DICT_ATTR const *da; VALUE_PAIR *vp, *cast; uint8_t const *input; char buffer[256]; uint8_t blob[256]; /* * FIXME: copy only the fields here, as we parse them. */ strlcpy(buffer, fmt, sizeof(buffer)); p = buffer; while (isspace((int) *p)) p++; /* skip leading spaces */ data_name = p; while (*p && !isspace((int) *p)) p++; if (!*p) { error: REDEBUG("Format string should be '<data> <offset> <type>' e.g. '&Class 1 integer'"); nothing: *out = '\0'; return -1; } while (isspace((int) *p)) *(p++) = '\0'; if (!*p) GOTO_ERROR; data_size = p; while (*p && !isspace((int) *p)) p++; if (!*p) GOTO_ERROR; while (isspace((int) *p)) *(p++) = '\0'; if (!*p) GOTO_ERROR; data_type = p; while (*p && !isspace((int) *p)) p++; if (*p) GOTO_ERROR; /* anything after the type is an error */ /* * Attribute reference */ if (*data_name == '&') { if (radius_get_vp(&vp, request, data_name) < 0) goto nothing; if ((vp->da->type != PW_TYPE_OCTETS) && (vp->da->type != PW_TYPE_STRING)) { REDEBUG("unpack requires the input attribute to be 'string' or 'octets'"); goto nothing; } input = vp->vp_octets; input_len = vp->vp_length; } else if ((data_name[0] == '0') && (data_name[1] == 'x')) { /* * Hex data. */ len = strlen(data_name + 2); if ((len & 0x01) != 0) { RDEBUG("Invalid hex string in '%s'", data_name); goto nothing; } input = blob; input_len = fr_hex2bin(blob, sizeof(blob), data_name + 2, len); vp = NULL; } else { GOTO_ERROR; } offset = (int) strtoul(data_size, &p, 10); if (*p) { REDEBUG("unpack requires a decimal number, not '%s'", data_size); goto nothing; } if ((size_t) offset >= input_len) { REDEBUG("Offset is larget then the input."); goto nothing; } /* * Allow for string(4) or octets(4), which says "take 4 * bytes from the thing. */ p = strchr(data_type, '('); if (p) { char *end; unsigned long to_copy; *p = '\0'; to_copy = strtoul(p + 1, &end, 10); if (to_copy > input_len) { REDEBUG("Invalid length at '%s'", p + 1); goto nothing; } if ((end[0] != ')') || (end[1] != '\0')) { REDEBUG("Invalid ending at '%s'", end); goto nothing; } type = fr_str2int(dict_attr_types, data_type, PW_TYPE_INVALID); if (type == PW_TYPE_INVALID) { REDEBUG("Invalid data type '%s'", data_type); goto nothing; } if ((type != PW_TYPE_OCTETS) && (type != PW_TYPE_STRING)) { REDEBUG("Cannot take substring of data type '%s'", data_type); goto nothing; } if (input_len < (offset + to_copy)) { REDEBUG("Insufficient data to unpack '%s' from '%s'", data_type, data_name); goto nothing; } /* * Just copy the string over. */ if (type == PW_TYPE_STRING) { if (outlen <= to_copy) { REDEBUG("Insufficient buffer space to unpack data"); goto nothing; } memcpy(out, input + offset, to_copy); out[to_copy] = '\0'; return to_copy; } /* * We hex encode octets. */ if (outlen <= (to_copy * 2)) { REDEBUG("Insufficient buffer space to unpack data"); goto nothing; } return fr_bin2hex(out, input + offset, to_copy); } type = fr_str2int(dict_attr_types, data_type, PW_TYPE_INVALID); if (type == PW_TYPE_INVALID) { REDEBUG("Invalid data type '%s'", data_type); goto nothing; } /* * Output must be a non-zero limited size. */ if ((dict_attr_sizes[type][0] == 0) || (dict_attr_sizes[type][0] != dict_attr_sizes[type][1])) { REDEBUG("unpack requires fixed-size output type, not '%s'", data_type); goto nothing; } if (input_len < (offset + dict_attr_sizes[type][0])) { REDEBUG("Insufficient data to unpack '%s' from '%s'", data_type, data_name); goto nothing; } da = dict_attrbyvalue(PW_CAST_BASE + type, 0); if (!da) { REDEBUG("Cannot decode type '%s'", data_type); goto nothing; } cast = fr_pair_afrom_da(request, da); if (!cast) goto nothing; memcpy(&(cast->data), input + offset, dict_attr_sizes[type][0]); cast->vp_length = dict_attr_sizes[type][0]; /* * Hacks */ switch (type) { case PW_TYPE_SIGNED: case PW_TYPE_INTEGER: case PW_TYPE_DATE: cast->vp_integer = ntohl(cast->vp_integer); break; case PW_TYPE_SHORT: cast->vp_short = ((input[offset] << 8) | input[offset + 1]); break; case PW_TYPE_INTEGER64: cast->vp_integer64 = ntohll(cast->vp_integer64); break; default: break; } len = vp_prints_value(out, outlen, cast, 0); talloc_free(cast); if (is_truncated(len, outlen)) { REDEBUG("Insufficient buffer space to unpack data"); goto nothing; } return len; }
/* * Generate the keys after the user has been authenticated. */ static int wimax_postauth(void *instance, REQUEST *request) { rlm_wimax_t *inst = instance; VALUE_PAIR *msk, *emsk, *vp; VALUE_PAIR *mn_nai, *ip, *fa_rk; HMAC_CTX hmac; unsigned int rk1_len, rk2_len, rk_len; int rk_lifetime = 3600; /* ? */ uint32_t mip_spi; uint8_t usage_data[24]; uint8_t mip_rk_1[EVP_MAX_MD_SIZE], mip_rk_2[EVP_MAX_MD_SIZE]; uint8_t mip_rk[2 * EVP_MAX_MD_SIZE]; msk = pairfind(request->reply->vps, 1129); emsk = pairfind(request->reply->vps, 1130); if (!msk || !emsk) { RDEBUG("No EAP-MSK or EAP-EMSK. Cannot create WiMAX keys."); return RLM_MODULE_NOOP; } /* * If we delete the MS-MPPE-*-Key attributes, then add in * the WiMAX-MSK so that the client has a key available. */ if (inst->delete_mppe_keys) { pairdelete(&request->reply->vps, ((311 << 16) | 16)); pairdelete(&request->reply->vps, ((311 << 16) | 17)); vp = radius_pairmake(request, &request->reply->vps, "WiMAX-MSK", "0x00", T_OP_EQ); if (vp) { memcpy(vp->vp_octets, msk->vp_octets, msk->length); vp->length = msk->length; } } /* * Initialize usage data. */ memcpy(usage_data, "*****@*****.**", 21); /* with trailing \0 */ usage_data[21] = 0x02; usage_data[22] = 0x00; usage_data[23] = 0x01; /* * MIP-RK-1 = HMAC-SSHA256(EMSK, usage-data | 0x01) */ HMAC_CTX_init(&hmac); HMAC_Init_ex(&hmac, emsk->vp_octets, emsk->length, EVP_sha256(), NULL); HMAC_Update(&hmac, &usage_data[0], sizeof(usage_data)); HMAC_Final(&hmac, &mip_rk_1[0], &rk1_len); /* * MIP-RK-2 = HMAC-SSHA256(EMSK, MIP-RK-1 | usage-data | 0x01) */ HMAC_CTX_init(&hmac); HMAC_Init_ex(&hmac, emsk->vp_octets, emsk->length, EVP_sha256(), NULL); HMAC_Update(&hmac, (const uint8_t *) &mip_rk_1, rk1_len); HMAC_Update(&hmac, &usage_data[0], sizeof(usage_data)); HMAC_Final(&hmac, &mip_rk_2[0], &rk2_len); vp = pairfind(request->reply->vps, PW_SESSION_TIMEOUT); if (vp) rk_lifetime = vp->vp_integer; memcpy(mip_rk, mip_rk_1, rk1_len); memcpy(mip_rk + rk1_len, mip_rk_2, rk2_len); rk_len = rk1_len + rk2_len; /* * MIP-SPI = HMAC-SSHA256(MIP-RK, "SPI CMIP PMIP"); */ HMAC_CTX_init(&hmac); HMAC_Init_ex(&hmac, mip_rk, rk_len, EVP_sha256(), NULL); HMAC_Update(&hmac, (const uint8_t *) "SPI CMIP PMIP", 12); HMAC_Final(&hmac, &mip_rk_1[0], &rk1_len); /* * Take the 4 most significant octets. * If less than 256, add 256. */ mip_spi = ((mip_rk_1[0] << 24) | (mip_rk_1[1] << 16) | (mip_rk_1[2] << 8) | mip_rk_1[3]); if (mip_spi < 256) mip_spi += 256; if (debug_flag) { int len = rk_len; char buffer[512]; if (len > 128) len = 128; /* buffer size */ fr_bin2hex(mip_rk, buffer, len); radlog_request(L_DBG, 0, request, "MIP-RK = 0x%s", buffer); radlog_request(L_DBG, 0, request, "MIP-SPI = %08x", ntohl(mip_spi)); } /* * FIXME: Perform SPI collision prevention */ /* * Calculate mobility keys */ mn_nai = pairfind(request->packet->vps, 1900); if (!mn_nai) mn_nai = pairfind(request->reply->vps, 1900); if (!mn_nai) { RDEBUG("WARNING: WiMAX-MN-NAI was not found in the request or in the reply."); RDEBUG("WARNING: We cannot calculate MN-HA keys."); } /* * WiMAX-IP-Technology */ vp = NULL; if (mn_nai) vp = pairfind(request->reply->vps, WIMAX2ATTR(23)); if (!vp) { RDEBUG("WARNING: WiMAX-IP-Technology not found in reply."); RDEBUG("WARNING: Not calculating MN-HA keys"); } if (vp) switch (vp->vp_integer) { case 2: /* PMIP4 */ /* * Look for WiMAX-hHA-IP-MIP4 */ ip = pairfind(request->reply->vps, WIMAX2ATTR(6)); if (!ip) { RDEBUG("WARNING: WiMAX-hHA-IP-MIP4 not found. Cannot calculate MN-HA-PMIP4 key"); break; } /* * MN-HA-PMIP4 = * H(MIP-RK, "PMIP4 MN HA" | HA-IPv4 | MN-NAI); */ HMAC_CTX_init(&hmac); HMAC_Init_ex(&hmac, mip_rk, rk_len, EVP_sha1(), NULL); HMAC_Update(&hmac, (const uint8_t *) "PMIP4 MN HA", 11); HMAC_Update(&hmac, (const uint8_t *) &ip->vp_ipaddr, 4); HMAC_Update(&hmac, (const uint8_t *) &mn_nai->vp_strvalue, mn_nai->length); HMAC_Final(&hmac, &mip_rk_1[0], &rk1_len); /* * Put MN-HA-PMIP4 into WiMAX-MN-hHA-MIP4-Key */ vp = pairfind(request->reply->vps, WIMAX2ATTR(10)); if (!vp) { vp = radius_paircreate(request, &request->reply->vps, WIMAX2ATTR(10), PW_TYPE_OCTETS); } if (!vp) { RDEBUG("WARNING: Failed creating WiMAX-MN-hHA-MIP4-Key"); break; } memcpy(vp->vp_octets, &mip_rk_1[0], rk1_len); vp->length = rk1_len; /* * Put MN-HA-PMIP4-SPI into WiMAX-MN-hHA-MIP4-SPI */ vp = pairfind(request->reply->vps, WIMAX2ATTR(11)); if (!vp) { vp = radius_paircreate(request, &request->reply->vps, WIMAX2ATTR(11), PW_TYPE_INTEGER); } if (!vp) { RDEBUG("WARNING: Failed creating WiMAX-MN-hHA-MIP4-SPI"); break; } vp->vp_integer = mip_spi + 1; break; case 3: /* CMIP4 */ /* * Look for WiMAX-hHA-IP-MIP4 */ ip = pairfind(request->reply->vps, WIMAX2ATTR(6)); if (!ip) { RDEBUG("WARNING: WiMAX-hHA-IP-MIP4 not found. Cannot calculate MN-HA-CMIP4 key"); break; } /* * MN-HA-CMIP4 = * H(MIP-RK, "CMIP4 MN HA" | HA-IPv4 | MN-NAI); */ HMAC_CTX_init(&hmac); HMAC_Init_ex(&hmac, mip_rk, rk_len, EVP_sha1(), NULL); HMAC_Update(&hmac, (const uint8_t *) "CMIP4 MN HA", 11); HMAC_Update(&hmac, (const uint8_t *) &ip->vp_ipaddr, 4); HMAC_Update(&hmac, (const uint8_t *) &mn_nai->vp_strvalue, mn_nai->length); HMAC_Final(&hmac, &mip_rk_1[0], &rk1_len); /* * Put MN-HA-CMIP4 into WiMAX-MN-hHA-MIP4-Key */ vp = pairfind(request->reply->vps, WIMAX2ATTR(10)); if (!vp) { vp = radius_paircreate(request, &request->reply->vps, WIMAX2ATTR(10), PW_TYPE_OCTETS); } if (!vp) { RDEBUG("WARNING: Failed creating WiMAX-MN-hHA-MIP4-Key"); break; } memcpy(vp->vp_octets, &mip_rk_1[0], rk1_len); vp->length = rk1_len; /* * Put MN-HA-CMIP4-SPI into WiMAX-MN-hHA-MIP4-SPI */ vp = pairfind(request->reply->vps, WIMAX2ATTR(11)); if (!vp) { vp = radius_paircreate(request, &request->reply->vps, WIMAX2ATTR(11), PW_TYPE_INTEGER); } if (!vp) { RDEBUG("WARNING: Failed creating WiMAX-MN-hHA-MIP4-SPI"); break; } vp->vp_integer = mip_spi; break; case 4: /* CMIP6 */ /* * Look for WiMAX-hHA-IP-MIP6 */ ip = pairfind(request->reply->vps, WIMAX2ATTR(7)); if (!ip) { RDEBUG("WARNING: WiMAX-hHA-IP-MIP6 not found. Cannot calculate MN-HA-CMIP6 key"); break; } /* * MN-HA-CMIP6 = * H(MIP-RK, "CMIP6 MN HA" | HA-IPv6 | MN-NAI); */ HMAC_CTX_init(&hmac); HMAC_Init_ex(&hmac, mip_rk, rk_len, EVP_sha1(), NULL); HMAC_Update(&hmac, (const uint8_t *) "CMIP6 MN HA", 11); HMAC_Update(&hmac, (const uint8_t *) &ip->vp_ipv6addr, 16); HMAC_Update(&hmac, (const uint8_t *) &mn_nai->vp_strvalue, mn_nai->length); HMAC_Final(&hmac, &mip_rk_1[0], &rk1_len); /* * Put MN-HA-CMIP6 into WiMAX-MN-hHA-MIP6-Key */ vp = pairfind(request->reply->vps, WIMAX2ATTR(12)); if (!vp) { vp = radius_paircreate(request, &request->reply->vps, WIMAX2ATTR(12), PW_TYPE_OCTETS); } if (!vp) { RDEBUG("WARNING: Failed creating WiMAX-MN-hHA-MIP6-Key"); break; } memcpy(vp->vp_octets, &mip_rk_1[0], rk1_len); vp->length = rk1_len; /* * Put MN-HA-CMIP6-SPI into WiMAX-MN-hHA-MIP6-SPI */ vp = pairfind(request->reply->vps, WIMAX2ATTR(13)); if (!vp) { vp = radius_paircreate(request, &request->reply->vps, WIMAX2ATTR(13), PW_TYPE_INTEGER); } if (!vp) { RDEBUG("WARNING: Failed creating WiMAX-MN-hHA-MIP6-SPI"); break; } vp->vp_integer = mip_spi + 2; break; default: break; /* do nothing */ } /* * Generate FA-RK, if requested. * * FA-RK= H(MIP-RK, "FA-RK") */ fa_rk = pairfind(request->reply->vps, WIMAX2ATTR(14)); if (fa_rk && (fa_rk->length == 0)) { HMAC_CTX_init(&hmac); HMAC_Init_ex(&hmac, mip_rk, rk_len, EVP_sha1(), NULL); HMAC_Update(&hmac, (const uint8_t *) "FA-RK", 5); HMAC_Final(&hmac, &mip_rk_1[0], &rk1_len); memcpy(fa_rk->vp_octets, &mip_rk_1[0], rk1_len); fa_rk->length = rk1_len; } /* * Create FA-RK-SPI, which is really SPI-CMIP4, which is * really MIP-SPI. Clear? Of course. This is WiMAX. */ if (fa_rk) { vp = pairfind(request->reply->vps, WIMAX2ATTR(61)); if (!vp) { vp = radius_paircreate(request, &request->reply->vps, WIMAX2ATTR(61), PW_TYPE_INTEGER); } if (!vp) { RDEBUG("WARNING: Failed creating WiMAX-FA-RK-SPI"); } else { vp->vp_integer = mip_spi; } } /* * Generate MN-FA = H(FA-RK, "MN FA" | FA-IP | MN-NAI) */ ip = pairfind(request->reply->vps, 1901); if (fa_rk && ip && mn_nai) { HMAC_CTX_init(&hmac); HMAC_Init_ex(&hmac, fa_rk->vp_octets, fa_rk->length, EVP_sha1(), NULL); HMAC_Update(&hmac, (const uint8_t *) "MN FA", 5); HMAC_Update(&hmac, (const uint8_t *) &ip->vp_ipaddr, 4); HMAC_Update(&hmac, (const uint8_t *) &mn_nai->vp_strvalue, mn_nai->length); HMAC_Final(&hmac, &mip_rk_1[0], &rk1_len); vp = radius_paircreate(request, &request->reply->vps, 1902, PW_TYPE_OCTETS); if (!vp) { RDEBUG("WARNING: Failed creating WiMAX-MN-FA"); } else { memcpy(vp->vp_octets, &mip_rk_1[0], rk1_len); vp->length = rk1_len; } } /* * Give additional information about requests && responses * * WiMAX-RRQ-MN-HA-SPI */ vp = pairfind(request->packet->vps, WIMAX2ATTR(20)); if (vp) { RDEBUG("Client requested MN-HA key: Should use SPI to look up key from storage."); if (!mn_nai) { RDEBUG("WARNING: MN-NAI was not found!"); } /* * WiMAX-RRQ-HA-IP */ if (!pairfind(request->packet->vps, WIMAX2ATTR(18))) { RDEBUG("WARNING: HA-IP was not found!"); } /* * WiMAX-HA-RK-Key-Requested */ vp = pairfind(request->packet->vps, WIMAX2ATTR(58)); if (vp && (vp->vp_integer == 1)) { RDEBUG("Client requested HA-RK: Should use IP to look it up from storage."); } } /* * Wipe the context of all sensitive information. */ HMAC_CTX_cleanup(&hmac); return RLM_MODULE_UPDATED; }
/* * Authenticate the user with the given password. */ static rlm_rcode_t mod_authenticate(void *instance, REQUEST *request) { rlm_yubikey_t *inst = instance; char *passcode; size_t i, len; uint32_t counter; const DICT_ATTR *da; VALUE_PAIR *key, *vp; yubikey_token_st token; char private_id[(YUBIKEY_UID_SIZE * 2) + 1]; /* * Can't do yubikey auth if there's no password. */ if (!request->password || (request->password->da->attr != PW_USER_PASSWORD)) { RDEBUGE("No Clear-Text password in the request. Can't do Yubikey authentication."); return RLM_MODULE_FAIL; } passcode = request->password->vp_strvalue; len = request->password->length; /* * Verify the passcode is the correct length (in its raw * modhex encoded form). * * <public_id (6-16 bytes)> + <aes-block (32 bytes)> */ if (len != (inst->id_len + 32)) { RDEBUGE("User-Password value is not the correct length, expected %u, got %zu", inst->id_len + 32, len); return RLM_MODULE_FAIL; } for (i = inst->id_len; i < len; i++) { if (!is_modhex(*passcode)) { RDEBUG2("User-Password (aes-block) value contains non modhex chars"); return RLM_MODULE_FAIL; } } da = dict_attrbyname("Yubikey-Key"); key = pairfind(request->config_items, da->attr, da->vendor, TAG_ANY); if (!key) { RDEBUGE("Yubikey-Key attribute not found in control list, can't decrypt OTP data"); return RLM_MODULE_FAIL; } if (key->length != YUBIKEY_KEY_SIZE) { RDEBUGE("Yubikey-Key length incorrect, expected %u got %zu", YUBIKEY_KEY_SIZE, key->length); return RLM_MODULE_FAIL; } yubikey_parse(request->password->vp_octets + inst->id_len, key->vp_octets, &token); /* * Apparently this just uses byte offsets... */ if (!yubikey_crc_ok_p((uint8_t *) &token)) { RDEBUGE("Decrypting OTP token data failed, rejecting"); return RLM_MODULE_REJECT; } RDEBUG("Token data decrypted successfully"); if (request->options && request->radlog) { (void) fr_bin2hex((uint8_t*) &token.uid, (char *) &private_id, YUBIKEY_UID_SIZE); RDEBUG2("Private ID : 0x%s", private_id); RDEBUG2("Session counter : %u", yubikey_counter(token.ctr)); RDEBUG2("# used in session : %u", token.use); RDEBUG2("Token timetamp : %u", (token.tstph << 16) | token.tstpl); RDEBUG2("Random data : %u", token.rnd); RDEBUG2("CRC data : 0x%x", token.crc); } /* * Private ID used for validation purposes */ vp = pairmake(request, &request->packet->vps, "Yubikey-Private-ID", NULL, T_OP_SET); memcpy(vp->vp_octets, token.uid, YUBIKEY_UID_SIZE); vp->length = YUBIKEY_UID_SIZE; /* * Token timestamp */ vp = pairmake(request, &request->packet->vps, "Yubikey-Timestamp", NULL, T_OP_SET); vp->vp_integer = (token.tstph << 16) | token.tstpl; vp->length = 4; /* * Token random */ vp = pairmake(request, &request->packet->vps, "Yubikey-Random", NULL, T_OP_SET); vp->vp_integer = token.rnd; vp->length = 4; /* * Combine the two counter fields together so we can do * replay attack checks. */ counter = (yubikey_counter(token.ctr) << 16) | token.use; vp = pairmake(request, &request->packet->vps, "Yubikey-Counter", NULL, T_OP_SET); vp->vp_integer = counter; vp->length = 4; /* * Now we check for replay attacks */ vp = pairfind(request->config_items, vp->da->attr, vp->da->vendor, TAG_ANY); if (!vp) { RDEBUGW("Yubikey-Counter not found in control list, skipping replay attack checks"); return RLM_MODULE_OK; } if (counter <= vp->vp_integer) { RDEBUGE("Replay attack detected! Counter value %u, is lt or eq to last known counter value %u", counter, vp->vp_integer); return RLM_MODULE_REJECT; } return RLM_MODULE_OK; }