crypto/x509: Fix interaction of DNS exclude constraints with wildcard DNS names. An exclusion of "foo.example.com" must match a DNS name of "*.example.com". Bug: 488306305 Change-Id: Id911cb841451da1568c4f938a42c75316a6a6964 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/90167 Auto-Submit: Rudolf Polzer <rpolzer@google.com> Commit-Queue: David Benjamin <davidben@google.com> Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/crypto/x509/v3_ncons.cc b/crypto/x509/v3_ncons.cc index 87d4c0e..64212db 100644 --- a/crypto/x509/v3_ncons.cc +++ b/crypto/x509/v3_ncons.cc
@@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include <string_view> + #include <stdio.h> #include <string.h> @@ -40,9 +42,11 @@ static int print_nc_ipadd(BIO *bp, const ASN1_OCTET_STRING *ip); static int nc_match(const GENERAL_NAME *gen, const NAME_CONSTRAINTS *nc); -static int nc_match_single(const GENERAL_NAME *sub, const GENERAL_NAME *gen); +static int nc_match_single(const GENERAL_NAME *sub, const GENERAL_NAME *gen, + bool excluding); static int nc_dn(const X509_NAME *sub, const X509_NAME *nm); -static int nc_dns(const ASN1_IA5STRING *sub, const ASN1_IA5STRING *dns); +static int nc_dns(const ASN1_IA5STRING *sub, const ASN1_IA5STRING *dns, + bool excluding); static int nc_email(const ASN1_IA5STRING *sub, const ASN1_IA5STRING *eml); static int nc_uri(const ASN1_IA5STRING *uri, const ASN1_IA5STRING *base); @@ -269,7 +273,7 @@ if (match == 0) { match = 1; } - int r = nc_match_single(gen, sub->base); + int r = nc_match_single(gen, sub->base, /*excluding=*/false); if (r == X509_V_OK) { match = 2; } else if (r != X509_V_ERR_PERMITTED_VIOLATION) { @@ -290,7 +294,7 @@ return X509_V_ERR_SUBTREE_MINMAX; } - int r = nc_match_single(gen, sub->base); + int r = nc_match_single(gen, sub->base, /*excluding=*/true); if (r == X509_V_OK) { return X509_V_ERR_EXCLUDED_VIOLATION; } else if (r != X509_V_ERR_PERMITTED_VIOLATION) { @@ -301,13 +305,14 @@ return X509_V_OK; } -static int nc_match_single(const GENERAL_NAME *gen, const GENERAL_NAME *base) { +static int nc_match_single(const GENERAL_NAME *gen, const GENERAL_NAME *base, + bool excluding) { switch (base->type) { case GEN_DIRNAME: return nc_dn(gen->d.directoryName, base->d.directoryName); case GEN_DNS: - return nc_dns(gen->d.dNSName, base->d.dNSName); + return nc_dns(gen->d.dNSName, base->d.dNSName, excluding); case GEN_EMAIL: return nc_email(gen->d.rfc822Name, base->d.rfc822Name); @@ -348,6 +353,15 @@ return CBS_len(cbs) > 0 && CBS_data(cbs)[0] == c; } +static int starts_with_str(const CBS *cbs, std::string_view str) { + return CBS_len(cbs) >= str.size() && + !OPENSSL_memcmp(CBS_data(cbs), str.data(), str.size()); +} + +static int ends_with(const CBS *cbs, uint8_t c) { + return CBS_len(cbs) > 0 && CBS_data(cbs)[CBS_len(cbs) - 1] == c; +} + static int equal_case(const CBS *a, const CBS *b) { if (CBS_len(a) != CBS_len(b)) { return 0; @@ -372,7 +386,8 @@ return equal_case(©, b); } -static int nc_dns(const ASN1_IA5STRING *dns, const ASN1_IA5STRING *base) { +static int nc_dns(const ASN1_IA5STRING *dns, const ASN1_IA5STRING *base, + bool excluding) { CBS dns_cbs, base_cbs; CBS_init(&dns_cbs, dns->data, dns->length); CBS_init(&base_cbs, base->data, base->length); @@ -382,6 +397,34 @@ return X509_V_OK; } + // Normalize absolute DNS names by removing the trailing dot, if any. + if (ends_with(&dns_cbs, '.')) { + uint8_t unused; + CBS_get_last_u8(&dns_cbs, &unused); + } + if (ends_with(&base_cbs, '.')) { + uint8_t unused; + CBS_get_last_u8(&base_cbs, &unused); + } + + // Wildcard partial-match handling ("*.bar.com" matching name constraint + // "foo.bar.com"). This only handles the case where the the dnsname and the + // constraint match after removing the leftmost label, otherwise it is handled + // by falling through to the check of whether the dnsname is fully within or + // fully outside of the constraint. + if (excluding && starts_with_str(&dns_cbs, "*.")) { + CBS unused; + CBS base_parent_cbs = base_cbs; + CBS dns_parent_cbs = dns_cbs; + CBS_skip(&dns_parent_cbs, 2); + if (CBS_get_until_first(&base_parent_cbs, &unused, '.') && + CBS_skip(&base_parent_cbs, 1)) { + if (equal_case(&dns_parent_cbs, &base_parent_cbs)) { + return X509_V_OK; + } + } + } + // If |base_cbs| begins with a '.', do a simple suffix comparison. This is // not part of RFC5280, but is part of OpenSSL's original behavior. if (starts_with(&base_cbs, '.')) {
diff --git a/crypto/x509/x509_test.cc b/crypto/x509/x509_test.cc index 929601c..394f3bd 100644 --- a/crypto/x509/x509_test.cc +++ b/crypto/x509/x509_test.cc
@@ -2191,151 +2191,208 @@ int type; std::string name; std::string constraint; - int result; + int permit_result; + int exclude_result; } kTests[] = { // Empty string matches everything. - {GEN_DNS, "foo.example.com", "", X509_V_OK}, + {GEN_DNS, "foo.example.com", "", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, // Name constraints match the entire subtree. - {GEN_DNS, "foo.example.com", "example.com", X509_V_OK}, - {GEN_DNS, "foo.example.com", "EXAMPLE.COM", X509_V_OK}, - {GEN_DNS, "foo.example.com", "xample.com", - X509_V_ERR_PERMITTED_VIOLATION}, + {GEN_DNS, "foo.example.com", "example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_DNS, "foo.example.com", "EXAMPLE.COM", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_DNS, "foo.example.com", "xample.com", X509_V_ERR_PERMITTED_VIOLATION, + X509_V_OK}, {GEN_DNS, "foo.example.com", "unrelated.much.longer.name.example", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, // A leading dot means at least one component must be added. - {GEN_DNS, "foo.example.com", ".example.com", X509_V_OK}, - {GEN_DNS, "foo.example.com", "foo.example.com", X509_V_OK}, + {GEN_DNS, "foo.example.com", ".example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_DNS, "foo.example.com", "foo.example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, {GEN_DNS, "foo.example.com", ".foo.example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_DNS, "foo.example.com", ".xample.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_DNS, "foo.example.com", ".unrelated.much.longer.name.example", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, + // Trailing dot is ignored. + {GEN_DNS, "foo.example.com.", "example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_DNS, "foo.example.com", "example.com.", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, // NUL bytes, if not rejected, should not confuse the matching logic. {GEN_DNS, std::string({'a', '\0', 'a'}), std::string({'a', '\0', 'b'}), - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, + + // Wildcard CN matching. + {GEN_DNS, "*.com", "foo.example.com", X509_V_ERR_PERMITTED_VIOLATION, + X509_V_OK}, + // A foo.example.com permitted subtree does not permit *.example.com. + // However, a foo.example.com excluded subtree does exclude *.example.com + // because there is a partial overlap between the two. + {GEN_DNS, "*.example.com", "foo.example.com", + X509_V_ERR_PERMITTED_VIOLATION, X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_DNS, "*.foo.example.com", "foo.example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_DNS, "*.sub.foo.example.com", "foo.example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_DNS, "*.bar.example.com", "foo.example.com", + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, + {GEN_DNS, "*.example.com", "net", X509_V_ERR_PERMITTED_VIOLATION, + X509_V_OK}, + {GEN_DNS, "*.example.com", "com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, // Names must be emails. {GEN_EMAIL, "not-an-email.example", "not-an-email.example", - X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, + X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, // A leading dot matches all local names and all subdomains - {GEN_EMAIL, "foo@bar.example.com", ".example.com", X509_V_OK}, - {GEN_EMAIL, "foo@bar.example.com", ".EXAMPLE.COM", X509_V_OK}, + {GEN_EMAIL, "foo@bar.example.com", ".example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_EMAIL, "foo@bar.example.com", ".EXAMPLE.COM", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, {GEN_EMAIL, "foo@bar.example.com", ".bar.example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, // Without a leading dot, the host must match exactly. - {GEN_EMAIL, "foo@example.com", "example.com", X509_V_OK}, - {GEN_EMAIL, "foo@example.com", "EXAMPLE.COM", X509_V_OK}, + {GEN_EMAIL, "foo@example.com", "example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_EMAIL, "foo@example.com", "EXAMPLE.COM", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, {GEN_EMAIL, "foo@bar.example.com", "example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, // If the constraint specifies a mailbox, it specifies the whole thing. // The halves are compared insensitively. - {GEN_EMAIL, "foo@example.com", "foo@example.com", X509_V_OK}, - {GEN_EMAIL, "foo@example.com", "foo@EXAMPLE.COM", X509_V_OK}, + {GEN_EMAIL, "foo@example.com", "foo@example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_EMAIL, "foo@example.com", "foo@EXAMPLE.COM", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, {GEN_EMAIL, "foo@example.com", "FOO@example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_EMAIL, "foo@example.com", "bar@example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, // OpenSSL ignores a stray leading @. - {GEN_EMAIL, "foo@example.com", "@example.com", X509_V_OK}, - {GEN_EMAIL, "foo@example.com", "@EXAMPLE.COM", X509_V_OK}, + {GEN_EMAIL, "foo@example.com", "@example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_EMAIL, "foo@example.com", "@EXAMPLE.COM", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, {GEN_EMAIL, "foo@bar.example.com", "@example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, // Basic syntax check. - {GEN_URI, "not-a-url", "not-a-url", X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, + {GEN_URI, "not-a-url", "not-a-url", X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, + X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, {GEN_URI, "foo:not-a-url", "not-a-url", - X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, + X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, {GEN_URI, "foo:/not-a-url", "not-a-url", - X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, + X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, {GEN_URI, "foo:///not-a-url", "not-a-url", - X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, + X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, {GEN_URI, "foo://:not-a-url", "not-a-url", + X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, + {GEN_URI, "foo://", "not-a-url", X509_V_ERR_UNSUPPORTED_NAME_SYNTAX, X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, - {GEN_URI, "foo://", "not-a-url", X509_V_ERR_UNSUPPORTED_NAME_SYNTAX}, // Hosts are an exact match. - {GEN_URI, "foo://example.com", "example.com", X509_V_OK}, - {GEN_URI, "foo://example.com:443", "example.com", X509_V_OK}, - {GEN_URI, "foo://example.com/whatever", "example.com", X509_V_OK}, + {GEN_URI, "foo://example.com", "example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_URI, "foo://example.com:443", "example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_URI, "foo://example.com/whatever", "example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, {GEN_URI, "foo://bar.example.com", "example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://bar.example.com:443", "example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://bar.example.com/whatever", "example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://bar.example.com", "xample.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://bar.example.com:443", "xample.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://bar.example.com/whatever", "xample.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://example.com", "some-other-name.example", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://example.com:443", "some-other-name.example", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://example.com/whatever", "some-other-name.example", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, // A leading dot allows components to be added. {GEN_URI, "foo://example.com", ".example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://example.com:443", ".example.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://example.com/whatever", ".example.com", - X509_V_ERR_PERMITTED_VIOLATION}, - {GEN_URI, "foo://bar.example.com", ".example.com", X509_V_OK}, - {GEN_URI, "foo://bar.example.com:443", ".example.com", X509_V_OK}, - {GEN_URI, "foo://bar.example.com/whatever", ".example.com", X509_V_OK}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, + {GEN_URI, "foo://bar.example.com", ".example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_URI, "foo://bar.example.com:443", ".example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, + {GEN_URI, "foo://bar.example.com/whatever", ".example.com", X509_V_OK, + X509_V_ERR_EXCLUDED_VIOLATION}, {GEN_URI, "foo://example.com", ".some-other-name.example", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://example.com:443", ".some-other-name.example", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://example.com/whatever", ".some-other-name.example", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://example.com", ".xample.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://example.com:443", ".xample.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, {GEN_URI, "foo://example.com/whatever", ".xample.com", - X509_V_ERR_PERMITTED_VIOLATION}, + X509_V_ERR_PERMITTED_VIOLATION, X509_V_OK}, }; for (const auto &t : kTests) { SCOPED_TRACE(t.type); SCOPED_TRACE(t.name); SCOPED_TRACE(t.constraint); - UniquePtr<GENERAL_NAME> name = MakeGeneralName(t.type, t.name); - ASSERT_TRUE(name); - UniquePtr<GENERAL_NAMES> names(GENERAL_NAMES_new()); - ASSERT_TRUE(names); - ASSERT_TRUE(PushToStack(names.get(), std::move(name))); + for (bool exclude : {false, true}) { + SCOPED_TRACE(exclude); - UniquePtr<NAME_CONSTRAINTS> nc(NAME_CONSTRAINTS_new()); - ASSERT_TRUE(nc); - nc->permittedSubtrees = sk_GENERAL_SUBTREE_new_null(); - ASSERT_TRUE(nc->permittedSubtrees); - UniquePtr<GENERAL_SUBTREE> subtree(GENERAL_SUBTREE_new()); - ASSERT_TRUE(subtree); - GENERAL_NAME_free(subtree->base); - subtree->base = MakeGeneralName(t.type, t.constraint).release(); - ASSERT_TRUE(subtree->base); - ASSERT_TRUE(PushToStack(nc->permittedSubtrees, std::move(subtree))); + UniquePtr<GENERAL_NAME> name = MakeGeneralName(t.type, t.name); + ASSERT_TRUE(name); + UniquePtr<GENERAL_NAMES> names(GENERAL_NAMES_new()); + ASSERT_TRUE(names); + ASSERT_TRUE(PushToStack(names.get(), std::move(name))); - UniquePtr<X509> root = - MakeTestCert("Root", "Root", key.get(), /*is_ca=*/true); - ASSERT_TRUE(root); - ASSERT_TRUE(X509_add1_ext_i2d(root.get(), NID_name_constraints, nc.get(), - /*crit=*/1, /*flags=*/0)); - ASSERT_TRUE(X509_sign(root.get(), key.get(), EVP_sha256())); + UniquePtr<NAME_CONSTRAINTS> nc(NAME_CONSTRAINTS_new()); + ASSERT_TRUE(nc); + STACK_OF(GENERAL_SUBTREE) **rule = + exclude ? &nc->excludedSubtrees : &nc->permittedSubtrees; + *rule = sk_GENERAL_SUBTREE_new_null(); + ASSERT_TRUE(*rule); + UniquePtr<GENERAL_SUBTREE> subtree(GENERAL_SUBTREE_new()); + ASSERT_TRUE(subtree); + GENERAL_NAME_free(subtree->base); + subtree->base = MakeGeneralName(t.type, t.constraint).release(); + ASSERT_TRUE(subtree->base); + ASSERT_TRUE(PushToStack(*rule, std::move(subtree))); - UniquePtr<X509> leaf = - MakeTestCert("Root", "Leaf", key.get(), /*is_ca=*/false); - ASSERT_TRUE(leaf); - ASSERT_TRUE(X509_add1_ext_i2d(leaf.get(), NID_subject_alt_name, names.get(), - /*crit=*/0, /*flags=*/0)); - ASSERT_TRUE(X509_sign(leaf.get(), key.get(), EVP_sha256())); + UniquePtr<X509> root = + MakeTestCert("Root", "Root", key.get(), /*is_ca=*/true); + ASSERT_TRUE(root); + ASSERT_TRUE(X509_add1_ext_i2d(root.get(), NID_name_constraints, nc.get(), + /*crit=*/1, /*flags=*/0)); + ASSERT_TRUE(X509_sign(root.get(), key.get(), EVP_sha256())); - int ret = Verify(leaf.get(), {root.get()}, {}, {}, 0); - EXPECT_EQ(t.result, ret) << X509_verify_cert_error_string(ret); + UniquePtr<X509> leaf = + MakeTestCert("Root", "Leaf", key.get(), /*is_ca=*/false); + ASSERT_TRUE(leaf); + ASSERT_TRUE(X509_add1_ext_i2d(leaf.get(), NID_subject_alt_name, + names.get(), + /*crit=*/0, /*flags=*/0)); + ASSERT_TRUE(X509_sign(leaf.get(), key.get(), EVP_sha256())); + + int got_result = Verify(leaf.get(), {root.get()}, {}, {}, 0); + int want_result = exclude ? t.exclude_result : t.permit_result; + EXPECT_EQ(want_result, got_result) + << "got \"" << X509_verify_cert_error_string(got_result) + << "\", want \"" << X509_verify_cert_error_string(want_result) + << "\""; + } } }