Skip to content

Commit 48b580c

Browse files
committed
Fix in CheckForAltNames to handle IPSAN
1 parent 43e44cb commit 48b580c

5 files changed

Lines changed: 273 additions & 7 deletions

File tree

src/internal.c

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13463,8 +13463,25 @@ int CheckForAltNames(DecodedCert* dCert, const char* domain, word32 domainLen,
1346313463
if (dCert != NULL)
1346413464
altName = dCert->altNames;
1346513465

13466-
if (checkCN != NULL)
13467-
*checkCN = (altName == NULL) ? 1 : 0;
13466+
if (checkCN != NULL) {
13467+
/* CN fallback is suppressed when the cert presents any altName
13468+
* usable for hostname matching. Without WOLFSSL_IP_ALT_NAME the
13469+
* iPAddress branch below is compiled out, so iPAddress entries
13470+
* cannot match anything here; treat them as absent so a cert
13471+
* presenting only iPAddress SANs still falls back to CN as it
13472+
* did before iPAddress entries were unconditionally added to
13473+
* altNames for name-constraint enforcement. */
13474+
DNS_entry* a = altName;
13475+
*checkCN = 1;
13476+
for (; a != NULL; a = a->next) {
13477+
#ifndef WOLFSSL_IP_ALT_NAME
13478+
if (a->type == ASN_IP_TYPE)
13479+
continue;
13480+
#endif
13481+
*checkCN = 0;
13482+
break;
13483+
}
13484+
}
1346813485

1346913486
for (; altName != NULL; altName = altName->next) {
1347013487
WOLFSSL_MSG("\tindividual AltName check");

src/x509.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4262,6 +4262,25 @@ char* wolfSSL_X509_get_next_altname(WOLFSSL_X509* cert)
42624262
return NULL;
42634263
}
42644264

4265+
#ifndef WOLFSSL_IP_ALT_NAME
4266+
/* In default builds iPAddress entries hold raw 4/16 octet payloads
4267+
* (no human-readable ipString), so returning them as a C string would
4268+
* truncate at any embedded NUL byte. Such entries are still parsed
4269+
* into altNames for name-constraint enforcement; skip them here so
4270+
* string-iteration callers see the same set of entries as before.
4271+
*
4272+
* With WOLFSSL_MULTICIRCULATE_ALTNAMELIST, a list consisting only of
4273+
* iPAddress entries collapses to "no entries" on the first pass and
4274+
* resets to head on the next call; the cycle shape matches the
4275+
* pre-fix behavior where such entries were never parsed. */
4276+
while (cert->altNamesNext != NULL &&
4277+
cert->altNamesNext->type == ASN_IP_TYPE) {
4278+
cert->altNamesNext = cert->altNamesNext->next;
4279+
}
4280+
if (cert->altNamesNext == NULL)
4281+
return NULL;
4282+
#endif
4283+
42654284
/* unsafe cast required for ABI compatibility. */
42664285
ret = (char *)(wc_ptr_t)cert->altNamesNext->name;
42674286
#ifdef WOLFSSL_IP_ALT_NAME

tests/api/test_certman.c

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1705,6 +1705,200 @@ int test_wolfSSL_CertManagerNameConstraint_DNS_CN(void)
17051705
return EXPECT_RESULT();
17061706
}
17071707

1708+
int test_wolfSSL_CertManagerNameConstraint_IP_SAN(void)
1709+
{
1710+
EXPECT_DECLS;
1711+
#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && \
1712+
!defined(NO_WOLFSSL_CM_VERIFY) && !defined(NO_RSA) && \
1713+
defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_GEN) && \
1714+
defined(WOLFSSL_CERT_EXT) && defined(WOLFSSL_ALT_NAMES) && \
1715+
!defined(NO_SHA256) && !defined(IGNORE_NAME_CONSTRAINTS)
1716+
/* Regression test for TALOS-2026-2409 (CVE-2026-28739).
1717+
*
1718+
* The CA at cert-ext-ncip.der declares a permittedSubtrees iPAddress
1719+
* constraint of 192.168.1.0/255.255.255.0. A leaf with an iPAddress
1720+
* SAN outside that subnet must be rejected. Prior to the fix, default
1721+
* builds (without WOLFSSL_IP_ALT_NAME) silently skipped iPAddress SANs
1722+
* during parsing, so the constraint loop saw no IP entries and the
1723+
* leaf was accepted.
1724+
*
1725+
* The bypass only existed when WOLFSSL_IP_ALT_NAME was undefined (the
1726+
* default). To exercise the regression target, this test must run in a
1727+
* configuration without --enable-ip-alt-name and without
1728+
* --enable-opensslall (which implies WOLFSSL_IP_ALT_NAME via
1729+
* settings.h). With WOLFSSL_IP_ALT_NAME defined the same assertions
1730+
* still hold, but the negative case there is enforcement of an
1731+
* already-working path rather than the regression itself.
1732+
*
1733+
* Scope: this test exercises the permittedSubtrees code path. The
1734+
* excludedSubtrees path uses the same parsing plumbing
1735+
* (DecodeGeneralName -> SetDNSEntry into cert->altNames) and the same
1736+
* ConfirmNameConstraints walk; the TALOS bug was strictly about
1737+
* iPAddress entries being absent from cert->altNames, so once that is
1738+
* fixed both directions are restored. The pre-existing
1739+
* test_wolfSSL_NAME_CONSTRAINTS_excluded test exercises the excluded
1740+
* direction more broadly. */
1741+
WOLFSSL_CERT_MANAGER* cm = NULL;
1742+
WOLFSSL_EVP_PKEY *priv = NULL;
1743+
WOLFSSL_X509_NAME* name = NULL;
1744+
const char* ca_cert = "./certs/test/cert-ext-ncip.der";
1745+
const char* server_cert = "./certs/test/server-goodcn.pem";
1746+
/* Raw IPv4 bytes for SAN values (not dotted-quad strings). */
1747+
static const byte ip_inside[] = { 192, 168, 1, 10 }; /* permitted */
1748+
static const byte ip_outside[] = { 10, 0, 0, 1 }; /* violates */
1749+
1750+
byte *der = NULL;
1751+
int derSz;
1752+
byte *pt;
1753+
WOLFSSL_X509 *x509 = NULL;
1754+
WOLFSSL_X509 *ca = NULL;
1755+
1756+
pt = (byte*)server_key_der_2048;
1757+
ExpectNotNull(priv = wolfSSL_d2i_PrivateKey(EVP_PKEY_RSA, NULL,
1758+
(const unsigned char**)&pt, sizeof_server_key_der_2048));
1759+
1760+
ExpectNotNull(cm = wolfSSL_CertManagerNew());
1761+
ExpectNotNull(ca = wolfSSL_X509_load_certificate_file(ca_cert,
1762+
WOLFSSL_FILETYPE_ASN1));
1763+
ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(ca, &derSz)));
1764+
ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, der, derSz,
1765+
WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS);
1766+
1767+
/* Negative case: leaf with IP SAN outside permitted subnet. Must be
1768+
* rejected with ASN_NAME_INVALID_E. */
1769+
ExpectNotNull(x509 = wolfSSL_X509_load_certificate_file(server_cert,
1770+
WOLFSSL_FILETYPE_PEM));
1771+
ExpectNotNull(name = wolfSSL_X509_get_subject_name(ca));
1772+
ExpectIntEQ(wolfSSL_X509_set_issuer_name(x509, name), WOLFSSL_SUCCESS);
1773+
name = NULL;
1774+
1775+
/* Use add_altname_ex with raw IP bytes so the test runs in default
1776+
* builds where add_altname (string form) requires WOLFSSL_IP_ALT_NAME. */
1777+
ExpectIntEQ(wolfSSL_X509_add_altname_ex(x509, (const char*)ip_outside,
1778+
sizeof(ip_outside), ASN_IP_TYPE), WOLFSSL_SUCCESS);
1779+
ExpectIntGT(wolfSSL_X509_sign(x509, priv, EVP_sha256()), 0);
1780+
ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(x509, &derSz)));
1781+
ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz,
1782+
WOLFSSL_FILETYPE_ASN1), WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
1783+
wolfSSL_X509_free(x509);
1784+
x509 = NULL;
1785+
1786+
/* Positive case: leaf with IP SAN inside the permitted subnet must be
1787+
* accepted. Confirms the fix does not over-reject. */
1788+
ExpectNotNull(x509 = wolfSSL_X509_load_certificate_file(server_cert,
1789+
WOLFSSL_FILETYPE_PEM));
1790+
ExpectNotNull(name = wolfSSL_X509_get_subject_name(ca));
1791+
ExpectIntEQ(wolfSSL_X509_set_issuer_name(x509, name), WOLFSSL_SUCCESS);
1792+
name = NULL;
1793+
1794+
ExpectIntEQ(wolfSSL_X509_add_altname_ex(x509, (const char*)ip_inside,
1795+
sizeof(ip_inside), ASN_IP_TYPE), WOLFSSL_SUCCESS);
1796+
ExpectIntGT(wolfSSL_X509_sign(x509, priv, EVP_sha256()), 0);
1797+
ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(x509, &derSz)));
1798+
ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz,
1799+
WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS);
1800+
1801+
wolfSSL_CertManagerFree(cm);
1802+
wolfSSL_X509_free(x509);
1803+
wolfSSL_X509_free(ca);
1804+
wolfSSL_EVP_PKEY_free(priv);
1805+
#endif
1806+
return EXPECT_RESULT();
1807+
}
1808+
1809+
int test_wolfSSL_X509_check_host_IP_only_SAN_CN_fallback(void)
1810+
{
1811+
EXPECT_DECLS;
1812+
#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && !defined(NO_RSA) && \
1813+
defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_GEN) && \
1814+
defined(WOLFSSL_CERT_EXT) && defined(WOLFSSL_ALT_NAMES) && \
1815+
!defined(NO_SHA256)
1816+
/* Companion regression test for the CheckForAltNames CN-fallback
1817+
* preservation introduced alongside TALOS-2026-2409.
1818+
*
1819+
* Once iPAddress SAN entries are unconditionally added to altNames
1820+
* (so name constraints can be enforced), a leaf that presents only
1821+
* iPAddress SANs would suppress CN fallback in CheckForAltNames in
1822+
* default builds, where the iPAddress matching path is compiled out.
1823+
* That would silently break TLS hostname verification for callers
1824+
* that previously relied on the CN fallback. The fix in
1825+
* src/internal.c treats iPAddress entries as absent for the
1826+
* *checkCN decision when WOLFSSL_IP_ALT_NAME is undefined.
1827+
*
1828+
* This test pins both directions:
1829+
* - default build (no WOLFSSL_IP_ALT_NAME): IP-only-SAN cert with a
1830+
* matching CN must succeed via CN fallback.
1831+
* - WOLFSSL_IP_ALT_NAME defined: the same cert must fail because
1832+
* the SAN presence suppresses CN fallback (RFC 6125 compliant).
1833+
* Independently, a cert with a non-matching DNS SAN must always fail
1834+
* regardless of build flags, since DNS SAN presence unambiguously
1835+
* suppresses CN fallback. */
1836+
WOLFSSL_EVP_PKEY *priv = NULL;
1837+
WOLFSSL_X509_NAME* name = NULL;
1838+
const char* server_cert = "./certs/test/server-goodcn.pem";
1839+
const char hostName[] = "cnhost.local";
1840+
static const byte ip_san[] = { 10, 0, 0, 1 };
1841+
byte *pt;
1842+
WOLFSSL_X509 *leafIp = NULL;
1843+
WOLFSSL_X509 *leafDns = NULL;
1844+
1845+
pt = (byte*)server_key_der_2048;
1846+
ExpectNotNull(priv = wolfSSL_d2i_PrivateKey(EVP_PKEY_RSA, NULL,
1847+
(const unsigned char**)&pt, sizeof_server_key_der_2048));
1848+
1849+
/* Leaf with CN matching hostName and only an iPAddress SAN. */
1850+
ExpectNotNull(leafIp = wolfSSL_X509_load_certificate_file(server_cert,
1851+
WOLFSSL_FILETYPE_PEM));
1852+
ExpectNotNull(name = X509_NAME_new());
1853+
ExpectIntEQ(X509_NAME_add_entry_by_txt(name, "commonName", MBSTRING_UTF8,
1854+
(byte*)hostName, (int)XSTRLEN(hostName), -1, 0), SSL_SUCCESS);
1855+
ExpectIntEQ(wolfSSL_X509_set_subject_name(leafIp, name), WOLFSSL_SUCCESS);
1856+
X509_NAME_free(name);
1857+
name = NULL;
1858+
ExpectIntEQ(wolfSSL_X509_add_altname_ex(leafIp, (const char*)ip_san,
1859+
sizeof(ip_san), ASN_IP_TYPE), WOLFSSL_SUCCESS);
1860+
ExpectIntGT(wolfSSL_X509_sign(leafIp, priv, EVP_sha256()), 0);
1861+
1862+
#ifndef WOLFSSL_IP_ALT_NAME
1863+
/* Default build: iPAddress entries are present in altNames for
1864+
* constraint enforcement but treated as absent for *checkCN, so the
1865+
* lookup falls back to the Subject CN, which matches. */
1866+
ExpectIntEQ(wolfSSL_X509_check_host(leafIp, hostName, XSTRLEN(hostName),
1867+
0, NULL), WOLFSSL_SUCCESS);
1868+
#else
1869+
/* IP_ALT_NAME build: SAN presence suppresses CN fallback per RFC 6125.
1870+
* The hostName ("cnhost.local") cannot match the iPAddress entry, so
1871+
* the check must fail. */
1872+
ExpectIntEQ(wolfSSL_X509_check_host(leafIp, hostName, XSTRLEN(hostName),
1873+
0, NULL), WC_NO_ERR_TRACE(WOLFSSL_FAILURE));
1874+
#endif
1875+
1876+
/* Leaf with CN matching hostName but a non-matching DNS SAN. CN
1877+
* fallback must be suppressed in every build (DNS SAN unambiguously
1878+
* counts toward *checkCN), so the check must fail. This pins the
1879+
* other side of the boundary so a future change that broadly skips
1880+
* altNames in *checkCN does not silently regress. */
1881+
ExpectNotNull(leafDns = wolfSSL_X509_load_certificate_file(server_cert,
1882+
WOLFSSL_FILETYPE_PEM));
1883+
ExpectNotNull(name = X509_NAME_new());
1884+
ExpectIntEQ(X509_NAME_add_entry_by_txt(name, "commonName", MBSTRING_UTF8,
1885+
(byte*)hostName, (int)XSTRLEN(hostName), -1, 0), SSL_SUCCESS);
1886+
ExpectIntEQ(wolfSSL_X509_set_subject_name(leafDns, name), WOLFSSL_SUCCESS);
1887+
X509_NAME_free(name);
1888+
name = NULL;
1889+
ExpectIntEQ(wolfSSL_X509_add_altname(leafDns, "other.example",
1890+
ASN_DNS_TYPE), WOLFSSL_SUCCESS);
1891+
ExpectIntGT(wolfSSL_X509_sign(leafDns, priv, EVP_sha256()), 0);
1892+
ExpectIntEQ(wolfSSL_X509_check_host(leafDns, hostName, XSTRLEN(hostName),
1893+
0, NULL), WC_NO_ERR_TRACE(WOLFSSL_FAILURE));
1894+
1895+
wolfSSL_X509_free(leafIp);
1896+
wolfSSL_X509_free(leafDns);
1897+
wolfSSL_EVP_PKEY_free(priv);
1898+
#endif
1899+
return EXPECT_RESULT();
1900+
}
1901+
17081902
int test_wolfSSL_CertManagerCRL(void)
17091903
{
17101904
EXPECT_DECLS;

tests/api/test_certman.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ int test_wolfSSL_CertManagerNameConstraint3(void);
3636
int test_wolfSSL_CertManagerNameConstraint4(void);
3737
int test_wolfSSL_CertManagerNameConstraint5(void);
3838
int test_wolfSSL_CertManagerNameConstraint_DNS_CN(void);
39+
int test_wolfSSL_CertManagerNameConstraint_IP_SAN(void);
40+
int test_wolfSSL_X509_check_host_IP_only_SAN_CN_fallback(void);
3941
int test_wolfSSL_CertManagerCRL(void);
4042
int test_wolfSSL_CRL_reason_extensions_cleanup(void);
4143
int test_wolfSSL_CRL_static_revoked_list(void);
@@ -59,6 +61,9 @@ int test_wolfSSL_CertManagerRejectMD5Cert(void);
5961
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint4), \
6062
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint5), \
6163
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint_DNS_CN), \
64+
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint_IP_SAN), \
65+
TEST_DECL_GROUP("certman", \
66+
test_wolfSSL_X509_check_host_IP_only_SAN_CN_fallback), \
6267
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerCRL), \
6368
TEST_DECL_GROUP("certman", test_wolfSSL_CRL_reason_extensions_cleanup), \
6469
TEST_DECL_GROUP("certman", test_wolfSSL_CRL_static_revoked_list), \

wolfcrypt/src/asn.c

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18115,16 +18115,39 @@ static int DecodeGeneralName(const byte* input, word32* inOutIdx, byte tag,
1811518115
idx += (word32)len;
1811618116
}
1811718117
}
18118-
#ifdef WOLFSSL_IP_ALT_NAME
18119-
/* GeneralName choice: iPAddress */
18118+
/* GeneralName choice: iPAddress
18119+
*
18120+
* Always parse iPAddress into cert->altNames so ConfirmNameConstraints
18121+
* can enforce permitted/excluded iPAddress subtrees (RFC 5280
18122+
* Sec. 4.2.1.10). The entry holds raw 4/16 octet payloads;
18123+
* WOLFSSL_IP_ALT_NAME still gates the human-readable ipString
18124+
* generation in SetDNSEntry.
18125+
*
18126+
* Consequences for downstream consumers when WOLFSSL_IP_ALT_NAME is
18127+
* undefined:
18128+
* - wolfSSL_X509_get_next_altname (string iterator): explicitly
18129+
* skips iPAddress entries, since returning raw bytes as a C
18130+
* string would truncate at any embedded NUL. This preserves the
18131+
* pre-fix behavior for that getter.
18132+
* - CheckForAltNames (TLS hostname matching): the iPAddress branch
18133+
* is compiled out, so iPAddress entries cannot match anything;
18134+
* they are also excluded from the *checkCN decision so an
18135+
* IP-only-SAN cert still falls back to CN matching as before.
18136+
* - All other altNames walkers (e.g. ALT_NAMES_OID handling in
18137+
* wolfSSL_X509_get_ext_d2i, wolfssl_x509_alt_names_to_gn,
18138+
* FlattenAltNames in cert generation) now see iPAddress entries
18139+
* unconditionally. This is intentional and brings wolfSSL closer
18140+
* to OpenSSL's SAN-exposure semantics; the OPENSSL_EXTRA APIs
18141+
* surface the raw octets as OCTET_STRING already (see the
18142+
* ASN_IP_TYPE case under WOLFSSL_GEN_IPADD in src/x509.c).
18143+
*/
1812018144
else if (tag == (ASN_CONTEXT_SPECIFIC | ASN_IP_TYPE)) {
1812118145
ret = SetDNSEntry(cert->heap, (const char*)(input + idx), len,
1812218146
ASN_IP_TYPE, &cert->altNames);
1812318147
if (ret == 0) {
1812418148
idx += (word32)len;
1812518149
}
1812618150
}
18127-
#endif /* WOLFSSL_IP_ALT_NAME */
1812818151
#ifdef WOLFSSL_RID_ALT_NAME
1812918152
/* GeneralName choice: registeredID */
1813018153
else if (tag == (ASN_CONTEXT_SPECIFIC | ASN_RID_TYPE)) {
@@ -37018,15 +37041,23 @@ static int DecodeAcertGeneralName(const byte* input, word32* inOutIdx,
3701837041
}
3701937042
#if defined(WOLFSSL_QT) || defined(OPENSSL_ALL) || \
3702037043
defined(WOLFSSL_IP_ALT_NAME)
37021-
/* GeneralName choice: iPAddress */
37044+
/* GeneralName choice: iPAddress
37045+
*
37046+
* Asymmetric with the X.509 DecodeGeneralName path on purpose:
37047+
* attribute-certificate names (RFC 5755) are not consumed by
37048+
* ConfirmNameConstraints, which only walks DecodedCert lists. These
37049+
* entries flow into AC holder/issuer name fields where the iPAddress
37050+
* is only consumed by callers that opt in (Qt, OpenSSL_ALL, or the
37051+
* IP-SAN compat layer). If iPAddress name-constraint enforcement is
37052+
* ever extended to attribute certificates, this gate must drop. */
3702237053
else if (tag == (ASN_CONTEXT_SPECIFIC | ASN_IP_TYPE)) {
3702337054
ret = SetDNSEntry(acert->heap, (const char*)(input + idx), len,
3702437055
ASN_IP_TYPE, entries);
3702537056
if (ret == 0) {
3702637057
idx += (word32)len;
3702737058
}
3702837059
}
37029-
#endif /* WOLFSSL_QT || OPENSSL_ALL */
37060+
#endif /* WOLFSSL_QT || OPENSSL_ALL || WOLFSSL_IP_ALT_NAME */
3703037061

3703137062
#ifdef OPENSSL_ALL
3703237063
/* GeneralName choice: registeredID */

0 commit comments

Comments
 (0)