mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-07-05 12:00:51 +02:00
Merge pull request #10354 from embhorn/zd21725
Fix IPSAN and registeredID handling
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEOjCCAyKgAwIBAgIUBD6sBdFHNsQ5qoW3MIkJ/BKsB/4wDQYJKoZIhvcNAQEL
|
||||
BQAwezELMAkGA1UEBhMCQVUxEzARBgNVBAgMClF1ZWVuc2xhbmQxETAPBgNVBAcM
|
||||
CEJyaXNiYW5lMRQwEgYDVQQKDAt3b2xmU1NMIEluYzEUMBIGA1UECwwLRW5naW5l
|
||||
ZXJpbmcxGDAWBgNVBAMMD3d3dy53b2xmc3NsLmNvbTAeFw0yNjA0MjkyMTAzMTZa
|
||||
Fw0yOTAxMjMyMTAzMTZaMHsxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApRdWVlbnNs
|
||||
YW5kMREwDwYDVQQHDAhCcmlzYmFuZTEUMBIGA1UECgwLd29sZlNTTCBJbmMxFDAS
|
||||
BgNVBAsMC0VuZ2luZWVyaW5nMRgwFgYDVQQDDA93d3cud29sZnNzbC5jb20wggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAlQjhV0HycW230kVBJwFlxkWu
|
||||
8rwkMLiVzi9O1vYciLx8n/uoZ3/+XJxRdfeKygfnNS+P4b17wC98q2SoF/zKXXu6
|
||||
4CHlci5vLobYlXParBtTuV8/1xkNJU/hY2NRiwtkP61DuKUcXDSzrgCgY8X2fwtZ
|
||||
aHhzpowYqQJtr8MZAS64EOPGzEC0aaNGM2mHbsS7F6bz6N2tc7x7LyG1/WZRDL1U
|
||||
s+FtXxy8I3PRCQOJFNIQuWTDKtChlkq84dQaW8egwMFjeA9ENzAyloAyI5Whd7oT
|
||||
0pdz4l0lyWoNwzlgpLSwaUJCCenYCLwzILNYIqeq68Th5mGDxdKW39nQT63XAgMB
|
||||
AAGjgbUwgbIwHQYDVR0OBBYEFLMRMsmSmITiyfjQO24DQsofDo48MB8GA1UdIwQY
|
||||
MBaAFLMRMsmSmITiyfjQO24DQsofDo48MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYD
|
||||
VR0PAQH/BAQDAgGGMBYGA1UdHgEB/wQMMAqgCDAGiAQqAwQFMDQGCWCGSAGG+EIB
|
||||
DQQnFiVUZXN0aW5nIHJlZ2lzdGVyZWRJRCBuYW1lIGNvbnN0cmFpbnRzMA0GCSqG
|
||||
SIb3DQEBCwUAA4IBAQARs/sdZs71KWRoyWuTevfnS5h5PH9S07xl+hFcfSXpI6sx
|
||||
gQoBMzEv8BBN3vLL4HOoNCwnqmFbwG5ogt/HuohAT6bXtdIkK1jNEP50t1pdDOmi
|
||||
/3fYKDvjvtX2DDQe2K8o/qVsu63MtKkOuc1ZURtsVrlrFFW7D7FsuNxa1dS8fbGl
|
||||
qrEnIXKemPmco5EIFDygXFIFkL/CP/LxHjTOuib68hBUDOqhMpMruSp7OfAt9Kfe
|
||||
rsEVY5mv82DfaCjo9bw17dSHUFo4vC+wzB5E8Wg33Pxbn0v8aRTvAsPz22Mlkd9b
|
||||
mxdLFh0mqPBj/UvWOK1xH0kXGsblNNAz5KeDceAZ
|
||||
-----END CERTIFICATE-----
|
||||
@@ -196,6 +196,35 @@ EOF
|
||||
gen_cert
|
||||
rm -f ./certs/test/cert-ext-ncip.cfg
|
||||
|
||||
OUT=certs/test/cert-ext-ncrid
|
||||
KEYFILE=certs/test/cert-ext-ncrid-key.der
|
||||
CONFIG=certs/test/cert-ext-ncrid.cfg
|
||||
tee >$CONFIG <<EOF
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
x509_extensions = v3_ca
|
||||
|
||||
[ req_distinguished_name ]
|
||||
C = AU
|
||||
ST = Queensland
|
||||
L = Brisbane
|
||||
O = wolfSSL Inc
|
||||
OU = Engineering
|
||||
CN = www.wolfssl.com
|
||||
|
||||
[ v3_ca ]
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always,issuer
|
||||
basicConstraints = critical, CA:true, pathlen:0
|
||||
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
||||
nameConstraints = critical,permitted;RID:1.2.3.4.5
|
||||
nsComment = "Testing registeredID name constraints"
|
||||
|
||||
EOF
|
||||
gen_cert
|
||||
rm -f ./certs/test/cert-ext-ncrid.cfg
|
||||
|
||||
OUT=certs/test/cert-ext-ia
|
||||
KEYFILE=certs/test/cert-ext-ia-key.der
|
||||
CONFIG=certs/test/cert-ext-ia.cfg
|
||||
|
||||
@@ -13,6 +13,8 @@ EXTRA_DIST += \
|
||||
certs/test/cert-ext-nc-combined.pem \
|
||||
certs/test/cert-ext-ncip.der \
|
||||
certs/test/cert-ext-ncip.pem \
|
||||
certs/test/cert-ext-ncrid.der \
|
||||
certs/test/cert-ext-ncrid.pem \
|
||||
certs/test/cert-ext-ncdns.der \
|
||||
certs/test/cert-ext-ncdns.pem \
|
||||
certs/test/cert-ext-ncmulti.der \
|
||||
|
||||
+30
-2
@@ -13575,8 +13575,29 @@ int CheckForAltNames(DecodedCert* dCert, const char* domain, word32 domainLen,
|
||||
if (dCert != NULL)
|
||||
altName = dCert->altNames;
|
||||
|
||||
if (checkCN != NULL)
|
||||
*checkCN = (altName == NULL) ? 1 : 0;
|
||||
if (checkCN != NULL) {
|
||||
/* CN fallback is suppressed when the cert presents any altName
|
||||
* usable for hostname matching. Without WOLFSSL_IP_ALT_NAME the
|
||||
* iPAddress branch below is compiled out, so iPAddress entries
|
||||
* cannot match anything here; treat them as absent so a cert
|
||||
* presenting only iPAddress SANs still falls back to CN as it
|
||||
* did before iPAddress entries were unconditionally added to
|
||||
* altNames for name-constraint enforcement. The same reasoning
|
||||
* applies to registeredID entries: hostname matching against an
|
||||
* OID is not meaningful, so they never count toward *checkCN. */
|
||||
DNS_entry* a = altName;
|
||||
*checkCN = 1;
|
||||
for (; a != NULL; a = a->next) {
|
||||
#ifndef WOLFSSL_IP_ALT_NAME
|
||||
if (a->type == ASN_IP_TYPE)
|
||||
continue;
|
||||
#endif
|
||||
if (a->type == ASN_RID_TYPE)
|
||||
continue;
|
||||
*checkCN = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (; altName != NULL; altName = altName->next) {
|
||||
WOLFSSL_MSG("\tindividual AltName check");
|
||||
@@ -13599,6 +13620,13 @@ int CheckForAltNames(DecodedCert* dCert, const char* domain, word32 domainLen,
|
||||
continue;
|
||||
}
|
||||
|
||||
/* registeredID entries hold raw OID bytes; they are not eligible
|
||||
* for hostname matching regardless of build flags. */
|
||||
if (altName->type == ASN_RID_TYPE) {
|
||||
WOLFSSL_MSG("\tAltName is registeredID, skipping for hostname");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (MatchDomainName(buf, (int)len, domain, domainLen, flags)) {
|
||||
match = 1;
|
||||
if (checkCN != NULL) {
|
||||
|
||||
+66
-6
@@ -650,8 +650,14 @@ static int DNS_to_GENERAL_NAME(WOLFSSL_GENERAL_NAME* gn, DNS_entry* dns)
|
||||
/* @TODO extract dir name info from DNS_entry */
|
||||
break;
|
||||
|
||||
#ifdef WOLFSSL_RID_ALT_NAME
|
||||
case WOLFSSL_GEN_RID:
|
||||
/* registeredID is parsed into altNames unconditionally so
|
||||
* ConfirmNameConstraints can enforce RID name constraints
|
||||
* (RFC 5280 Sec. 4.2.1.10). The body uses only the raw OID
|
||||
* bytes carried in dns->name/dns->len and constructs a
|
||||
* proper ASN1_OBJECT, so this case is independent of
|
||||
* WOLFSSL_RID_ALT_NAME (which only gates the human-readable
|
||||
* ridString form). */
|
||||
gn->type = dns->type;
|
||||
/* wolfSSL_GENERAL_NAME_new() mallocs this by default */
|
||||
wolfSSL_ASN1_STRING_free(gn->d.ia5);
|
||||
@@ -681,7 +687,6 @@ static int DNS_to_GENERAL_NAME(WOLFSSL_GENERAL_NAME* gn, DNS_entry* dns)
|
||||
gn->d.registeredID->dynamic |= WOLFSSL_ASN1_DYNAMIC_DATA;
|
||||
gn->d.registeredID->grp = oidCertExtType;
|
||||
break;
|
||||
#endif
|
||||
|
||||
case WOLFSSL_GEN_X400:
|
||||
/* Unsupported: fall through */
|
||||
@@ -2533,8 +2538,12 @@ void* wolfSSL_X509_get_ext_d2i(const WOLFSSL_X509* x509, int nid, int* c,
|
||||
gn->d.iPAddress->type = WOLFSSL_V_ASN1_OCTET_STRING;
|
||||
break;
|
||||
|
||||
#ifdef WOLFSSL_RID_ALT_NAME
|
||||
case ASN_RID_TYPE:
|
||||
/* Always handle registeredID: the union
|
||||
* member d.registeredID is populated from
|
||||
* raw OID body bytes. WOLFSSL_RID_ALT_NAME
|
||||
* only gates the human-readable ridString,
|
||||
* which this path does not need. */
|
||||
gn->type = dns->type;
|
||||
/* Free ia5 before using union for registeredID */
|
||||
wolfSSL_ASN1_STRING_free(gn->d.ia5);
|
||||
@@ -2567,7 +2576,6 @@ void* wolfSSL_X509_get_ext_d2i(const WOLFSSL_X509* x509, int nid, int* c,
|
||||
WOLFSSL_ASN1_DYNAMIC_DATA;
|
||||
gn->d.registeredID->grp = oidCertExtType;
|
||||
break;
|
||||
#endif /* WOLFSSL_RID_ALT_NAME */
|
||||
|
||||
default:
|
||||
gn->type = dns->type;
|
||||
@@ -4272,6 +4280,36 @@ char* wolfSSL_X509_get_next_altname(WOLFSSL_X509* cert)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* In default builds iPAddress entries hold raw 4/16 octet payloads
|
||||
* and registeredID entries hold raw OID body bytes (no human-readable
|
||||
* ipString/ridString), so returning them as a C string would truncate
|
||||
* at any embedded NUL byte. Such entries are still parsed into
|
||||
* altNames for name-constraint enforcement; skip them here so
|
||||
* string-iteration callers see the same set of entries as before.
|
||||
*
|
||||
* With WOLFSSL_MULTICIRCULATE_ALTNAMELIST, a list consisting only of
|
||||
* skipped entries collapses to "no entries" on the first pass and
|
||||
* resets to head on the next call; the cycle shape matches the
|
||||
* pre-fix behavior where such entries were never parsed. */
|
||||
#if !defined(WOLFSSL_IP_ALT_NAME) || !defined(WOLFSSL_RID_ALT_NAME)
|
||||
while (cert->altNamesNext != NULL) {
|
||||
int skip = 0;
|
||||
#ifndef WOLFSSL_IP_ALT_NAME
|
||||
if (cert->altNamesNext->type == ASN_IP_TYPE)
|
||||
skip = 1;
|
||||
#endif
|
||||
#ifndef WOLFSSL_RID_ALT_NAME
|
||||
if (cert->altNamesNext->type == ASN_RID_TYPE)
|
||||
skip = 1;
|
||||
#endif
|
||||
if (!skip)
|
||||
break;
|
||||
cert->altNamesNext = cert->altNamesNext->next;
|
||||
}
|
||||
if (cert->altNamesNext == NULL)
|
||||
return NULL;
|
||||
#endif /* !WOLFSSL_IP_ALT_NAME || !WOLFSSL_RID_ALT_NAME */
|
||||
|
||||
/* unsafe cast required for ABI compatibility. */
|
||||
ret = (char *)(wc_ptr_t)cert->altNamesNext->name;
|
||||
#ifdef WOLFSSL_IP_ALT_NAME
|
||||
@@ -4279,6 +4317,12 @@ char* wolfSSL_X509_get_next_altname(WOLFSSL_X509* cert)
|
||||
if (cert->altNamesNext->type == ASN_IP_TYPE) {
|
||||
ret = cert->altNamesNext->ipString;
|
||||
}
|
||||
#endif
|
||||
#ifdef WOLFSSL_RID_ALT_NAME
|
||||
/* return the registeredID as a string */
|
||||
if (cert->altNamesNext->type == ASN_RID_TYPE) {
|
||||
ret = cert->altNamesNext->ridString;
|
||||
}
|
||||
#endif
|
||||
cert->altNamesNext = cert->altNamesNext->next;
|
||||
|
||||
@@ -6909,6 +6953,14 @@ static int X509_print_name_entry(WOLFSSL_BIO* bio,
|
||||
len = XSNPRINTF(scratch, MAX_WIDTH, "IP Address:%s",
|
||||
entry->ipString);
|
||||
}
|
||||
#else
|
||||
else if (entry->type == ASN_IP_TYPE) {
|
||||
/* iPAddress entries are now always parsed into altNames so
|
||||
* name constraints can be enforced. Without the
|
||||
* human-readable ipString field, emit a fixed label so this
|
||||
* print path does not fail. */
|
||||
len = XSNPRINTF(scratch, MAX_WIDTH, "IP Address:<unavailable>");
|
||||
}
|
||||
#endif /* OPENSSL_ALL || WOLFSSL_IP_ALT_NAME */
|
||||
else if (entry->type == ASN_RFC822_TYPE) {
|
||||
len = XSNPRINTF(scratch, MAX_WIDTH, "email:%s",
|
||||
@@ -6921,12 +6973,20 @@ static int X509_print_name_entry(WOLFSSL_BIO* bio,
|
||||
len = XSNPRINTF(scratch, MAX_WIDTH, "URI:%s",
|
||||
entry->name);
|
||||
}
|
||||
#if defined(OPENSSL_ALL)
|
||||
#ifdef WOLFSSL_RID_ALT_NAME
|
||||
else if (entry->type == ASN_RID_TYPE) {
|
||||
len = XSNPRINTF(scratch, MAX_WIDTH, "Registered ID:%s",
|
||||
entry->ridString);
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
else if (entry->type == ASN_RID_TYPE) {
|
||||
/* registeredID entries are now always parsed into altNames
|
||||
* so name constraints can be enforced. Without the
|
||||
* human-readable ridString field, emit a fixed label so
|
||||
* this print path does not fail. */
|
||||
len = XSNPRINTF(scratch, MAX_WIDTH, "Registered ID:<unavailable>");
|
||||
}
|
||||
#endif /* WOLFSSL_RID_ALT_NAME */
|
||||
else if (entry->type == ASN_OTHER_TYPE) {
|
||||
len = XSNPRINTF(scratch, MAX_WIDTH,
|
||||
"othername <unsupported>");
|
||||
|
||||
+61
-22
@@ -24006,11 +24006,25 @@ static word32 build_otherName_san(byte* out, word32 outSz, const char* val7)
|
||||
return (word32)(sizeof(prefix) + 7);
|
||||
}
|
||||
|
||||
/* Build a SubjectAltName extension value with a single registeredID
|
||||
* GeneralName for OID 1.2.3.4, to use as the leaf SAN in RID tests. */
|
||||
static word32 build_registeredID_san(byte* out, word32 outSz)
|
||||
{
|
||||
static const byte ridSan[] = {
|
||||
0x30, 0x05, /* SEQUENCE, 5 */
|
||||
0x88, 0x03, /* [8] regId, 3 */
|
||||
0x2A, 0x03, 0x04 /* OID 1.2.3.4 */
|
||||
};
|
||||
if (outSz < sizeof(ridSan))
|
||||
return 0;
|
||||
XMEMCPY(out, ridSan, sizeof(ridSan));
|
||||
return (word32)sizeof(ridSan);
|
||||
}
|
||||
|
||||
/* Build a NameConstraints extension value with a single excludedSubtree
|
||||
* carrying a registeredID GeneralName for OID 1.2.3.4. registeredID is a
|
||||
* GeneralName form wolfSSL does not enforce, so DecodeSubtree() must
|
||||
* record it as 'unsupported' and ConfirmNameConstraints() must fail
|
||||
* closed when the extension is critical (RFC 5280 4.2.1.10). */
|
||||
* carrying a registeredID GeneralName for OID 1.2.3.4. wolfSSL enforces
|
||||
* registeredID name constraints by byte-comparing OID bodies, so a leaf
|
||||
* whose registeredID SAN matches this exclusion must be rejected. */
|
||||
static word32 build_registeredID_nameConstraints(byte* out, word32 outSz)
|
||||
{
|
||||
static const byte ridNc[] = {
|
||||
@@ -24187,10 +24201,12 @@ done:
|
||||
* (excluded is enforced regardless of criticality)
|
||||
* 4. Critical permitted subtree, leaf SAN matches -> accept
|
||||
* 5. Critical permitted subtree, leaf SAN does NOT match -> reject
|
||||
* 6. Critical nameConstraints carrying an unsupported form
|
||||
* (registeredID), leaf has no relevant SAN -> reject
|
||||
* (RFC 5280 4.2.1.10 fail-closed for unprocessed forms)
|
||||
* 7. Same as (6) but non-critical -> accept
|
||||
* 6. Critical excluded registeredID subtree, leaf SAN matches -> reject
|
||||
* (registeredID is enforced by byte-comparison of OID bodies)
|
||||
* 7. Non-critical excluded registeredID subtree, leaf SAN matches -> reject
|
||||
* (excluded subtrees are enforced regardless of criticality)
|
||||
* 8. Critical excluded registeredID subtree, leaf has no RID SAN -> accept
|
||||
* (no match in excluded list, constraint is satisfied)
|
||||
*/
|
||||
static int test_NameConstraints_OtherName(void)
|
||||
{
|
||||
@@ -24203,16 +24219,19 @@ static int test_NameConstraints_OtherName(void)
|
||||
defined(HAVE_OID_ENCODING) && !defined(IGNORE_NAME_CONSTRAINTS)
|
||||
byte sanBlocked[64];
|
||||
byte sanAllowed[64];
|
||||
byte sanRegisteredID[16];
|
||||
byte ncExcludedBlocked[64];
|
||||
byte ncPermittedAllowed[64];
|
||||
byte ncRegisteredID[16];
|
||||
word32 sanBlockedSz, sanAllowedSz;
|
||||
word32 sanBlockedSz, sanAllowedSz, sanRegisteredIDSz;
|
||||
word32 ncExcludedBlockedSz, ncPermittedAllowedSz, ncRegisteredIDSz;
|
||||
|
||||
sanBlockedSz =
|
||||
build_otherName_san(sanBlocked, sizeof(sanBlocked), "blocked");
|
||||
sanAllowedSz =
|
||||
build_otherName_san(sanAllowed, sizeof(sanAllowed), "allowed");
|
||||
sanRegisteredIDSz =
|
||||
build_registeredID_san(sanRegisteredID, sizeof(sanRegisteredID));
|
||||
ncExcludedBlockedSz = build_otherName_nameConstraints(
|
||||
ncExcludedBlocked, sizeof(ncExcludedBlocked), 1, "blocked");
|
||||
ncPermittedAllowedSz = build_otherName_nameConstraints(
|
||||
@@ -24221,6 +24240,7 @@ static int test_NameConstraints_OtherName(void)
|
||||
ncRegisteredID, sizeof(ncRegisteredID));
|
||||
ExpectIntGT((int)sanBlockedSz, 0);
|
||||
ExpectIntGT((int)sanAllowedSz, 0);
|
||||
ExpectIntGT((int)sanRegisteredIDSz, 0);
|
||||
ExpectIntGT((int)ncExcludedBlockedSz, 0);
|
||||
ExpectIntGT((int)ncPermittedAllowedSz, 0);
|
||||
ExpectIntGT((int)ncRegisteredIDSz, 0);
|
||||
@@ -24263,20 +24283,25 @@ static int test_NameConstraints_OtherName(void)
|
||||
sanBlocked, sanBlockedSz),
|
||||
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
|
||||
/* (6) Critical nameConstraints carrying a GeneralName form wolfSSL
|
||||
* does not enforce (registeredID). RFC 5280 4.2.1.10 requires the
|
||||
* verifier to either process the constraint or reject; we reject
|
||||
* fail-closed. The leaf needs no SAN to exercise this path. */
|
||||
/* (6) Critical excluded registeredID subtree, leaf SAN matches:
|
||||
* registeredID is now enforced by byte-comparing OID bodies, so a
|
||||
* matching excluded entry must be rejected. */
|
||||
ExpectIntEQ(verify_with_otherName_chain(
|
||||
ncRegisteredID, ncRegisteredIDSz, 1, NULL, 0),
|
||||
ncRegisteredID, ncRegisteredIDSz, 1,
|
||||
sanRegisteredID, sanRegisteredIDSz),
|
||||
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
|
||||
/* (7) Same as (6) but non-critical: RFC 5280 only mandates the
|
||||
* fail-closed reject when the extension is critical, so a
|
||||
* non-critical unsupported constraint form is silently ignored
|
||||
* and verification succeeds. */
|
||||
/* (7) Non-critical excluded registeredID subtree, leaf SAN matches:
|
||||
* excluded subtrees are enforced regardless of criticality. */
|
||||
ExpectIntEQ(verify_with_otherName_chain(
|
||||
ncRegisteredID, ncRegisteredIDSz, 0, NULL, 0),
|
||||
ncRegisteredID, ncRegisteredIDSz, 0,
|
||||
sanRegisteredID, sanRegisteredIDSz),
|
||||
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
|
||||
/* (8) Critical excluded registeredID subtree, leaf has no RID SAN:
|
||||
* no excluded match, so verification succeeds. */
|
||||
ExpectIntEQ(verify_with_otherName_chain(
|
||||
ncRegisteredID, ncRegisteredIDSz, 1, NULL, 0),
|
||||
0);
|
||||
#endif
|
||||
return EXPECT_RESULT();
|
||||
@@ -26733,11 +26758,25 @@ static int test_wolfSSL_X509_print(void)
|
||||
/* Will print IP address subject alt name. */
|
||||
ExpectIntEQ(BIO_get_mem_data(bio, NULL), 3350);
|
||||
#endif
|
||||
#elif defined(NO_ASN_TIME)
|
||||
/* With NO_ASN_TIME defined, X509_print skips printing Validity. */
|
||||
#elif defined(IGNORE_NAME_CONSTRAINTS)
|
||||
/* DecodeGeneralName skips iPAddress entries when name constraints
|
||||
* are disabled, so the IP SAN never reaches the print path. */
|
||||
#if defined(NO_ASN_TIME)
|
||||
ExpectIntEQ(BIO_get_mem_data(bio, NULL), 3213);
|
||||
#else
|
||||
#else
|
||||
ExpectIntEQ(BIO_get_mem_data(bio, NULL), 3328);
|
||||
#endif
|
||||
#elif defined(NO_ASN_TIME)
|
||||
/* With NO_ASN_TIME defined, X509_print skips printing Validity.
|
||||
* iPAddress SAN now always parsed; prints as
|
||||
* "IP Address:<unavailable>" (+26 bytes) without
|
||||
* WOLFSSL_IP_ALT_NAME. */
|
||||
ExpectIntEQ(BIO_get_mem_data(bio, NULL), 3239);
|
||||
#else
|
||||
/* iPAddress SAN now always parsed; prints as
|
||||
* "IP Address:<unavailable>" (+26 bytes) without
|
||||
* WOLFSSL_IP_ALT_NAME. */
|
||||
ExpectIntEQ(BIO_get_mem_data(bio, NULL), 3354);
|
||||
#endif
|
||||
BIO_free(bio);
|
||||
bio = NULL;
|
||||
|
||||
@@ -1705,6 +1705,410 @@ int test_wolfSSL_CertManagerNameConstraint_DNS_CN(void)
|
||||
return EXPECT_RESULT();
|
||||
}
|
||||
|
||||
int test_wolfSSL_CertManagerNameConstraint_IP_SAN(void)
|
||||
{
|
||||
EXPECT_DECLS;
|
||||
#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && \
|
||||
!defined(NO_WOLFSSL_CM_VERIFY) && !defined(NO_RSA) && \
|
||||
defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_GEN) && \
|
||||
defined(WOLFSSL_CERT_EXT) && defined(WOLFSSL_ALT_NAMES) && \
|
||||
!defined(NO_SHA256) && !defined(IGNORE_NAME_CONSTRAINTS)
|
||||
/* Regression test for iPAddress name-constraint enforcement.
|
||||
*
|
||||
* The CA at cert-ext-ncip.der declares a permittedSubtrees iPAddress
|
||||
* constraint of 192.168.1.0/255.255.255.0. A leaf with an iPAddress
|
||||
* SAN outside that subnet must be rejected. Prior to the fix, default
|
||||
* builds (without WOLFSSL_IP_ALT_NAME) silently skipped iPAddress SANs
|
||||
* during parsing, so the constraint loop saw no IP entries and the
|
||||
* leaf was accepted.
|
||||
*
|
||||
* The bypass only existed when WOLFSSL_IP_ALT_NAME was undefined (the
|
||||
* default). To exercise the regression target, this test must run in a
|
||||
* configuration without --enable-ip-alt-name and without
|
||||
* --enable-opensslall (which implies WOLFSSL_IP_ALT_NAME via
|
||||
* settings.h). With WOLFSSL_IP_ALT_NAME defined the same assertions
|
||||
* still hold, but the negative case there is enforcement of an
|
||||
* already-working path rather than the regression itself.
|
||||
*
|
||||
* Scope: this test exercises the permittedSubtrees code path. The
|
||||
* excludedSubtrees path uses the same parsing plumbing
|
||||
* (DecodeGeneralName -> SetDNSEntry into cert->altNames) and the same
|
||||
* ConfirmNameConstraints walk; the TALOS bug was strictly about
|
||||
* iPAddress entries being absent from cert->altNames, so once that is
|
||||
* fixed both directions are restored. The pre-existing
|
||||
* test_wolfSSL_NAME_CONSTRAINTS_excluded test exercises the excluded
|
||||
* direction more broadly. */
|
||||
WOLFSSL_CERT_MANAGER* cm = NULL;
|
||||
WOLFSSL_EVP_PKEY *priv = NULL;
|
||||
WOLFSSL_X509_NAME* name = NULL;
|
||||
const char* ca_cert = "./certs/test/cert-ext-ncip.der";
|
||||
const char* server_cert = "./certs/test/server-goodcn.pem";
|
||||
/* Raw IPv4 bytes for SAN values (not dotted-quad strings). */
|
||||
static const byte ip_inside[] = { 192, 168, 1, 10 }; /* permitted */
|
||||
static const byte ip_outside[] = { 10, 0, 0, 1 }; /* violates */
|
||||
|
||||
byte *der = NULL;
|
||||
int derSz;
|
||||
byte *pt;
|
||||
WOLFSSL_X509 *x509 = NULL;
|
||||
WOLFSSL_X509 *ca = NULL;
|
||||
|
||||
pt = (byte*)server_key_der_2048;
|
||||
ExpectNotNull(priv = wolfSSL_d2i_PrivateKey(EVP_PKEY_RSA, NULL,
|
||||
(const unsigned char**)&pt, sizeof_server_key_der_2048));
|
||||
|
||||
ExpectNotNull(cm = wolfSSL_CertManagerNew());
|
||||
ExpectNotNull(ca = wolfSSL_X509_load_certificate_file(ca_cert,
|
||||
WOLFSSL_FILETYPE_ASN1));
|
||||
ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(ca, &derSz)));
|
||||
ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, der, derSz,
|
||||
WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS);
|
||||
|
||||
/* Negative case: leaf with IP SAN outside permitted subnet. Must be
|
||||
* rejected with ASN_NAME_INVALID_E. */
|
||||
ExpectNotNull(x509 = wolfSSL_X509_load_certificate_file(server_cert,
|
||||
WOLFSSL_FILETYPE_PEM));
|
||||
ExpectNotNull(name = wolfSSL_X509_get_subject_name(ca));
|
||||
ExpectIntEQ(wolfSSL_X509_set_issuer_name(x509, name), WOLFSSL_SUCCESS);
|
||||
name = NULL;
|
||||
|
||||
/* Use add_altname_ex with raw IP bytes so the test runs in default
|
||||
* builds where add_altname (string form) requires WOLFSSL_IP_ALT_NAME. */
|
||||
ExpectIntEQ(wolfSSL_X509_add_altname_ex(x509, (const char*)ip_outside,
|
||||
sizeof(ip_outside), ASN_IP_TYPE), WOLFSSL_SUCCESS);
|
||||
ExpectIntGT(wolfSSL_X509_sign(x509, priv, EVP_sha256()), 0);
|
||||
ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(x509, &derSz)));
|
||||
ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz,
|
||||
WOLFSSL_FILETYPE_ASN1), WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
wolfSSL_X509_free(x509);
|
||||
x509 = NULL;
|
||||
|
||||
/* Positive case: leaf with IP SAN inside the permitted subnet must be
|
||||
* accepted. Confirms the fix does not over-reject. */
|
||||
ExpectNotNull(x509 = wolfSSL_X509_load_certificate_file(server_cert,
|
||||
WOLFSSL_FILETYPE_PEM));
|
||||
ExpectNotNull(name = wolfSSL_X509_get_subject_name(ca));
|
||||
ExpectIntEQ(wolfSSL_X509_set_issuer_name(x509, name), WOLFSSL_SUCCESS);
|
||||
name = NULL;
|
||||
|
||||
ExpectIntEQ(wolfSSL_X509_add_altname_ex(x509, (const char*)ip_inside,
|
||||
sizeof(ip_inside), ASN_IP_TYPE), WOLFSSL_SUCCESS);
|
||||
ExpectIntGT(wolfSSL_X509_sign(x509, priv, EVP_sha256()), 0);
|
||||
ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(x509, &derSz)));
|
||||
ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz,
|
||||
WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS);
|
||||
|
||||
wolfSSL_CertManagerFree(cm);
|
||||
wolfSSL_X509_free(x509);
|
||||
wolfSSL_X509_free(ca);
|
||||
wolfSSL_EVP_PKEY_free(priv);
|
||||
#endif
|
||||
return EXPECT_RESULT();
|
||||
}
|
||||
|
||||
int test_wolfSSL_CertManagerNameConstraint_RID_SAN(void)
|
||||
{
|
||||
EXPECT_DECLS;
|
||||
#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && \
|
||||
!defined(NO_WOLFSSL_CM_VERIFY) && !defined(NO_RSA) && \
|
||||
defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_GEN) && \
|
||||
defined(WOLFSSL_CERT_EXT) && defined(WOLFSSL_ALT_NAMES) && \
|
||||
!defined(NO_SHA256) && !defined(IGNORE_NAME_CONSTRAINTS)
|
||||
/* Regression test for registeredID name-constraint enforcement.
|
||||
*
|
||||
* The CA at cert-ext-ncrid.der declares a permittedSubtrees
|
||||
* registeredID constraint of OID 1.2.3.4.5. A leaf with a
|
||||
* registeredID SAN outside that permitted set must be rejected.
|
||||
*
|
||||
* Pre-fix, three independent failures hid the registeredID type from
|
||||
* enforcement in all build configurations:
|
||||
* 1. ConfirmNameConstraints' nameTypes[] array did not include
|
||||
* ASN_RID_TYPE, so the constraint loop never considered
|
||||
* registeredID entries.
|
||||
* 2. DecodeSubtree's accepted-tag set excluded
|
||||
* ASN_CONTEXT_SPECIFIC|ASN_RID_TYPE, so registeredID subtrees
|
||||
* in the CA's nameConstraints were dropped during parsing and
|
||||
* never reached signer->permittedNames/excludedNames.
|
||||
* 3. DecodeGeneralName gated registeredID SAN parsing on
|
||||
* WOLFSSL_RID_ALT_NAME; without that define cert->altNames had
|
||||
* no ASN_RID_TYPE entries.
|
||||
* The bug was unconditional - WOLFSSL_RID_ALT_NAME alone did not
|
||||
* fix it because layers 1 and 2 still discarded the data. */
|
||||
WOLFSSL_CERT_MANAGER* cm = NULL;
|
||||
WOLFSSL_EVP_PKEY *priv = NULL;
|
||||
WOLFSSL_X509_NAME* name = NULL;
|
||||
const char* ca_cert = "./certs/test/cert-ext-ncrid.der";
|
||||
const char* server_cert = "./certs/test/server-goodcn.pem";
|
||||
/* DER-encoded OID body bytes (no tag/length, since registeredID is
|
||||
* IMPLICIT [8] OBJECT IDENTIFIER inside a SAN). The first arc encodes
|
||||
* 40*X + Y in one byte for X<=2,Y<=39, then each subsequent arc is
|
||||
* base-128 with continuation bits. For values <128 the encoding is a
|
||||
* single byte. */
|
||||
static const byte rid_inside[] = { 0x2A, 0x03, 0x04, 0x05 }; /* 1.2.3.4.5 - permitted */
|
||||
static const byte rid_outside[] = { 0x2A, 0x03, 0x04, 0x63 }; /* 1.2.3.4.99 - violates */
|
||||
|
||||
byte *der = NULL;
|
||||
int derSz;
|
||||
byte *pt;
|
||||
WOLFSSL_X509 *x509 = NULL;
|
||||
WOLFSSL_X509 *ca = NULL;
|
||||
|
||||
pt = (byte*)server_key_der_2048;
|
||||
ExpectNotNull(priv = wolfSSL_d2i_PrivateKey(EVP_PKEY_RSA, NULL,
|
||||
(const unsigned char**)&pt, sizeof_server_key_der_2048));
|
||||
|
||||
ExpectNotNull(cm = wolfSSL_CertManagerNew());
|
||||
ExpectNotNull(ca = wolfSSL_X509_load_certificate_file(ca_cert,
|
||||
WOLFSSL_FILETYPE_ASN1));
|
||||
ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(ca, &derSz)));
|
||||
ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, der, derSz,
|
||||
WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS);
|
||||
|
||||
/* Negative case: leaf with a registeredID SAN outside the permitted
|
||||
* OID set. Must be rejected with ASN_NAME_INVALID_E. */
|
||||
ExpectNotNull(x509 = wolfSSL_X509_load_certificate_file(server_cert,
|
||||
WOLFSSL_FILETYPE_PEM));
|
||||
ExpectNotNull(name = wolfSSL_X509_get_subject_name(ca));
|
||||
ExpectIntEQ(wolfSSL_X509_set_issuer_name(x509, name), WOLFSSL_SUCCESS);
|
||||
name = NULL;
|
||||
|
||||
ExpectIntEQ(wolfSSL_X509_add_altname_ex(x509, (const char*)rid_outside,
|
||||
sizeof(rid_outside), ASN_RID_TYPE), WOLFSSL_SUCCESS);
|
||||
ExpectIntGT(wolfSSL_X509_sign(x509, priv, EVP_sha256()), 0);
|
||||
ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(x509, &derSz)));
|
||||
ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz,
|
||||
WOLFSSL_FILETYPE_ASN1), WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
wolfSSL_X509_free(x509);
|
||||
x509 = NULL;
|
||||
|
||||
/* Positive case: leaf with a registeredID SAN matching the permitted
|
||||
* OID exactly must be accepted. */
|
||||
ExpectNotNull(x509 = wolfSSL_X509_load_certificate_file(server_cert,
|
||||
WOLFSSL_FILETYPE_PEM));
|
||||
ExpectNotNull(name = wolfSSL_X509_get_subject_name(ca));
|
||||
ExpectIntEQ(wolfSSL_X509_set_issuer_name(x509, name), WOLFSSL_SUCCESS);
|
||||
name = NULL;
|
||||
|
||||
ExpectIntEQ(wolfSSL_X509_add_altname_ex(x509, (const char*)rid_inside,
|
||||
sizeof(rid_inside), ASN_RID_TYPE), WOLFSSL_SUCCESS);
|
||||
ExpectIntGT(wolfSSL_X509_sign(x509, priv, EVP_sha256()), 0);
|
||||
ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(x509, &derSz)));
|
||||
ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz,
|
||||
WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS);
|
||||
|
||||
wolfSSL_CertManagerFree(cm);
|
||||
wolfSSL_X509_free(x509);
|
||||
wolfSSL_X509_free(ca);
|
||||
wolfSSL_EVP_PKEY_free(priv);
|
||||
#endif
|
||||
return EXPECT_RESULT();
|
||||
}
|
||||
|
||||
int test_wolfSSL_X509_get_ext_d2i_RID_SAN(void)
|
||||
{
|
||||
EXPECT_DECLS;
|
||||
#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && !defined(NO_RSA) && \
|
||||
defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_GEN) && \
|
||||
defined(WOLFSSL_CERT_EXT) && defined(WOLFSSL_ALT_NAMES) && \
|
||||
!defined(NO_SHA256)
|
||||
/* Regression test: a cert with a registeredID SAN must be exposable
|
||||
* through the OPENSSL_EXTRA SAN APIs even when WOLFSSL_RID_ALT_NAME
|
||||
* is undefined.
|
||||
*
|
||||
* The registeredID name-constraint fix adds registeredID entries to
|
||||
* altNames unconditionally so name constraints can be enforced. The
|
||||
* OPENSSL_EXTRA helpers DNS_to_GENERAL_NAME (used by
|
||||
* wolfSSL_X509_get_ext) and the ALT_NAMES_OID arm of
|
||||
* wolfSSL_X509_get_ext_d2i previously gated registeredID handling on
|
||||
* WOLFSSL_RID_ALT_NAME, which is NOT auto-enabled by
|
||||
* --enable-opensslextra. Without the fix, any cert with a
|
||||
* registeredID SAN would cause those getters to return NULL or
|
||||
* produce a malformed GENERAL_NAME (type==GEN_RID but
|
||||
* d.dNSName-as-IA5STRING in the union). */
|
||||
WOLFSSL_EVP_PKEY *priv = NULL;
|
||||
const char* server_cert = "./certs/test/server-goodcn.pem";
|
||||
static const byte rid_oid[] = { 0x2A, 0x03, 0x04, 0x05 };
|
||||
byte *pt;
|
||||
byte *der = NULL;
|
||||
int derSz;
|
||||
WOLFSSL_X509 *leaf = NULL;
|
||||
WOLFSSL_X509 *parsed = NULL;
|
||||
WOLFSSL_STACK *altNames = NULL;
|
||||
WOLFSSL_X509_EXTENSION *ext = NULL;
|
||||
WOLFSSL_GENERAL_NAME *gn = NULL;
|
||||
int found = 0;
|
||||
int i;
|
||||
int loc;
|
||||
|
||||
pt = (byte*)server_key_der_2048;
|
||||
ExpectNotNull(priv = wolfSSL_d2i_PrivateKey(EVP_PKEY_RSA, NULL,
|
||||
(const unsigned char**)&pt, sizeof_server_key_der_2048));
|
||||
|
||||
/* Build and sign a leaf with a registeredID SAN. */
|
||||
ExpectNotNull(leaf = wolfSSL_X509_load_certificate_file(server_cert,
|
||||
WOLFSSL_FILETYPE_PEM));
|
||||
ExpectIntEQ(wolfSSL_X509_add_altname_ex(leaf, (const char*)rid_oid,
|
||||
sizeof(rid_oid), ASN_RID_TYPE), WOLFSSL_SUCCESS);
|
||||
ExpectIntGT(wolfSSL_X509_sign(leaf, priv, EVP_sha256()), 0);
|
||||
ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(leaf, &derSz)));
|
||||
|
||||
/* Re-parse the signed leaf so altNames are populated through
|
||||
* DecodeGeneralName (the path the fix touches). */
|
||||
ExpectNotNull(parsed = wolfSSL_X509_load_certificate_buffer(der, derSz,
|
||||
WOLFSSL_FILETYPE_ASN1));
|
||||
|
||||
/* wolfSSL_X509_get_ext_d2i for SAN must return a non-NULL stack
|
||||
* containing a registeredID GENERAL_NAME with the union populated
|
||||
* via d.registeredID (not d.dNSName). */
|
||||
ExpectNotNull(altNames = (WOLFSSL_STACK*)wolfSSL_X509_get_ext_d2i(parsed,
|
||||
NID_subject_alt_name, NULL, NULL));
|
||||
if (altNames != NULL) {
|
||||
for (i = 0; i < wolfSSL_sk_num(altNames); i++) {
|
||||
gn = (WOLFSSL_GENERAL_NAME*)wolfSSL_sk_value(altNames, i);
|
||||
if (gn != NULL && gn->type == ASN_RID_TYPE) {
|
||||
/* The union must hold a real ASN1_OBJECT, not the
|
||||
* default IA5 string the bypass path would produce.
|
||||
* Verify the encoded OID bytes match what we put on
|
||||
* the leaf so a future mis-encoding (wrong tag, off-by
|
||||
* -one length, swapped fields) is caught. */
|
||||
ExpectNotNull(gn->d.registeredID);
|
||||
if (gn->d.registeredID != NULL) {
|
||||
/* DER-wrapped: ASN_OBJECT_ID + length-byte + body */
|
||||
word32 expectedSz = 1 + 1 + sizeof(rid_oid);
|
||||
ExpectIntEQ(gn->d.registeredID->objSz, expectedSz);
|
||||
ExpectNotNull(gn->d.registeredID->obj);
|
||||
if (gn->d.registeredID->obj != NULL &&
|
||||
gn->d.registeredID->objSz == expectedSz) {
|
||||
ExpectIntEQ(gn->d.registeredID->obj[0], ASN_OBJECT_ID);
|
||||
ExpectIntEQ(gn->d.registeredID->obj[1],
|
||||
(byte)sizeof(rid_oid));
|
||||
ExpectIntEQ(XMEMCMP(gn->d.registeredID->obj + 2,
|
||||
rid_oid, sizeof(rid_oid)), 0);
|
||||
}
|
||||
}
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ExpectIntEQ(found, 1);
|
||||
|
||||
if (altNames != NULL) {
|
||||
wolfSSL_sk_pop_free(altNames,
|
||||
(void (*)(void*))wolfSSL_GENERAL_NAME_free);
|
||||
}
|
||||
|
||||
/* Also exercise wolfSSL_X509_get_ext, which routes through
|
||||
* DNS_to_GENERAL_NAME. Pre-fix that helper would return WOLFSSL_FAILURE
|
||||
* for any RID entry in default builds, causing X509_get_ext to return
|
||||
* NULL even though the SAN extension is present. */
|
||||
loc = wolfSSL_X509_get_ext_by_NID(parsed, NID_subject_alt_name, -1);
|
||||
ExpectIntGE(loc, 0);
|
||||
if (loc >= 0) {
|
||||
ExpectNotNull(ext = wolfSSL_X509_get_ext(parsed, loc));
|
||||
}
|
||||
|
||||
wolfSSL_X509_free(parsed);
|
||||
wolfSSL_X509_free(leaf);
|
||||
wolfSSL_EVP_PKEY_free(priv);
|
||||
#endif
|
||||
return EXPECT_RESULT();
|
||||
}
|
||||
|
||||
int test_wolfSSL_X509_check_host_IP_only_SAN_CN_fallback(void)
|
||||
{
|
||||
EXPECT_DECLS;
|
||||
#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && !defined(NO_RSA) && \
|
||||
defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_GEN) && \
|
||||
defined(WOLFSSL_CERT_EXT) && defined(WOLFSSL_ALT_NAMES) && \
|
||||
!defined(NO_SHA256)
|
||||
/* Companion regression test for the CheckForAltNames CN-fallback
|
||||
* preservation introduced alongside the iPAddress name-constraint
|
||||
* enforcement fix.
|
||||
*
|
||||
* Once iPAddress SAN entries are unconditionally added to altNames
|
||||
* (so name constraints can be enforced), a leaf that presents only
|
||||
* iPAddress SANs would suppress CN fallback in CheckForAltNames in
|
||||
* default builds, where the iPAddress matching path is compiled out.
|
||||
* That would silently break TLS hostname verification for callers
|
||||
* that previously relied on the CN fallback. The fix in
|
||||
* src/internal.c treats iPAddress entries as absent for the
|
||||
* *checkCN decision when WOLFSSL_IP_ALT_NAME is undefined.
|
||||
*
|
||||
* This test pins both directions:
|
||||
* - default build (no WOLFSSL_IP_ALT_NAME): IP-only-SAN cert with a
|
||||
* matching CN must succeed via CN fallback.
|
||||
* - WOLFSSL_IP_ALT_NAME defined: the same cert must fail because
|
||||
* the SAN presence suppresses CN fallback (RFC 6125 compliant).
|
||||
* Independently, a cert with a non-matching DNS SAN must always fail
|
||||
* regardless of build flags, since DNS SAN presence unambiguously
|
||||
* suppresses CN fallback. */
|
||||
WOLFSSL_EVP_PKEY *priv = NULL;
|
||||
WOLFSSL_X509_NAME* name = NULL;
|
||||
const char* server_cert = "./certs/test/server-goodcn.pem";
|
||||
const char hostName[] = "cnhost.local";
|
||||
static const byte ip_san[] = { 10, 0, 0, 1 };
|
||||
byte *pt;
|
||||
WOLFSSL_X509 *leafIp = NULL;
|
||||
WOLFSSL_X509 *leafDns = NULL;
|
||||
|
||||
pt = (byte*)server_key_der_2048;
|
||||
ExpectNotNull(priv = wolfSSL_d2i_PrivateKey(EVP_PKEY_RSA, NULL,
|
||||
(const unsigned char**)&pt, sizeof_server_key_der_2048));
|
||||
|
||||
/* Leaf with CN matching hostName and only an iPAddress SAN. */
|
||||
ExpectNotNull(leafIp = wolfSSL_X509_load_certificate_file(server_cert,
|
||||
WOLFSSL_FILETYPE_PEM));
|
||||
ExpectNotNull(name = X509_NAME_new());
|
||||
ExpectIntEQ(X509_NAME_add_entry_by_txt(name, "commonName", MBSTRING_UTF8,
|
||||
(byte*)hostName, (int)XSTRLEN(hostName), -1, 0), SSL_SUCCESS);
|
||||
ExpectIntEQ(wolfSSL_X509_set_subject_name(leafIp, name), WOLFSSL_SUCCESS);
|
||||
X509_NAME_free(name);
|
||||
name = NULL;
|
||||
ExpectIntEQ(wolfSSL_X509_add_altname_ex(leafIp, (const char*)ip_san,
|
||||
sizeof(ip_san), ASN_IP_TYPE), WOLFSSL_SUCCESS);
|
||||
ExpectIntGT(wolfSSL_X509_sign(leafIp, priv, EVP_sha256()), 0);
|
||||
|
||||
#ifndef WOLFSSL_IP_ALT_NAME
|
||||
/* Default build: iPAddress entries are present in altNames for
|
||||
* constraint enforcement but treated as absent for *checkCN, so the
|
||||
* lookup falls back to the Subject CN, which matches. */
|
||||
ExpectIntEQ(wolfSSL_X509_check_host(leafIp, hostName, XSTRLEN(hostName),
|
||||
0, NULL), WOLFSSL_SUCCESS);
|
||||
#else
|
||||
/* IP_ALT_NAME build: SAN presence suppresses CN fallback per RFC 6125.
|
||||
* The hostName ("cnhost.local") cannot match the iPAddress entry, so
|
||||
* the check must fail. */
|
||||
ExpectIntEQ(wolfSSL_X509_check_host(leafIp, hostName, XSTRLEN(hostName),
|
||||
0, NULL), WC_NO_ERR_TRACE(WOLFSSL_FAILURE));
|
||||
#endif
|
||||
|
||||
/* Leaf with CN matching hostName but a non-matching DNS SAN. CN
|
||||
* fallback must be suppressed in every build (DNS SAN unambiguously
|
||||
* counts toward *checkCN), so the check must fail. This pins the
|
||||
* other side of the boundary so a future change that broadly skips
|
||||
* altNames in *checkCN does not silently regress. */
|
||||
ExpectNotNull(leafDns = wolfSSL_X509_load_certificate_file(server_cert,
|
||||
WOLFSSL_FILETYPE_PEM));
|
||||
ExpectNotNull(name = X509_NAME_new());
|
||||
ExpectIntEQ(X509_NAME_add_entry_by_txt(name, "commonName", MBSTRING_UTF8,
|
||||
(byte*)hostName, (int)XSTRLEN(hostName), -1, 0), SSL_SUCCESS);
|
||||
ExpectIntEQ(wolfSSL_X509_set_subject_name(leafDns, name), WOLFSSL_SUCCESS);
|
||||
X509_NAME_free(name);
|
||||
name = NULL;
|
||||
ExpectIntEQ(wolfSSL_X509_add_altname(leafDns, "other.example",
|
||||
ASN_DNS_TYPE), WOLFSSL_SUCCESS);
|
||||
ExpectIntGT(wolfSSL_X509_sign(leafDns, priv, EVP_sha256()), 0);
|
||||
ExpectIntEQ(wolfSSL_X509_check_host(leafDns, hostName, XSTRLEN(hostName),
|
||||
0, NULL), WC_NO_ERR_TRACE(WOLFSSL_FAILURE));
|
||||
|
||||
wolfSSL_X509_free(leafIp);
|
||||
wolfSSL_X509_free(leafDns);
|
||||
wolfSSL_EVP_PKEY_free(priv);
|
||||
#endif
|
||||
return EXPECT_RESULT();
|
||||
}
|
||||
|
||||
int test_wolfSSL_CertManagerCRL(void)
|
||||
{
|
||||
EXPECT_DECLS;
|
||||
|
||||
@@ -36,6 +36,10 @@ int test_wolfSSL_CertManagerNameConstraint3(void);
|
||||
int test_wolfSSL_CertManagerNameConstraint4(void);
|
||||
int test_wolfSSL_CertManagerNameConstraint5(void);
|
||||
int test_wolfSSL_CertManagerNameConstraint_DNS_CN(void);
|
||||
int test_wolfSSL_CertManagerNameConstraint_IP_SAN(void);
|
||||
int test_wolfSSL_CertManagerNameConstraint_RID_SAN(void);
|
||||
int test_wolfSSL_X509_get_ext_d2i_RID_SAN(void);
|
||||
int test_wolfSSL_X509_check_host_IP_only_SAN_CN_fallback(void);
|
||||
int test_wolfSSL_CertManagerCRL(void);
|
||||
int test_wolfSSL_CRL_reason_extensions_cleanup(void);
|
||||
int test_wolfSSL_CRL_static_revoked_list(void);
|
||||
@@ -60,6 +64,11 @@ int test_wolfSSL_CertManagerRejectMD5Cert(void);
|
||||
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint4), \
|
||||
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint5), \
|
||||
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint_DNS_CN), \
|
||||
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint_IP_SAN), \
|
||||
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint_RID_SAN), \
|
||||
TEST_DECL_GROUP("certman", test_wolfSSL_X509_get_ext_d2i_RID_SAN), \
|
||||
TEST_DECL_GROUP("certman", \
|
||||
test_wolfSSL_X509_check_host_IP_only_SAN_CN_fallback), \
|
||||
TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerCRL), \
|
||||
TEST_DECL_GROUP("certman", test_wolfSSL_CRL_reason_extensions_cleanup), \
|
||||
TEST_DECL_GROUP("certman", test_wolfSSL_CRL_static_revoked_list), \
|
||||
|
||||
+99
-18
@@ -18081,6 +18081,17 @@ static int PermittedListOk(DNS_entry* name, Base_entry* dnsList, byte nameType)
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (nameType == ASN_RID_TYPE) {
|
||||
/* registeredID matches when the OID bodies are bytewise
|
||||
* equal. RFC 5280 Sec. 4.2.1.10 does not define a
|
||||
* subtree relation for OIDs, so use exact-match. */
|
||||
if (name->len == current->nameSz &&
|
||||
XMEMCMP(name->name, current->name,
|
||||
(size_t)name->len) == 0) {
|
||||
match = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (name->len >= current->nameSz &&
|
||||
wolfssl_local_MatchBaseName(nameType, name->name, name->len,
|
||||
current->name, current->nameSz)) {
|
||||
@@ -18134,6 +18145,16 @@ static int IsInExcludedList(DNS_entry* name, Base_entry* dnsList, byte nameType)
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (nameType == ASN_RID_TYPE) {
|
||||
/* registeredID matches when the OID bodies are bytewise
|
||||
* equal. See PermittedListOk for the rationale. */
|
||||
if (name->len == current->nameSz &&
|
||||
XMEMCMP(name->name, current->name,
|
||||
(size_t)name->len) == 0) {
|
||||
ret = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (name->len >= current->nameSz &&
|
||||
wolfssl_local_MatchBaseName(nameType, name->name, name->len,
|
||||
current->name, current->nameSz)) {
|
||||
@@ -18151,7 +18172,8 @@ static int IsInExcludedList(DNS_entry* name, Base_entry* dnsList, byte nameType)
|
||||
static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert)
|
||||
{
|
||||
const byte nameTypes[] = {ASN_RFC822_TYPE, ASN_DNS_TYPE, ASN_DIR_TYPE,
|
||||
ASN_IP_TYPE, ASN_URI_TYPE, ASN_OTHER_TYPE};
|
||||
ASN_IP_TYPE, ASN_URI_TYPE, ASN_OTHER_TYPE,
|
||||
ASN_RID_TYPE};
|
||||
int i;
|
||||
|
||||
if (signer == NULL || cert == NULL)
|
||||
@@ -18231,6 +18253,11 @@ static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert)
|
||||
* against the issuing CA's subtree. */
|
||||
name = cert->altOtherNamesRaw;
|
||||
break;
|
||||
case ASN_RID_TYPE:
|
||||
/* registeredID entries also live on cert->altNames as
|
||||
* raw OID body bytes. */
|
||||
name = cert->altNames;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -18275,9 +18302,9 @@ static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert)
|
||||
/* RFC 5280 4.2.1.10: "If a name constraints extension that is marked as
|
||||
* critical imposes constraints on a particular name form ... the
|
||||
* application MUST either process the constraint or reject the
|
||||
* certificate." otherName is processed by byte-comparison above; any
|
||||
* remaining unsupported forms (registeredID, x400Address, ediPartyName)
|
||||
* trigger the fail-closed reject below. */
|
||||
* certificate." otherName and registeredID are both processed by
|
||||
* byte-comparison above; any remaining unsupported forms
|
||||
* (x400Address, ediPartyName) trigger the fail-closed reject below. */
|
||||
if (signer->extNameConstraintCrit &&
|
||||
signer->extNameConstraintHasUnsupported) {
|
||||
WOLFSSL_MSG("Critical nameConstraints contains unsupported "
|
||||
@@ -18592,8 +18619,32 @@ static int DecodeGeneralName(const byte* input, word32* inOutIdx, byte tag,
|
||||
idx += (word32)len;
|
||||
}
|
||||
}
|
||||
#ifdef WOLFSSL_IP_ALT_NAME
|
||||
/* GeneralName choice: iPAddress */
|
||||
/* GeneralName choice: iPAddress
|
||||
*
|
||||
* Always parse iPAddress into cert->altNames so ConfirmNameConstraints
|
||||
* can enforce permitted/excluded iPAddress subtrees (RFC 5280
|
||||
* Sec. 4.2.1.10). The entry holds raw 4/16 octet payloads;
|
||||
* WOLFSSL_IP_ALT_NAME still gates the human-readable ipString
|
||||
* generation in SetDNSEntry.
|
||||
*
|
||||
* Consequences for downstream consumers when WOLFSSL_IP_ALT_NAME is
|
||||
* undefined:
|
||||
* - wolfSSL_X509_get_next_altname (string iterator): explicitly
|
||||
* skips iPAddress entries, since returning raw bytes as a C
|
||||
* string would truncate at any embedded NUL. This preserves the
|
||||
* pre-fix behavior for that getter.
|
||||
* - CheckForAltNames (TLS hostname matching): the iPAddress branch
|
||||
* is compiled out, so iPAddress entries cannot match anything;
|
||||
* they are also excluded from the *checkCN decision so an
|
||||
* IP-only-SAN cert still falls back to CN matching as before.
|
||||
* - All other altNames walkers (e.g. ALT_NAMES_OID handling in
|
||||
* wolfSSL_X509_get_ext_d2i, wolfssl_x509_alt_names_to_gn,
|
||||
* FlattenAltNames in cert generation) now see iPAddress entries
|
||||
* unconditionally. This is intentional and brings wolfSSL closer
|
||||
* to OpenSSL's SAN-exposure semantics; the OPENSSL_EXTRA APIs
|
||||
* surface the raw octets as OCTET_STRING already (see the
|
||||
* ASN_IP_TYPE case under WOLFSSL_GEN_IPADD in src/x509.c).
|
||||
*/
|
||||
else if (tag == (ASN_CONTEXT_SPECIFIC | ASN_IP_TYPE)) {
|
||||
ret = SetDNSEntry(cert->heap, (const char*)(input + idx), len,
|
||||
ASN_IP_TYPE, &cert->altNames);
|
||||
@@ -18601,9 +18652,27 @@ static int DecodeGeneralName(const byte* input, word32* inOutIdx, byte tag,
|
||||
idx += (word32)len;
|
||||
}
|
||||
}
|
||||
#endif /* WOLFSSL_IP_ALT_NAME */
|
||||
#ifdef WOLFSSL_RID_ALT_NAME
|
||||
/* GeneralName choice: registeredID */
|
||||
/* GeneralName choice: registeredID
|
||||
*
|
||||
* Always parse registeredID into cert->altNames so
|
||||
* ConfirmNameConstraints can enforce permitted/excluded subtrees
|
||||
* (RFC 5280 Sec. 4.2.1.10). The entry holds raw OID body bytes;
|
||||
* WOLFSSL_RID_ALT_NAME only gates the human-readable ridString
|
||||
* generation in SetDNSEntry. Downstream consumer treatment in
|
||||
* default builds:
|
||||
* - wolfSSL_X509_get_next_altname (string iterator): skips
|
||||
* ASN_RID_TYPE entries (raw OID bytes are not a C string).
|
||||
* - CheckForAltNames (TLS hostname matching): skips ASN_RID_TYPE
|
||||
* unconditionally and excludes them from *checkCN, so a cert
|
||||
* with only registeredID SANs still falls back to CN.
|
||||
* - DNS_to_GENERAL_NAME (used by wolfSSL_X509_get_ext) and the
|
||||
* ALT_NAMES_OID arm of wolfSSL_X509_get_ext_d2i: build a proper
|
||||
* ASN1_OBJECT in d.registeredID from raw OID bytes regardless
|
||||
* of WOLFSSL_RID_ALT_NAME, so OPENSSL_EXTRA-style callers see
|
||||
* correctly-typed GENERAL_NAME entries.
|
||||
* - X509_print_name_entry: emits "Registered ID:<unavailable>"
|
||||
* when ridString is not generated, instead of failing the
|
||||
* whole print operation. */
|
||||
else if (tag == (ASN_CONTEXT_SPECIFIC | ASN_RID_TYPE)) {
|
||||
ret = SetDNSEntry(cert->heap, (const char*)(input + idx), len,
|
||||
ASN_RID_TYPE, &cert->altNames);
|
||||
@@ -18611,7 +18680,6 @@ static int DecodeGeneralName(const byte* input, word32* inOutIdx, byte tag,
|
||||
idx += (word32)len;
|
||||
}
|
||||
}
|
||||
#endif /* WOLFSSL_RID_ALT_NAME */
|
||||
#endif /* IGNORE_NAME_CONSTRAINTS */
|
||||
#ifndef IGNORE_NAME_CONSTRAINTS
|
||||
/* GeneralName choice: otherName.
|
||||
@@ -19898,24 +19966,29 @@ static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head,
|
||||
if (ret == 0) {
|
||||
byte t = dataASN[SUBTREEASN_IDX_BASE].tag;
|
||||
|
||||
/* Check GeneralName tag is one of the types we can handle. */
|
||||
/* Check GeneralName tag is one of the types we can handle.
|
||||
* registeredID is included so that ConfirmNameConstraints can
|
||||
* enforce permitted/excluded subtrees of OIDs (RFC 5280
|
||||
* Sec. 4.2.1.10). */
|
||||
if (t == (ASN_CONTEXT_SPECIFIC | ASN_DNS_TYPE) ||
|
||||
t == (ASN_CONTEXT_SPECIFIC | ASN_RFC822_TYPE) ||
|
||||
t == (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | ASN_DIR_TYPE) ||
|
||||
t == (ASN_CONTEXT_SPECIFIC | ASN_IP_TYPE) ||
|
||||
t == (ASN_CONTEXT_SPECIFIC | ASN_URI_TYPE) ||
|
||||
t == (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED |
|
||||
ASN_OTHER_TYPE)) {
|
||||
ASN_OTHER_TYPE) ||
|
||||
t == (ASN_CONTEXT_SPECIFIC | ASN_RID_TYPE)) {
|
||||
/* Parse the general name and store a new entry. */
|
||||
ret = DecodeSubtreeGeneralName(input +
|
||||
GetASNItem_DataIdx(dataASN[SUBTREEASN_IDX_BASE], input),
|
||||
dataASN[SUBTREEASN_IDX_BASE].length, t, head, heap);
|
||||
}
|
||||
else {
|
||||
/* GeneralName form (e.g. registeredID, x400Address,
|
||||
* ediPartyName) we do not enforce. Record so the caller can
|
||||
* fail-closed when the nameConstraints extension is critical
|
||||
* (RFC 5280 4.2.1.10). */
|
||||
/* GeneralName form (e.g. x400Address, ediPartyName) we do
|
||||
* not enforce. Record so the caller can fail-closed when the
|
||||
* nameConstraints extension is critical (RFC 5280 4.2.1.10).
|
||||
* registeredID is handled above; this branch covers truly
|
||||
* unrecognised forms. */
|
||||
*hasUnsupported = 1;
|
||||
}
|
||||
}
|
||||
@@ -37582,7 +37655,15 @@ static int DecodeAcertGeneralName(const byte* input, word32* inOutIdx,
|
||||
}
|
||||
#if defined(WOLFSSL_QT) || defined(OPENSSL_ALL) || \
|
||||
defined(WOLFSSL_IP_ALT_NAME)
|
||||
/* GeneralName choice: iPAddress */
|
||||
/* GeneralName choice: iPAddress
|
||||
*
|
||||
* Asymmetric with the X.509 DecodeGeneralName path on purpose:
|
||||
* attribute-certificate names (RFC 5755) are not consumed by
|
||||
* ConfirmNameConstraints, which only walks DecodedCert lists. These
|
||||
* entries flow into AC holder/issuer name fields where the iPAddress
|
||||
* is only consumed by callers that opt in (Qt, OpenSSL_ALL, or the
|
||||
* IP-SAN compat layer). If iPAddress name-constraint enforcement is
|
||||
* ever extended to attribute certificates, this gate must drop. */
|
||||
else if (tag == (ASN_CONTEXT_SPECIFIC | ASN_IP_TYPE)) {
|
||||
ret = SetDNSEntry(acert->heap, (const char*)(input + idx), len,
|
||||
ASN_IP_TYPE, entries);
|
||||
@@ -37590,7 +37671,7 @@ static int DecodeAcertGeneralName(const byte* input, word32* inOutIdx,
|
||||
idx += (word32)len;
|
||||
}
|
||||
}
|
||||
#endif /* WOLFSSL_QT || OPENSSL_ALL */
|
||||
#endif /* WOLFSSL_QT || OPENSSL_ALL || WOLFSSL_IP_ALT_NAME */
|
||||
|
||||
#ifdef OPENSSL_ALL
|
||||
/* GeneralName choice: registeredID */
|
||||
|
||||
@@ -3486,7 +3486,10 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert)
|
||||
length -= strLen;
|
||||
idx += (word32)strLen;
|
||||
}
|
||||
#ifdef WOLFSSL_IP_ALT_NAME
|
||||
/* iPAddress is parsed unconditionally so ConfirmNameConstraints
|
||||
* can enforce permitted/excluded iPAddress subtrees
|
||||
* (RFC 5280 Sec. 4.2.1.10). WOLFSSL_IP_ALT_NAME only gates the
|
||||
* human-readable ipString form. */
|
||||
else if (current_byte == (ASN_CONTEXT_SPECIFIC | ASN_IP_TYPE)) {
|
||||
DNS_entry* ipAddr;
|
||||
int strLen;
|
||||
@@ -3521,6 +3524,7 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert)
|
||||
XMEMCPY((void *)(wc_ptr_t)ipAddr->name, &input[idx], strLen);
|
||||
((char *)(wc_ptr_t)ipAddr->name)[strLen] = '\0';
|
||||
|
||||
#ifdef WOLFSSL_IP_ALT_NAME
|
||||
if (GenerateDNSEntryIPString(ipAddr, cert->heap) != 0) {
|
||||
WOLFSSL_MSG("\tOut of Memory for IP string");
|
||||
XFREE((void *)(wc_ptr_t)ipAddr->name, cert->heap,
|
||||
@@ -3528,6 +3532,7 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert)
|
||||
XFREE(ipAddr, cert->heap, DYNAMIC_TYPE_ALTNAME);
|
||||
return MEMORY_E;
|
||||
}
|
||||
#endif /* WOLFSSL_IP_ALT_NAME */
|
||||
AddAltName(cert, ipAddr);
|
||||
|
||||
if (strLen > length) {
|
||||
@@ -3536,8 +3541,9 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert)
|
||||
length -= strLen;
|
||||
idx += (word32)strLen;
|
||||
}
|
||||
#endif /* WOLFSSL_IP_ALT_NAME */
|
||||
#ifdef WOLFSSL_RID_ALT_NAME
|
||||
/* registeredID is parsed unconditionally so ConfirmNameConstraints
|
||||
* can enforce permitted/excluded subtrees (RFC 5280 Sec. 4.2.1.10).
|
||||
* WOLFSSL_RID_ALT_NAME only gates the human-readable ridString. */
|
||||
else if (current_byte == (ASN_CONTEXT_SPECIFIC | ASN_RID_TYPE)) {
|
||||
DNS_entry* rid;
|
||||
int strLen;
|
||||
@@ -3573,6 +3579,7 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert)
|
||||
XMEMCPY((void *)(wc_ptr_t)rid->name, &input[idx], strLen);
|
||||
((char *)(wc_ptr_t)rid->name)[strLen] = '\0';
|
||||
|
||||
#ifdef WOLFSSL_RID_ALT_NAME
|
||||
if (GenerateDNSEntryRIDString(rid, cert->heap) != 0) {
|
||||
WOLFSSL_MSG("\tOut of Memory for registered Id string");
|
||||
XFREE((void *)(wc_ptr_t)rid->name, cert->heap,
|
||||
@@ -3580,6 +3587,7 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert)
|
||||
XFREE(rid, cert->heap, DYNAMIC_TYPE_ALTNAME);
|
||||
return MEMORY_E;
|
||||
}
|
||||
#endif /* WOLFSSL_RID_ALT_NAME */
|
||||
|
||||
AddAltName(cert, rid);
|
||||
|
||||
@@ -3589,7 +3597,6 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert)
|
||||
length -= strLen;
|
||||
idx += (word32)strLen;
|
||||
}
|
||||
#endif /* WOLFSSL_RID_ALT_NAME */
|
||||
#endif /* IGNORE_NAME_CONSTRAINTS */
|
||||
else if (current_byte ==
|
||||
(ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | ASN_OTHER_TYPE)) {
|
||||
@@ -4117,9 +4124,12 @@ static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head,
|
||||
/* Get type, LSB 4-bits */
|
||||
bType = (byte)(b & ASN_TYPE_MASK);
|
||||
|
||||
/* registeredID is included so ConfirmNameConstraints can enforce
|
||||
* permitted/excluded subtrees of OIDs (RFC 5280 Sec. 4.2.1.10). */
|
||||
if (bType == ASN_DNS_TYPE || bType == ASN_RFC822_TYPE ||
|
||||
bType == ASN_DIR_TYPE || bType == ASN_IP_TYPE ||
|
||||
bType == ASN_URI_TYPE || bType == ASN_OTHER_TYPE) {
|
||||
bType == ASN_URI_TYPE || bType == ASN_OTHER_TYPE ||
|
||||
bType == ASN_RID_TYPE) {
|
||||
Base_entry* entry;
|
||||
|
||||
/* directoryName is encoded as [4] CONSTRUCTED { Name } where
|
||||
|
||||
Reference in New Issue
Block a user