| /* |
| * chap.c - support for (mutual) CHAP authentication. |
| * |
| * Copyright (C) 2004 Xiranet Communications GmbH <arne.redlich@xiranet.com> |
| * Copyright (C) 2002 - 2003 Ardis Technologies <roman@ardistech.com>, |
| * Copyright (C) 2007 - 2018 Vladislav Bolkhovitin |
| * Copyright (C) 2007 - 2018 Western Digital Corporation |
| * |
| * and code taken from UNH iSCSI software: |
| * Copyright (C) 2001-2003 InterOperability Lab (IOL) |
| * University of New Hampshire (UNH) |
| * Durham, NH 03824 |
| * |
| * This program 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, version 2 |
| * of the License. |
| * |
| * 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. |
| * |
| * Heavily based on code from UNH iSCSI iscsid.c |
| */ |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <fcntl.h> |
| #include <unistd.h> |
| #include "sha1.h" |
| #include "md5.h" |
| |
| #include "iscsid.h" |
| |
| #define HEX_FORMAT 0x01 |
| #define BASE64_FORMAT 0x02 |
| |
| #define CHAP_DIGEST_ALG_MD5 5 |
| #define CHAP_DIGEST_ALG_SHA1 6 |
| |
| #define CHAP_MD5_DIGEST_LEN 16 |
| #define CHAP_SHA1_DIGEST_LEN 20 |
| |
| #define CHAP_INITIATOR_ERROR -1 |
| #define CHAP_AUTH_ERROR -2 |
| #define CHAP_TARGET_ERROR -3 |
| |
| #define CHAP_AUTH_STATE_START AUTH_STATE_START |
| #define CHAP_AUTH_STATE_CHALLENGE 1 |
| #define CHAP_AUTH_STATE_RESPONSE 2 |
| |
| #define CHAP_INITIATOR_AUTH 0 |
| #define CHAP_TARGET_AUTH 1 |
| |
| #define CHAP_CHALLENGE_MAX 50 |
| |
| static inline int decode_hex_digit(char c) |
| { |
| switch (c) { |
| case '0' ... '9': |
| return c - '0'; |
| case 'a' ... 'f': |
| return c - 'a' + 10; |
| case 'A' ... 'F': |
| return c - 'A' + 10; |
| } |
| return 0; |
| } |
| |
| static void decode_hex_string(char *hex_string, u8 *intnum, int intlen) |
| { |
| char *ptr; |
| int j; |
| |
| j = strlen(hex_string); |
| ptr = hex_string + j; |
| j = --intlen; |
| do { |
| intnum[j] = decode_hex_digit(*--ptr); |
| intnum[j] |= decode_hex_digit(*--ptr) << 4; |
| j--; |
| } while (ptr > hex_string); |
| |
| while (j >= 0) |
| intnum[j--] = 0; |
| } |
| |
| /* Base64 decoding, taken from UNH-iSCSI "Base64codeToNumber()" */ |
| static u8 decode_base64_digit(char base64) |
| { |
| switch (base64) { |
| case '=': |
| return 64; |
| case '/': |
| return 63; |
| case '+': |
| return 62; |
| default: |
| if ((base64 >= 'A') && (base64 <= 'Z')) |
| return base64 - 'A'; |
| else if ((base64 >= 'a') && (base64 <= 'z')) |
| return 26 + (base64 - 'a'); |
| else if ((base64 >= '0') && (base64 <= '9')) |
| return 52 + (base64 - '0'); |
| else |
| //XXX This return value should be unsigned; and anyway |
| //XXX in case of a bad character in the string, our |
| //XXX caller (sometimes) checks for 65, not 255 or -1 |
| return -1; |
| } |
| } |
| |
| /* Base64 decoding, taken from UNH-iSCSI "Base64StringToInteger()" */ |
| static void decode_base64_string(char *string, u8 *intnum, int int_len) |
| { |
| int len; |
| int count; |
| int intptr; |
| u8 num[4]; |
| int octets; |
| |
| if ((string == NULL) || (intnum == NULL)) |
| return; |
| len = strlen(string); |
| if (len == 0) |
| return; |
| if ((len % 4) != 0) |
| return; |
| count = 0; |
| intptr = 0; |
| while (count < len - 4) { |
| num[0] = decode_base64_digit(string[count]); |
| num[1] = decode_base64_digit(string[count + 1]); |
| num[2] = decode_base64_digit(string[count + 2]); |
| num[3] = decode_base64_digit(string[count + 3]); |
| if ((num[0] == 65) || (num[1] == 65) || (num[2] == 65) || (num[3] == 65)) |
| return; |
| count += 4; |
| octets = |
| (num[0] << 18) | (num[1] << 12) | (num[2] << 6) | num[3]; |
| intnum[intptr] = (octets & 0xFF0000) >> 16; |
| intnum[intptr + 1] = (octets & 0x00FF00) >> 8; |
| intnum[intptr + 2] = octets & 0x0000FF; |
| intptr += 3; |
| } |
| num[0] = decode_base64_digit(string[count]); |
| num[1] = decode_base64_digit(string[count + 1]); |
| num[2] = decode_base64_digit(string[count + 2]); |
| num[3] = decode_base64_digit(string[count + 3]); |
| //XXX Check for the special "bad character in string" value here like above? |
| //XXX Also check the string for missing/incorrect padding? |
| if ((num[0] == 64) || (num[1] == 64)) |
| return; |
| if (num[2] == 64) { |
| if (num[3] != 64) |
| return; |
| intnum[intptr] = (num[0] << 2) | (num[1] >> 4); |
| } else if (num[3] == 64) { |
| intnum[intptr] = (num[0] << 2) | (num[1] >> 4); |
| intnum[intptr + 1] = (num[1] << 4) | (num[2] >> 2); |
| } else { |
| octets = |
| (num[0] << 18) | (num[1] << 12) | (num[2] << 6) | num[3]; |
| intnum[intptr] = (octets & 0xFF0000) >> 16; |
| intnum[intptr + 1] = (octets & 0x00FF00) >> 8; |
| intnum[intptr + 2] = octets & 0x0000FF; |
| } |
| } |
| |
| static inline void encode_hex_string(u8 *intnum, long length, char *string) |
| { |
| int i; |
| char *strptr; |
| |
| strptr = string; |
| for (i = 0; i < length; i++, strptr += 2) |
| sprintf(strptr, "%.2hhx", intnum[i]); |
| } |
| |
| /* Base64 encoding, taken from UNH iSCSI "IntegerToBase64String()" */ |
| static void encode_base64_string(u8 *intnum, long length, char *string) |
| { |
| int count, octets, strptr, delta; |
| static const char base64code[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', |
| 'H', 'I', 'J', 'K', 'L', 'M', 'N', |
| 'O', 'P', 'Q', 'R', 'S', 'T', 'U', |
| 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', |
| 'c', 'd', 'e', 'f', 'g', 'h', 'i', |
| 'j', 'k', 'l', 'm', 'n', 'o', 'p', |
| 'q', 'r', 's', 't', 'u', 'v', 'w', |
| 'x', 'y', 'z', '0', '1', '2', '3', |
| '4', '5', '6', '7', '8', '9', '+', |
| '/', '=' }; |
| |
| if ((!intnum) || (!string) || (!length)) |
| return; |
| |
| count = 0; |
| octets = 0; |
| strptr = 0; |
| |
| while ((delta = (length - count)) > 2) { |
| octets = (intnum[count] << 16) | (intnum[count + 1] << 8) | intnum[count + 2]; |
| string[strptr] = base64code[(octets & 0xfc0000) >> 18]; |
| string[strptr + 1] = base64code[(octets & 0x03f000) >> 12]; |
| string[strptr + 2] = base64code[(octets & 0x000fc0) >> 6]; |
| string[strptr + 3] = base64code[octets & 0x00003f]; |
| count += 3; |
| strptr += 4; |
| } |
| if (delta == 1) { |
| string[strptr] = base64code[(intnum[count] & 0xfc) >> 2]; |
| string[strptr + 1] = base64code[(intnum[count] & 0x03) << 4]; |
| string[strptr + 2] = base64code[64]; |
| string[strptr + 3] = base64code[64]; |
| strptr += 4; |
| } else if (delta == 2) { |
| string[strptr] = base64code[(intnum[count] & 0xfc) >> 2]; |
| string[strptr + 1] = base64code[((intnum[count] & 0x03) << 4) | ((intnum[count + 1] & 0xf0) >> 4)]; |
| string[strptr + 2] = base64code[(intnum[count + 1] & 0x0f) << 2]; |
| string[strptr + 3] = base64code[64]; |
| strptr += 4; |
| } |
| string[strptr] = '\0'; |
| } |
| |
| static inline int chap_check_encoding_format(char *encoded) |
| { |
| int encoding_fmt; |
| |
| if (!encoded) |
| return -1; |
| if ((strlen(encoded) < 3) || (encoded[0] != '0')) |
| return -1; |
| |
| if (encoded[1] == 'x' || encoded[1] == 'X') |
| encoding_fmt = HEX_FORMAT; |
| else if (encoded[1] == 'b' || encoded[1] == 'B') |
| encoding_fmt = BASE64_FORMAT; |
| else |
| return -1; |
| |
| return encoding_fmt; |
| } |
| |
| static int chap_alloc_decode_buffer(char *encoded, u8 **decode_buf, int encoding_fmt) |
| { |
| int i; |
| int decode_len = 0; |
| |
| i = strlen(encoded); |
| i -= 2; |
| |
| if (encoding_fmt == HEX_FORMAT) |
| decode_len = (i - 1) / 2 + 1; |
| else if (encoding_fmt == BASE64_FORMAT) { |
| if (i % 4) |
| return CHAP_INITIATOR_ERROR; |
| |
| decode_len = i / 4 * 3; |
| if (encoded[i + 1] == '=') |
| decode_len--; |
| if (encoded[i] == '=') |
| decode_len--; |
| } |
| |
| if (!decode_len) |
| return CHAP_INITIATOR_ERROR; |
| |
| *decode_buf = malloc(decode_len); |
| if (!*decode_buf) |
| return CHAP_TARGET_ERROR; |
| |
| return decode_len; |
| } |
| |
| static int chap_decode_string(char *encoded, u8 *decode_buf, int buf_len, int encoding_fmt) |
| { |
| if (encoding_fmt == HEX_FORMAT) { |
| if ((strlen(encoded) - 2) > (2 * buf_len)) { |
| log_error("%s(%d) BUG? " |
| " buf[%d] !sufficient to decode string[%d]", |
| __func__, __LINE__, buf_len, (int) strlen(encoded)); |
| return CHAP_TARGET_ERROR; |
| } |
| decode_hex_string(encoded + 2, decode_buf, buf_len); |
| |
| } else if (encoding_fmt == BASE64_FORMAT) { |
| if ((strlen(encoded) - 2) > ((buf_len - 1) / 3 + 1) * 4) { |
| log_error("%s(%d) BUG? " |
| " buf[%d] !sufficient to decode string[%d]", |
| __func__, __LINE__, buf_len, (int) strlen(encoded)); |
| return CHAP_TARGET_ERROR; |
| } |
| decode_base64_string(encoded + 2, decode_buf, buf_len); |
| |
| } else |
| return CHAP_INITIATOR_ERROR; |
| |
| return 0; |
| } |
| |
| static inline void chap_encode_string(u8 *intnum, int buf_len, char *encode_buf, int encoding_fmt) |
| { |
| encode_buf[0] = '0'; |
| if (encoding_fmt == HEX_FORMAT) { |
| encode_buf[1] = 'x'; |
| encode_hex_string(intnum, buf_len, encode_buf + 2); |
| } else if (encoding_fmt == BASE64_FORMAT) { |
| encode_buf[1] = 'b'; |
| encode_base64_string(intnum, buf_len, encode_buf + 2); |
| } |
| } |
| |
| static inline void chap_calc_digest_md5(char chap_id, const char *secret, int secret_len, |
| const u8 *challenge, int challenge_len, u8 *digest) |
| { |
| struct md5_ctx ctx; |
| |
| md5_init(&ctx); |
| md5_update(&ctx, &chap_id, 1); |
| md5_update(&ctx, secret, secret_len); |
| md5_update(&ctx, challenge, challenge_len); |
| md5_final(&ctx, digest); |
| } |
| |
| static inline void chap_calc_digest_sha1(char chap_id, const char *secret, int secret_len, |
| const u8 *challenge, int challenge_len, u8 *digest) |
| { |
| struct sha1_ctx ctx; |
| |
| sha1_init(&ctx); |
| sha1_update(&ctx, &chap_id, 1); |
| sha1_update(&ctx, secret, secret_len); |
| sha1_update(&ctx, challenge, challenge_len); |
| sha1_final(&ctx, digest); |
| } |
| |
| /* |
| * To generate challenge for CHAP, use stronger random number generator as |
| * opposed to simple rand(). |
| */ |
| static int chap_rand(void) |
| { |
| int fd; |
| int r; |
| |
| fd = open("/dev/urandom", O_RDONLY); |
| assert(fd != -1); |
| if (read(fd, &r, sizeof(r)) < sizeof(r)) { |
| } |
| close(fd); |
| return r; |
| } |
| |
| static int chap_initiator_auth_create_challenge(struct connection *conn) |
| { |
| char *value, *p; |
| char text[CHAP_CHALLENGE_MAX * 2 + 8]; |
| static int chap_id; |
| int i; |
| |
| value = text_key_find(conn, "CHAP_A"); |
| if (!value) |
| return CHAP_INITIATOR_ERROR; |
| while ((p = strsep(&value, ","))) { |
| if (!strcmp(p, "5")) { |
| conn->auth.chap.digest_alg = CHAP_DIGEST_ALG_MD5; |
| conn->auth_state = CHAP_AUTH_STATE_CHALLENGE; |
| break; |
| } else if (!strcmp(p, "6")) { |
| conn->auth.chap.digest_alg = CHAP_DIGEST_ALG_SHA1; |
| conn->auth_state = CHAP_AUTH_STATE_CHALLENGE; |
| break; |
| } |
| } |
| if (!p) |
| return CHAP_INITIATOR_ERROR; |
| |
| text_key_add(conn, "CHAP_A", p); |
| conn->auth.chap.id = ++chap_id; |
| sprintf(text, "%u", (unsigned char)conn->auth.chap.id); |
| text_key_add(conn, "CHAP_I", text); |
| |
| /* |
| * ToDo: does a random challenge length provide any benefits security- |
| * wise, or should we rather always use the max. allowed length of |
| * 1024 for the (unencoded) challenge? |
| */ |
| conn->auth.chap.challenge_size = (chap_rand() % (CHAP_CHALLENGE_MAX / 2)) + CHAP_CHALLENGE_MAX / 2; |
| |
| conn->auth.chap.challenge = malloc(conn->auth.chap.challenge_size); |
| if (!conn->auth.chap.challenge) |
| return CHAP_TARGET_ERROR; |
| |
| p = text; |
| strcpy(p, "0x"); |
| p += 2; |
| for (i = 0; i < conn->auth.chap.challenge_size; i++) { |
| conn->auth.chap.challenge[i] = chap_rand(); |
| sprintf(p, "%.2hhx", conn->auth.chap.challenge[i]); |
| p += 2; |
| } |
| text_key_add(conn, "CHAP_C", text); |
| |
| return 0; |
| } |
| |
| static int chap_initiator_auth_check_response(struct connection *conn) |
| { |
| char *value; |
| u8 *his_digest = NULL, *our_digest = NULL; |
| int digest_len = 0, retval = 0, encoding_format; |
| char pass[ISCSI_NAME_LEN]; |
| |
| if (accounts_empty(conn->tid, ISCSI_USER_DIR_INCOMING)) { |
| log_warning("CHAP initiator auth.: " |
| "No CHAP credentials configured"); |
| retval = CHAP_TARGET_ERROR; |
| goto out; |
| } |
| |
| if (!(value = text_key_find(conn, "CHAP_N"))) { |
| retval = CHAP_INITIATOR_ERROR; |
| goto out; |
| } |
| |
| conn->user = strdup(value); |
| if (conn->user == NULL) { |
| log_error("Unable to duplicate initiator's USER %s", value); |
| } |
| |
| memset(pass, 0, sizeof(pass)); |
| if (config_account_query(conn->tid, ISCSI_USER_DIR_INCOMING, value, pass) < 0) { |
| log_warning("CHAP initiator auth.: " |
| "No valid user/pass combination for initiator %s " |
| "found", conn->initiator); |
| retval = CHAP_AUTH_ERROR; |
| goto out; |
| } |
| |
| if (!(value = text_key_find(conn, "CHAP_R"))) { |
| retval = CHAP_INITIATOR_ERROR; |
| goto out; |
| } |
| |
| if ((encoding_format = chap_check_encoding_format(value)) < 0) { |
| retval = CHAP_INITIATOR_ERROR; |
| goto out; |
| } |
| |
| switch (conn->auth.chap.digest_alg) { |
| case CHAP_DIGEST_ALG_MD5: |
| digest_len = CHAP_MD5_DIGEST_LEN; |
| break; |
| case CHAP_DIGEST_ALG_SHA1: |
| digest_len = CHAP_SHA1_DIGEST_LEN; |
| break; |
| default: |
| retval = CHAP_TARGET_ERROR; |
| goto out; |
| } |
| |
| if (!(his_digest = malloc(digest_len))) { |
| retval = CHAP_TARGET_ERROR; |
| goto out; |
| } |
| if (!(our_digest = malloc(digest_len))) { |
| retval = CHAP_TARGET_ERROR; |
| goto out; |
| } |
| |
| if (chap_decode_string(value, his_digest, digest_len, encoding_format) < 0) { |
| retval = CHAP_INITIATOR_ERROR; |
| goto out; |
| } |
| |
| switch (conn->auth.chap.digest_alg) { |
| case CHAP_DIGEST_ALG_MD5: |
| chap_calc_digest_md5(conn->auth.chap.id, pass, strlen(pass), |
| conn->auth.chap.challenge, |
| conn->auth.chap.challenge_size, |
| our_digest); |
| break; |
| case CHAP_DIGEST_ALG_SHA1: |
| chap_calc_digest_sha1(conn->auth.chap.id, pass, strlen(pass), |
| conn->auth.chap.challenge, |
| conn->auth.chap.challenge_size, |
| our_digest); |
| break; |
| default: |
| retval = CHAP_TARGET_ERROR; |
| goto out; |
| } |
| |
| if (memcmp(our_digest, his_digest, digest_len)) { |
| log_warning("CHAP initiator auth.: " |
| "authentication of %s failed (wrong secret!?)", |
| conn->initiator); |
| retval = CHAP_AUTH_ERROR; |
| goto out; |
| } |
| |
| conn->state = CHAP_AUTH_STATE_RESPONSE; |
| out: |
| if (his_digest) |
| free(his_digest); |
| if (our_digest) |
| free(our_digest); |
| return retval; |
| } |
| |
| static int chap_target_auth_create_response(struct connection *conn) |
| { |
| char chap_id, *value, *response = NULL; |
| u8 *challenge = NULL, *digest = NULL; |
| int encoding_format, response_len; |
| int challenge_len = 0, digest_len = 0, retval = 0; |
| struct iscsi_attr *user; |
| |
| if (!(value = text_key_find(conn, "CHAP_I"))) { |
| /* Initiator doesn't want target auth!? */ |
| conn->state = STATE_SECURITY_DONE; |
| retval = 0; |
| goto out; |
| } |
| chap_id = strtol(value, &value, 10); |
| |
| user = account_get_first(conn->tid, ISCSI_USER_DIR_OUTGOING); |
| if (user == NULL) { |
| log_warning("CHAP target auth.: " |
| "no outgoing credentials configured%s", |
| conn->tid ? "." : " for discovery."); |
| retval = CHAP_AUTH_ERROR; |
| goto out; |
| } |
| |
| if (!(value = text_key_find(conn, "CHAP_C"))) { |
| log_warning("CHAP target auth.: " |
| "got no challenge from initiator %s", |
| conn->initiator); |
| retval = CHAP_INITIATOR_ERROR; |
| goto out; |
| } |
| |
| if ((encoding_format = chap_check_encoding_format(value)) < 0) { |
| retval = CHAP_INITIATOR_ERROR; |
| goto out; |
| } |
| |
| retval = chap_alloc_decode_buffer(value, &challenge, encoding_format); |
| if (retval <= 0) |
| goto out; |
| else if (retval > 1024) { |
| log_warning("CHAP target auth.: " |
| "initiator %s sent challenge of invalid length %d", |
| conn->initiator, challenge_len); |
| retval = CHAP_INITIATOR_ERROR; |
| goto out; |
| } |
| |
| challenge_len = retval; |
| retval = 0; |
| |
| switch (conn->auth.chap.digest_alg) { |
| case CHAP_DIGEST_ALG_MD5: |
| digest_len = CHAP_MD5_DIGEST_LEN; |
| break; |
| case CHAP_DIGEST_ALG_SHA1: |
| digest_len = CHAP_SHA1_DIGEST_LEN; |
| break; |
| default: |
| retval = CHAP_TARGET_ERROR; |
| goto out; |
| } |
| |
| if (encoding_format == HEX_FORMAT) |
| response_len = 2 * digest_len; |
| else |
| response_len = ((digest_len - 1) / 3 + 1) * 4; |
| /* "0x" / "0b" and "\0": */ |
| response_len += 3; |
| |
| if (!(digest = malloc(digest_len))) { |
| retval = CHAP_TARGET_ERROR; |
| goto out; |
| } |
| if (!(response = malloc(response_len))) { |
| retval = CHAP_TARGET_ERROR; |
| goto out; |
| } |
| |
| if (chap_decode_string(value, challenge, challenge_len, encoding_format) < 0) { |
| retval = CHAP_INITIATOR_ERROR; |
| goto out; |
| } |
| |
| /* RFC 3720, 8.2.1: CHAP challenges MUST NOT be reused */ |
| if (challenge_len == conn->auth.chap.challenge_size) { |
| if (!memcmp(challenge, conn->auth.chap.challenge, |
| challenge_len)) { |
| /* ToDo: RFC 3720 demands to close TCP conn */ |
| log_warning("CHAP target auth.: " |
| "initiator %s reflected our challenge", |
| conn->initiator); |
| retval = CHAP_INITIATOR_ERROR; |
| goto out; |
| } |
| } |
| |
| switch (conn->auth.chap.digest_alg) { |
| case CHAP_DIGEST_ALG_MD5: |
| chap_calc_digest_md5(chap_id, ISCSI_USER_PASS(user), |
| strlen(ISCSI_USER_PASS(user)), challenge, challenge_len, digest); |
| break; |
| case CHAP_DIGEST_ALG_SHA1: |
| chap_calc_digest_sha1(chap_id, ISCSI_USER_PASS(user), |
| strlen(ISCSI_USER_PASS(user)), challenge, challenge_len, digest); |
| break; |
| default: |
| retval = CHAP_TARGET_ERROR; |
| goto out; |
| } |
| |
| memset(response, 0x0, response_len); |
| chap_encode_string(digest, digest_len, response, encoding_format); |
| text_key_add(conn, "CHAP_N", ISCSI_USER_NAME(user)); |
| text_key_add(conn, "CHAP_R", response); |
| |
| conn->state = STATE_SECURITY_DONE; |
| out: |
| if (challenge) |
| free(challenge); |
| if (digest) |
| free(digest); |
| if (response) |
| free(response); |
| return retval; |
| } |
| |
| int cmnd_exec_auth_chap(struct connection *conn) |
| { |
| int res; |
| |
| switch (conn->auth_state) { |
| case CHAP_AUTH_STATE_START: |
| res = chap_initiator_auth_create_challenge(conn); |
| break; |
| case CHAP_AUTH_STATE_CHALLENGE: |
| res = chap_initiator_auth_check_response(conn); |
| if (res < 0) |
| break; |
| /* fall through */ |
| case CHAP_AUTH_STATE_RESPONSE: |
| res = chap_target_auth_create_response(conn); |
| break; |
| default: |
| log_error("%s(%d): BUG. unknown conn->auth_state %d", |
| __func__, __LINE__, conn->auth_state); |
| res = CHAP_TARGET_ERROR; |
| } |
| |
| return res; |
| } |