botan: patch CVE-2026-32883

Details: https://nvd.nist.gov/vuln/detail/CVE-2026-32883

Backport the patch that was identified by Debian[1].
The included test passed successfully (along with the other tests).

[1]: https://security-tracker.debian.org/tracker/CVE-2026-32883

Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Gyorgy Sarvari
2026-04-06 20:32:54 +02:00
committed by Anuj Mittal
parent c4b5bca1e8
commit 70a903c888
2 changed files with 172 additions and 0 deletions
@@ -0,0 +1,171 @@
From 38a49aafc8115ddb6275ca6bbb8748b3d7b3064d Mon Sep 17 00:00:00 2001
From: Jack Lloyd <jack@randombit.net>
Date: Sun, 15 Mar 2026 10:28:52 -0400
Subject: [PATCH] Fix OCSP response verification
During a refactoring of OCSP (#3067) the check that the OCSP response signature
is valid was omitted. This allows OCSP responses to be forged.
CVE: CVE-2026-32883
Upstream-Status: Backport [https://github.com/randombit/botan/commit/acbffadcede18b36eea42beae57e6cae4b4da4a0]
Signed-off-by: Gyorgy Sarvari <skandigraun@gmail.com>
---
src/lib/x509/x509path.cpp | 67 +++++++++++++++++++++++----------------
src/tests/test_ocsp.cpp | 47 +++++++++++++++++++++++++++
2 files changed, 87 insertions(+), 27 deletions(-)
diff --git a/src/lib/x509/x509path.cpp b/src/lib/x509/x509path.cpp
index 4059d32..8212bb2 100644
--- a/src/lib/x509/x509path.cpp
+++ b/src/lib/x509/x509path.cpp
@@ -298,6 +298,41 @@ Certificate_Status_Code verify_ocsp_signing_cert(const X509_Certificate& signing
return validation_result.result();
}
+std::set<Certificate_Status_Code> evaluate_ocsp_response(const OCSP::Response& ocsp_response,
+ const X509_Certificate& subject,
+ const X509_Certificate& ca,
+ const std::vector<X509_Certificate>& cert_path,
+ const std::vector<Certificate_Store*>& certstores,
+ std::chrono::system_clock::time_point ref_time,
+ const Path_Validation_Restrictions& restrictions) {
+ // Handle softfail conditions (eg. OCSP unavailable)
+ if(auto dummy_status = ocsp_response.dummy_status()) {
+ return {dummy_status.value()};
+ }
+
+ // Find the certificate that signed this OCSP response
+ auto signing_cert = ocsp_response.find_signing_certificate(ca, restrictions.trusted_ocsp_responders());
+ if(!signing_cert) {
+ return {Certificate_Status_Code::OCSP_ISSUER_NOT_FOUND};
+ }
+
+ // Verify the signing certificate is trusted
+ auto cert_status = verify_ocsp_signing_cert(
+ signing_cert.value(), ca, concat(ocsp_response.certificates(), cert_path), certstores, ref_time, restrictions);
+ if(cert_status > Certificate_Status_Code::FIRST_ERROR_STATUS) {
+ return {cert_status, Certificate_Status_Code::OCSP_ISSUER_NOT_TRUSTED};
+ }
+
+ // Verify the cryptographic signature on the OCSP response
+ auto sig_status = ocsp_response.verify_signature(signing_cert.value());
+ if(sig_status != Certificate_Status_Code::OCSP_SIGNATURE_OK) {
+ return {sig_status};
+ }
+
+ // All checks passed, return the certificate's revocation status
+ return {ocsp_response.status_for(ca, subject, ref_time, restrictions.max_ocsp_age())};
+}
+
} // namespace
CertificatePathStatusCodes PKIX::check_ocsp(const std::vector<X509_Certificate>& cert_path,
@@ -312,38 +347,16 @@ CertificatePathStatusCodes PKIX::check_ocsp(const std::vector<X509_Certificate>&
CertificatePathStatusCodes cert_status(cert_path.size() - 1);
for(size_t i = 0; i != cert_path.size() - 1; ++i) {
- std::set<Certificate_Status_Code>& status = cert_status.at(i);
-
const X509_Certificate& subject = cert_path.at(i);
const X509_Certificate& ca = cert_path.at(i + 1);
- if(i < ocsp_responses.size() && (ocsp_responses.at(i) != std::nullopt) &&
- (ocsp_responses.at(i)->status() == OCSP::Response_Status_Code::Successful)) {
+ if(i < ocsp_responses.size() && ocsp_responses.at(i).has_value() &&
+ ocsp_responses.at(i)->status() == OCSP::Response_Status_Code::Successful) {
try {
- const auto& ocsp_response = ocsp_responses.at(i);
-
- if(auto dummy_status = ocsp_response->dummy_status()) {
- // handle softfail conditions
- status.insert(dummy_status.value());
- } else if(auto signing_cert =
- ocsp_response->find_signing_certificate(ca, restrictions.trusted_ocsp_responders());
- !signing_cert) {
- status.insert(Certificate_Status_Code::OCSP_ISSUER_NOT_FOUND);
- } else if(auto ocsp_signing_cert_status =
- verify_ocsp_signing_cert(signing_cert.value(),
- ca,
- concat(ocsp_response->certificates(), cert_path),
- certstores,
- ref_time,
- restrictions);
- ocsp_signing_cert_status > Certificate_Status_Code::FIRST_ERROR_STATUS) {
- status.insert(ocsp_signing_cert_status);
- status.insert(Certificate_Status_Code::OCSP_ISSUER_NOT_TRUSTED);
- } else {
- status.insert(ocsp_response->status_for(ca, subject, ref_time, restrictions.max_ocsp_age()));
- }
+ cert_status.at(i) = evaluate_ocsp_response(
+ ocsp_responses.at(i).value(), subject, ca, cert_path, certstores, ref_time, restrictions);
} catch(Exception&) {
- status.insert(Certificate_Status_Code::OCSP_RESPONSE_INVALID);
+ cert_status.at(i).insert(Certificate_Status_Code::OCSP_RESPONSE_INVALID);
}
}
}
diff --git a/src/tests/test_ocsp.cpp b/src/tests/test_ocsp.cpp
index 06383cd..54b2c03 100644
--- a/src/tests/test_ocsp.cpp
+++ b/src/tests/test_ocsp.cpp
@@ -408,6 +408,52 @@ class OCSP_Tests final : public Test {
return result;
}
+ static Test::Result test_forged_ocsp_signature_is_rejected() {
+ Test::Result result("OCSP response with forged signature is rejected by path validation");
+
+ auto ee = load_test_X509_cert("x509/ocsp/randombit.pem");
+ auto ca = load_test_X509_cert("x509/ocsp/letsencrypt.pem");
+ auto trust_root = load_test_X509_cert("x509/ocsp/geotrust.pem");
+
+ const std::vector<Botan::X509_Certificate> cert_path = {ee, ca, trust_root};
+
+ Botan::Certificate_Store_In_Memory certstore;
+ certstore.add_certificate(trust_root);
+
+ const auto valid_time = Botan::calendar_point(2016, 11, 18, 12, 30, 0).to_std_timepoint();
+
+ // Verify the unmodified response is accepted
+ {
+ auto ocsp = load_test_OCSP_resp("x509/ocsp/randombit_ocsp.der");
+ const auto ocsp_status = Botan::PKIX::check_ocsp(
+ cert_path, {ocsp}, {&certstore}, valid_time, Botan::Path_Validation_Restrictions());
+
+ if(result.test_eq("Legitimate: expected result count", ocsp_status.size(), 1) &&
+ result.test_eq("Legitimate: expected status count", ocsp_status[0].size(), 1)) {
+ result.test_eq("Legitimate response is accepted",
+ ocsp_status[0].contains(Botan::Certificate_Status_Code::OCSP_RESPONSE_GOOD), true);
+ }
+ }
+
+ // Tamper with the signature and verify check_ocsp rejects it
+ {
+ auto ocsp_bytes = Test::read_binary_data_file("x509/ocsp/randombit_ocsp.der");
+ ocsp_bytes.back() ^= 0x01;
+ Botan::OCSP::Response forged_ocsp(ocsp_bytes.data(), ocsp_bytes.size());
+
+ const auto ocsp_status = Botan::PKIX::check_ocsp(
+ cert_path, {forged_ocsp}, {&certstore}, valid_time, Botan::Path_Validation_Restrictions());
+
+ if(result.test_eq("Forged: expected result count", ocsp_status.size(), 1) &&
+ result.test_eq("Forged: expected status count", ocsp_status[0].size(), 1)) {
+ result.test_eq("Forged signature is rejected",
+ ocsp_status[0].contains(Botan::Certificate_Status_Code::OCSP_SIGNATURE_ERROR), true);
+ }
+ }
+
+ return result;
+ }
+
static Test::Result test_responder_cert_with_nocheck_extension() {
Test::Result result("BDr's OCSP response contains certificate featuring NoCheck extension");
@@ -436,6 +482,7 @@ class OCSP_Tests final : public Test {
results.push_back(test_response_verification_without_next_update_without_max_age());
results.push_back(test_response_verification_softfail());
results.push_back(test_response_verification_with_additionally_trusted_responder());
+ results.push_back(test_forged_ocsp_signature_is_rejected());
results.push_back(test_responder_cert_with_nocheck_extension());
#if defined(BOTAN_HAS_ONLINE_REVOCATION_CHECKS)
@@ -6,6 +6,7 @@ SECTION = "libs"
SRC_URI = "https://botan.randombit.net/releases/Botan-${PV}.tar.xz \ SRC_URI = "https://botan.randombit.net/releases/Botan-${PV}.tar.xz \
file://CVE-2026-32877.patch \ file://CVE-2026-32877.patch \
file://CVE-2026-32883.patch \
" "
SRC_URI[sha256sum] = "fde194236f6d5434f136ea0a0627f6cc9d26af8b96e9f1e1c7d8c82cd90f4f24" SRC_URI[sha256sum] = "fde194236f6d5434f136ea0a0627f6cc9d26af8b96e9f1e1c7d8c82cd90f4f24"