/*
 * Copyright (C) 2024 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * 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; either version 2 of the
 * License, or any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 * You can also choose to distribute this program under the terms of
 * the Unmodified Binary Distribution Licence (as given in the file
 * COPYING.UBDL), provided that you have satisfied its requirements.
 */

FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
FILE_SECBOOT ( PERMITTED );

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <byteswap.h>
#include <ipxe/mschapv2.h>
#include <ipxe/eap.h>

/** @file
 *
 * EAP MS-CHAPv2 authentication method
 *
 * EAP-MSCHAPv2 was described in a draft RFC first published in 2002
 * (draft-kamath-pppext-eap-mschapv2-02.txt).  The draft eventually
 * expired in 2007 without becoming an official RFC, quite possibly
 * because the protocol design was too ugly to be called an IETF
 * standard.  It is, however, fairly widely used.
 */

/** An EAP MS-CHAPv2 request message */
struct eap_mschapv2_request {
	/** EAP-MSCHAPv2 header */
	struct eap_mschapv2 hdr;
	/** MS-CHAPv2 challenge length (fixed value) */
	uint8_t len;
	/** MS-CHAPv2 challenge */
	struct mschapv2_challenge msg;
} __attribute__ (( packed ));

/** An EAP MS-CHAPv2 response message */
struct eap_mschapv2_response {
	/** EAP-MSCHAPv2 header */
	struct eap_mschapv2 hdr;
	/** MS-CHAPv2 response length (fixed value) */
	uint8_t len;
	/** MS-CHAPv2 response */
	struct mschapv2_response msg;
	/** User name */
	char name[0];
} __attribute__ (( packed ));

/** An EAP MS-CHAPv2 success request message */
struct eap_mschapv2_success_request {
	/** EAP-MSCHAPv2 header */
	struct eap_mschapv2 hdr;
	/** Message */
	char message[0];
} __attribute__ (( packed ));

/** An EAP MS-CHAPv2 success response message */
struct eap_mschapv2_success_response {
	/** Opcode */
	uint8_t code;
} __attribute__ (( packed ));

/**
 * Handle EAP MS-CHAPv2 request
 *
 * @v supplicant	EAP supplicant
 * @v hdr		EAP-MSCHAPv2 header
 * @v len		Message length
 * @ret rc		Return status code
 */
static int eap_rx_mschapv2_request ( struct eap_supplicant *supplicant,
				     const struct eap_mschapv2 *hdr,
				     size_t len ) {
	struct net_device *netdev = supplicant->netdev;
	struct settings *settings = netdev_settings ( netdev );
	const struct eap_mschapv2_request *msreq =
		container_of ( hdr, struct eap_mschapv2_request, hdr );
	struct eap_mschapv2_response *msrsp;
	struct mschapv2_challenge peer;
	char *username;
	char *password;
	int username_len;
	int password_len;
	size_t msrsp_len;
	unsigned int i;
	int rc;

	/* Sanity check */
	if ( len < sizeof ( *msreq ) ) {
		DBGC ( netdev, "EAP %s underlength MS-CHAPv2 request\n",
		       netdev->name );
		DBGC_HDA ( netdev, 0, hdr, len );
		rc = -EINVAL;
		goto err_sanity;
	}

	/* Fetch username and password */
	username_len = fetch_string_setting_copy ( settings, &username_setting,
						   &username );
	if ( username_len < 0 ) {
		rc = username_len;
		DBGC ( netdev, "EAP %s has no username: %s\n",
		       netdev->name, strerror ( rc ) );
		goto err_username;
	}
	password_len = fetch_string_setting_copy ( settings, &password_setting,
						   &password );
	if ( password_len < 0 ) {
		rc = password_len;
		DBGC ( netdev, "EAP %s has no password: %s\n",
		       netdev->name, strerror ( rc ) );
		goto err_password;
	}

	/* Construct a peer challenge.  We do not perform mutual
	 * authentication, so this does not need to be strong.
	 */
	for ( i = 0 ; i < ( sizeof ( peer.byte ) /
			    sizeof ( peer.byte[0] ) ) ; i++ ) {
		peer.byte[i] = random();
	}

	/* Allocate response */
	msrsp_len = ( sizeof ( *msrsp ) + username_len );
	msrsp = malloc ( msrsp_len );
	if ( ! msrsp ) {
		rc = -ENOMEM;
		goto err_alloc;
	}

	/* Construct response */
	msrsp->hdr.code = EAP_CODE_RESPONSE;
	msrsp->hdr.id = msreq->hdr.id;
	msrsp->hdr.len = htons ( msrsp_len );
	msrsp->len = sizeof ( msrsp->msg );
	mschapv2_response ( username, password, &msreq->msg, &peer,
			    &msrsp->msg );
	memcpy ( msrsp->name, username, username_len );

	/* Send response */
	if ( ( rc = eap_tx_response ( supplicant, msrsp, msrsp_len ) ) != 0 )
		goto err_tx;

 err_tx:
	free ( msrsp );
 err_alloc:
	free ( password );
 err_password:
	free ( username );
 err_username:
 err_sanity:
	return rc;
}

/**
 * Handle EAP MS-CHAPv2 success request
 *
 * @v supplicant	EAP supplicant
 * @v hdr		EAP-MSCHAPv2 header
 * @v len		Message length
 * @ret rc		Return status code
 */
static int eap_rx_mschapv2_success ( struct eap_supplicant *supplicant,
				     const struct eap_mschapv2 *hdr,
				     size_t len ) {
	const struct eap_mschapv2_success_request *msreq =
		container_of ( hdr, struct eap_mschapv2_success_request, hdr );
	static const struct eap_mschapv2_success_response msrsp = {
		.code = EAP_CODE_SUCCESS,
	};

	/* Sanity check */
	assert ( len >= sizeof ( *msreq ) );

	/* The success request contains the MS-CHAPv2 authenticator
	 * response, which could potentially be used to verify that
	 * the EAP authenticator also knew the password (or, at least,
	 * the MD4 hash of the password).
	 *
	 * Our model for EAP does not encompass mutual authentication:
	 * we will starting sending plaintext packets (e.g. DHCP
	 * requests) over the link even before EAP completes, and our
	 * only use for an EAP success is to mark the link as
	 * unblocked.
	 *
	 * We therefore ignore the content of the success request and
	 * just send back a success response, so that the EAP
	 * authenticator will complete the process and send through
	 * the real EAP success packet (which will, in turn, cause us
	 * to unblock the link).
	 */
	return eap_tx_response ( supplicant, &msrsp, sizeof ( msrsp ) );
}

/**
 * Handle EAP MS-CHAPv2
 *
 * @v supplicant	EAP supplicant
 * @v req		Request type data
 * @v req_len		Length of request type data
 * @ret rc		Return status code
 */
static int eap_rx_mschapv2 ( struct eap_supplicant *supplicant,
			     const void *req, size_t req_len ) {
	struct net_device *netdev = supplicant->netdev;
	const struct eap_mschapv2 *hdr = req;

	/* Sanity check */
	if ( req_len < sizeof ( *hdr ) ) {
		DBGC ( netdev, "EAP %s underlength MS-CHAPv2:\n",
		       netdev->name );
		DBGC_HDA ( netdev, 0, req, req_len );
		return -EINVAL;
	}

	/* Handle according to opcode */
	switch ( hdr->code ) {
	case EAP_CODE_REQUEST:
		return eap_rx_mschapv2_request ( supplicant, hdr, req_len );
	case EAP_CODE_SUCCESS:
		return eap_rx_mschapv2_success ( supplicant, hdr, req_len );
	default:
		DBGC ( netdev, "EAP %s unsupported MS-CHAPv2 opcode %d\n",
		       netdev->name, hdr->code );
		DBGC_HDA ( netdev, 0, req, req_len );
		return -ENOTSUP;
	}
}

/** EAP MS-CHAPv2 method */
struct eap_method eap_mschapv2_method __eap_method = {
	.type = EAP_TYPE_MSCHAPV2,
	.rx = eap_rx_mschapv2,
};
