Skip to content

Commit 358e3b7

Browse files
Tests/RequestServer: Add unit tests for AIA certificate parsing
Cover the two AIA-parsing primitives directly: extracting http caIssuers URLs from a cert’s AIA extension (https URLs skipped, capped per-cert), and parsing a fetched response body as a DER cert, a PKCS#7 certs-only bundle, or PEM. The test compiles AIA.cpp into its own binary, and links OpenSSL directly — rather than the whole RequestServer service.
1 parent 81eccdc commit 358e3b7

3 files changed

Lines changed: 127 additions & 0 deletions

File tree

Tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ add_subdirectory(LibUnicode)
1818
add_subdirectory(LibURL)
1919
add_subdirectory(LibWasm)
2020
add_subdirectory(LibXML)
21+
add_subdirectory(RequestServer)
2122
add_subdirectory(Meta)
2223

2324
if (ENABLE_GUI_TARGETS)

Tests/RequestServer/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ladybird_test(TestAIA.cpp RequestServer LIBS OpenSSL::Crypto OpenSSL::SSL CURL::libcurl)
2+
target_sources(TestAIA PRIVATE ${LADYBIRD_SOURCE_DIR}/Services/RequestServer/AIA.cpp)
3+
target_include_directories(TestAIA PRIVATE ${LADYBIRD_SOURCE_DIR}/Services)

Tests/RequestServer/TestAIA.cpp

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright (c) 2026-present, the Ladybird developers.
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include <AK/Array.h>
8+
#include <AK/Base64.h>
9+
#include <AK/ByteBuffer.h>
10+
#include <LibTest/TestCase.h>
11+
#include <RequestServer/AIA.h>
12+
13+
#include <openssl/x509.h>
14+
15+
// Generated certificate fixtures (EC/prime256v1, fixed validity 2020-2050):
16+
// - LEAF: subject CN "leaf.aia.test"; AIA extension with one http caIssuers URL, one https caIssuers URL, one http OCSP URL.
17+
// - CA: self-signed "AIA Test Root CA"; no AIA extension.
18+
// - MANY: subject CN "many.aia.test"; AIA extension carrying six http caIssuers URLs.
19+
// - PKCS7_TWO_CERTS: a PKCS#7 "certs-only" bundle containing [CA, leaf].
20+
static constexpr auto leaf_der_base64 = "MIIBnTCCAUSgAwIBAgIBAjAKBggqhkjOPQQDAjAbMRkwFwYDVQQDDBBBSUEgVGVzdCBSb290IENBMCAXDTIwMDEwMTAwMDAwMFoYDzIwNTAwMTAxMDAwMDAwWjAYMRYwFAYDVQQDDA1sZWFmLmFpYS50ZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYsSP5b9NEG+zYqFyllVmL2kHe/eNXc0XUsRDFdTEBEFxn7I2cT7f8h8yjcrf63UgeEToVHWW5ySz1tzdbzCk9aN6MHgwdgYIKwYBBQUHAQEEajBoMCIGCCsGAQUFBzAChhZodHRwOi8vYWlhLnRlc3QvY2EuY3J0MCMGCCsGAQUFBzAChhdodHRwczovL2FpYS50ZXN0L2NhLmNydDAdBggrBgEFBQcwAYYRaHR0cDovL29jc3AudGVzdC8wCgYIKoZIzj0EAwIDRwAwRAIgR3a5MGrx7BBesYV8arbwfeBIprTELDzniCerSZuyHjgCIHn480IuXwqGJBJcBwqwvFcyRLEnfVI1HqmkCDElMA3C"sv;
21+
static constexpr auto ca_der_base64 = "MIIBODCB4KADAgECAgEBMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMMEEFJQSBUZXN0IFJvb3QgQ0EwIBcNMjAwMTAxMDAwMDAwWhgPMjA1MDAxMDEwMDAwMDBaMBsxGTAXBgNVBAMMEEFJQSBUZXN0IFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQvFaFCRjw2v74zuhfRNy6CQSj16eQo3DvL5UjoJo6Ic+jlA/En7yPQWBTdksQFW/3mxWqeoSShdsEzEVEkC84toxMwETAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIFcoD2A6nwl2nz8sgjQUl1h/vEmP2ou36HfWDjHNrUDBAiBuf54FrlwV+CbAPcABgP5ktbYmt7Ho0zaKWSuDSI/EeA=="sv;
22+
static constexpr auto many_der_base64 = "MIICGTCCAb+gAwIBAgIBAjAKBggqhkjOPQQDAjAbMRkwFwYDVQQDDBBBSUEgVGVzdCBSb290IENBMCAXDTIwMDEwMTAwMDAwMFoYDzIwNTAwMTAxMDAwMDAwWjAYMRYwFAYDVQQDDA1tYW55LmFpYS50ZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExI4Mkk2uxbNU9fEXyoU9VSo172CiHbWDDbRkOhesAmAJHuBTleAkmTLAEWugT6bILtQTKCq5GldbYwMrHMJ2PaOB9DCB8TCB7gYIKwYBBQUHAQEEgeEwgd4wIwYIKwYBBQUHMAKGF2h0dHA6Ly9haWEudGVzdC9jYTAuY3J0MCMGCCsGAQUFBzAChhdodHRwOi8vYWlhLnRlc3QvY2ExLmNydDAjBggrBgEFBQcwAoYXaHR0cDovL2FpYS50ZXN0L2NhMi5jcnQwIwYIKwYBBQUHMAKGF2h0dHA6Ly9haWEudGVzdC9jYTMuY3J0MCMGCCsGAQUFBzAChhdodHRwOi8vYWlhLnRlc3QvY2E0LmNydDAjBggrBgEFBQcwAoYXaHR0cDovL2FpYS50ZXN0L2NhNS5jcnQwCgYIKoZIzj0EAwIDSAAwRQIgGwvZA7qgH9wcYmBZGRbWDbdsKQ7vbzyt27lQkobpVOgCIQCZZz5VBu6Ycv7e7i44STvL7tpiu2CeTqq5WQk6ZSvH+g=="sv;
23+
static constexpr auto pkcs7_two_certs_base64 = "MIIDCAYJKoZIhvcNAQcCoIIC+TCCAvUCAQExADALBgkqhkiG9w0BBwGgggLdMIIBODCB4KADAgECAgEBMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMMEEFJQSBUZXN0IFJvb3QgQ0EwIBcNMjAwMTAxMDAwMDAwWhgPMjA1MDAxMDEwMDAwMDBaMBsxGTAXBgNVBAMMEEFJQSBUZXN0IFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQvFaFCRjw2v74zuhfRNy6CQSj16eQo3DvL5UjoJo6Ic+jlA/En7yPQWBTdksQFW/3mxWqeoSShdsEzEVEkC84toxMwETAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIFcoD2A6nwl2nz8sgjQUl1h/vEmP2ou36HfWDjHNrUDBAiBuf54FrlwV+CbAPcABgP5ktbYmt7Ho0zaKWSuDSI/EeDCCAZ0wggFEoAMCAQICAQIwCgYIKoZIzj0EAwIwGzEZMBcGA1UEAwwQQUlBIFRlc3QgUm9vdCBDQTAgFw0yMDAxMDEwMDAwMDBaGA8yMDUwMDEwMTAwMDAwMFowGDEWMBQGA1UEAwwNbGVhZi5haWEudGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGLEj+W/TRBvs2KhcpZVZi9pB3v3jV3NF1LEQxXUxARBcZ+yNnE+3/IfMo3K3+t1IHhE6FR1lucks9bc3W8wpPWjejB4MHYGCCsGAQUFBwEBBGowaDAiBggrBgEFBQcwAoYWaHR0cDovL2FpYS50ZXN0L2NhLmNydDAjBggrBgEFBQcwAoYXaHR0cHM6Ly9haWEudGVzdC9jYS5jcnQwHQYIKwYBBQUHMAGGEWh0dHA6Ly9vY3NwLnRlc3QvMAoGCCqGSM49BAMCA0cAMEQCIEd2uTBq8ewQXrGFfGq28H3gSKa0xCw854gnq0mbsh44AiB5+PNCLl8KhiQSXAcKsLxXMkSxJ31SNR6ppAgxJTANwjEA"sv;
24+
static constexpr auto ca_pem = R"PEM(-----BEGIN CERTIFICATE-----
25+
MIIBODCB4KADAgECAgEBMAoGCCqGSM49BAMCMBsxGTAXBgNVBAMMEEFJQSBUZXN0
26+
IFJvb3QgQ0EwIBcNMjAwMTAxMDAwMDAwWhgPMjA1MDAxMDEwMDAwMDBaMBsxGTAX
27+
BgNVBAMMEEFJQSBUZXN0IFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
28+
AAQvFaFCRjw2v74zuhfRNy6CQSj16eQo3DvL5UjoJo6Ic+jlA/En7yPQWBTdksQF
29+
W/3mxWqeoSShdsEzEVEkC84toxMwETAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49
30+
BAMCA0cAMEQCIFcoD2A6nwl2nz8sgjQUl1h/vEmP2ou36HfWDjHNrUDBAiBuf54F
31+
rlwV+CbAPcABgP5ktbYmt7Ho0zaKWSuDSI/EeA==
32+
-----END CERTIFICATE-----)PEM"sv;
33+
34+
static X509* parse_der(StringView base64)
35+
{
36+
auto der = MUST(decode_base64(base64));
37+
auto const* data = der.data();
38+
return d2i_X509(nullptr, &data, static_cast<long>(der.size()));
39+
}
40+
41+
static ByteString subject_common_name(X509* certificate)
42+
{
43+
char buffer[256] = {};
44+
X509_NAME_get_text_by_NID(X509_get_subject_name(certificate), NID_commonName, buffer, sizeof(buffer));
45+
return ByteString { buffer };
46+
}
47+
48+
TEST_CASE(ca_issuers_urls_returns_only_http_ca_issuers)
49+
{
50+
auto* leaf = parse_der(leaf_der_base64);
51+
EXPECT_NE(leaf, nullptr);
52+
53+
// Of the three access descriptions, only the http caIssuers URL is returned: The https caIssuers URL is skipped
54+
// (fetching over TLS would itself need verification), and the OCSP URL isn't a caIssuers entry.
55+
auto urls = RequestServer::ca_issuers_urls(leaf);
56+
EXPECT_EQ(urls.size(), 1u);
57+
EXPECT_EQ(urls[0], "http://aia.test/ca.crt"sv);
58+
59+
X509_free(leaf);
60+
}
61+
62+
TEST_CASE(ca_issuers_urls_is_capped)
63+
{
64+
auto* many = parse_der(many_der_base64);
65+
EXPECT_NE(many, nullptr);
66+
67+
// The certificate carries six http caIssuers URLs; extraction is capped at five.
68+
EXPECT_EQ(RequestServer::ca_issuers_urls(many).size(), 5u);
69+
70+
X509_free(many);
71+
}
72+
73+
TEST_CASE(ca_issuers_urls_empty_without_extension)
74+
{
75+
auto* ca = parse_der(ca_der_base64);
76+
EXPECT_NE(ca, nullptr);
77+
78+
EXPECT(RequestServer::ca_issuers_urls(ca).is_empty());
79+
80+
X509_free(ca);
81+
}
82+
83+
TEST_CASE(parse_certificates_reads_der)
84+
{
85+
auto der = MUST(decode_base64(leaf_der_base64));
86+
auto certificates = RequestServer::parse_certificates(der.bytes());
87+
EXPECT_EQ(certificates.size(), 1u);
88+
EXPECT_EQ(subject_common_name(certificates[0]), "leaf.aia.test"sv);
89+
for (auto* certificate : certificates)
90+
X509_free(certificate);
91+
}
92+
93+
TEST_CASE(parse_certificates_reads_pkcs7_certs_only_bundle)
94+
{
95+
// A PKCS#7 "certs-only" response can carry more than one certificate; all are returned.
96+
auto der = MUST(decode_base64(pkcs7_two_certs_base64));
97+
auto certificates = RequestServer::parse_certificates(der.bytes());
98+
EXPECT_EQ(certificates.size(), 2u);
99+
for (auto* certificate : certificates)
100+
X509_free(certificate);
101+
}
102+
103+
TEST_CASE(parse_certificates_reads_pem)
104+
{
105+
auto certificates = RequestServer::parse_certificates(ca_pem.bytes());
106+
EXPECT_EQ(certificates.size(), 1u);
107+
EXPECT_EQ(subject_common_name(certificates[0]), "AIA Test Root CA"sv);
108+
for (auto* certificate : certificates)
109+
X509_free(certificate);
110+
}
111+
112+
TEST_CASE(parse_certificates_rejects_garbage)
113+
{
114+
EXPECT(RequestServer::parse_certificates("this is not a certificate"sv.bytes()).is_empty());
115+
}
116+
117+
TEST_CASE(parse_certificates_handles_degenerate_pkcs7_without_crashing)
118+
{
119+
// A PKCS#7 SignedData whose (OPTIONAL) content is absent decodes with a NULL content pointer; parsing must not
120+
// dereference it. Body: SEQUENCE { OBJECT IDENTIFIER signedData } with no content.
121+
static constexpr Array<u8, 13> degenerate_pkcs7 { 0x30, 0x0B, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02 };
122+
EXPECT(RequestServer::parse_certificates(degenerate_pkcs7).is_empty());
123+
}

0 commit comments

Comments
 (0)