new file mode 100644
@@ -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 \
file://CVE-2026-32877.patch \
+ file://CVE-2026-32883.patch \
"
SRC_URI[sha256sum] = "fde194236f6d5434f136ea0a0627f6cc9d26af8b96e9f1e1c7d8c82cd90f4f24"
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> --- .../botan/botan/CVE-2026-32883.patch | 171 ++++++++++++++++++ meta-oe/recipes-crypto/botan/botan_3.10.0.bb | 1 + 2 files changed, 172 insertions(+) create mode 100644 meta-oe/recipes-crypto/botan/botan/CVE-2026-32883.patch