Skip to content

Commit e2429bb

Browse files
author
Emma Stensland
committed
san inline paring support added
1 parent 58a29c4 commit e2429bb

3 files changed

Lines changed: 225 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-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)

tests/x509/x509-req-test.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""Tests for wolfssl req and x509 -req (converted from x509-req-test.sh)."""
33

44
import os
5+
import re
56
import shutil
67
import subprocess
78
import sys
@@ -249,6 +250,107 @@ def test_req_extensions_v3_alt_ca(self):
249250
self.assertIn("CA:TRUE", r2.stdout)
250251

251252

253+
def _get_san_line(self, stdout):
254+
"""Return the value line that follows an 'X509v3 Subject Alternative
255+
Name' header in `x509 -text` output, or None if not found."""
256+
lines = stdout.splitlines()
257+
for i, line in enumerate(lines):
258+
if "X509v3 Subject Alternative Name" in line:
259+
self.assertLess(i + 1, len(lines), "Missing SAN value line")
260+
return lines[i + 1]
261+
return None
262+
263+
def test_req_inline_subjectaltname_openssl_compat(self):
264+
"""A config subjectAltName in the OpenSSL inline form (TYPE:value list,
265+
not the @section indirection) is accepted and applied to the cert.
266+
267+
Pins the behavior in wolfCLU_setExtensions (clu_config.c): the inline
268+
form is parsed via the same path as -addext (wolfCLU_setInlineAltNames),
269+
so an OpenSSL-style config is neither silently dropped nor rejected.
270+
Whitespace after the comma must be tolerated. Skipped on builds without
271+
cert extensions, where the parsing path is absent."""
272+
conf = _tmp("test_req_inline_san.conf")
273+
out = _tmp("test_req_inline_san.crt")
274+
self._clean(conf, out)
275+
with open(conf, "w", encoding="utf-8", newline="\n") as f:
276+
f.write(
277+
"[ req ]\n"
278+
"distinguished_name = dn\n"
279+
"prompt = no\n"
280+
"x509_extensions = v3_ext\n"
281+
"[ dn ]\n"
282+
"commonName = inline-san-test\n"
283+
"[ v3_ext ]\n"
284+
"basicConstraints = CA:FALSE\n"
285+
"subjectAltName = DNS:example.com, IP:10.0.0.1\n")
286+
r = run_wolfssl("req", "-new", "-x509",
287+
"-key", os.path.join(CERTS_DIR, "server-key.pem"),
288+
"-config", conf, "-out", out)
289+
combined = r.stdout + r.stderr
290+
if "not compiled with cert extensions" in combined:
291+
self.skipTest("cert extensions not compiled in")
292+
if "WOLFSSL_ALT_NAMES" in combined:
293+
self.skipTest("alt names not compiled in")
294+
self.assertEqual(r.returncode, 0,
295+
"inline (OpenSSL-form) subjectAltName should be "
296+
"accepted: " + combined)
297+
self.assertTrue(os.path.isfile(out) and os.path.getsize(out) > 0,
298+
"certificate should be written")
299+
# The SAN must actually be present in the issued cert.
300+
r2 = run_wolfssl("x509", "-in", out, "-text", "-noout")
301+
self.assertEqual(r2.returncode, 0, r2.stderr)
302+
san_line = self._get_san_line(r2.stdout)
303+
self.assertIsNotNone(san_line, "SAN not found in cert output")
304+
self.assertIn("DNS:example.com", san_line,
305+
"DNS SAN not applied from inline config form")
306+
self.assertIn("IP Address:10.0.0.1", san_line,
307+
"IP SAN not applied from inline config form")
308+
309+
def test_req_inline_subjectaltname_trims_whitespace(self):
310+
"""Inline subjectAltName entries are trimmed of surrounding whitespace
311+
like OpenSSL: whitespace BEFORE the comma (a trailing space on the
312+
value) and AFTER the colon must not end up in the stored name. Pins the
313+
trailing/leading trim in wolfCLU_setInlineAltNames (clu_config.c)."""
314+
conf = _tmp("test_req_inline_san_ws.conf")
315+
out = _tmp("test_req_inline_san_ws.crt")
316+
self._clean(conf, out)
317+
with open(conf, "w", encoding="utf-8", newline="\n") as f:
318+
f.write(
319+
"[ req ]\n"
320+
"distinguished_name = dn\n"
321+
"prompt = no\n"
322+
"x509_extensions = v3_ext\n"
323+
"[ dn ]\n"
324+
"commonName = inline-san-ws-test\n"
325+
"[ v3_ext ]\n"
326+
"basicConstraints = CA:FALSE\n"
327+
# space before the comma (trailing on DNS value) and after the
328+
# colon (leading on IP value)
329+
"subjectAltName = DNS:trim.example.com , IP: 10.0.0.1\n")
330+
r = run_wolfssl("req", "-new", "-x509",
331+
"-key", os.path.join(CERTS_DIR, "server-key.pem"),
332+
"-config", conf, "-out", out)
333+
combined = r.stdout + r.stderr
334+
if "not compiled with cert extensions" in combined:
335+
self.skipTest("cert extensions not compiled in")
336+
if "WOLFSSL_ALT_NAMES" in combined:
337+
self.skipTest("alt names not compiled in")
338+
self.assertEqual(r.returncode, 0,
339+
"inline subjectAltName with whitespace should be "
340+
"accepted: " + combined)
341+
r2 = run_wolfssl("x509", "-in", out, "-text", "-noout")
342+
self.assertEqual(r2.returncode, 0, r2.stderr)
343+
san_line = self._get_san_line(r2.stdout)
344+
self.assertIsNotNone(san_line, "SAN not found in cert output")
345+
# Extract the exact DNS value; a trailing space would make this
346+
# "trim.example.com " and fail the equality (assertIn would not).
347+
m = re.search(r"DNS:([^,]*)", san_line)
348+
self.assertIsNotNone(m, "DNS SAN missing: " + san_line)
349+
self.assertEqual(m.group(1), "trim.example.com",
350+
"DNS SAN not trimmed of surrounding whitespace")
351+
self.assertIn("IP Address:10.0.0.1", san_line,
352+
"IP SAN not applied/trimmed from inline config form")
353+
252354
def test_req_x509_addext_subject_alt_name(self):
253355
"""req -x509 -addext subjectAltName adds IP and DNS alt names."""
254356
crt = _tmp("test_req_addext.crt")

0 commit comments

Comments
 (0)