diff --git a/0008-Fix-IP-address-handling-in-CA-certificate-SAN-constr.patch b/0008-Fix-IP-address-handling-in-CA-certificate-SAN-constr.patch new file mode 100644 index 0000000..770641a --- /dev/null +++ b/0008-Fix-IP-address-handling-in-CA-certificate-SAN-constr.patch @@ -0,0 +1,1115 @@ +From aac0351de2fade86572eac4f24e22bd667177f7e Mon Sep 17 00:00:00 2001 +From: Stephen Gallagher +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 +--- + 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 + #include + #include ++#include ++#include + + #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 + diff --git a/sscg.spec b/sscg.spec index d5bd281..e0a5d84 100644 --- a/sscg.spec +++ b/sscg.spec @@ -9,7 +9,7 @@ Name: sscg Version: 3.0.0 -Release: 9%{?dist} +Release: 10%{?dist} Summary: Simple SSL certificate generator License: GPLv3+ with exceptions @@ -33,6 +33,7 @@ Patch: 0004-dhparams-don-t-fail-if-default-file-can-t-be-created.patch Patch: 0005-dhparams-Fix-the-FIPS_mode-call-for-OpenSSL-3.0.patch Patch: 0006-x509-Use-proper-version-for-CSR.patch Patch: 0007-Ensure-critical-basicConstraint-for-CA-cert.patch +Patch: 0008-Fix-IP-address-handling-in-CA-certificate-SAN-constr.patch %description A utility to aid in the creation of more secure "self-signed" @@ -63,6 +64,10 @@ false signatures from the service certificate. %{_mandir}/man8/%{name}.8* %changelog +* Mon Aug 11 2025 Stephen Gallagher - 3.0.0-10 +- Fix IP address handling in CA certificate SAN constraints +- Resolves: RHEL-107289 + * Tue Apr 22 2025 Stephen Gallagher - 3.0.0-9 - Ensure 'critical' basicConstraint for CA cert - Resolves: RHEL-88119