Skip to content

Commit db8f3e4

Browse files
author
Emma Stensland
committed
san inline paring support added
1 parent e3844b4 commit db8f3e4

4 files changed

Lines changed: 368 additions & 27 deletions

File tree

src/x509/clu_config.c

Lines changed: 121 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,76 @@ static int wolfCLU_setAltNames(WOLFSSL_X509* x509, WOLFSSL_CONF* conf,
543543
}
544544

545545

546+
#ifdef WOLFSSL_ALT_NAMES
547+
/* Apply an inline subjectAltName list to x509, e.g.
548+
* "DNS:example.com,IP:10.0.0.1". Leading whitespace per entry is skipped.
549+
* Buffer is tokenized in place, so callers pass a writable string. Returns
550+
* WOLFCLU_SUCCESS, or WOLFCLU_FATAL_ERROR on a malformed entry so a bad SAN is
551+
* never silently ignored. */
552+
static int wolfCLU_setInlineAltNames(WOLFSSL_X509* x509, char* val)
553+
{
554+
int ret = WOLFCLU_SUCCESS;
555+
char* token;
556+
char* ptr = NULL;
557+
558+
if (x509 == NULL || val == NULL) {
559+
return WOLFCLU_FATAL_ERROR;
560+
}
561+
562+
token = XSTRTOK(val, ",", &ptr);
563+
while (token != NULL) {
564+
char* colon;
565+
char* value;
566+
size_t len;
567+
568+
/* trim whitespace around entries and trailing whitespace */
569+
while (*token == ' ' || *token == '\t' || *token == '\r' || *token == '\n') {
570+
token++;
571+
}
572+
len = XSTRLEN(token);
573+
while (len > 0 && (token[len - 1] == ' ' || token[len - 1] == '\t' ||
574+
token[len - 1] == '\r' || token[len - 1] == '\n')) {
575+
token[--len] = '\0';
576+
}
577+
colon = XSTRSTR(token, ":");
578+
if (colon == NULL) {
579+
wolfCLU_LogError("bad subjectAltName entry \"%s\", expected "
580+
"TYPE:value", token);
581+
ret = WOLFCLU_FATAL_ERROR;
582+
break;
583+
}
584+
*colon = '\0';
585+
/* The trailing-whitespace trim above already NUL-terminated the token
586+
* at the correct boundary, so value needs no second trailing trim. */
587+
/* drop whitespace between the colon and the value */
588+
value = colon + 1;
589+
while (*value == ' ' || *value == '\t' || *value == '\r' || *value == '\n') {
590+
value++;
591+
}
592+
593+
/* Check for empty type or value after trimming */
594+
if (XSTRLEN(token) == 0) {
595+
wolfCLU_LogError("bad subjectAltName entry: empty type prefix");
596+
ret = WOLFCLU_FATAL_ERROR;
597+
break;
598+
}
599+
if (XSTRLEN(value) == 0) {
600+
wolfCLU_LogError("bad subjectAltName entry: empty value for type \"%s\"", token);
601+
ret = WOLFCLU_FATAL_ERROR;
602+
break;
603+
}
604+
605+
ret = wolfCLU_addAltName(x509, token, value);
606+
if (ret != WOLFCLU_SUCCESS) {
607+
break;
608+
}
609+
token = XSTRTOK(NULL, ",", &ptr);
610+
}
611+
return ret;
612+
}
613+
#endif /* WOLFSSL_ALT_NAMES */
614+
615+
546616
/* return WOLFCLU_SUCCESS on success */
547617
int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect)
548618
{
@@ -576,9 +646,40 @@ int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect)
576646
}
577647

578648
current = wolfSSL_NCONF_get_string(conf, sect, "subjectAltName");
579-
if (current != NULL && current[0] == '@') {
580-
current = current+1;
581-
ret = wolfCLU_setAltNames(x509, conf, current);
649+
if (current != NULL) {
650+
if (current[0] == '@') {
651+
ret = wolfCLU_setAltNames(x509, conf, current + 1);
652+
}
653+
else {
654+
/* Accept inline form for config compatibility. */
655+
#ifndef WOLFSSL_ALT_NAMES
656+
/* Intentional: mirror the pre-existing silent-skip behaviour of
657+
* the @section form (wolfCLU_setAltNames is also a no-op when
658+
* WOLFSSL_ALT_NAMES is not defined). We log the skip but do NOT
659+
* promote ret to WOLFCLU_FATAL_ERROR so that a config containing
660+
* a subjectAltName line is still usable in builds where alt-name
661+
* support was compiled out. */
662+
WOLFCLU_LOG(WOLFCLU_L0, "Skipping alt names, recompile wolfSSL "
663+
"with WOLFSSL_ALT_NAMES...");
664+
#else
665+
{
666+
int len = (int)XSTRLEN(current);
667+
char* dup = (char*)XMALLOC(len + 1, NULL,
668+
DYNAMIC_TYPE_TMP_BUFFER);
669+
670+
if (dup == NULL) {
671+
wolfCLU_LogError("out of memory duplicating "
672+
"subjectAltName value");
673+
ret = WOLFCLU_FATAL_ERROR;
674+
}
675+
else {
676+
XMEMCPY(dup, current, len + 1);
677+
ret = wolfCLU_setInlineAltNames(x509, dup);
678+
XFREE(dup, NULL, DYNAMIC_TYPE_TMP_BUFFER);
679+
}
680+
}
681+
#endif
682+
}
582683
}
583684
return ret;
584685
}
@@ -621,26 +722,8 @@ int wolfCLU_parseAddExt(WOLFSSL_X509* x509, char* addExt)
621722
(void)val;
622723
WOLFCLU_LOG(WOLFCLU_L0, "Skipping alt names, recompile wolfSSL with WOLFSSL_ALT_NAMES...");
623724
#else
624-
char* token;
625-
char* ptr = NULL;
626-
627725
/* value is a comma separated list of TYPE:value pairs */
628-
token = XSTRTOK(val, ",", &ptr);
629-
while (token != NULL) {
630-
char* colon = XSTRSTR(token, ":");
631-
if (colon == NULL) {
632-
wolfCLU_LogError("bad subjectAltName entry \"%s\", expected "
633-
"TYPE:value", token);
634-
ret = WOLFCLU_FATAL_ERROR;
635-
break;
636-
}
637-
*colon = '\0';
638-
ret = wolfCLU_addAltName(x509, token, colon + 1);
639-
if (ret != WOLFCLU_SUCCESS) {
640-
break;
641-
}
642-
token = XSTRTOK(NULL, ",", &ptr);
643-
}
726+
ret = wolfCLU_setInlineAltNames(x509, val);
644727
#endif
645728
}
646729
else {
@@ -656,9 +739,18 @@ int wolfCLU_setExtensions(WOLFSSL_X509* x509, WOLFSSL_CONF* conf, char* sect)
656739
{
657740
(void)x509;
658741
(void)conf;
659-
(void)sect;
660742

661-
wolfCLU_LogError("wolfSSL not compiled with cert extensions");
743+
/* No extension section requested, so not having WOLFSSL_CERT_EXT
744+
* can be ignored. (Coupled with `ret = ` in wolfCLU_readConfig) */
745+
if (sect == NULL) {
746+
return WOLFCLU_SUCCESS;
747+
}
748+
749+
/* If not compiled with WOLFSSL_CERT_EXT, fail so certs can be built as
750+
* intended by user. */
751+
wolfCLU_LogError("wolfSSL not compiled with cert extensions "
752+
"(WOLFSSL_CERT_EXT); cannot apply requested x509_extensions "
753+
"section \"%s\"", sect);
662754
return NOT_COMPILED_IN;
663755
}
664756

@@ -918,7 +1010,11 @@ int wolfCLU_readConfig(WOLFSSL_X509* x509, char* config, char* sect, char* ext)
9181010
wolfCLU_setAttributes(x509, conf,
9191011
wolfSSL_NCONF_get_string(conf, sect, "attributes"));
9201012
if (ext == NULL) {
921-
(void)wolfCLU_setExtensions(x509, conf,
1013+
/* Note: we capture this return code because the !WOLFSSL_CERT_EXT stub
1014+
* of wolfCLU_setExtensions gracefully returns SUCCESS when the string
1015+
* is NULL, but fails loudly if an extension section IS requested and
1016+
* WOLFSSL_CERT_EXT is disabled. These two behaviors are coupled. */
1017+
ret = wolfCLU_setExtensions(x509, conf,
9221018
wolfSSL_NCONF_get_string(conf, sect, "x509_extensions"));
9231019
}
9241020
else {

tests/x509/x509-ca-test.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,36 @@ def test_create_and_verify(self):
412412
self.assertEqual(r2.returncode, 0, r2.stderr)
413413

414414

415+
def test_create_der_outform(self):
416+
"""Conventional (RSA/ECDSA) CA signing honors -outform der.
417+
418+
Passes the relative ``out_name`` to ``-out`` (matching
419+
``test_create_and_verify``) so that the ``new_certs_dir``
420+
path-prepending logic inside ``wolfCLU_CertSign`` is exercised even
421+
when ``-outform der`` is in effect.
422+
"""
423+
out_name = "test_ca_cv.der"
424+
out = _tmp(out_name)
425+
self._clean(out)
426+
r = run_wolfssl("ca", "-config", self.conf,
427+
"-in", self.csr, "-out", out_name,
428+
"-outform", "der")
429+
self.assertEqual(r.returncode, 0, r.stderr)
430+
self.assertTrue(os.path.isfile(out),
431+
"Signed cert not written to {}".format(out))
432+
self.assertGreater(os.path.getsize(out), 0, "Signed cert file is empty")
433+
434+
r2 = run_wolfssl("x509", "-in", out, "-inform", "der",
435+
"-text", "-noout")
436+
self.assertEqual(r2.returncode, 0,
437+
"DER signed cert not parseable: " + r2.stderr)
438+
439+
r3 = run_wolfssl("verify", "-CAfile",
440+
os.path.join(CERTS_DIR, "ca-cert.pem"),
441+
"-inform", "der", out)
442+
self.assertEqual(r3.returncode, 0, r3.stderr)
443+
444+
415445
class TestCAOverrideConfig(unittest.TestCase):
416446
"""Override config options with command-line flags."""
417447

@@ -1365,5 +1395,118 @@ def test_mldsa_verify_partial_chain_intermediate(self):
13651395

13661396

13671397

1398+
@unittest.skipUnless(_has_ed25519(), "Ed25519 not available")
1399+
def test_config_unsupported_private_key_fails(self):
1400+
"""Config private_key with unsupported type must not emit unsigned cert.
1401+
1402+
The assertion block below is intentionally similar to
1403+
``TestCAUnsupportedConfigKey.test_read_sign_config_unsupported_key_fails``.
1404+
Both tests verify that an Ed25519 ``private_key`` in the config is
1405+
rejected before any output is written, but they differ in the signing
1406+
CA used: this test uses the ML-DSA CA cert (exercises
1407+
``wolfCLU_readSignConfig`` via the ML-DSA code path), while
1408+
``TestCAUnsupportedConfigKey`` uses the default RSA CA cert. Keep
1409+
both tests in sync if the assertion logic changes.
1410+
"""
1411+
ed_key = _tmp("ca_mldsa_badconf_ed")
1412+
bad_conf = _tmp("ca_mldsa_bad_key.conf")
1413+
out = _tmp("tmp_mldsa_badconf.pem")
1414+
self._clean(ed_key + ".priv", ed_key + ".pub", bad_conf, out)
1415+
1416+
r = run_wolfssl("-genkey", "ed25519", "-out", ed_key,
1417+
"-output", "keypair", "-outform", "PEM")
1418+
if r.returncode != 0:
1419+
self.skipTest("Ed25519 keygen failed: " + r.stderr)
1420+
1421+
bad_conf_body = CA_CONF.replace(
1422+
f"private_key = {_CERTS}/ca-key.pem",
1423+
f"private_key = {ed_key}.priv")
1424+
with open(bad_conf, "w", encoding="utf-8", newline="\n") as f:
1425+
f.write(bad_conf_body)
1426+
1427+
r = run_wolfssl("ca", "-config", bad_conf,
1428+
"-in", self.rsa_csr, "-out", out,
1429+
"-cert", self.mldsa_ca_cert)
1430+
self.assertNotEqual(r.returncode, 0,
1431+
"unsupported config private_key should fail")
1432+
msg = (r.stdout + r.stderr).lower()
1433+
self.assertTrue(
1434+
"unsupported" in msg or "signing key" in msg
1435+
or "unable to load private key" in msg,
1436+
"expected unsupported-key error, got: " + msg)
1437+
self.assertFalse(
1438+
os.path.isfile(out) and os.path.getsize(out) > 0,
1439+
"no unsigned certificate should be written")
1440+
1441+
1442+
@unittest.skipUnless(_has_ed25519(), "Ed25519 not available")
1443+
class TestCAUnsupportedConfigKey(unittest.TestCase):
1444+
"""Regression: wolfCLU_readSignConfig must reject unsupported private_key."""
1445+
1446+
@classmethod
1447+
def setUpClass(cls):
1448+
cls.ed_key = _tmp("ca_ed25519_key")
1449+
r = run_wolfssl("-genkey", "ed25519", "-out", cls.ed_key,
1450+
"-output", "keypair", "-outform", "PEM")
1451+
assert r.returncode == 0, "Ed25519 keygen failed: " + r.stderr
1452+
1453+
cls.conf = _tmp("ca_ed25519_bad.conf")
1454+
bad_conf_body = CA_CONF.replace(
1455+
f"private_key = {_CERTS}/ca-key.pem",
1456+
f"private_key = {cls.ed_key}.priv")
1457+
with open(cls.conf, "w", encoding="utf-8", newline="\n") as f:
1458+
f.write(bad_conf_body)
1459+
1460+
cls.csr = _tmp("ca_ed25519_bad.csr")
1461+
r = run_wolfssl("req", "-key",
1462+
os.path.join(CERTS_DIR, "server-key.pem"),
1463+
"-subj", "/O=wolfSSL/C=US/ST=MT/L=Bozeman/CN=server",
1464+
"-out", cls.csr)
1465+
assert r.returncode == 0, "CSR creation failed: " + r.stderr
1466+
1467+
@classmethod
1468+
def tearDownClass(cls):
1469+
_cleanup(cls.ed_key + ".priv", cls.ed_key + ".pub",
1470+
cls.conf, cls.csr, _tmp("index.txt"))
1471+
1472+
def setUp(self):
1473+
_cleanup(_tmp("index.txt"))
1474+
_touch(_tmp("index.txt"))
1475+
1476+
def tearDown(self):
1477+
_cleanup(_tmp("index.txt"))
1478+
1479+
def _clean(self, *files):
1480+
for f in files:
1481+
self.addCleanup(lambda p=f: _cleanup(p))
1482+
1483+
def test_read_sign_config_unsupported_key_fails(self):
1484+
"""ca -config with Ed25519 private_key must fail without writing cert.
1485+
1486+
The assertion block below mirrors
1487+
``TestCAMLDSA.test_config_unsupported_private_key_fails``. Both
1488+
tests verify that an Ed25519 ``private_key`` in the config is
1489+
rejected before any output is written, but the signing CA differs:
1490+
this test uses the default RSA CA cert (exercises the
1491+
``wolfCLU_readSignConfig`` RSA code path), while ``TestCAMLDSA``
1492+
uses the ML-DSA CA cert. Keep both tests in sync if the assertion
1493+
logic changes.
1494+
"""
1495+
out = _tmp("tmp_ed25519_bad_ca.pem")
1496+
self._clean(out)
1497+
r = run_wolfssl("ca", "-config", self.conf,
1498+
"-in", self.csr, "-out", out)
1499+
self.assertNotEqual(r.returncode, 0,
1500+
"Ed25519 config private_key should fail")
1501+
msg = (r.stdout + r.stderr).lower()
1502+
self.assertTrue(
1503+
"unsupported" in msg or "signing key" in msg
1504+
or "unable to load private key" in msg,
1505+
"expected unsupported-key error, got: " + msg)
1506+
self.assertFalse(
1507+
os.path.isfile(out) and os.path.getsize(out) > 0,
1508+
"no unsigned certificate should be written")
1509+
1510+
13681511
if __name__ == "__main__":
13691512
test_main()

tests/x509/x509-process-test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,13 +492,13 @@ def test_4d_pem_inform_with_der_file(self):
492492
"output file should not be created on error")
493493

494494
def test_4e_nonexistent_file_der(self):
495-
r = run_wolfssl("x509", "-inform", "der", "-in", "ca-cert.pem",
495+
r = run_wolfssl("x509", "-inform", "der", "-in", "nonexistent-file.pem",
496496
"-outform", "der", "-out", "out.txt")
497497
self._clean("out.txt")
498498
self.assertNotEqual(r.returncode, 0)
499499

500500
def test_4f_nonexistent_file_pem(self):
501-
r = run_wolfssl("x509", "-inform", "pem", "-in", "ca-cert.pem",
501+
r = run_wolfssl("x509", "-inform", "pem", "-in", "nonexistent-file.pem",
502502
"-outform", "pem", "-out", "out.txt")
503503
self._clean("out.txt")
504504
self.assertNotEqual(r.returncode, 0)

0 commit comments

Comments
 (0)