Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
David Howells 1277 100.00% 1 100.00%
Total 1277 1


// SPDX-License-Identifier: GPL-2.0-or-later
/* RxGK transport key derivation.
 *
 * Copyright (C) 2025 Red Hat, Inc. All Rights Reserved.
 * Written by David Howells (dhowells@redhat.com)
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/key-type.h>
#include <linux/slab.h>
#include <keys/rxrpc-type.h>
#include "ar-internal.h"
#include "rxgk_common.h"

#define round16(x) (((x) + 15) & ~15)

/*
 * Constants used to derive the keys and hmacs actually used for doing stuff.
 */
#define RXGK_CLIENT_ENC_PACKET		1026U // 0x402
#define RXGK_CLIENT_MIC_PACKET          1027U // 0x403
#define RXGK_SERVER_ENC_PACKET          1028U // 0x404
#define RXGK_SERVER_MIC_PACKET          1029U // 0x405
#define RXGK_CLIENT_ENC_RESPONSE        1030U // 0x406
#define RXGK_SERVER_ENC_TOKEN           1036U // 0x40c

static void rxgk_free(struct rxgk_context *gk)
{
	if (gk->tx_Kc)
		crypto_free_shash(gk->tx_Kc);
	if (gk->rx_Kc)
		crypto_free_shash(gk->rx_Kc);
	if (gk->tx_enc)
		crypto_free_aead(gk->tx_enc);
	if (gk->rx_enc)
		crypto_free_aead(gk->rx_enc);
	if (gk->resp_enc)
		crypto_free_aead(gk->resp_enc);
	kfree(gk);
}

void rxgk_put(struct rxgk_context *gk)
{
	if (gk && refcount_dec_and_test(&gk->usage))
		rxgk_free(gk);
}

/*
 * Transport key derivation function.
 *
 *      TK = random-to-key(PRF+(K0, L,
 *                         epoch || cid || start_time || key_number))
 *      [tools.ietf.org/html/draft-wilkinson-afs3-rxgk-11 sec 8.3]
 */
static int rxgk_derive_transport_key(struct rxrpc_connection *conn,
				     struct rxgk_context *gk,
				     const struct rxgk_key *rxgk,
				     struct krb5_buffer *TK,
				     gfp_t gfp)
{
	const struct krb5_enctype *krb5 = gk->krb5;
	struct krb5_buffer conn_info;
	unsigned int L = krb5->key_bytes;
	__be32 *info;
	u8 *buffer;
	int ret;

	_enter("");

	conn_info.len = sizeof(__be32) * 5;

	buffer = kzalloc(round16(conn_info.len), gfp);
	if (!buffer)
		return -ENOMEM;

	conn_info.data = buffer;

	info = (__be32 *)conn_info.data;
	info[0] = htonl(conn->proto.epoch);
	info[1] = htonl(conn->proto.cid);
	info[2] = htonl(conn->rxgk.start_time >> 32);
	info[3] = htonl(conn->rxgk.start_time >>  0);
	info[4] = htonl(gk->key_number);

	ret = crypto_krb5_calc_PRFplus(krb5, &rxgk->key, L, &conn_info, TK, gfp);
	kfree_sensitive(buffer);
	_leave(" = %d", ret);
	return ret;
}

/*
 * Set up the ciphers for the usage keys.
 */
static int rxgk_set_up_ciphers(struct rxrpc_connection *conn,
			       struct rxgk_context *gk,
			       const struct rxgk_key *rxgk,
			       gfp_t gfp)
{
	const struct krb5_enctype *krb5 = gk->krb5;
	struct crypto_shash *shash;
	struct crypto_aead *aead;
	struct krb5_buffer TK;
	bool service = rxrpc_conn_is_service(conn);
	int ret;
	u8 *buffer;

	buffer = kzalloc(krb5->key_bytes, gfp);
	if (!buffer)
		return -ENOMEM;

	TK.len = krb5->key_bytes;
	TK.data = buffer;

	ret = rxgk_derive_transport_key(conn, gk, rxgk, &TK, gfp);
	if (ret < 0)
		goto out;

	aead = crypto_krb5_prepare_encryption(krb5, &TK, RXGK_CLIENT_ENC_RESPONSE, gfp);
	if (IS_ERR(aead))
		goto aead_error;
	gk->resp_enc = aead;

	if (crypto_aead_blocksize(gk->resp_enc) != krb5->block_len ||
	    crypto_aead_authsize(gk->resp_enc) != krb5->cksum_len) {
		pr_notice("algo inconsistent with krb5 table %u!=%u or %u!=%u\n",
			  crypto_aead_blocksize(gk->resp_enc), krb5->block_len,
			  crypto_aead_authsize(gk->resp_enc), krb5->cksum_len);
		ret = -EINVAL;
		goto out;
	}

	if (service) {
		switch (conn->security_level) {
		case RXRPC_SECURITY_AUTH:
			shash = crypto_krb5_prepare_checksum(
				krb5, &TK, RXGK_SERVER_MIC_PACKET, gfp);
			if (IS_ERR(shash))
				goto hash_error;
			gk->tx_Kc = shash;
			shash = crypto_krb5_prepare_checksum(
				krb5, &TK, RXGK_CLIENT_MIC_PACKET, gfp);
			if (IS_ERR(shash))
				goto hash_error;
			gk->rx_Kc = shash;
			break;
		case RXRPC_SECURITY_ENCRYPT:
			aead = crypto_krb5_prepare_encryption(
				krb5, &TK, RXGK_SERVER_ENC_PACKET, gfp);
			if (IS_ERR(aead))
				goto aead_error;
			gk->tx_enc = aead;
			aead = crypto_krb5_prepare_encryption(
				krb5, &TK, RXGK_CLIENT_ENC_PACKET, gfp);
			if (IS_ERR(aead))
				goto aead_error;
			gk->rx_enc = aead;
			break;
		}
	} else {
		switch (conn->security_level) {
		case RXRPC_SECURITY_AUTH:
			shash = crypto_krb5_prepare_checksum(
				krb5, &TK, RXGK_CLIENT_MIC_PACKET, gfp);
			if (IS_ERR(shash))
				goto hash_error;
			gk->tx_Kc = shash;
			shash = crypto_krb5_prepare_checksum(
				krb5, &TK, RXGK_SERVER_MIC_PACKET, gfp);
			if (IS_ERR(shash))
				goto hash_error;
			gk->rx_Kc = shash;
			break;
		case RXRPC_SECURITY_ENCRYPT:
			aead = crypto_krb5_prepare_encryption(
				krb5, &TK, RXGK_CLIENT_ENC_PACKET, gfp);
			if (IS_ERR(aead))
				goto aead_error;
			gk->tx_enc = aead;
			aead = crypto_krb5_prepare_encryption(
				krb5, &TK, RXGK_SERVER_ENC_PACKET, gfp);
			if (IS_ERR(aead))
				goto aead_error;
			gk->rx_enc = aead;
			break;
		}
	}

	ret = 0;
out:
	kfree_sensitive(buffer);
	return ret;
aead_error:
	ret = PTR_ERR(aead);
	goto out;
hash_error:
	ret = PTR_ERR(shash);
	goto out;
}

/*
 * Derive a transport key for a connection and then derive a bunch of usage
 * keys from it and set up ciphers using them.
 */
struct rxgk_context *rxgk_generate_transport_key(struct rxrpc_connection *conn,
						 const struct rxgk_key *key,
						 unsigned int key_number,
						 gfp_t gfp)
{
	struct rxgk_context *gk;
	unsigned long lifetime;
	int ret = -ENOPKG;

	_enter("");

	gk = kzalloc(sizeof(*gk), GFP_KERNEL);
	if (!gk)
		return ERR_PTR(-ENOMEM);
	refcount_set(&gk->usage, 1);
	gk->key		= key;
	gk->key_number	= key_number;

	gk->krb5 = crypto_krb5_find_enctype(key->enctype);
	if (!gk->krb5)
		goto err_tk;

	ret = rxgk_set_up_ciphers(conn, gk, key, gfp);
	if (ret)
		goto err_tk;

	/* Set the remaining number of bytes encrypted with this key that may
	 * be transmitted before rekeying.  Note that the spec has been
	 * interpreted differently on this point...
	 */
	switch (key->bytelife) {
	case 0:
	case 63:
		gk->bytes_remaining = LLONG_MAX;
		break;
	case 1 ... 62:
		gk->bytes_remaining = 1LL << key->bytelife;
		break;
	default:
		gk->bytes_remaining = key->bytelife;
		break;
	}

	/* Set the time after which rekeying must occur */
	if (key->lifetime) {
		lifetime = min_t(u64, key->lifetime, INT_MAX / HZ);
		lifetime *= HZ;
	} else {
		lifetime = MAX_JIFFY_OFFSET;
	}
	gk->expiry = jiffies + lifetime;
	return gk;

err_tk:
	rxgk_put(gk);
	_leave(" = %d", ret);
	return ERR_PTR(ret);
}

/*
 * Use the server secret key to set up the ciphers that will be used to extract
 * the token from a response packet.
 */
int rxgk_set_up_token_cipher(const struct krb5_buffer *server_key,
			     struct crypto_aead **token_aead,
			     unsigned int enctype,
			     const struct krb5_enctype **_krb5,
			     gfp_t gfp)
{
	const struct krb5_enctype *krb5;
	struct crypto_aead *aead;

	krb5 = crypto_krb5_find_enctype(enctype);
	if (!krb5)
		return -ENOPKG;

	aead = crypto_krb5_prepare_encryption(krb5, server_key, RXGK_SERVER_ENC_TOKEN, gfp);
	if (IS_ERR(aead))
		return PTR_ERR(aead);

	*_krb5 = krb5;
	*token_aead = aead;
	return 0;
}