/*	$OpenBSD: x509_name_test.c,v 1.3 2025/05/05 06:33:34 tb Exp $ */

/*
 * Copyright (c) 2025 Theo Buehler <tb@openbsd.org>
 * Copyright (c) 2025 Kenjiro Nakayama <nakayamakenjiro@gmail.com>
 * Copyright (c) 2018 Ingo Schwarze <schwarze@openbsd.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <err.h>
#include <stdio.h>
#include <string.h>

#include <openssl/x509.h>

static const struct x509_name_legacy {
	const char	*compat;
	const char	*oneline;
	const uint8_t	 der[255];
	size_t		 der_len;
} x509_name_legacy_test[] = {
	{
		.compat =
		    "C=HU, "
		    "L=Budapest, "
		    "O=Microsec Ltd., "
		    "CN=Microsec e-Szigno Root CA 2009, "
		    "emailAddress=info@e-szigno.hu",
		.oneline =
		    "/C=HU"
		    "/L=Budapest"
		    "/O=Microsec Ltd."
		    "/CN=Microsec e-Szigno Root CA 2009"
		    "/emailAddress=info@e-szigno.hu",
		.der = {
			0x30, 0x81, 0x82, 0x31, 0x0b, 0x30, 0x09, 0x06,
			0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x48, 0x55,
			0x31, 0x11, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x04,
			0x07, 0x0c, 0x08, 0x42, 0x75, 0x64, 0x61, 0x70,
			0x65, 0x73, 0x74, 0x31, 0x16, 0x30, 0x14, 0x06,
			0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0d, 0x4d, 0x69,
			0x63, 0x72, 0x6f, 0x73, 0x65, 0x63, 0x20, 0x4c,
			0x74, 0x64, 0x2e, 0x31, 0x27, 0x30, 0x25, 0x06,
			0x03, 0x55, 0x04, 0x03, 0x0c, 0x1e, 0x4d, 0x69,
			0x63, 0x72, 0x6f, 0x73, 0x65, 0x63, 0x20, 0x65,
			0x2d, 0x53, 0x7a, 0x69, 0x67, 0x6e, 0x6f, 0x20,
			0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20,
			0x32, 0x30, 0x30, 0x39, 0x31, 0x1f, 0x30, 0x1d,
			0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d,
			0x01, 0x09, 0x01, 0x16, 0x10, 0x69, 0x6e, 0x66,
			0x6f, 0x40, 0x65, 0x2d, 0x73, 0x7a, 0x69, 0x67,
			0x6e, 0x6f, 0x2e, 0x68, 0x75,
		},
		.der_len = 133,
	},

	{
		.compat =
		    "serialNumber=G63287510, "
		    "C=ES, "
		    "O=ANF Autoridad de Certificacion, "
		    "OU=ANF CA Raiz, "
		    "CN=ANF Secure Server Root CA",
		.oneline =
		    "/serialNumber=G63287510"
		    "/C=ES"
		    "/O=ANF Autoridad de Certificacion"
		    "/OU=ANF CA Raiz"
		    "/CN=ANF Secure Server Root CA",
		.der = {
			0x30, 0x81, 0x84, 0x31, 0x12, 0x30, 0x10, 0x06,
			0x03, 0x55, 0x04, 0x05, 0x13, 0x09, 0x47, 0x36,
			0x33, 0x32, 0x38, 0x37, 0x35, 0x31, 0x30, 0x31,
			0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06,
			0x13, 0x02, 0x45, 0x53, 0x31, 0x27, 0x30, 0x25,
			0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x1e, 0x41,
			0x4e, 0x46, 0x20, 0x41, 0x75, 0x74, 0x6f, 0x72,
			0x69, 0x64, 0x61, 0x64, 0x20, 0x64, 0x65, 0x20,
			0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63,
			0x61, 0x63, 0x69, 0x6f, 0x6e, 0x31, 0x14, 0x30,
			0x12, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x0b,
			0x41, 0x4e, 0x46, 0x20, 0x43, 0x41, 0x20, 0x52,
			0x61, 0x69, 0x7a, 0x31, 0x22, 0x30, 0x20, 0x06,
			0x03, 0x55, 0x04, 0x03, 0x13, 0x19, 0x41, 0x4e,
			0x46, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65,
			0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20,
			0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41,
		},
		.der_len = 135,
	},

	{
		.compat =
		    "C=GB, "
		    "ST=Greater Manchester, "
		    "L=Salford, "
		    "O=COMODO CA Limited, "
		    "CN=COMODO Certification Authority",
		.oneline =
		    "/C=GB"
		    "/ST=Greater Manchester"
		    "/L=Salford"
		    "/O=COMODO CA Limited"
		    "/CN=COMODO Certification Authority",
		.der = {
			0x30, 0x81, 0x81, 0x31, 0x0b, 0x30, 0x09, 0x06,
			0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x47, 0x42,
			0x31, 0x1b, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04,
			0x08, 0x13, 0x12, 0x47, 0x72, 0x65, 0x61, 0x74,
			0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, 0x63, 0x68,
			0x65, 0x73, 0x74, 0x65, 0x72, 0x31, 0x10, 0x30,
			0x0e, 0x06, 0x03, 0x55, 0x04, 0x07, 0x13, 0x07,
			0x53, 0x61, 0x6c, 0x66, 0x6f, 0x72, 0x64, 0x31,
			0x1a, 0x30, 0x18, 0x06, 0x03, 0x55, 0x04, 0x0a,
			0x13, 0x11, 0x43, 0x4f, 0x4d, 0x4f, 0x44, 0x4f,
			0x20, 0x43, 0x41, 0x20, 0x4c, 0x69, 0x6d, 0x69,
			0x74, 0x65, 0x64, 0x31, 0x27, 0x30, 0x25, 0x06,
			0x03, 0x55, 0x04, 0x03, 0x13, 0x1e, 0x43, 0x4f,
			0x4d, 0x4f, 0x44, 0x4f, 0x20, 0x43, 0x65, 0x72,
			0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
			0x6f, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6f,
			0x72, 0x69, 0x74, 0x79,
		},
		.der_len = 132,
	},

	{
		.compat =
		    "C=HU, "
		    "L=Budapest, "
		    "O=Microsec Ltd., "
		    "2.5.4.97=VATHU-23584497, "
		    "CN=e-Szigno Root CA 2017",
		.oneline =
		    "/C=HU"
		    "/L=Budapest"
		    "/O=Microsec Ltd."
		    "/2.5.4.97=VATHU-23584497"
		    "/CN=e-Szigno Root CA 2017",
		.der = {
			0x30, 0x71, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03,
			0x55, 0x04, 0x06, 0x13, 0x02, 0x48, 0x55, 0x31,
			0x11, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x04, 0x07,
			0x0c, 0x08, 0x42, 0x75, 0x64, 0x61, 0x70, 0x65,
			0x73, 0x74, 0x31, 0x16, 0x30, 0x14, 0x06, 0x03,
			0x55, 0x04, 0x0a, 0x0c, 0x0d, 0x4d, 0x69, 0x63,
			0x72, 0x6f, 0x73, 0x65, 0x63, 0x20, 0x4c, 0x74,
			0x64, 0x2e, 0x31, 0x17, 0x30, 0x15, 0x06, 0x03,
			0x55, 0x04, 0x61, 0x0c, 0x0e, 0x56, 0x41, 0x54,
			0x48, 0x55, 0x2d, 0x32, 0x33, 0x35, 0x38, 0x34,
			0x34, 0x39, 0x37, 0x31, 0x1e, 0x30, 0x1c, 0x06,
			0x03, 0x55, 0x04, 0x03, 0x0c, 0x15, 0x65, 0x2d,
			0x53, 0x7a, 0x69, 0x67, 0x6e, 0x6f, 0x20, 0x52,
			0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x32,
			0x30, 0x31, 0x37,
		},
		.der_len = 115,
	},

	{

		.compat =
		    "C=ES, "
		    "O=FNMT-RCM, "
		    "OU=Ceres, "
		    "2.5.4.97=VATES-Q2826004J, "
		    "CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS",
		.oneline =
		    "/C=ES"
		    "/O=FNMT-RCM"
		    "/OU=Ceres"
		    "/2.5.4.97=VATES-Q2826004J"
		    "/CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS",
		.der = {
			0x30, 0x78, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03,
			0x55, 0x04, 0x06, 0x13, 0x02, 0x45, 0x53, 0x31,
			0x11, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x04, 0x0a,
			0x0c, 0x08, 0x46, 0x4e, 0x4d, 0x54, 0x2d, 0x52,
			0x43, 0x4d, 0x31, 0x0e, 0x30, 0x0c, 0x06, 0x03,
			0x55, 0x04, 0x0b, 0x0c, 0x05, 0x43, 0x65, 0x72,
			0x65, 0x73, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03,
			0x55, 0x04, 0x61, 0x0c, 0x0f, 0x56, 0x41, 0x54,
			0x45, 0x53, 0x2d, 0x51, 0x32, 0x38, 0x32, 0x36,
			0x30, 0x30, 0x34, 0x4a, 0x31, 0x2c, 0x30, 0x2a,
			0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x23, 0x41,
			0x43, 0x20, 0x52, 0x41, 0x49, 0x5a, 0x20, 0x46,
			0x4e, 0x4d, 0x54, 0x2d, 0x52, 0x43, 0x4d, 0x20,
			0x53, 0x45, 0x52, 0x56, 0x49, 0x44, 0x4f, 0x52,
			0x45, 0x53, 0x20, 0x53, 0x45, 0x47, 0x55, 0x52,
			0x4f, 0x53
		},
		.der_len = 122,
	},
};

#define N_X509_NAME_COMPAT \
	(sizeof(x509_name_legacy_test) / sizeof(x509_name_legacy_test[0]))

static int
x509_name_compat_testcase(const struct x509_name_legacy *test)
{
	const uint8_t *p;
	X509_NAME *name = NULL;
	unsigned char *der = NULL;
	int der_len = 0;
	BIO *bio = NULL;
	char *got;
	int got_len;
	char *buf = NULL;
	int failed = 1;

	p = test->der;
	if ((name = d2i_X509_NAME(NULL, &p, test->der_len)) == NULL)
		errx(1, "d2i_X509_NAME");

	if ((der_len = i2d_X509_NAME(name, &der)) <= 0) {
		fprintf(stderr, "FAIL: %s: i2d_X509_NAME", __func__);
		der_len = 0;
		goto err;
	}

	if (test->der_len != (size_t)der_len) {
		fprintf(stderr, "FAIL: %s: der len: want %zu, got %d\n",
		    __func__, test->der_len, der_len);
		goto err;
	}

	if (memcmp(test->der, der, test->der_len) != 0) {
		fprintf(stderr, "FAIL: %s: DER mismatch\n", __func__);
		goto err;
	}

	if ((bio = BIO_new(BIO_s_mem())) == NULL)
		errx(1, "BIO_new");

	if (!X509_NAME_print_ex(bio, name, 0, XN_FLAG_COMPAT)) {
		fprintf(stderr, "FAIL: %s: X509_NAME_print_ex", __func__);
		goto err;
	}

	if ((got_len = BIO_get_mem_data(bio, &got)) < 0)
		errx(1, "BIO_get_mem_data");

	if (strcmp(test->compat, got) != 0) {
		fprintf(stderr, "FAIL: %s compat:\nwant: \"%s\",\ngot:  \"%s\"\n",
		    __func__, test->compat, got);
		goto err;
	}

	if ((buf = X509_NAME_oneline(name, NULL, 0)) == NULL)
		errx(1, "X509_NAME_oneline");

	if (strcmp(test->oneline, buf) != 0) {
		fprintf(stderr, "FAIL: %s oneline:\nwant: \"%s\",\ngot:  \"%s\"\n",
		    __func__, test->compat, got);
		goto err;
	}

	failed = 0;

 err:
	BIO_free(bio);
	free(buf);
	X509_NAME_free(name);
	freezero(der, der_len);

	return failed;
}

static int
x509_name_compat_test(void)
{
	size_t i;
	int failed = 0;

	for (i = 0; i < N_X509_NAME_COMPAT; i++)
		failed |= x509_name_compat_testcase(&x509_name_legacy_test[i]);

	return failed;
}

static const struct x509_name_entry_test {
	const char *field;
	const char *value;
	int loc;
	int set;
	const char *expected_str;
	const int expected_set[4];
	const int expected_count;
} entry_tests[] = {
	{
		.field = "ST",
		.value = "BaWue",
		.loc = -1,
		.set = 0,
		.expected_str = "ST=BaWue",
		.expected_set = { 0 },
		.expected_count = 1,
	},
	{
		.field = "O",
		.value = "KIT",
		.loc = -1,
		.set = 0,
		.expected_str = "ST=BaWue, O=KIT",
		.expected_set = { 0, 1 },
		.expected_count = 2,
	},
	{
		.field = "L",
		.value = "Karlsruhe",
		.loc = 1,
		.set = 0,
		.expected_str = "ST=BaWue, L=Karlsruhe, O=KIT",
		.expected_set = { 0, 1, 2 },
		.expected_count = 3,
	},
	{
		.field = "C",
		.value = "DE",
		.loc = 0,
		.set = 1,
		.expected_str = "C=DE + ST=BaWue, L=Karlsruhe, O=KIT",
		.expected_set = { 0, 0, 1, 2 },
		.expected_count = 4,
	},
};

#define N_ENTRY_TESTS (sizeof(entry_tests) / sizeof(entry_tests[0]))

static int
verify_x509_name_output(X509_NAME *name, const struct x509_name_entry_test *tc)
{
	BIO *bio;
	char *got;
	long got_len;
	int loc, ret;
	int failed = 1;

	if ((bio = BIO_new(BIO_s_mem())) == NULL)
		goto fail;

	if ((ret = X509_NAME_print_ex(bio, name, 0, XN_FLAG_SEP_CPLUS_SPC)) == -1)
		goto fail;

	if ((got_len = BIO_get_mem_data(bio, &got)) < 0)
		goto fail;

	if (ret != got_len || strlen(tc->expected_str) != (size_t)ret)
		goto fail;

	if (strncmp(tc->expected_str, got, got_len) != 0)
		goto fail;

	if (X509_NAME_entry_count(name) != tc->expected_count)
		goto fail;

	for (loc = 0; loc < X509_NAME_entry_count(name); loc++) {
		X509_NAME_ENTRY *e = X509_NAME_get_entry(name, loc);
		if (e == NULL || X509_NAME_ENTRY_set(e) != tc->expected_set[loc])
			goto fail;
	}

	failed = 0;

 fail:
	BIO_free(bio);

	return failed;
}

static int
x509_name_add_entry_test(void)
{
	X509_NAME *name;
	int failed = 1;

	if ((name = X509_NAME_new()) == NULL)
		goto done;

	for (size_t i = 0; i < N_ENTRY_TESTS; i++) {
		const struct x509_name_entry_test *t = &entry_tests[i];

		if (!X509_NAME_add_entry_by_txt(name, t->field, MBSTRING_ASC,
		    (const unsigned char *)t->value, -1, t->loc, t->set))
			goto done;

		if (verify_x509_name_output(name, t))
			goto done;
	}

	failed = 0;

 done:
	X509_NAME_free(name);

	return failed;
}

int
main(void)
{
	int failed = 0;

	failed |= x509_name_compat_test();
	failed |= x509_name_add_entry_test();

	return failed;
}
