From 82a95a44d6a1d8a57484b4a79530b7712771758b Mon Sep 17 00:00:00 2001 From: Rene Meusel Date: Mon, 12 Jan 2026 16:25:50 +0100 Subject: [PATCH] FFI: add *_count functions for some enumerators This adds functions to query the number of entries in index-based enumerator functions that were introduced recently. --- doc/api_ref/ffi.rst | 24 ++++++++++++ src/lib/ffi/ffi.h | 5 +++ src/lib/ffi/ffi_cert.cpp | 82 ++++++++++++++++++++++++++++++++++++++++ src/tests/test_ffi.cpp | 72 +++++++++++++++++++++++++---------- 4 files changed, 163 insertions(+), 20 deletions(-) diff --git a/doc/api_ref/ffi.rst b/doc/api_ref/ffi.rst index 90594df2439..f797b0d6e15 100644 --- a/doc/api_ref/ffi.rst +++ b/doc/api_ref/ffi.rst @@ -1825,6 +1825,11 @@ X.509 Certificates objects. If the given index is not available, :cpp:enumerator:`BOTAN_FFI_ERROR_OUT_OF_RANGE` is returned. +.. cpp:function:: int botan_x509_cert_permitted_name_constraints_count(botan_x509_cert_t cert, \ + size_t* count) + + Get the number of permitted name constraints in the certificate. + .. cpp:function:: int botan_x509_cert_excluded_name_constraints(botan_x509_cert_t cert, \ size_t index, \ botan_x509_general_name_t* constraint) @@ -1833,6 +1838,11 @@ X.509 Certificates objects. If the given index is not available, :cpp:enumerator:`BOTAN_FFI_ERROR_OUT_OF_RANGE` is returned. +.. cpp:function:: int botan_x509_cert_excluded_name_constraints_count(botan_x509_cert_t cert, \ + size_t* count) + + Get the number of excluded name constraints in the certificate. + .. cpp:function:: int botan_x509_cert_subject_alternative_names(botan_x509_cert_t cert, \ size_t index, \ botan_x509_general_name_t* alt_name) @@ -1841,6 +1851,11 @@ X.509 Certificates objects. If the given index is not available, :cpp:enumerator:`BOTAN_FFI_ERROR_OUT_OF_RANGE` is returned. +.. cpp:function:: int botan_x509_cert_subject_alternative_names_count(botan_x509_cert_t cert, \ + size_t* count) + + Get the number of subject alternative names in the certificate. + .. cpp:function:: int botan_x509_cert_issuer_alternative_names(botan_x509_cert_t cert, \ size_t index, \ botan_x509_general_name_t* alt_name) @@ -1849,6 +1864,11 @@ X.509 Certificates objects. If the given index is not available, :cpp:enumerator:`BOTAN_FFI_ERROR_OUT_OF_RANGE` is returned. +.. cpp:function:: int botan_x509_cert_issuer_alternative_names_count(botan_x509_cert_t cert, \ + size_t* count) + + Get the number of issuer alternative names in the certificate. + .. cpp:function:: int botan_x509_cert_verify(int* validation_result, \ botan_x509_cert_t cert, \ const botan_x509_cert_t* intermediates, \ @@ -1952,6 +1972,10 @@ X.509 Certificate Revocation Lists enumerate all entries in the CRL. If the list of entries is exhausted, this will return :cpp:enumerator:`BOTAN_FFI_ERROR_OUT_OF_RANGE`. +.. cpp:function:: int botan_x509_crl_entries_count(botan_x509_crl_t crl, size_t* count) + + Get the number of entries in the CRL. + .. cpp:function:: int botan_x509_crl_entry_reason(botan_x509_crl_entry_t entry, int* reason_code) Get the revocation reason code for the given CRL entry. The reason code is diff --git a/src/lib/ffi/ffi.h b/src/lib/ffi/ffi.h index 726cc627246..0fa22566799 100644 --- a/src/lib/ffi/ffi.h +++ b/src/lib/ffi/ffi.h @@ -2297,6 +2297,7 @@ BOTAN_FFI_EXPORT(3, 11) int botan_x509_cert_permitted_name_constraints(botan_x509_cert_t cert, size_t index, botan_x509_general_name_t* constraint); +BOTAN_FFI_EXPORT(3, 11) int botan_x509_cert_permitted_name_constraints_count(botan_x509_cert_t cert, size_t* count); /** * Extracts "excluded" name constraints from a given @p cert one-by-one. @@ -2307,6 +2308,7 @@ BOTAN_FFI_EXPORT(3, 11) int botan_x509_cert_excluded_name_constraints(botan_x509_cert_t cert, size_t index, botan_x509_general_name_t* constraint); +BOTAN_FFI_EXPORT(3, 11) int botan_x509_cert_excluded_name_constraints_count(botan_x509_cert_t cert, size_t* count); /** * Provides access to all "subject alternative names", where each entry is @@ -2319,6 +2321,7 @@ BOTAN_FFI_EXPORT(3, 11) int botan_x509_cert_subject_alternative_names(botan_x509_cert_t cert, size_t index, botan_x509_general_name_t* alt_name); +BOTAN_FFI_EXPORT(3, 11) int botan_x509_cert_subject_alternative_names_count(botan_x509_cert_t cert, size_t* count); /** * Provides access to all "issuer alternative names", where each entry is @@ -2329,6 +2332,7 @@ int botan_x509_cert_subject_alternative_names(botan_x509_cert_t cert, */ BOTAN_FFI_EXPORT(3, 11) int botan_x509_cert_issuer_alternative_names(botan_x509_cert_t cert, size_t index, botan_x509_general_name_t* alt_name); +BOTAN_FFI_EXPORT(3, 11) int botan_x509_cert_issuer_alternative_names_count(botan_x509_cert_t cert, size_t* count); /** * Check if the certificate matches the specified hostname via alternative name or CN match. @@ -2396,6 +2400,7 @@ BOTAN_FFI_EXPORT(2, 13) int botan_x509_is_revoked(botan_x509_crl_t crl, botan_x5 */ BOTAN_FFI_EXPORT(3, 11) int botan_x509_crl_entries(botan_x509_crl_t crl, size_t index, botan_x509_crl_entry_t* entry); +BOTAN_FFI_EXPORT(3, 11) int botan_x509_crl_entries_count(botan_x509_crl_t crl, size_t* count); /** * Return the revocation reason code for the given CRL @p entry. diff --git a/src/lib/ffi/ffi_cert.cpp b/src/lib/ffi/ffi_cert.cpp index b841c916c78..7709ab04334 100644 --- a/src/lib/ffi/ffi_cert.cpp +++ b/src/lib/ffi/ffi_cert.cpp @@ -448,6 +448,19 @@ int botan_x509_cert_permitted_name_constraints(botan_x509_cert_t cert, #endif } +int botan_x509_cert_permitted_name_constraints_count(botan_x509_cert_t cert, size_t* count) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + if(Botan::any_null_pointers(count)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + + return BOTAN_FFI_VISIT(cert, [=](const auto& c) { *count = c.name_constraints().permitted().size(); }); +#else + BOTAN_UNUSED(cert, count); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + int botan_x509_cert_excluded_name_constraints(botan_x509_cert_t cert, size_t index, botan_x509_general_name_t* constraint) { @@ -469,6 +482,19 @@ int botan_x509_cert_excluded_name_constraints(botan_x509_cert_t cert, return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; #endif } + +int botan_x509_cert_excluded_name_constraints_count(botan_x509_cert_t cert, size_t* count) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + if(Botan::any_null_pointers(count)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + + return BOTAN_FFI_VISIT(cert, [=](const auto& c) { *count = c.name_constraints().excluded().size(); }); +#else + BOTAN_UNUSED(cert, count); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} } namespace { @@ -478,6 +504,9 @@ namespace { * collection of GeneralNames. This allows mapping a single entry of @p altnames * to a GeneralName by its @p index. If the index is out of range, std::nullopt * is returned. + * + * NOTE: if the set of alternative name types handled here is extended, + * count_general_names_in() must be updated accordingly! */ std::optional extract_general_name_at(const Botan::AlternativeName& altnames, size_t index) { if(index < altnames.email().size()) { @@ -517,6 +546,18 @@ std::optional extract_general_name_at(const Botan::Alternati return std::nullopt; } +/** + * Counts the total number of GeneralNames contained in the given + * AlternativeName @p alt_names. + * + * NOTE: if the set of alternative name types handled here is extended, + * extract_general_name_at() must be updated accordingly! + */ +size_t count_general_names_in(const Botan::AlternativeName& alt_names) { + return alt_names.email().size() + alt_names.dns().size() + alt_names.directory_names().size() + + alt_names.uris().size() + alt_names.ipv4_address().size(); +} + } // namespace extern "C" { @@ -546,6 +587,20 @@ int botan_x509_cert_subject_alternative_names(botan_x509_cert_t cert, #endif } +int botan_x509_cert_subject_alternative_names_count(botan_x509_cert_t cert, size_t* count) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + if(Botan::any_null_pointers(count)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + + return BOTAN_FFI_VISIT( + cert, [=](const Botan::X509_Certificate& c) { *count = count_general_names_in(c.subject_alt_name()); }); +#else + BOTAN_UNUSED(cert, count); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + int botan_x509_cert_issuer_alternative_names(botan_x509_cert_t cert, size_t index, botan_x509_general_name_t* alt_name) { @@ -571,6 +626,20 @@ int botan_x509_cert_issuer_alternative_names(botan_x509_cert_t cert, #endif } +int botan_x509_cert_issuer_alternative_names_count(botan_x509_cert_t cert, size_t* count) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + if(Botan::any_null_pointers(count)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + + return BOTAN_FFI_VISIT( + cert, [=](const Botan::X509_Certificate& c) { *count = count_general_names_in(c.issuer_alt_name()); }); +#else + BOTAN_UNUSED(cert, count); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + int botan_x509_cert_hostname_match(botan_x509_cert_t cert, const char* hostname) { if(hostname == nullptr) { return BOTAN_FFI_ERROR_NULL_POINTER; @@ -780,6 +849,19 @@ int botan_x509_crl_entries(botan_x509_crl_t crl, size_t index, botan_x509_crl_en #endif } +int botan_x509_crl_entries_count(botan_x509_crl_t crl, size_t* count) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + if(Botan::any_null_pointers(count)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + + return BOTAN_FFI_VISIT(crl, [=](const Botan::X509_CRL& c) { *count = c.get_revoked().size(); }); +#else + BOTAN_UNUSED(crl, count); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + int botan_x509_crl_entry_destroy(botan_x509_crl_entry_t entry) { #if defined(BOTAN_HAS_X509_CERTIFICATES) return BOTAN_FFI_CHECKED_DELETE(entry); diff --git a/src/tests/test_ffi.cpp b/src/tests/test_ffi.cpp index ce0ccfda9ef..0ad8bf206eb 100644 --- a/src/tests/test_ffi.cpp +++ b/src/tests/test_ffi.cpp @@ -743,6 +743,12 @@ class FFI_CRL_Test final : public FFI_Test { TEST_FFI_OK(botan_x509_cert_get_serial_number, (cert2, cert2_serial.data(), &cert2_serial_len)); TEST_FFI_OK(botan_x509_cert_destroy, (cert2)); + size_t entries; + TEST_FFI_OK(botan_x509_crl_entries_count, (crl, &entries)); + result.test_eq("one revoked cert", entries, 1); + TEST_FFI_OK(botan_x509_crl_entries_count, (bytecrl, &entries)); + result.test_eq("no revoked cert", entries, 0); + botan_x509_crl_entry_t entry; TEST_FFI_OK(botan_x509_crl_entries, (crl, 0, &entry)); @@ -1069,10 +1075,12 @@ auto read_distinguished_name(std::span bytes) { class FFI_Cert_AlternativeNames_Test final : public FFI_Test { private: template EnumeratorT, + std::invocable CountFnT, std::invocable VisitorT> static void visit_general_names(Test::Result& result, botan_x509_cert_t cert, EnumeratorT enumerator_fn, + CountFnT count_fn, VisitorT visitor_fn) { int rc = BOTAN_FFI_SUCCESS; for(size_t i = 0; rc == BOTAN_FFI_SUCCESS; ++i) { @@ -1081,21 +1089,27 @@ class FFI_Cert_AlternativeNames_Test final : public FFI_Test { if(rc == BOTAN_FFI_SUCCESS) { visitor_fn(gn); TEST_FFI_OK(botan_x509_general_name_destroy, (gn)); - } else if(rc != BOTAN_FFI_ERROR_OUT_OF_RANGE) { + } else if(rc == BOTAN_FFI_ERROR_OUT_OF_RANGE) { + // Now check we are at the expected index + size_t count; + TEST_FFI_OK(count_fn, (cert, &count)); + result.test_eq("enumerator reached end at expected index", i, count); + } else { result.test_note( Botan::fmt("enumerator produced unexpected return code: {}", botan_error_description(rc))); } } } - template + template static auto read_string_alternative_names(Test::Result& result, botan_x509_cert_t cert, - EnumeratorT fn, + EnumeratorT enumerator_fn, + CountFnT count_fn, botan_x509_general_name_types type) { std::vector out; - visit_general_names(result, cert, fn, [&](botan_x509_general_name_t gn) { + visit_general_names(result, cert, enumerator_fn, count_fn, [&](botan_x509_general_name_t gn) { unsigned int gn_type; TEST_FFI_OK(botan_x509_general_name_get_type, (gn, &gn_type)); if(static_cast(gn_type) == type) { @@ -1108,14 +1122,15 @@ class FFI_Cert_AlternativeNames_Test final : public FFI_Test { return out; } - template + template static auto read_binary_alternative_names(Test::Result& result, botan_x509_cert_t cert, - EnumeratorT fn, + EnumeratorT enumerator_fn, + CountFnT count_fn, botan_x509_general_name_types type) { std::vector> out; - visit_general_names(result, cert, fn, [&](botan_x509_general_name_t gn) { + visit_general_names(result, cert, enumerator_fn, count_fn, [&](botan_x509_general_name_t gn) { unsigned int gn_type; TEST_FFI_OK(botan_x509_general_name_get_type, (gn, &gn_type)); if(static_cast(gn_type) == type) { @@ -1159,31 +1174,35 @@ class FFI_Cert_AlternativeNames_Test final : public FFI_Test { } const auto get_san = botan_x509_cert_subject_alternative_names; + const auto count_san = botan_x509_cert_subject_alternative_names_count; - const auto san_email = read_string_alternative_names(result, cert, get_san, BOTAN_X509_EMAIL_ADDRESS); + const auto san_email = + read_string_alternative_names(result, cert, get_san, count_san, BOTAN_X509_EMAIL_ADDRESS); result.test_eq("expected number of emails in SAN", san_email.size(), 2); result.confirm("testing@x509-labs.com", Botan::value_exists(san_email, "testing@x509-labs.com")); result.confirm("info@x509-labs.com", Botan::value_exists(san_email, "info@x509-labs.com")); - const auto san_dns = read_string_alternative_names(result, cert, get_san, BOTAN_X509_DNS_NAME); + const auto san_dns = read_string_alternative_names(result, cert, get_san, count_san, BOTAN_X509_DNS_NAME); result.test_eq("expected number of hostnames in SAN", san_dns.size(), 3); result.confirm("test.x509-labs.com", Botan::value_exists(san_dns, "test.x509-labs.com")); result.confirm("versuch.x509-labs.com", Botan::value_exists(san_dns, "versuch.x509-labs.com")); result.confirm("trail.x509-labs.com", Botan::value_exists(san_dns, "trail.x509-labs.com")); - const auto san_uri = read_string_alternative_names(result, cert, get_san, BOTAN_X509_URI); + const auto san_uri = read_string_alternative_names(result, cert, get_san, count_san, BOTAN_X509_URI); result.test_eq("expected number of URIs in SAN", san_uri.size(), 2); result.confirm("https://x509-labs.com", Botan::value_exists(san_uri, "https://x509-labs.com")); result.confirm("http://x509-labs.com", Botan::value_exists(san_uri, "http://x509-labs.com")); - const auto san_ip4 = read_string_alternative_names(result, cert, get_san, BOTAN_X509_IP_ADDRESS); + const auto san_ip4 = read_string_alternative_names(result, cert, get_san, count_san, BOTAN_X509_IP_ADDRESS); result.test_eq("expected number of IPv4 addresses", san_ip4.size(), 1); result.confirm("127.0.0.1", Botan::value_exists(san_ip4, "127.0.0.1")); - const auto san_ip4_bin = read_binary_alternative_names(result, cert, get_san, BOTAN_X509_IP_ADDRESS); + const auto san_ip4_bin = + read_binary_alternative_names(result, cert, get_san, count_san, BOTAN_X509_IP_ADDRESS); result.test_eq("expected number of IPv4 addresses (bin)", san_ip4_bin.size(), 1); result.test_eq("127.0.0.1 (bin)", san_ip4_bin.front(), Botan::store_be(uint32_t(0x7F000001))); - const auto san_dn_bytes = read_binary_alternative_names(result, cert, get_san, BOTAN_X509_DIRECTORY_NAME); + const auto san_dn_bytes = + read_binary_alternative_names(result, cert, get_san, count_san, BOTAN_X509_DIRECTORY_NAME); result.test_eq("expected number of DNs in SAN", san_dn_bytes.size(), 3); const auto san_dn_cns = read_common_names(san_dn_bytes); result.confirm("First Name", Botan::value_exists(san_dn_cns, "First Name")); @@ -1191,29 +1210,33 @@ class FFI_Cert_AlternativeNames_Test final : public FFI_Test { result.confirm("Last Name", Botan::value_exists(san_dn_cns, "Last Name")); auto get_ian = botan_x509_cert_issuer_alternative_names; + auto count_ian = botan_x509_cert_issuer_alternative_names_count; - const auto ian_email = read_string_alternative_names(result, cert, get_ian, BOTAN_X509_EMAIL_ADDRESS); + const auto ian_email = + read_string_alternative_names(result, cert, get_ian, count_ian, BOTAN_X509_EMAIL_ADDRESS); result.test_eq("expected number of emails in IAN", ian_email.size(), 0); - const auto ian_dns = read_string_alternative_names(result, cert, get_ian, BOTAN_X509_DNS_NAME); + const auto ian_dns = read_string_alternative_names(result, cert, get_ian, count_ian, BOTAN_X509_DNS_NAME); result.test_eq("expected number of hostnames in IAN", ian_dns.size(), 3); result.confirm("test.x509-labs-ca.com", Botan::value_exists(ian_dns, "test.x509-labs-ca.com")); result.confirm("versuch.x509-labs-ca.com", Botan::value_exists(ian_dns, "versuch.x509-labs-ca.com")); result.confirm("trail.x509-labs-ca.com", Botan::value_exists(ian_dns, "trail.x509-labs-ca.com")); - const auto ian_uri = read_string_alternative_names(result, cert, get_ian, BOTAN_X509_URI); + const auto ian_uri = read_string_alternative_names(result, cert, get_ian, count_ian, BOTAN_X509_URI); result.test_eq("expected number of URIs in IAN", ian_uri.size(), 2); result.confirm("https://x509-labs-ca.com", Botan::value_exists(ian_uri, "https://x509-labs-ca.com")); result.confirm("http://x509-labs-ca.com", Botan::value_exists(ian_uri, "http://x509-labs-ca.com")); - const auto ian_ip4 = read_string_alternative_names(result, cert, get_ian, BOTAN_X509_IP_ADDRESS); + const auto ian_ip4 = read_string_alternative_names(result, cert, get_ian, count_ian, BOTAN_X509_IP_ADDRESS); result.test_eq("expected number of IPv4 addresses", ian_ip4.size(), 1); result.confirm("192.168.1.1", Botan::value_exists(ian_ip4, "192.168.1.1")); - const auto ian_ip4_bin = read_binary_alternative_names(result, cert, get_ian, BOTAN_X509_IP_ADDRESS); + const auto ian_ip4_bin = + read_binary_alternative_names(result, cert, get_ian, count_ian, BOTAN_X509_IP_ADDRESS); result.test_eq("expected number of IPv4 addresses (bin)", ian_ip4_bin.size(), 1); result.test_eq("192.168.1.1 (bin)", ian_ip4_bin.front(), Botan::store_be(uint32_t(0xC0A80101))); - const auto ian_dn_bytes = read_binary_alternative_names(result, cert, get_ian, BOTAN_X509_DIRECTORY_NAME); + const auto ian_dn_bytes = + read_binary_alternative_names(result, cert, get_ian, count_ian, BOTAN_X509_DIRECTORY_NAME); result.test_eq("expected number of DNs in IAN", ian_dn_bytes.size(), 3); const auto ian_dn_cns = read_common_names(ian_dn_bytes); result.confirm("First CA", Botan::value_exists(ian_dn_cns, "First CA")); @@ -1271,7 +1294,16 @@ class FFI_Cert_NameConstraints_Test final : public FFI_Test { } TEST_FFI_OK(botan_x509_general_name_destroy, (constraint)); - } else if(rc != BOTAN_FFI_ERROR_OUT_OF_RANGE) { + } else if(rc == BOTAN_FFI_ERROR_OUT_OF_RANGE) { + // Now check that we are at the expected index + size_t count; + if(permitted) { + TEST_FFI_OK(botan_x509_cert_permitted_name_constraints_count, (cert, &count)); + } else { + TEST_FFI_OK(botan_x509_cert_excluded_name_constraints_count, (cert, &count)); + } + result.test_eq("expected length of name constraint list", i, count); + } else { result.test_failure(Botan::fmt("unexpected error code: {}", botan_error_description(rc))); } }