blob: 4519c572d37ef5ff50ddb087ba42009a9c6ca47a [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0-or-later
/* Kerberos library self-testing
*
* 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/slab.h>
#include <crypto/skcipher.h>
#include <crypto/hash.h>
#include "internal.h"
#define VALID(X) \
({ \
bool __x = (X); \
if (__x) { \
pr_warn("!!! TESTINVAL %s:%u\n", __FILE__, __LINE__); \
ret = -EBADMSG; \
} \
__x; \
})
#define CHECK(X) \
({ \
bool __x = (X); \
if (__x) { \
pr_warn("!!! TESTFAIL %s:%u\n", __FILE__, __LINE__); \
ret = -EBADMSG; \
} \
__x; \
})
enum which_key {
TEST_KC, TEST_KE, TEST_KI,
};
#if 0
static void dump_sg(struct scatterlist *sg, unsigned int limit)
{
unsigned int index = 0, n = 0;
for (; sg && limit > 0; sg = sg_next(sg)) {
unsigned int off = sg->offset, len = umin(sg->length, limit);
const void *p = kmap_local_page(sg_page(sg));
limit -= len;
while (len > 0) {
unsigned int part = umin(len, 32);
pr_notice("[%x] %04x: %*phN\n", n, index, part, p + off);
index += part;
off += part;
len -= part;
}
kunmap_local(p);
n++;
}
}
#endif
static int prep_buf(struct krb5_buffer *buf)
{
buf->data = kmalloc(buf->len, GFP_KERNEL);
if (!buf->data)
return -ENOMEM;
return 0;
}
#define PREP_BUF(BUF, LEN) \
do { \
(BUF)->len = (LEN); \
ret = prep_buf((BUF)); \
if (ret < 0) \
goto out; \
} while (0)
static int load_buf(struct krb5_buffer *buf, const char *from)
{
size_t len = strlen(from);
int ret;
if (len > 1 && from[0] == '\'') {
PREP_BUF(buf, len - 1);
memcpy(buf->data, from + 1, len - 1);
ret = 0;
goto out;
}
if (VALID(len & 1))
return -EINVAL;
PREP_BUF(buf, len / 2);
ret = hex2bin(buf->data, from, buf->len);
if (ret < 0) {
VALID(1);
goto out;
}
out:
return ret;
}
#define LOAD_BUF(BUF, FROM) do { ret = load_buf(BUF, FROM); if (ret < 0) goto out; } while (0)
static void clear_buf(struct krb5_buffer *buf)
{
kfree(buf->data);
buf->len = 0;
buf->data = NULL;
}
/*
* Perform a pseudo-random function check.
*/
static int krb5_test_one_prf(const struct krb5_prf_test *test)
{
const struct krb5_enctype *krb5 = crypto_krb5_find_enctype(test->etype);
struct krb5_buffer key = {}, octet = {}, result = {}, prf = {};
int ret;
if (!krb5)
return -EOPNOTSUPP;
pr_notice("Running %s %s\n", krb5->name, test->name);
LOAD_BUF(&key, test->key);
LOAD_BUF(&octet, test->octet);
LOAD_BUF(&prf, test->prf);
PREP_BUF(&result, krb5->prf_len);
if (VALID(result.len != prf.len)) {
ret = -EINVAL;
goto out;
}
ret = krb5->profile->calc_PRF(krb5, &key, &octet, &result, GFP_KERNEL);
if (ret < 0) {
CHECK(1);
pr_warn("PRF calculation failed %d\n", ret);
goto out;
}
if (memcmp(result.data, prf.data, result.len) != 0) {
CHECK(1);
ret = -EKEYREJECTED;
goto out;
}
ret = 0;
out:
clear_buf(&result);
clear_buf(&prf);
clear_buf(&octet);
clear_buf(&key);
return ret;
}
/*
* Perform a key derivation check.
*/
static int krb5_test_key(const struct krb5_enctype *krb5,
const struct krb5_buffer *base_key,
const struct krb5_key_test_one *test,
enum which_key which)
{
struct krb5_buffer key = {}, result = {};
int ret;
LOAD_BUF(&key, test->key);
PREP_BUF(&result, key.len);
switch (which) {
case TEST_KC:
ret = krb5_derive_Kc(krb5, base_key, test->use, &result, GFP_KERNEL);
break;
case TEST_KE:
ret = krb5_derive_Ke(krb5, base_key, test->use, &result, GFP_KERNEL);
break;
case TEST_KI:
ret = krb5_derive_Ki(krb5, base_key, test->use, &result, GFP_KERNEL);
break;
default:
VALID(1);
ret = -EINVAL;
goto out;
}
if (ret < 0) {
CHECK(1);
pr_warn("Key derivation failed %d\n", ret);
goto out;
}
if (memcmp(result.data, key.data, result.len) != 0) {
CHECK(1);
ret = -EKEYREJECTED;
goto out;
}
out:
clear_buf(&key);
clear_buf(&result);
return ret;
}
static int krb5_test_one_key(const struct krb5_key_test *test)
{
const struct krb5_enctype *krb5 = crypto_krb5_find_enctype(test->etype);
struct krb5_buffer base_key = {};
int ret;
if (!krb5)
return -EOPNOTSUPP;
pr_notice("Running %s %s\n", krb5->name, test->name);
LOAD_BUF(&base_key, test->key);
ret = krb5_test_key(krb5, &base_key, &test->Kc, TEST_KC);
if (ret < 0)
goto out;
ret = krb5_test_key(krb5, &base_key, &test->Ke, TEST_KE);
if (ret < 0)
goto out;
ret = krb5_test_key(krb5, &base_key, &test->Ki, TEST_KI);
if (ret < 0)
goto out;
out:
clear_buf(&base_key);
return ret;
}
/*
* Perform an encryption test.
*/
static int krb5_test_one_enc(const struct krb5_enc_test *test, void *buf)
{
const struct krb5_enctype *krb5 = crypto_krb5_find_enctype(test->etype);
struct crypto_aead *ci = NULL;
struct krb5_buffer K0 = {}, Ke = {}, Ki = {}, keys = {};
struct krb5_buffer conf = {}, plain = {}, ct = {};
struct scatterlist sg[1];
size_t data_len, data_offset, message_len;
int ret;
if (!krb5)
return -EOPNOTSUPP;
pr_notice("Running %s %s\n", krb5->name, test->name);
/* Load the test data into binary buffers. */
LOAD_BUF(&conf, test->conf);
LOAD_BUF(&plain, test->plain);
LOAD_BUF(&ct, test->ct);
if (test->K0) {
LOAD_BUF(&K0, test->K0);
} else {
LOAD_BUF(&Ke, test->Ke);
LOAD_BUF(&Ki, test->Ki);
ret = krb5->profile->load_encrypt_keys(krb5, &Ke, &Ki, &keys, GFP_KERNEL);
if (ret < 0)
goto out;
}
if (VALID(conf.len != krb5->conf_len) ||
VALID(ct.len != krb5->conf_len + plain.len + krb5->cksum_len))
goto out;
data_len = plain.len;
message_len = crypto_krb5_how_much_buffer(krb5, KRB5_ENCRYPT_MODE,
data_len, &data_offset);
if (CHECK(message_len != ct.len)) {
pr_warn("Encrypted length mismatch %zu != %u\n", message_len, ct.len);
goto out;
}
if (CHECK(data_offset != conf.len)) {
pr_warn("Data offset mismatch %zu != %u\n", data_offset, conf.len);
goto out;
}
memcpy(buf, conf.data, conf.len);
memcpy(buf + data_offset, plain.data, plain.len);
/* Allocate a crypto object and set its key. */
if (test->K0)
ci = crypto_krb5_prepare_encryption(krb5, &K0, test->usage, GFP_KERNEL);
else
ci = krb5_prepare_encryption(krb5, &keys, GFP_KERNEL);
if (IS_ERR(ci)) {
ret = PTR_ERR(ci);
ci = NULL;
pr_err("Couldn't alloc AEAD %s: %d\n", krb5->encrypt_name, ret);
goto out;
}
/* Encrypt the message. */
sg_init_one(sg, buf, message_len);
ret = crypto_krb5_encrypt(krb5, ci, sg, 1, message_len,
data_offset, data_len, true);
if (ret < 0) {
CHECK(1);
pr_warn("Encryption failed %d\n", ret);
goto out;
}
if (ret != message_len) {
CHECK(1);
pr_warn("Encrypted message wrong size %x != %zx\n", ret, message_len);
goto out;
}
if (memcmp(buf, ct.data, ct.len) != 0) {
CHECK(1);
pr_warn("Ciphertext mismatch\n");
pr_warn("BUF %*phN\n", ct.len, buf);
pr_warn("CT %*phN\n", ct.len, ct.data);
pr_warn("PT %*phN%*phN\n", conf.len, conf.data, plain.len, plain.data);
ret = -EKEYREJECTED;
goto out;
}
/* Decrypt the encrypted message. */
data_offset = 0;
data_len = message_len;
ret = crypto_krb5_decrypt(krb5, ci, sg, 1, &data_offset, &data_len);
if (ret < 0) {
CHECK(1);
pr_warn("Decryption failed %d\n", ret);
goto out;
}
if (CHECK(data_offset != conf.len) ||
CHECK(data_len != plain.len))
goto out;
if (memcmp(buf, conf.data, conf.len) != 0) {
CHECK(1);
pr_warn("Confounder mismatch\n");
pr_warn("ENC %*phN\n", conf.len, buf);
pr_warn("DEC %*phN\n", conf.len, conf.data);
ret = -EKEYREJECTED;
goto out;
}
if (memcmp(buf + conf.len, plain.data, plain.len) != 0) {
CHECK(1);
pr_warn("Plaintext mismatch\n");
pr_warn("BUF %*phN\n", plain.len, buf + conf.len);
pr_warn("PT %*phN\n", plain.len, plain.data);
ret = -EKEYREJECTED;
goto out;
}
ret = 0;
out:
clear_buf(&ct);
clear_buf(&plain);
clear_buf(&conf);
clear_buf(&keys);
clear_buf(&Ki);
clear_buf(&Ke);
clear_buf(&K0);
if (ci)
crypto_free_aead(ci);
return ret;
}
/*
* Perform a checksum test.
*/
static int krb5_test_one_mic(const struct krb5_mic_test *test, void *buf)
{
const struct krb5_enctype *krb5 = crypto_krb5_find_enctype(test->etype);
struct crypto_shash *ci = NULL;
struct scatterlist sg[1];
struct krb5_buffer K0 = {}, Kc = {}, keys = {}, plain = {}, mic = {};
size_t offset, len, message_len;
int ret;
if (!krb5)
return -EOPNOTSUPP;
pr_notice("Running %s %s\n", krb5->name, test->name);
/* Allocate a crypto object and set its key. */
if (test->K0) {
LOAD_BUF(&K0, test->K0);
ci = crypto_krb5_prepare_checksum(krb5, &K0, test->usage, GFP_KERNEL);
} else {
LOAD_BUF(&Kc, test->Kc);
ret = krb5->profile->load_checksum_key(krb5, &Kc, &keys, GFP_KERNEL);
if (ret < 0)
goto out;
ci = krb5_prepare_checksum(krb5, &Kc, GFP_KERNEL);
}
if (IS_ERR(ci)) {
ret = PTR_ERR(ci);
ci = NULL;
pr_err("Couldn't alloc shash %s: %d\n", krb5->cksum_name, ret);
goto out;
}
/* Load the test data into binary buffers. */
LOAD_BUF(&plain, test->plain);
LOAD_BUF(&mic, test->mic);
len = plain.len;
message_len = crypto_krb5_how_much_buffer(krb5, KRB5_CHECKSUM_MODE,
len, &offset);
if (CHECK(message_len != mic.len + plain.len)) {
pr_warn("MIC length mismatch %zu != %u\n",
message_len, mic.len + plain.len);
goto out;
}
memcpy(buf + offset, plain.data, plain.len);
/* Generate a MIC generation request. */
sg_init_one(sg, buf, 1024);
ret = crypto_krb5_get_mic(krb5, ci, NULL, sg, 1, 1024,
krb5->cksum_len, plain.len);
if (ret < 0) {
CHECK(1);
pr_warn("Get MIC failed %d\n", ret);
goto out;
}
len = ret;
if (CHECK(len != plain.len + mic.len)) {
pr_warn("MIC length mismatch %zu != %u\n", len, plain.len + mic.len);
goto out;
}
if (memcmp(buf, mic.data, mic.len) != 0) {
CHECK(1);
pr_warn("MIC mismatch\n");
pr_warn("BUF %*phN\n", mic.len, buf);
pr_warn("MIC %*phN\n", mic.len, mic.data);
ret = -EKEYREJECTED;
goto out;
}
/* Generate a verification request. */
offset = 0;
ret = crypto_krb5_verify_mic(krb5, ci, NULL, sg, 1, &offset, &len);
if (ret < 0) {
CHECK(1);
pr_warn("Verify MIC failed %d\n", ret);
goto out;
}
if (CHECK(offset != mic.len) ||
CHECK(len != plain.len))
goto out;
if (memcmp(buf + offset, plain.data, plain.len) != 0) {
CHECK(1);
pr_warn("Plaintext mismatch\n");
pr_warn("BUF %*phN\n", plain.len, buf + offset);
pr_warn("PT %*phN\n", plain.len, plain.data);
ret = -EKEYREJECTED;
goto out;
}
ret = 0;
out:
clear_buf(&mic);
clear_buf(&plain);
clear_buf(&keys);
clear_buf(&K0);
clear_buf(&Kc);
if (ci)
crypto_free_shash(ci);
return ret;
}
int krb5_selftest(void)
{
void *buf;
int ret = 0, i;
buf = kmalloc(4096, GFP_KERNEL);
if (!buf)
return -ENOMEM;
pr_notice("\n");
pr_notice("Running selftests\n");
for (i = 0; krb5_prf_tests[i].name; i++) {
ret = krb5_test_one_prf(&krb5_prf_tests[i]);
if (ret < 0) {
if (ret != -EOPNOTSUPP)
goto out;
pr_notice("Skipping %s\n", krb5_prf_tests[i].name);
}
}
for (i = 0; krb5_key_tests[i].name; i++) {
ret = krb5_test_one_key(&krb5_key_tests[i]);
if (ret < 0) {
if (ret != -EOPNOTSUPP)
goto out;
pr_notice("Skipping %s\n", krb5_key_tests[i].name);
}
}
for (i = 0; krb5_enc_tests[i].name; i++) {
memset(buf, 0x5a, 4096);
ret = krb5_test_one_enc(&krb5_enc_tests[i], buf);
if (ret < 0) {
if (ret != -EOPNOTSUPP)
goto out;
pr_notice("Skipping %s\n", krb5_enc_tests[i].name);
}
}
for (i = 0; krb5_mic_tests[i].name; i++) {
memset(buf, 0x5a, 4096);
ret = krb5_test_one_mic(&krb5_mic_tests[i], buf);
if (ret < 0) {
if (ret != -EOPNOTSUPP)
goto out;
pr_notice("Skipping %s\n", krb5_mic_tests[i].name);
}
}
ret = 0;
out:
pr_notice("Selftests %s\n", ret == 0 ? "succeeded" : "failed");
kfree(buf);
return ret;
}