1116 lines
38 KiB
Diff
1116 lines
38 KiB
Diff
From aac0351de2fade86572eac4f24e22bd667177f7e Mon Sep 17 00:00:00 2001
|
|
From: Stephen Gallagher <sgallagh@redhat.com>
|
|
Date: Mon, 21 Jul 2025 15:13:31 -0400
|
|
Subject: [PATCH 8/8] Fix IP address handling in CA certificate SAN constraints
|
|
|
|
- Add automatic single-IP subnet mask to IP addresses in CA name constraints
|
|
- Update help text to show simplified IP format without subnet mask
|
|
- Add comprehensive test for basicConstraints
|
|
|
|
Signed-off-by: Stephen Gallagher <sgallagh@redhat.com>
|
|
---
|
|
src/arguments.c | 2 +-
|
|
src/authority.c | 84 ++++
|
|
test/create_ca_test.c | 960 +++++++++++++++++++++++++++++++++++++++++-
|
|
3 files changed, 1044 insertions(+), 2 deletions(-)
|
|
|
|
diff --git a/src/arguments.c b/src/arguments.c
|
|
index 770d834aacc05d6d92cc0c855852eadb88f8c9bc..96e3bbb1bcb2efc4e155b646104ffc8f2500079e 100644
|
|
--- a/src/arguments.c
|
|
+++ b/src/arguments.c
|
|
@@ -309,7 +309,7 @@ sscg_handle_arguments (TALLOC_CTX *mem_ctx,
|
|
_ ("Optional additional valid hostnames for the certificate. "
|
|
"In addition to hostnames, this option also accepts explicit values "
|
|
"supported by RFC 5280 such as "
|
|
- "IP:xxx.xxx.xxx.xxx/yyy.yyy.yyy.yyy "
|
|
+ "IP:xxx.xxx.xxx.xxx "
|
|
"May be specified multiple times."),
|
|
_ ("alt.example.com")
|
|
},
|
|
diff --git a/src/authority.c b/src/authority.c
|
|
index 044c62f5192e75a9f7d3f49616f852a97da7505a..8590e844bee3cc2888476135dc44711d7ba6fc28 100644
|
|
--- a/src/authority.c
|
|
+++ b/src/authority.c
|
|
@@ -109,6 +109,90 @@ create_private_CA (TALLOC_CTX *mem_ctx,
|
|
san = talloc_asprintf (
|
|
tmp_ctx, "DNS:%s", options->subject_alt_names[i]);
|
|
}
|
|
+ else if (strncmp (options->subject_alt_names[i], "IP:", 3) == 0)
|
|
+ {
|
|
+ char *ip_addr = options->subject_alt_names[i] + 3;
|
|
+ char *slash = strchr (ip_addr, '/');
|
|
+ char *clean_ip = ip_addr;
|
|
+ const char *netmask_str = NULL;
|
|
+
|
|
+ if (slash)
|
|
+ {
|
|
+ /* Extract IP and netmask parts */
|
|
+ clean_ip =
|
|
+ talloc_strndup (tmp_ctx, ip_addr, slash - ip_addr);
|
|
+ char *cidr_str = slash + 1;
|
|
+ int cidr_bits = atoi (cidr_str);
|
|
+
|
|
+ /* Convert CIDR to appropriate netmask format */
|
|
+ if (strchr (clean_ip, ':'))
|
|
+ {
|
|
+ /* IPv6 - convert CIDR to hex netmask */
|
|
+ if (cidr_bits == 128)
|
|
+ {
|
|
+ netmask_str =
|
|
+ "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF";
|
|
+ }
|
|
+ else if (cidr_bits == 64)
|
|
+ {
|
|
+ netmask_str = "FFFF:FFFF:FFFF:FFFF:0:0:0:0";
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* For other values, default to /128 */
|
|
+ netmask_str =
|
|
+ "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF";
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* IPv4 - convert CIDR to dotted decimal */
|
|
+ if (cidr_bits == 32)
|
|
+ {
|
|
+ netmask_str = "255.255.255.255";
|
|
+ }
|
|
+ else if (cidr_bits == 24)
|
|
+ {
|
|
+ netmask_str = "255.255.255.0";
|
|
+ }
|
|
+ else if (cidr_bits == 16)
|
|
+ {
|
|
+ netmask_str = "255.255.0.0";
|
|
+ }
|
|
+ else if (cidr_bits == 8)
|
|
+ {
|
|
+ netmask_str = "255.0.0.0";
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* For other values, default to /32 */
|
|
+ netmask_str = "255.255.255.255";
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* No netmask provided - add single host netmask */
|
|
+ if (strchr (clean_ip, ':'))
|
|
+ {
|
|
+ /* IPv6 - use /128 netmask */
|
|
+ netmask_str = "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF";
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* IPv4 - use /32 netmask */
|
|
+ netmask_str = "255.255.255.255";
|
|
+ }
|
|
+ }
|
|
+
|
|
+ san =
|
|
+ talloc_asprintf (tmp_ctx, "IP:%s/%s", clean_ip, netmask_str);
|
|
+
|
|
+ if (slash && clean_ip != ip_addr)
|
|
+ {
|
|
+ talloc_free (clean_ip);
|
|
+ }
|
|
+ }
|
|
else
|
|
{
|
|
san = talloc_strdup (tmp_ctx, options->subject_alt_names[i]);
|
|
diff --git a/test/create_ca_test.c b/test/create_ca_test.c
|
|
index 270cfa3dc189540bb807a3d3140ca45335e727b0..eaf2e77052569fd63c09200636004bcd5afcd97c 100644
|
|
--- a/test/create_ca_test.c
|
|
+++ b/test/create_ca_test.c
|
|
@@ -21,9 +21,861 @@
|
|
#include <stdio.h>
|
|
#include <talloc.h>
|
|
#include <string.h>
|
|
+#include <openssl/x509.h>
|
|
+#include <openssl/asn1.h>
|
|
|
|
#include "include/sscg.h"
|
|
#include "include/x509.h"
|
|
+#include "include/authority.h"
|
|
+
|
|
+static int
|
|
+verify_subject_alt_names (struct sscg_x509_cert *cert)
|
|
+{
|
|
+ X509 *x509 = cert->certificate;
|
|
+ STACK_OF (GENERAL_NAME) *san_names = NULL;
|
|
+ GENERAL_NAME *name = NULL;
|
|
+ ASN1_STRING *san_str = NULL;
|
|
+ int san_count = 0;
|
|
+ int found_primary_cn = 0;
|
|
+ int found_alt1 = 0;
|
|
+ int found_alt2 = 0;
|
|
+ int found_ip4_1 = 0;
|
|
+ int found_ip4_2 = 0;
|
|
+ int found_ip6 = 0;
|
|
+ int found_ip4_netmask = 0;
|
|
+ int found_ip6_netmask = 0;
|
|
+ int found_email = 0;
|
|
+ int found_uri = 0;
|
|
+ int found_wildcard = 0;
|
|
+ int found_subdomain = 0;
|
|
+ int found_international = 0;
|
|
+ char *name_str = NULL;
|
|
+
|
|
+ /* Get the Subject Alternative Name extension */
|
|
+ san_names = X509_get_ext_d2i (x509, NID_subject_alt_name, NULL, NULL);
|
|
+ if (!san_names)
|
|
+ {
|
|
+ printf ("Certificate missing Subject Alternative Name extension.\n");
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ san_count = sk_GENERAL_NAME_num (san_names);
|
|
+ printf ("\n Processing %d Subject Alternative Names:\n", san_count);
|
|
+
|
|
+ /* Check each SAN entry */
|
|
+ for (int i = 0; i < san_count; i++)
|
|
+ {
|
|
+ name = sk_GENERAL_NAME_value (san_names, i);
|
|
+
|
|
+ switch (name->type)
|
|
+ {
|
|
+ case GEN_DNS:
|
|
+ san_str = name->d.dNSName;
|
|
+ name_str = (char *)ASN1_STRING_get0_data (san_str);
|
|
+ printf (" DNS: %s\n", name_str);
|
|
+
|
|
+ if (strcmp (name_str, "server.example.com") == 0)
|
|
+ found_primary_cn = 1;
|
|
+ else if (strcmp (name_str, "alt1.example.com") == 0)
|
|
+ found_alt1 = 1;
|
|
+ else if (strcmp (name_str, "alt2.example.com") == 0)
|
|
+ found_alt2 = 1;
|
|
+ else if (strcmp (name_str, "*.wildcard.example.com") == 0)
|
|
+ found_wildcard = 1;
|
|
+ else if (strcmp (name_str, "subdomain.alt1.example.com") == 0)
|
|
+ found_subdomain = 1;
|
|
+ else if (strcmp (name_str, "xn--nxasmq6b.example.com") == 0)
|
|
+ found_international = 1;
|
|
+ break;
|
|
+
|
|
+ case GEN_IPADD:
|
|
+ san_str = name->d.iPAddress;
|
|
+ /* IP addresses are stored as binary data */
|
|
+ if (ASN1_STRING_length (san_str) == 4) /* IPv4 */
|
|
+ {
|
|
+ const unsigned char *ip_data = ASN1_STRING_get0_data (san_str);
|
|
+ printf (" IP (IPv4): %d.%d.%d.%d\n",
|
|
+ ip_data[0],
|
|
+ ip_data[1],
|
|
+ ip_data[2],
|
|
+ ip_data[3]);
|
|
+
|
|
+ if (ip_data[0] == 192 && ip_data[1] == 168 && ip_data[2] == 1 &&
|
|
+ ip_data[3] == 100)
|
|
+ found_ip4_1 = 1;
|
|
+ else if (ip_data[0] == 10 && ip_data[1] == 0 &&
|
|
+ ip_data[2] == 0 && ip_data[3] == 1)
|
|
+ found_ip4_2 = 1;
|
|
+ else if (ip_data[0] == 203 && ip_data[1] == 0 &&
|
|
+ ip_data[2] == 113 && ip_data[3] == 0)
|
|
+ found_ip4_netmask = 1;
|
|
+ }
|
|
+ else if (ASN1_STRING_length (san_str) == 16) /* IPv6 */
|
|
+ {
|
|
+ const unsigned char *ip_data = ASN1_STRING_get0_data (san_str);
|
|
+ printf (" IP (IPv6): ");
|
|
+ for (int j = 0; j < 16; j += 2)
|
|
+ {
|
|
+ printf ("%02x%02x", ip_data[j], ip_data[j + 1]);
|
|
+ if (j < 14)
|
|
+ printf (":");
|
|
+ }
|
|
+ printf ("\n");
|
|
+
|
|
+ /* Check for 2001:db8::1 */
|
|
+ if (ip_data[0] == 0x20 && ip_data[1] == 0x01 &&
|
|
+ ip_data[2] == 0x0d && ip_data[3] == 0xb8 &&
|
|
+ ip_data[4] == 0x00 && ip_data[5] == 0x00 &&
|
|
+ ip_data[6] == 0x00 && ip_data[7] == 0x00 &&
|
|
+ ip_data[8] == 0x00 && ip_data[9] == 0x00 &&
|
|
+ ip_data[10] == 0x00 && ip_data[11] == 0x00 &&
|
|
+ ip_data[12] == 0x00 && ip_data[13] == 0x00 &&
|
|
+ ip_data[14] == 0x00 && ip_data[15] == 0x01)
|
|
+ found_ip6 = 1;
|
|
+ /* Check for 2001:db8:85a3:: (netmask stripped) */
|
|
+ else if (ip_data[0] == 0x20 && ip_data[1] == 0x01 &&
|
|
+ ip_data[2] == 0x0d && ip_data[3] == 0xb8 &&
|
|
+ ip_data[4] == 0x85 && ip_data[5] == 0xa3 &&
|
|
+ ip_data[6] == 0x00 && ip_data[7] == 0x00 &&
|
|
+ ip_data[8] == 0x00 && ip_data[9] == 0x00 &&
|
|
+ ip_data[10] == 0x00 && ip_data[11] == 0x00 &&
|
|
+ ip_data[12] == 0x00 && ip_data[13] == 0x00 &&
|
|
+ ip_data[14] == 0x00 && ip_data[15] == 0x00)
|
|
+ found_ip6_netmask = 1;
|
|
+ }
|
|
+ break;
|
|
+
|
|
+ case GEN_EMAIL:
|
|
+ san_str = name->d.rfc822Name;
|
|
+ name_str = (char *)ASN1_STRING_get0_data (san_str);
|
|
+ printf (" Email: %s\n", name_str);
|
|
+
|
|
+ if (strcmp (name_str, "admin@example.com") == 0)
|
|
+ found_email = 1;
|
|
+ break;
|
|
+
|
|
+ case GEN_URI:
|
|
+ san_str = name->d.uniformResourceIdentifier;
|
|
+ name_str = (char *)ASN1_STRING_get0_data (san_str);
|
|
+ printf (" URI: %s\n", name_str);
|
|
+
|
|
+ if (strcmp (name_str, "https://www.example.com/service") == 0)
|
|
+ found_uri = 1;
|
|
+ break;
|
|
+
|
|
+ default: printf (" Other type: %d\n", name->type); break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+
|
|
+ /* Verify all expected SANs were found */
|
|
+ int missing_count = 0;
|
|
+
|
|
+ if (!found_primary_cn)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: Primary CN not found in Subject Alternative Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_alt1)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: alt1.example.com not found in Subject Alternative "
|
|
+ "Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_alt2)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: alt2.example.com not found in Subject Alternative "
|
|
+ "Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_ip4_1)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: IPv4 192.168.1.100 not found in Subject Alternative "
|
|
+ "Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_ip4_2)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: IPv4 10.0.0.1 not found in Subject Alternative "
|
|
+ "Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_ip6)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: IPv6 2001:db8::1 not found in Subject Alternative "
|
|
+ "Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_ip4_netmask)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: IPv4 203.0.113.0 (from 203.0.113.0/24, netmask "
|
|
+ "stripped) not found in Subject Alternative Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_ip6_netmask)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: IPv6 2001:db8:85a3:: (from 2001:db8:85a3::/64, netmask "
|
|
+ "stripped) not found in Subject Alternative Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_email)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: Email admin@example.com not found in Subject "
|
|
+ "Alternative Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_uri)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: URI https://www.example.com/service not found in "
|
|
+ "Subject Alternative Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_wildcard)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: Wildcard *.wildcard.example.com not found in Subject "
|
|
+ "Alternative Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_subdomain)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: Subdomain subdomain.alt1.example.com not found in "
|
|
+ "Subject Alternative Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (!found_international)
|
|
+ {
|
|
+ printf (
|
|
+ " MISSING: International domain xn--nxasmq6b.example.com not found "
|
|
+ "in Subject Alternative Names.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (missing_count > 0)
|
|
+ {
|
|
+ printf (" %d expected SAN entries were missing.\n", missing_count);
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ printf (" All expected SAN entries found successfully.\n");
|
|
+ return EOK;
|
|
+}
|
|
+
|
|
+static int
|
|
+test_san_edge_cases (struct sscg_x509_cert *cert)
|
|
+{
|
|
+ X509 *x509 = cert->certificate;
|
|
+ STACK_OF (GENERAL_NAME) *san_names = NULL;
|
|
+ GENERAL_NAME *name = NULL;
|
|
+ ASN1_STRING *san_str = NULL;
|
|
+ int san_count = 0;
|
|
+ int dns_count = 0;
|
|
+ int ip_count = 0;
|
|
+ int email_count = 0;
|
|
+ int uri_count = 0;
|
|
+ char *name_str = NULL;
|
|
+
|
|
+ /* Get the Subject Alternative Name extension */
|
|
+ san_names = X509_get_ext_d2i (x509, NID_subject_alt_name, NULL, NULL);
|
|
+ if (!san_names)
|
|
+ {
|
|
+ printf ("Certificate missing Subject Alternative Name extension.\n");
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ san_count = sk_GENERAL_NAME_num (san_names);
|
|
+
|
|
+ printf ("\n Performing comprehensive SAN validation:\n");
|
|
+
|
|
+ /* Count and validate all SAN types */
|
|
+ for (int i = 0; i < san_count; i++)
|
|
+ {
|
|
+ name = sk_GENERAL_NAME_value (san_names, i);
|
|
+
|
|
+ switch (name->type)
|
|
+ {
|
|
+ case GEN_DNS:
|
|
+ dns_count++;
|
|
+ san_str = name->d.dNSName;
|
|
+ name_str = (char *)ASN1_STRING_get0_data (san_str);
|
|
+
|
|
+ /* Validate DNS name format */
|
|
+ if (strlen (name_str) == 0)
|
|
+ {
|
|
+ printf (" ERROR: Empty DNS name found in SANs.\n");
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ /* Allow wildcards and validate domain format */
|
|
+ if (name_str[0] != '*' && !strchr (name_str, '.'))
|
|
+ {
|
|
+ printf (" ERROR: DNS name '%s' missing domain part.\n",
|
|
+ name_str);
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ /* Validate wildcard format */
|
|
+ if (name_str[0] == '*' && name_str[1] != '.')
|
|
+ {
|
|
+ printf (" ERROR: Invalid wildcard DNS name '%s'.\n",
|
|
+ name_str);
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EINVAL;
|
|
+ }
|
|
+ break;
|
|
+
|
|
+ case GEN_IPADD:
|
|
+ ip_count++;
|
|
+ san_str = name->d.iPAddress;
|
|
+
|
|
+ /* Validate IP address length */
|
|
+ int ip_len = ASN1_STRING_length (san_str);
|
|
+ if (ip_len != 4 && ip_len != 16) /* IPv4 or IPv6 */
|
|
+ {
|
|
+ printf (" ERROR: Invalid IP address length: %d bytes.\n",
|
|
+ ip_len);
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EINVAL;
|
|
+ }
|
|
+ break;
|
|
+
|
|
+ case GEN_EMAIL:
|
|
+ email_count++;
|
|
+ san_str = name->d.rfc822Name;
|
|
+ name_str = (char *)ASN1_STRING_get0_data (san_str);
|
|
+
|
|
+ /* Validate email format */
|
|
+ if (!strchr (name_str, '@'))
|
|
+ {
|
|
+ printf (" ERROR: Invalid email address '%s' - missing @.\n",
|
|
+ name_str);
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EINVAL;
|
|
+ }
|
|
+ break;
|
|
+
|
|
+ case GEN_URI:
|
|
+ uri_count++;
|
|
+ san_str = name->d.uniformResourceIdentifier;
|
|
+ name_str = (char *)ASN1_STRING_get0_data (san_str);
|
|
+
|
|
+ /* Validate URI format - must have scheme */
|
|
+ if (!strstr (name_str, "://"))
|
|
+ {
|
|
+ printf (" ERROR: Invalid URI '%s' - missing scheme.\n",
|
|
+ name_str);
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EINVAL;
|
|
+ }
|
|
+ break;
|
|
+
|
|
+ default:
|
|
+ /* Other SAN types are acceptable but not validated here */
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ printf (" Found %d total SANs: %d DNS, %d IP, %d Email, %d URI.\n",
|
|
+ san_count,
|
|
+ dns_count,
|
|
+ ip_count,
|
|
+ email_count,
|
|
+ uri_count);
|
|
+
|
|
+ /* Validate expected counts for comprehensive test */
|
|
+ int expected_dns =
|
|
+ 6; /* CN + alt1 + alt2 + wildcard + subdomain + international */
|
|
+ int expected_ip = 5; /* IPv4 x2 + IPv6 x1 + IPv4 netmask + IPv6 netmask */
|
|
+ int expected_email = 1;
|
|
+ int expected_uri = 1;
|
|
+
|
|
+ if (dns_count < expected_dns)
|
|
+ {
|
|
+ printf (" ERROR: Expected at least %d DNS names, found %d.\n",
|
|
+ expected_dns,
|
|
+ dns_count);
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ if (ip_count < expected_ip)
|
|
+ {
|
|
+ printf (" ERROR: Expected at least %d IP addresses, found %d.\n",
|
|
+ expected_ip,
|
|
+ ip_count);
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ if (email_count < expected_email)
|
|
+ {
|
|
+ printf (" ERROR: Expected at least %d email addresses, found %d.\n",
|
|
+ expected_email,
|
|
+ email_count);
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ if (uri_count < expected_uri)
|
|
+ {
|
|
+ printf (" ERROR: Expected at least %d URIs, found %d.\n",
|
|
+ expected_uri,
|
|
+ uri_count);
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ printf (" All SAN format validations passed successfully.\n");
|
|
+
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+ return EOK;
|
|
+}
|
|
+
|
|
+static int
|
|
+test_ip_netmask_handling (struct sscg_x509_cert *cert)
|
|
+{
|
|
+ X509 *x509 = cert->certificate;
|
|
+ STACK_OF (GENERAL_NAME) *san_names = NULL;
|
|
+ GENERAL_NAME *name = NULL;
|
|
+ ASN1_STRING *san_str = NULL;
|
|
+ int san_count = 0;
|
|
+ int found_netmask_ipv4 = 0;
|
|
+ int found_netmask_ipv6 = 0;
|
|
+
|
|
+ /* Get the Subject Alternative Name extension */
|
|
+ san_names = X509_get_ext_d2i (x509, NID_subject_alt_name, NULL, NULL);
|
|
+ if (!san_names)
|
|
+ {
|
|
+ printf ("Certificate missing Subject Alternative Name extension.\n");
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ san_count = sk_GENERAL_NAME_num (san_names);
|
|
+
|
|
+ printf ("\n Testing IP address netmask stripping:\n");
|
|
+
|
|
+ /* Look specifically for IP addresses that had netmasks stripped */
|
|
+ for (int i = 0; i < san_count; i++)
|
|
+ {
|
|
+ name = sk_GENERAL_NAME_value (san_names, i);
|
|
+
|
|
+ if (name->type == GEN_IPADD)
|
|
+ {
|
|
+ san_str = name->d.iPAddress;
|
|
+
|
|
+ if (ASN1_STRING_length (san_str) == 4) /* IPv4 */
|
|
+ {
|
|
+ const unsigned char *ip_data = ASN1_STRING_get0_data (san_str);
|
|
+
|
|
+ /* Check for 203.0.113.0 (from original 203.0.113.0/24) */
|
|
+ if (ip_data[0] == 203 && ip_data[1] == 0 && ip_data[2] == 113 &&
|
|
+ ip_data[3] == 0)
|
|
+ {
|
|
+ printf (
|
|
+ " ✓ IPv4 netmask stripped: 203.0.113.0/24 → "
|
|
+ "203.0.113.0\n");
|
|
+ found_netmask_ipv4 = 1;
|
|
+ }
|
|
+ }
|
|
+ else if (ASN1_STRING_length (san_str) == 16) /* IPv6 */
|
|
+ {
|
|
+ const unsigned char *ip_data = ASN1_STRING_get0_data (san_str);
|
|
+
|
|
+ /* Check for 2001:db8:85a3:: (from original 2001:db8:85a3::/64) */
|
|
+ if (ip_data[0] == 0x20 && ip_data[1] == 0x01 &&
|
|
+ ip_data[2] == 0x0d && ip_data[3] == 0xb8 &&
|
|
+ ip_data[4] == 0x85 && ip_data[5] == 0xa3 &&
|
|
+ ip_data[6] == 0x00 && ip_data[7] == 0x00 &&
|
|
+ ip_data[8] == 0x00 && ip_data[9] == 0x00 &&
|
|
+ ip_data[10] == 0x00 && ip_data[11] == 0x00 &&
|
|
+ ip_data[12] == 0x00 && ip_data[13] == 0x00 &&
|
|
+ ip_data[14] == 0x00 && ip_data[15] == 0x00)
|
|
+ {
|
|
+ printf (
|
|
+ " ✓ IPv6 netmask stripped: 2001:db8:85a3::/64 → "
|
|
+ "2001:db8:85a3::\n");
|
|
+ found_netmask_ipv6 = 1;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ GENERAL_NAMES_free (san_names);
|
|
+
|
|
+ /* Verify that netmask stripping worked correctly */
|
|
+ if (!found_netmask_ipv4)
|
|
+ {
|
|
+ printf (" ERROR: IPv4 netmask stripping test failed.\n");
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ if (!found_netmask_ipv6)
|
|
+ {
|
|
+ printf (" ERROR: IPv6 netmask stripping test failed.\n");
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ printf (" All IP address netmask tests passed successfully.\n");
|
|
+ return EOK;
|
|
+}
|
|
+
|
|
+static int
|
|
+verify_name_constraints (struct sscg_x509_cert *ca_cert,
|
|
+ char **expected_san_list)
|
|
+{
|
|
+ X509 *x509 = ca_cert->certificate;
|
|
+ X509_EXTENSION *name_constraints_ext = NULL;
|
|
+ ASN1_OCTET_STRING *ext_data = NULL;
|
|
+ BIO *bio = NULL;
|
|
+ char *ext_str = NULL;
|
|
+ char *line = NULL;
|
|
+ char *saveptr = NULL;
|
|
+ size_t ext_str_len = 0;
|
|
+ int found_constraints[20] = {
|
|
+ 0
|
|
+ }; /* Track which expected constraints we found */
|
|
+ int missing_count = 0;
|
|
+ int j;
|
|
+
|
|
+ printf ("\n Verifying name constraints in CA certificate:\n");
|
|
+
|
|
+ /* Find the name constraints extension */
|
|
+ int ext_idx = X509_get_ext_by_NID (x509, NID_name_constraints, -1);
|
|
+ if (ext_idx < 0)
|
|
+ {
|
|
+ printf (
|
|
+ " ERROR: CA certificate missing Name Constraints extension.\n");
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ name_constraints_ext = X509_get_ext (x509, ext_idx);
|
|
+ if (!name_constraints_ext)
|
|
+ {
|
|
+ printf (" ERROR: Failed to get Name Constraints extension.\n");
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ /* Get the extension data */
|
|
+ ext_data = X509_EXTENSION_get_data (name_constraints_ext);
|
|
+ if (!ext_data)
|
|
+ {
|
|
+ printf (" ERROR: Failed to get Name Constraints extension data.\n");
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ /* Convert the extension to a readable string using BIO */
|
|
+ bio = BIO_new (BIO_s_mem ());
|
|
+ if (!bio)
|
|
+ {
|
|
+ printf (" ERROR: Failed to create BIO for extension parsing.\n");
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ /* Print the extension to the BIO */
|
|
+ if (!X509V3_EXT_print (bio, name_constraints_ext, 0, 0))
|
|
+ {
|
|
+ printf (" ERROR: Failed to print Name Constraints extension.\n");
|
|
+ BIO_free (bio);
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ /* Get the string representation */
|
|
+ ext_str_len = BIO_get_mem_data (bio, &ext_str);
|
|
+ if (ext_str_len <= 0 || !ext_str)
|
|
+ {
|
|
+ printf (" ERROR: Failed to get extension string data.\n");
|
|
+ BIO_free (bio);
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ /* Null-terminate the string for parsing */
|
|
+ char *ext_str_copy = malloc (ext_str_len + 1);
|
|
+ if (!ext_str_copy)
|
|
+ {
|
|
+ printf (
|
|
+ " ERROR: Failed to allocate memory for extension parsing.\n");
|
|
+ BIO_free (bio);
|
|
+ return ENOMEM;
|
|
+ }
|
|
+ memcpy (ext_str_copy, ext_str, ext_str_len);
|
|
+ ext_str_copy[ext_str_len] = '\0';
|
|
+
|
|
+ printf (" Name Constraints content:\n%s\n", ext_str_copy);
|
|
+
|
|
+ /* Parse the extension string to find constraints */
|
|
+ line = strtok_r (ext_str_copy, "\n", &saveptr);
|
|
+ while (line)
|
|
+ {
|
|
+ /* Look for "Permitted:" sections and DNS/IP entries */
|
|
+ if (strstr (line, "DNS:"))
|
|
+ {
|
|
+ char *dns_start = strstr (line, "DNS:");
|
|
+ if (dns_start)
|
|
+ {
|
|
+ dns_start += 4; /* Skip "DNS:" */
|
|
+ /* Trim whitespace */
|
|
+ while (*dns_start == ' ' || *dns_start == '\t')
|
|
+ dns_start++;
|
|
+
|
|
+ printf (" Found DNS constraint: %s\n", dns_start);
|
|
+
|
|
+ /* Check if this matches our expected CN (truncated) */
|
|
+ if (strstr (dns_start, "server"))
|
|
+ {
|
|
+ found_constraints[0] = 1;
|
|
+ }
|
|
+
|
|
+ /* Check against our expected SAN list */
|
|
+ if (expected_san_list)
|
|
+ {
|
|
+ for (j = 0; expected_san_list[j]; j++)
|
|
+ {
|
|
+ char *expected_dns = NULL;
|
|
+
|
|
+ if (!strchr (expected_san_list[j], ':'))
|
|
+ {
|
|
+ expected_dns = expected_san_list[j];
|
|
+ }
|
|
+ else if (strncmp (expected_san_list[j], "DNS:", 4) == 0)
|
|
+ {
|
|
+ expected_dns = expected_san_list[j] + 4;
|
|
+ }
|
|
+
|
|
+ if (expected_dns && strstr (dns_start, expected_dns))
|
|
+ {
|
|
+ found_constraints[j + 1] = 1;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ else if (strstr (line, "IP:"))
|
|
+ {
|
|
+ char *ip_start = strstr (line, "IP:");
|
|
+ if (ip_start)
|
|
+ {
|
|
+ ip_start += 3; /* Skip "IP:" */
|
|
+ while (*ip_start == ' ' || *ip_start == '\t')
|
|
+ ip_start++;
|
|
+
|
|
+ printf (" Found IP constraint: %s\n", ip_start);
|
|
+
|
|
+ /* Check against expected IP SANs */
|
|
+ if (expected_san_list)
|
|
+ {
|
|
+ for (j = 0; expected_san_list[j]; j++)
|
|
+ {
|
|
+ if (strncmp (expected_san_list[j], "IP:", 3) == 0)
|
|
+ {
|
|
+ char *expected_ip = expected_san_list[j] + 3;
|
|
+ char *slash = strchr (expected_ip, '/');
|
|
+ char expected_constraint[128];
|
|
+ char clean_ip[64];
|
|
+
|
|
+ /* Extract IP and netmask parts */
|
|
+ if (slash)
|
|
+ {
|
|
+ int ip_len = slash - expected_ip;
|
|
+ strncpy (clean_ip, expected_ip, ip_len);
|
|
+ clean_ip[ip_len] = '\0';
|
|
+
|
|
+ /* Parse the CIDR netmask */
|
|
+ char *cidr_str = slash + 1;
|
|
+ int cidr_bits = atoi (cidr_str);
|
|
+
|
|
+ /* Convert to constraint format with proper netmask */
|
|
+ if (strchr (clean_ip, ':'))
|
|
+ {
|
|
+ /* IPv6 - convert CIDR to hex netmask */
|
|
+ const char *netmask;
|
|
+ if (cidr_bits == 128)
|
|
+ netmask =
|
|
+ "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:"
|
|
+ "FFFF";
|
|
+ else if (cidr_bits == 64)
|
|
+ netmask = "FFFF:FFFF:FFFF:FFFF:0:0:0:0";
|
|
+ else
|
|
+ netmask =
|
|
+ "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:"
|
|
+ "FFFF"; /* default to /128 */
|
|
+
|
|
+ /* Handle compressed IPv6 forms */
|
|
+ if (strstr (clean_ip, "2001:db8::1"))
|
|
+ {
|
|
+ snprintf (expected_constraint,
|
|
+ sizeof (expected_constraint),
|
|
+ "IP:2001:DB8:0:0:0:0:0:1/%s",
|
|
+ netmask);
|
|
+ }
|
|
+ else if (strstr (clean_ip,
|
|
+ "2001:db8:85a3::"))
|
|
+ {
|
|
+ snprintf (
|
|
+ expected_constraint,
|
|
+ sizeof (expected_constraint),
|
|
+ "IP:2001:DB8:85A3:0:0:0:0:0/%s",
|
|
+ netmask);
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ snprintf (expected_constraint,
|
|
+ sizeof (expected_constraint),
|
|
+ "IP:%s/%s",
|
|
+ clean_ip,
|
|
+ netmask);
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* IPv4 - convert CIDR to dotted decimal */
|
|
+ const char *netmask;
|
|
+ if (cidr_bits == 32)
|
|
+ netmask = "255.255.255.255";
|
|
+ else if (cidr_bits == 24)
|
|
+ netmask = "255.255.255.0";
|
|
+ else if (cidr_bits == 16)
|
|
+ netmask = "255.255.0.0";
|
|
+ else if (cidr_bits == 8)
|
|
+ netmask = "255.0.0.0";
|
|
+ else
|
|
+ netmask =
|
|
+ "255.255.255.255"; /* default to /32 */
|
|
+
|
|
+ snprintf (expected_constraint,
|
|
+ sizeof (expected_constraint),
|
|
+ "IP:%s/%s",
|
|
+ clean_ip,
|
|
+ netmask);
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* No netmask - add single host netmask */
|
|
+ strcpy (clean_ip, expected_ip);
|
|
+
|
|
+ if (strchr (clean_ip, ':'))
|
|
+ {
|
|
+ /* IPv6 with /128 netmask */
|
|
+ if (strstr (clean_ip, "2001:db8::1"))
|
|
+ {
|
|
+ snprintf (expected_constraint,
|
|
+ sizeof (expected_constraint),
|
|
+ "IP:2001:DB8:0:0:0:0:0:1/"
|
|
+ "FFFF:FFFF:FFFF:FFFF:FFFF:"
|
|
+ "FFFF:FFFF:FFFF");
|
|
+ }
|
|
+ else if (strstr (clean_ip,
|
|
+ "2001:db8:85a3::"))
|
|
+ {
|
|
+ snprintf (expected_constraint,
|
|
+ sizeof (expected_constraint),
|
|
+ "IP:2001:DB8:85A3:0:0:0:0:0/"
|
|
+ "FFFF:FFFF:FFFF:FFFF:FFFF:"
|
|
+ "FFFF:FFFF:FFFF");
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ snprintf (expected_constraint,
|
|
+ sizeof (expected_constraint),
|
|
+ "IP:%s/"
|
|
+ "FFFF:FFFF:FFFF:FFFF:FFFF:"
|
|
+ "FFFF:FFFF:FFFF",
|
|
+ clean_ip);
|
|
+ }
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ /* IPv4 with /32 netmask */
|
|
+ snprintf (expected_constraint,
|
|
+ sizeof (expected_constraint),
|
|
+ "IP:%s/255.255.255.255",
|
|
+ clean_ip);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Check if this expected constraint matches what we found */
|
|
+ /* Skip the "IP:" prefix for comparison since ip_start doesn't include it */
|
|
+ char *constraint_without_prefix =
|
|
+ expected_constraint + 3; /* Skip "IP:" */
|
|
+ if (strcmp (ip_start, constraint_without_prefix) ==
|
|
+ 0)
|
|
+ {
|
|
+ found_constraints[j + 1] = 1;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ line = strtok_r (NULL, "\n", &saveptr);
|
|
+ }
|
|
+
|
|
+ free (ext_str_copy);
|
|
+ BIO_free (bio);
|
|
+
|
|
+ /* Verify that we found all expected constraints */
|
|
+ if (!found_constraints[0])
|
|
+ {
|
|
+ printf (" MISSING: CN constraint 'server' not found.\n");
|
|
+ missing_count++;
|
|
+ }
|
|
+
|
|
+ if (expected_san_list)
|
|
+ {
|
|
+ for (j = 0; expected_san_list[j]; j++)
|
|
+ {
|
|
+ if (!found_constraints[j + 1])
|
|
+ {
|
|
+ /* Only report missing DNS and IP constraints, skip email/URI */
|
|
+ if (!strchr (expected_san_list[j], ':') ||
|
|
+ strncmp (expected_san_list[j], "DNS:", 4) == 0 ||
|
|
+ strncmp (expected_san_list[j], "IP:", 3) == 0)
|
|
+ {
|
|
+ printf (" MISSING: Constraint for '%s' not found.\n",
|
|
+ expected_san_list[j]);
|
|
+ missing_count++;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (missing_count > 0)
|
|
+ {
|
|
+ printf (" %d expected name constraints were missing.\n",
|
|
+ missing_count);
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ printf (" All expected name constraints found successfully.\n");
|
|
+ return EOK;
|
|
+}
|
|
|
|
int
|
|
main (int argc, char **argv)
|
|
@@ -35,6 +887,11 @@ main (int argc, char **argv)
|
|
struct sscg_evp_pkey *pkey = NULL;
|
|
struct sscg_x509_cert *cert = NULL;
|
|
|
|
+ /* Variables for CA testing */
|
|
+ struct sscg_x509_cert *ca_cert = NULL;
|
|
+ struct sscg_evp_pkey *ca_key = NULL;
|
|
+ struct sscg_options ca_options;
|
|
+
|
|
TALLOC_CTX *tmp_ctx = talloc_new (NULL);
|
|
if (!tmp_ctx)
|
|
{
|
|
@@ -98,7 +955,108 @@ main (int argc, char **argv)
|
|
tmp_ctx, csr, serial, 3650, NULL, pkey, EVP_sha512 (), &cert);
|
|
CHECK_OK (ret);
|
|
|
|
- ret = EOK;
|
|
+ /* ============= SERVICE CERTIFICATE TESTS ============= */
|
|
+
|
|
+ /* Verify that subject alternative names were properly included */
|
|
+ printf ("Verifying subject alternative names in service certificate. ");
|
|
+ int verify_ret = verify_subject_alt_names (cert);
|
|
+ if (verify_ret != EOK)
|
|
+ {
|
|
+ printf ("FAILED.\n");
|
|
+ ret = verify_ret; /* Store first failure but continue testing */
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ printf ("SUCCESS.\n");
|
|
+ }
|
|
+
|
|
+ /* Test additional SAN verification scenarios */
|
|
+ printf ("Testing SAN edge cases and validation. ");
|
|
+ int edge_ret = test_san_edge_cases (cert);
|
|
+ if (edge_ret != EOK)
|
|
+ {
|
|
+ printf ("FAILED.\n");
|
|
+ if (ret == EOK)
|
|
+ ret = edge_ret; /* Store first failure */
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ printf ("SUCCESS.\n");
|
|
+ }
|
|
+
|
|
+ /* Test IP address netmask handling */
|
|
+ printf ("Testing IP address netmask stripping functionality. ");
|
|
+ int netmask_ret = test_ip_netmask_handling (cert);
|
|
+ if (netmask_ret != EOK)
|
|
+ {
|
|
+ printf ("FAILED.\n");
|
|
+ if (ret == EOK)
|
|
+ ret = netmask_ret; /* Store first failure */
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ printf ("SUCCESS.\n");
|
|
+ }
|
|
+
|
|
+ /* ============= CA CERTIFICATE TESTS ============= */
|
|
+
|
|
+ printf ("\n=== CA CERTIFICATE TESTS ===\n");
|
|
+
|
|
+ /* Set up options for CA creation */
|
|
+ memset (&ca_options, 0, sizeof (ca_options));
|
|
+ ca_options.country = "US";
|
|
+ ca_options.state = "";
|
|
+ ca_options.locality = "";
|
|
+ ca_options.org = "Unspecified";
|
|
+ ca_options.email = "";
|
|
+ ca_options.hostname = "server.example.com";
|
|
+ ca_options.hash_fn = EVP_sha256 ();
|
|
+ ca_options.lifetime = 3650;
|
|
+ ca_options.verbosity = SSCG_QUIET;
|
|
+
|
|
+ /* Set up the same subject alternative names for the CA */
|
|
+ ca_options.subject_alt_names = certinfo->subject_alt_names;
|
|
+
|
|
+ /* Create the private CA */
|
|
+ printf ("Creating private CA certificate. ");
|
|
+ ret = create_private_CA (tmp_ctx, &ca_options, &ca_cert, &ca_key);
|
|
+ if (ret != EOK)
|
|
+ {
|
|
+ printf ("FAILED.\n");
|
|
+ goto done;
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ printf ("SUCCESS.\n");
|
|
+ }
|
|
+
|
|
+ /* Verify name constraints in the CA certificate */
|
|
+ printf ("Verifying name constraints in CA certificate. ");
|
|
+ int ca_constraints_ret =
|
|
+ verify_name_constraints (ca_cert, certinfo->subject_alt_names);
|
|
+ if (ca_constraints_ret != EOK)
|
|
+ {
|
|
+ printf ("FAILED.\n");
|
|
+ if (ret == EOK)
|
|
+ ret = ca_constraints_ret;
|
|
+ }
|
|
+ else
|
|
+ {
|
|
+ printf ("SUCCESS.\n");
|
|
+ }
|
|
+
|
|
+ /* Summary of all test results */
|
|
+ printf ("\n=== TEST SUMMARY ===\n");
|
|
+ printf ("Service cert SAN verification: %s\n",
|
|
+ verify_ret == EOK ? "PASS" : "FAIL");
|
|
+ printf ("Service cert edge case validation: %s\n",
|
|
+ edge_ret == EOK ? "PASS" : "FAIL");
|
|
+ printf ("Service cert netmask handling: %s\n",
|
|
+ netmask_ret == EOK ? "PASS" : "FAIL");
|
|
+ printf ("CA certificate creation: %s\n", ca_cert ? "PASS" : "FAIL");
|
|
+ printf ("CA name constraints verification: %s\n",
|
|
+ ca_constraints_ret == EOK ? "PASS" : "FAIL");
|
|
+
|
|
done:
|
|
if (ret != EOK)
|
|
{
|
|
--
|
|
2.50.1
|
|
|