etc.git


etc

~ scripts and random things
git clone https://github.com/djnnvx/etc.git
log | files | refs | archive

commit a9486ff87a71e8c4f17ad2b3b0aba73a5faf0c6c
parent 57bf3d3baca9e824ac109016230b042bf98bcb26
Author: djnn <[email protected]>
Date: Wed, 27 May 2026 11:51:44 +0900

feat(fuzzers): libksba experiment

Diffstat:
Afuzzing/libksba/README.md | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/cleanup.sh | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/conf/ca.cnf | 36++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/dictionaries/asn1.dict | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/fuzz_cert.c | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/fuzz_cms.c | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/fuzz_crl.c | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/fuzz_ocsp.c | 39+++++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/gen_corpus.sh | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/harness.h | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/setup.sh | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afuzzing/libksba/start-fuzzers.sh | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

12 files changed, 848 insertions(+), 0 deletions(-)

diff --git a/fuzzing/libksba/README.md b/fuzzing/libksba/README.md
@@ -0,0 +1,72 @@
+# libksba fuzzing
+
+Fuzzing harnesses for **libksba** (GnuPG's ASN.1/X.509/CMS/OCSP/CRL parser).
+Same machine setup as the libusb harness: AFL++ (CLASSIC + laf-intel), a CmpLog
+sibling per harness, ASan + UBSan + signed-integer-overflow at `-O0 -g`.
+
+Why libksba: hand-rolled DER parsing, tiny maintainer team, ubiquitous (every
+GnuPG install), **not on OSS-Fuzz**, no fuzz harness in-tree. CRL and OCSP parsing
+are reachable **remotely** through `dirmngr` (HTTP/LDAP fetch); cert and CMS
+through `gpgsm`. Prior art: CVE-2022-3515 (integer overflow -> heap overflow).
+
+## Harnesses
+
+| harness      | public entrypoint                       | parsers driven                          |
+|--------------|-----------------------------------------|-----------------------------------------|
+| `fuzz_cert`  | `ksba_cert_init_from_mem` + accessors   | cert.c, dn.c, keyinfo.c, name.c, oid.c, time.c |
+| `fuzz_crl`   | `ksba_crl_set_reader` + `ksba_crl_parse`| crl.c, ber-help.c, keyinfo.c, time.c    |
+| `fuzz_ocsp`  | `ksba_ocsp_parse_response`              | ocsp.c, time.c                          |
+| `fuzz_cms`   | `ksba_cms_set_reader_writer` + `ksba_cms_parse` | cms.c, cms-parser.c             |
+
+Cert parsing is lazy, so `fuzz_cert` calls the full accessor set to force the
+deep parse. All parsers funnel through the BER decoder (`ber-decoder.c`,
+`ber-help.c`).
+
+## Usage
+
+```
+bash setup.sh            # deps + AFL++ + instrumented libksba + harnesses + corpus
+bash start-fuzzers.sh    # 8 instances in tmux (master + CmpLog secondary per harness)
+bash start-fuzzers.sh --status
+bash cleanup.sh          # kill + remove artifacts (flags: --keep-corpus --keep-source ...)
+```
+
+`setup.sh` flags: `--skip-deps` (deps already installed), `--skip-afl` (use a
+system afl-clang-fast instead of building AFL++ from source).
+
+Each harness builds three ways: `H` (AFL persistent), `H.cmplog` (CmpLog), and
+`H.repro` (plain clang + ASan, reads files from argv) for crash triage:
+
+```
+./fuzz_crl.repro output/master_crl/crashes/id:000000*
+```
+
+## Target version
+
+`setup.sh` defaults to libksba git tip (1.8.x) - bugs there are reportable
+upstream immediately. The line distros ship for `dirmngr`/`gpgsm` is older and
+missing the 1.7.0 hardening; to fuzz that instead:
+
+```
+LIBKSBA_TAG=libksba-1.6.6 bash setup.sh
+```
+
+## Soft spots flagged during the audit (corpus is seeded toward these)
+
+- `time.c` `_ksba_asntime_to_iso`: terminator check reads one byte past the time
+  value when it has no trailing `Z`. Shared by cert/crl/ocsp time fields.
+- `dn.c` `append_ucs2_value` / `append_ucs4_value`: odd-length BMPString /
+  misaligned UniversalString over-read in subject/issuer DN.
+
+These are 1-byte over-reads that land in decoder slack as shipped; the point of
+fuzzing is to find the variant that doesn't.
+
+## Notes
+
+- The `#include`-the-`.c` trick used for libusb is not needed here: the bugs are
+  reachable through the public API, so harnesses link the instrumented
+  `libksba.a` and drive it normally.
+- `gen_corpus.sh` mints real DER seeds with a throwaway openssl mini-CA
+  (`conf/ca.cnf`): cert, empty + revoked CRL, signed CMS, OCSP response.
+- LLVM-18 caveat carried from libusb: `AFL_LLVM_LAF_ALL=1` needs
+  `AFL_LLVM_INSTRUMENT=CLASSIC` (PCGUARD miscompiles split comparisons).
diff --git a/fuzzing/libksba/cleanup.sh b/fuzzing/libksba/cleanup.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+# cleanup.sh - kill fuzzers and remove artifacts.
+# Usage: bash cleanup.sh [--keep-corpus] [--keep-builds] [--keep-source] [--crashes-only]
+set -uo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+SESSION="libksba-fuzz"
+KEEP_CORPUS=0 KEEP_BUILDS=0 KEEP_SOURCE=0 CRASHES_ONLY=0
+
+for arg in "$@"; do
+    case "${arg}" in
+        --keep-corpus)  KEEP_CORPUS=1  ;;
+        --keep-builds)  KEEP_BUILDS=1  ;;
+        --keep-source)  KEEP_SOURCE=1  ;;
+        --crashes-only) CRASHES_ONLY=1 ;;
+        *) echo "[!] Unknown option: ${arg}" >&2 ;;
+    esac
+done
+
+log() { echo "[*] $*"; }
+ok()  { echo "[+] $*"; }
+
+tmux kill-session -t "${SESSION}" 2>/dev/null || true
+
+pkill -u "$(id -un)" -f "afl-fuzz.*fuzz_" 2>/dev/null || true
+for h in fuzz_cert fuzz_crl fuzz_ocsp fuzz_cms; do
+    pkill -u "$(id -un)" -f "${h}" 2>/dev/null || true
+done
+
+[[ "${CRASHES_ONLY}" -eq 1 ]] && { ok "Processes killed."; exit 0; }
+
+if [[ "${KEEP_BUILDS}" -eq 0 ]]; then
+    for h in fuzz_cert fuzz_crl fuzz_ocsp fuzz_cms; do
+        rm -f "${h}" "${h}.cmplog" "${h}.repro"
+    done
+    rm -rf build-cls build-cmplog build-plain
+    ok "Binaries and build dirs removed."
+fi
+
+if [[ "${KEEP_CORPUS}" -eq 0 ]]; then
+    rm -rf corpus/ output/
+    ok "Corpus and output removed."
+fi
+
+if [[ "${KEEP_SOURCE}" -eq 0 ]]; then
+    rm -rf libksba-src/ AFLplusplus/ afl-build/
+    ok "Source trees removed."
+fi
+
+ok "Done."
diff --git a/fuzzing/libksba/conf/ca.cnf b/fuzzing/libksba/conf/ca.cnf
@@ -0,0 +1,36 @@
+# Minimal offline mini-CA used only by gen_corpus.sh to mint realistic DER
+# seeds (cert / CRL / OCSP). Paths come from $ENV::CADIR set by the script.
+
+[ ca ]
+default_ca = CA_default
+
+[ CA_default ]
+dir              = $ENV::CADIR
+database         = $dir/index.txt
+new_certs_dir    = $dir/newcerts
+certificate      = $dir/ca.crt
+private_key      = $dir/ca.key
+serial           = $dir/serial
+crlnumber        = $dir/crlnumber
+default_md       = sha256
+default_days     = 365
+default_crl_days = 30
+policy           = policy_any
+x509_extensions  = leaf_exts
+copy_extensions  = none
+
+[ policy_any ]
+commonName = supplied
+
+# Rich extension set so the cert/CRL seeds exercise the extension parsers:
+# SAN (multiple GeneralName types), AIA (OCSP + caIssuers), CRL dist points,
+# key usage, EKU, SKI, AKI.
+[ leaf_exts ]
+basicConstraints       = CA:FALSE
+subjectAltName         = DNS:leaf.example, email:[email protected], URI:https://leaf.example/x
+keyUsage               = digitalSignature, keyEncipherment
+extendedKeyUsage       = serverAuth, clientAuth
+authorityInfoAccess    = OCSP;URI:http://ocsp.example/, caIssuers;URI:http://ca.example/ca.crt
+crlDistributionPoints  = URI:http://crl.example/a.crl
+subjectKeyIdentifier   = hash
+authorityKeyIdentifier = keyid,issuer
diff --git a/fuzzing/libksba/dictionaries/asn1.dict b/fuzzing/libksba/dictionaries/asn1.dict
@@ -0,0 +1,55 @@
+# ASN.1 / DER tokens for libksba harnesses (AFL -x format).
+# Tags, long-form length prefixes, time strings, and common OID encodings so
+# the mutator can splice valid structure into seeds.
+
+# --- universal tags ---
+tag_bool="\x01"
+tag_int="\x02"
+tag_bitstring="\x03"
+tag_octetstring="\x04"
+tag_null="\x05"
+tag_oid="\x06"
+tag_utf8="\x0c"
+tag_seq="\x30"
+tag_set="\x31"
+tag_printable="\x13"
+tag_ia5="\x16"
+tag_utctime="\x17"
+tag_gentime="\x18"
+tag_bmpstring="\x1e"
+tag_universalstring="\x1c"
+
+# --- context / constructed tags (extensions, GeneralNames, [n]) ---
+ctx_a0="\xa0"
+ctx_a1="\xa1"
+ctx_a2="\xa2"
+ctx_a3="\xa3"
+ctx_80="\x80"
+ctx_81="\x81"
+ctx_82="\x82"
+ctx_86="\x86"
+
+# --- long-form length prefixes (overflow / OOB triggers) ---
+len_1b="\x81"
+len_2b="\x82"
+len_3b="\x83"
+len_4b="\x84"
+len_indef="\x80"
+len_big="\x84\xff\xff\xff\xff"
+
+# --- time values (drive time.c; no trailing Z = OOB-read trigger) ---
+gentime_noz="\x18\x0e\x32\x30\x32\x34\x30\x31\x30\x31\x30\x30\x30\x30\x30\x30"
+utctime="\x17\x0d\x32\x34\x30\x31\x30\x31\x30\x30\x30\x30\x30\x30\x5a"
+
+# --- common OIDs (DER value bytes) ---
+oid_rsa="\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01"
+oid_sha256rsa="\x2a\x86\x48\x86\xf7\x0d\x01\x01\x0b"
+oid_cn="\x55\x04\x03"
+oid_keyusage="\x55\x1d\x0f"
+oid_san="\x55\x1d\x11"
+oid_aki="\x55\x1d\x23"
+oid_ski="\x55\x1d\x0e"
+oid_crldp="\x55\x1d\x1f"
+oid_aia="\x2b\x06\x01\x05\x05\x07\x01\x01"
+oid_ocsp_basic="\x2b\x06\x01\x05\x05\x07\x30\x01\x01"
+oid_pkcs7_signed="\x2a\x86\x48\x86\xf7\x0d\x01\x07\x02"
diff --git a/fuzzing/libksba/fuzz_cert.c b/fuzzing/libksba/fuzz_cert.c
@@ -0,0 +1,90 @@
+/*
+ * X.509 cert parsing via ksba_cert_init_from_mem. Parsing is lazy, so the
+ * accessors below force the deep parse (cert/dn/keyinfo/name/oid/time).
+ * Reachable via gpgsm with an attacker-supplied certificate.
+ */
+#include "harness.h"
+
+static int fuzz_one(const uint8_t *data, size_t size)
+{
+    ksba_cert_t cert;
+
+    if (ksba_cert_new(&cert))
+        return 0;
+
+    if (!ksba_cert_init_from_mem(cert, data, size)) {
+        char *s;
+        ksba_isotime_t t;
+        ksba_sexp_t kid, serial;
+        ksba_name_t nm, nm2;
+        ksba_crl_reason_t reason;
+        const char *oid;
+        int i, crit, ca, pathlen;
+        unsigned int flags;
+        size_t off, len;
+
+        ksba_free(ksba_cert_get_serial(cert));
+        (void)ksba_cert_get_digest_algo(cert);          /* const, do not free */
+
+        for (i = 0; i < KSBA_FUZZ_MAXITER && (s = ksba_cert_get_issuer(cert, i)); i++)
+            ksba_free(s);
+        for (i = 0; i < KSBA_FUZZ_MAXITER && (s = ksba_cert_get_subject(cert, i)); i++)
+            ksba_free(s);
+
+        ksba_cert_get_validity(cert, 0, t);
+        ksba_cert_get_validity(cert, 1, t);
+
+        ksba_free(ksba_cert_get_public_key(cert));
+        ksba_free(ksba_cert_get_sig_val(cert));
+
+        for (i = 0; i < KSBA_FUZZ_MAXITER &&
+             !ksba_cert_get_extension(cert, i, &oid, &crit, &off, &len); i++)
+            ;
+
+        ksba_cert_is_ca(cert, &ca, &pathlen);
+        ksba_cert_get_key_usage(cert, &flags);
+        if (!ksba_cert_get_cert_policies(cert, &s))
+            ksba_free(s);
+        if (!ksba_cert_get_ext_key_usages(cert, &s))
+            ksba_free(s);
+
+        for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+            nm = nm2 = NULL;
+            if (ksba_cert_get_crl_dist_point(cert, i, &nm, &nm2, &reason))
+                break;
+            drain_name(nm);
+            drain_name(nm2);
+        }
+
+        kid = serial = NULL;
+        nm = NULL;
+        if (!ksba_cert_get_auth_key_id(cert, &kid, &nm, &serial)) {
+            ksba_free(kid);
+            ksba_free(serial);
+            drain_name(nm);
+        }
+        kid = NULL;
+        if (!ksba_cert_get_subj_key_id(cert, &crit, &kid))
+            ksba_free(kid);
+
+        for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+            char *method = NULL;
+            nm = NULL;
+            if (ksba_cert_get_authority_info_access(cert, i, &method, &nm))
+                break;
+            ksba_free(method);
+            drain_name(nm);
+        }
+        for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+            char *method = NULL;
+            nm = NULL;
+            if (ksba_cert_get_subject_info_access(cert, i, &method, &nm))
+                break;
+            ksba_free(method);
+            drain_name(nm);
+        }
+    }
+
+    ksba_cert_release(cert);
+    return 0;
+}
diff --git a/fuzzing/libksba/fuzz_cms.c b/fuzzing/libksba/fuzz_cms.c
@@ -0,0 +1,79 @@
+/*
+ * CMS / PKCS#7 (S/MIME) parsing, reachable via gpgsm on an attacker-supplied
+ * message. Iterated ksba_cms_parse drives the state machine, then the accessors.
+ * Largest parser in the tree: cms.c + cms-parser.c.
+ */
+#include "harness.h"
+
+static int fuzz_one(const uint8_t *data, size_t size)
+{
+    ksba_reader_t reader;
+    ksba_cms_t cms;
+    ksba_stop_reason_t sr = KSBA_SR_RUNNING;
+    int guard = 0, i;
+
+    if (ksba_reader_new(&reader))
+        return 0;
+    if (ksba_reader_set_mem(reader, data, size)) {
+        ksba_reader_release(reader);
+        return 0;
+    }
+    if (ksba_cms_new(&cms)) {
+        ksba_reader_release(reader);
+        return 0;
+    }
+    ksba_cms_set_reader_writer(cms, reader, NULL);
+
+    do {
+        if (ksba_cms_parse(cms, &sr))
+            break;
+    } while (sr != KSBA_SR_READY && ++guard < 100000);
+
+    ksba_cms_get_content_type(cms, 0);
+    ksba_cms_get_content_type(cms, 1);
+
+    for (i = 0; i < KSBA_FUZZ_MAXITER && ksba_cms_get_digest_algo_list(cms, i); i++)
+        ;
+    for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+        char *issuer = NULL;
+        ksba_sexp_t serial = NULL;
+        if (ksba_cms_get_issuer_serial(cms, i, &issuer, &serial))
+            break;
+        ksba_free(issuer);
+        ksba_free(serial);
+    }
+    for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+        ksba_cert_t c = ksba_cms_get_cert(cms, i);
+        if (!c)
+            break;
+        ksba_cert_release(c);
+    }
+    for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+        char *digest = NULL;
+        size_t dlen;
+        if (ksba_cms_get_message_digest(cms, i, &digest, &dlen))
+            break;
+        ksba_free(digest);
+    }
+    for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+        ksba_isotime_t st;
+        if (ksba_cms_get_signing_time(cms, i, st))
+            break;
+    }
+    for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+        ksba_sexp_t v = ksba_cms_get_sig_val(cms, i);
+        if (!v)
+            break;
+        ksba_free(v);
+    }
+    for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+        ksba_sexp_t v = ksba_cms_get_enc_val(cms, i);
+        if (!v)
+            break;
+        ksba_free(v);
+    }
+
+    ksba_cms_release(cms);
+    ksba_reader_release(reader);
+    return 0;
+}
diff --git a/fuzzing/libksba/fuzz_crl.c b/fuzzing/libksba/fuzz_crl.c
@@ -0,0 +1,71 @@
+/*
+ * CRL parsing: dirmngr fetches CRLs over HTTP/LDAP and parses them here, so this
+ * is a remote attacker-controlled path (and CVE-2022-3515's home). Iterated
+ * ksba_crl_parse pulls each revoked entry, then the accessors drive the rest.
+ */
+#include "harness.h"
+
+static int fuzz_one(const uint8_t *data, size_t size)
+{
+    ksba_reader_t reader;
+    ksba_crl_t crl;
+    ksba_stop_reason_t sr = KSBA_SR_RUNNING;
+    int guard = 0;
+
+    if (ksba_reader_new(&reader))
+        return 0;
+    if (ksba_reader_set_mem(reader, data, size)) {
+        ksba_reader_release(reader);
+        return 0;
+    }
+    if (ksba_crl_new(&crl)) {
+        ksba_reader_release(reader);
+        return 0;
+    }
+    ksba_crl_set_reader(crl, reader);
+
+    do {
+        if (ksba_crl_parse(crl, &sr))
+            break;
+        if (sr == KSBA_SR_GOT_ITEM) {
+            ksba_sexp_t serial = NULL;
+            ksba_isotime_t rdate;
+            ksba_crl_reason_t reason;
+            if (!ksba_crl_get_item(crl, &serial, rdate, &reason))
+                ksba_free(serial);
+        }
+    } while (sr != KSBA_SR_READY && ++guard < 100000);
+
+    {
+        char *issuer = NULL;
+        ksba_isotime_t this_update, next_update;
+        ksba_sexp_t number = NULL, kid = NULL, ser = NULL;
+        ksba_name_t nm = NULL;
+        int i;
+
+        if (!ksba_crl_get_issuer(crl, &issuer))
+            ksba_free(issuer);
+        (void)ksba_crl_get_digest_algo(crl);             /* const, do not free */
+        ksba_crl_get_update_times(crl, this_update, next_update);
+        ksba_free(ksba_crl_get_sig_val(crl));
+        if (!ksba_crl_get_crl_number(crl, &number))
+            ksba_free(number);
+        if (!ksba_crl_get_auth_key_id(crl, &kid, &nm, &ser)) {
+            ksba_free(kid);
+            ksba_free(ser);
+            drain_name(nm);
+        }
+        for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+            const char *oid;
+            const unsigned char *der;
+            size_t derlen;
+            int crit;
+            if (ksba_crl_get_extension(crl, i, &oid, &crit, &der, &derlen))
+                break;
+        }
+    }
+
+    ksba_crl_release(crl);
+    ksba_reader_release(reader);
+    return 0;
+}
diff --git a/fuzzing/libksba/fuzz_ocsp.c b/fuzzing/libksba/fuzz_ocsp.c
@@ -0,0 +1,39 @@
+/*
+ * OCSP response parsing: dirmngr fetches these over HTTP, so it is a remote
+ * attacker-controlled path. ksba_ocsp_parse_response takes a raw buffer and
+ * parses eagerly (ocsp.c + time.c); the accessors drain the remainder.
+ */
+#include "harness.h"
+
+static int fuzz_one(const uint8_t *data, size_t size)
+{
+    ksba_ocsp_t ocsp;
+    ksba_ocsp_response_status_t rstatus;
+
+    if (ksba_ocsp_new(&ocsp))
+        return 0;
+
+    if (!ksba_ocsp_parse_response(ocsp, data, size, &rstatus)) {
+        char *name = NULL;
+        ksba_sexp_t kid = NULL;
+        ksba_isotime_t produced_at;
+        int i;
+
+        if (!ksba_ocsp_get_responder_id(ocsp, &name, &kid)) {
+            ksba_free(name);
+            ksba_free(kid);
+        }
+        ksba_free(ksba_ocsp_get_sig_val(ocsp, produced_at));
+        (void)ksba_ocsp_get_digest_algo(ocsp);           /* const, do not free */
+
+        for (i = 0; i < KSBA_FUZZ_MAXITER; i++) {
+            ksba_cert_t c = ksba_ocsp_get_cert(ocsp, i);
+            if (!c)
+                break;
+            ksba_cert_release(c);
+        }
+    }
+
+    ksba_ocsp_release(ocsp);
+    return 0;
+}
diff --git a/fuzzing/libksba/gen_corpus.sh b/fuzzing/libksba/gen_corpus.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+# gen_corpus.sh - mint realistic DER seeds for the libksba harnesses with openssl.
+# Offline only: a throwaway mini-CA (conf/ca.cnf) signs a leaf, a CRL and an OCSP
+# response. Output: corpus/{cert,crl,cms,ocsp}/*.der
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+CADIR="$(mktemp -d)"
+export CADIR
+CONF="${SCRIPT_DIR}/conf/ca.cnf"
+trap 'rm -rf "${CADIR}"' EXIT
+
+log() { echo "[*] $*"; }
+ok()  { echo "[+] $*"; }
+
+mkdir -p corpus/cert corpus/crl corpus/cms corpus/ocsp
+mkdir -p "${CADIR}/newcerts"
+: > "${CADIR}/index.txt"
+echo 1000 > "${CADIR}/serial"
+echo 1000 > "${CADIR}/crlnumber"
+
+log "CA + leaf..."
+openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
+    -keyout "${CADIR}/ca.key" -out "${CADIR}/ca.crt" \
+    -subj "/CN=Fuzz Root CA/O=djnn/C=FR" \
+    -addext "basicConstraints=critical,CA:TRUE" \
+    -addext "keyUsage=critical,keyCertSign,cRLSign" 2>/dev/null
+
+openssl req -newkey rsa:2048 -nodes \
+    -keyout "${CADIR}/leaf.key" -out "${CADIR}/leaf.csr" \
+    -subj "/CN=leaf.example/O=djnn/C=FR" 2>/dev/null
+
+openssl ca -batch -config "${CONF}" -notext \
+    -in "${CADIR}/leaf.csr" -out "${CADIR}/leaf.crt" 2>/dev/null
+
+openssl x509 -in "${CADIR}/ca.crt"   -outform DER -out corpus/cert/ca.der
+openssl x509 -in "${CADIR}/leaf.crt" -outform DER -out corpus/cert/leaf.der
+ok "cert seeds"
+
+log "CRL (empty + one revoked entry)..."
+openssl ca -batch -config "${CONF}" -gencrl -out "${CADIR}/crl-empty.pem" 2>/dev/null
+openssl crl -in "${CADIR}/crl-empty.pem" -outform DER -out corpus/crl/empty.der
+openssl ca -batch -config "${CONF}" -revoke "${CADIR}/leaf.crt" -crl_reason keyCompromise 2>/dev/null
+openssl ca -batch -config "${CONF}" -gencrl -out "${CADIR}/crl-revoked.pem" 2>/dev/null
+openssl crl -in "${CADIR}/crl-revoked.pem" -outform DER -out corpus/crl/revoked.der
+ok "CRL seeds"
+
+log "CMS / PKCS#7 signed..."
+echo "fuzz me" > "${CADIR}/msg.txt"
+openssl cms -sign -binary -nodetach -outform DER \
+    -signer "${CADIR}/leaf.crt" -inkey "${CADIR}/leaf.key" \
+    -in "${CADIR}/msg.txt" -out corpus/cms/signed.der 2>/dev/null
+ok "CMS seed"
+
+log "OCSP response (offline responder)..."
+if openssl ocsp -issuer "${CADIR}/ca.crt" -cert "${CADIR}/leaf.crt" \
+        -reqout "${CADIR}/req.der" -no_nonce 2>/dev/null \
+   && openssl ocsp -index "${CADIR}/index.txt" -CA "${CADIR}/ca.crt" \
+        -rsigner "${CADIR}/ca.crt" -rkey "${CADIR}/ca.key" \
+        -reqin "${CADIR}/req.der" -respout corpus/ocsp/resp.der -no_nonce 2>/dev/null; then
+    ok "OCSP seed"
+else
+    echo "[!] OCSP seed skipped (openssl ocsp responder unavailable); seeding with cert DER"
+    cp corpus/cert/leaf.der corpus/ocsp/leaf.der
+fi
+
+echo
+ok "corpus ready:"
+find corpus -type f -printf '    %p (%s bytes)\n' | sort
diff --git a/fuzzing/libksba/harness.h b/fuzzing/libksba/harness.h
@@ -0,0 +1,74 @@
+/*
+ * harness.h - shared AFL++/libFuzzer entry boilerplate for the libksba harnesses.
+ *
+ * Each fuzz_*.c includes this and defines:  static int fuzz_one(const uint8_t*, size_t);
+ *
+ * Build modes (set by setup.sh):
+ *   afl-clang-fast            -> persistent loop, reads from AFL shared memory
+ *   clang -DKSBA_FUZZ_STANDALONE -> reads files from argv, for crash replay/triage
+ *   <libFuzzer>               -> LLVMFuzzerTestOneInput
+ */
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <gpg-error.h>
+#include "ksba.h"
+
+/* Bound on idx-style accessor loops so adversarial inputs can't spin forever
+ * inside the harness; real hangs in the library still surface via afl -t. */
+#define KSBA_FUZZ_MAXITER 256
+
+static int fuzz_one(const uint8_t *data, size_t size);
+
+/* Walk and free a ksba_name_t (GeneralNames), exercising the name accessors. */
+static inline void drain_name(ksba_name_t nm)
+{
+    int i;
+    if (!nm)
+        return;
+    for (i = 0; i < KSBA_FUZZ_MAXITER && ksba_name_enum(nm, i); i++) {
+        char *uri = ksba_name_get_uri(nm, i);
+        ksba_free(uri);
+    }
+    ksba_name_release(nm);
+}
+
+#ifdef __AFL_FUZZ_TESTCASE_LEN
+__AFL_FUZZ_INIT();
+int main(void)
+{
+    __AFL_INIT();
+    unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
+    while (__AFL_LOOP(10000))
+        fuzz_one(buf, (size_t)__AFL_FUZZ_TESTCASE_LEN);
+    return 0;
+}
+#else
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
+{
+    return fuzz_one(data, size);
+}
+#ifdef KSBA_FUZZ_STANDALONE
+int main(int argc, char **argv)
+{
+    static unsigned char buf[1 << 20];
+    int i;
+    for (i = 1; i < argc; i++) {
+        FILE *f = fopen(argv[i], "rb");
+        size_t n;
+        if (!f)
+            continue;
+        n = fread(buf, 1, sizeof buf, f);
+        fclose(f);
+        fprintf(stderr, "[*] %s (%zu bytes)\n", argv[i], n);
+        fuzz_one(buf, n);
+    }
+    return 0;
+}
+#endif
+#endif
diff --git a/fuzzing/libksba/setup.sh b/fuzzing/libksba/setup.sh
@@ -0,0 +1,125 @@
+#!/bin/bash
+# setup.sh - bootstrap libksba fuzzing environment.
+# Same machine specs as the libusb harness: AFL++ from source, CLASSIC+LAF
+# instrumentation, a CmpLog sibling per harness, ASan+UBSan+signed-overflow at -O0 -g.
+# Usage: bash setup.sh [--skip-deps] [--skip-afl]
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+AFL_REPO="https://github.com/AFLplusplus/AFLplusplus.git"
+AFL_BIN="${SCRIPT_DIR}/afl-build/usr/local/bin"
+
+# libksba defaults to git tip (1.8.x, reportable upstream now). For the line
+# dirmngr/gpgsm actually link against in distros, set LIBKSBA_TAG=libksba-1.6.6.
+LIBKSBA_REPO="${LIBKSBA_REPO:-https://github.com/gpg/libksba.git}"
+LIBKSBA_TAG="${LIBKSBA_TAG:-master}"
+
+CFLAGS_FUZZ="-fsanitize=address,undefined,signed-integer-overflow -g -O0 -fno-omit-frame-pointer"
+CFLAGS_FUZZ+=" -Wno-implicit-function-declaration -Wno-unused-function -Wno-unused-variable"
+
+HARNESSES="fuzz_cert fuzz_crl fuzz_ocsp fuzz_cms"
+
+SKIP_DEPS=0
+SKIP_AFL=0
+for arg in "$@"; do
+    [[ "$arg" == "--skip-deps" ]] && SKIP_DEPS=1
+    [[ "$arg" == "--skip-afl"  ]] && SKIP_AFL=1
+done
+
+log() { echo "[*] $*"; }
+ok()  { echo "[+] $*"; }
+die() { echo "[!] $*" >&2; exit 1; }
+
+if [[ "${SKIP_DEPS}" -eq 0 ]]; then
+    log "Installing dependencies..."
+    sudo apt-get update -qq
+    sudo apt-get install -y \
+        build-essential git tmux python3 \
+        clang llvm lld pkg-config gdb \
+        autoconf automake libtool gettext bison texinfo \
+        libgpg-error-dev openssl
+    ok "Dependencies installed."
+fi
+
+if [[ "${SKIP_AFL}" -eq 1 ]]; then
+    command -v afl-clang-fast > /dev/null 2>&1 || die "afl-clang-fast not found."
+elif [[ -x "${AFL_BIN}/afl-clang-fast" ]]; then
+    ok "AFL++ already built."
+    export PATH="${AFL_BIN}:${PATH}"
+else
+    log "Building AFL++..."
+    [[ -d "AFLplusplus" ]] || git clone --depth=1 "${AFL_REPO}" AFLplusplus
+    ( cd AFLplusplus && make source-only -j"$(nproc)" \
+        && make install PREFIX="${SCRIPT_DIR}/afl-build/usr/local" )
+    export PATH="${AFL_BIN}:${PATH}"
+    ok "AFL++ built."
+fi
+export PATH="${AFL_BIN}:${PATH}"
+
+if [[ -d "libksba-src/.git" ]]; then
+    ok "libksba-src present ($(cd libksba-src && git describe --tags --always))."
+else
+    log "Cloning libksba ${LIBKSBA_TAG}..."
+    git clone "${LIBKSBA_REPO}" libksba-src
+    ( cd libksba-src && git checkout -q "${LIBKSBA_TAG}" )
+    ok "libksba-src cloned."
+fi
+
+[[ -x libksba-src/configure ]] || ( cd libksba-src && ./autogen.sh >/dev/null 2>&1 )
+
+# Out-of-tree builds keep the CLASSIC/CmpLog/plain object sets separate. Build gl
+# then just libksba.la: the rest of src (ber-dump, tests) needs more deps.
+build_lib() {
+    local dir="$1" cc="$2"; shift 2
+    log "  building ${dir} (CC=${cc})..."
+    rm -rf "${dir}"
+    mkdir -p "${dir}"
+    ( cd "${dir}" \
+      && env "$@" "${SCRIPT_DIR}/libksba-src/configure" \
+            CC="${cc}" CFLAGS="${CFLAGS_FUZZ}" \
+            --enable-static --disable-shared --disable-maintainer-mode >/dev/null \
+      && env "$@" make -C gl  -j"$(nproc)" >/dev/null \
+      && env "$@" make -C src libksba.la -j"$(nproc)" >/dev/null )
+    [[ -f "${dir}/src/.libs/libksba.a" ]] || die "${dir}: libksba.a not produced"
+}
+
+log "Building libksba: instrumented (CLASSIC+LAF), CmpLog, and a plain-ASan copy for repro..."
+build_lib build-cls     afl-clang-fast AFL_LLVM_INSTRUMENT=CLASSIC AFL_LLVM_LAF_ALL=1
+build_lib build-cmplog  afl-clang-fast AFL_LLVM_INSTRUMENT=CLASSIC AFL_LLVM_LAF_ALL=1 AFL_LLVM_CMPLOG=1
+build_lib build-plain   clang
+ok "libksba built."
+
+GPGERR_CFLAGS="$(gpg-error-config --cflags 2>/dev/null || pkg-config --cflags gpg-error 2>/dev/null || true)"
+GPGERR_LIBS="$(gpg-error-config --libs 2>/dev/null || pkg-config --libs gpg-error 2>/dev/null || echo -lgpg-error)"
+
+# gnulib convenience archive libksba links against; omit gracefully if absent.
+gnulib() { local a="$1/gl/.libs/libgnu.a"; [[ -f "$a" ]] && echo "$a"; }
+CLS_LIBS="build-cls/src/.libs/libksba.a $(gnulib build-cls)"
+CMP_LIBS="build-cmplog/src/.libs/libksba.a $(gnulib build-cmplog)"
+PLAIN_LIBS="build-plain/src/.libs/libksba.a $(gnulib build-plain)"
+
+# harnesses: AFL persistent, CmpLog sibling, and a plain-clang ASan repro binary.
+log "Building harnesses..."
+for h in ${HARNESSES}; do
+    log "  ${h}..."
+    AFL_LLVM_INSTRUMENT=CLASSIC AFL_LLVM_LAF_ALL=1 \
+        afl-clang-fast ${CFLAGS_FUZZ} -I build-cls/src -I libksba-src/src ${GPGERR_CFLAGS} \
+        "${h}.c" ${CLS_LIBS} ${GPGERR_LIBS} -o "${h}"
+    AFL_LLVM_INSTRUMENT=CLASSIC AFL_LLVM_LAF_ALL=1 AFL_LLVM_CMPLOG=1 \
+        afl-clang-fast ${CFLAGS_FUZZ} -I build-cmplog/src -I libksba-src/src ${GPGERR_CFLAGS} \
+        "${h}.c" ${CMP_LIBS} ${GPGERR_LIBS} -o "${h}.cmplog"
+    clang ${CFLAGS_FUZZ} -DKSBA_FUZZ_STANDALONE -I build-plain/src -I libksba-src/src ${GPGERR_CFLAGS} \
+        "${h}.c" ${PLAIN_LIBS} ${GPGERR_LIBS} -o "${h}.repro"
+done
+ok "Harnesses built."
+
+if [[ -d corpus/cert && "$(ls corpus/cert 2>/dev/null | wc -l)" -gt 0 ]]; then
+    ok "Corpus present."
+else
+    log "Generating corpus..."
+    bash gen_corpus.sh
+fi
+
+ok "Done. Run: bash start-fuzzers.sh"
diff --git a/fuzzing/libksba/start-fuzzers.sh b/fuzzing/libksba/start-fuzzers.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+# start-fuzzers.sh - launch libksba fuzzing in tmux (8 instances across 4 harnesses).
+# Each harness gets a -M master (explore) and a -S secondary driven by its CmpLog
+# binary. Usage: bash start-fuzzers.sh [--rerun|--status]
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${0}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+AFL_BIN="${SCRIPT_DIR}/afl-build/usr/local/bin"
+[[ -d "${AFL_BIN}" ]] && export PATH="${AFL_BIN}:${PATH}"
+
+SESSION="libksba-fuzz"
+OUTPUT="${SCRIPT_DIR}/output"
+CORPUS="${SCRIPT_DIR}/corpus"
+DICT="${SCRIPT_DIR}/dictionaries/asn1.dict"
+
+log() { echo "[*] $*"; }
+ok()  { echo "[+] $*"; }
+die() { echo "[!] $*" >&2; exit 1; }
+
+case "${1:-}" in
+    --status)
+        afl-whatsup "${OUTPUT}" 2>/dev/null || echo "Output: ${OUTPUT}"
+        exit 0 ;;
+    --rerun)
+        tmux kill-session -t "${SESSION}" 2>/dev/null || true ;;
+    "") ;;
+    *) die "Unknown option: ${1}" ;;
+esac
+
+command -v afl-fuzz > /dev/null 2>&1 || die "afl-fuzz not found. Run setup.sh first."
+for h in fuzz_cert fuzz_crl fuzz_ocsp fuzz_cms; do
+    [[ -f "${h}" ]]        || die "${h} not found. Run setup.sh first."
+    [[ -f "${h}.cmplog" ]] || die "${h}.cmplog not found. Run setup.sh first."
+done
+tmux list-sessions 2>/dev/null | grep -q "${SESSION}" && \
+    die "Session '${SESSION}' already running. Use --rerun."
+
+sudo sysctl -w kernel.core_pattern=core > /dev/null 2>&1 || true
+echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor > /dev/null 2>&1 || true
+mkdir -p "${OUTPUT}"
+
+export AFL_SKIP_CPUFREQ=1
+export AFL_AUTORESUME=1
+export AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1
+export AFL_DISABLE_TRIM=1
+export AFL_KEEP_TIMEOUTS=1
+export ASAN_OPTIONS="abort_on_error=1:symbolize=0:detect_leaks=0:allocator_may_return_null=1:detect_stack_use_after_scope=1"
+export UBSAN_OPTIONS="print_stacktrace=0:halt_on_error=1"
+
+# role: -M/-S flag; kind: master|cmplog|sec; target+corpus subdir; schedule
+afl_cmd() {
+    local role="$1" kind="$2" target="$3" corpus_sub="$4" sched="${5:-fast}"
+    local cmplog=""
+    [[ "${kind}" == "cmplog" ]] && cmplog="-c ./${target}.cmplog -l 2AT"
+    echo "afl-fuzz ${role} -i '${CORPUS}/${corpus_sub}' -o '${OUTPUT}' -x '${DICT}' -t 1000 -p ${sched} -m none ${cmplog} -- './${target}'"
+}
+
+log "Starting session '${SESSION}'..."
+tmux new-session  -d -s "${SESSION}" -n "cert+crl" -x 220 -y 50
+tmux split-window -t "${SESSION}:0"   -h
+tmux split-window -t "${SESSION}:0.0" -v
+tmux split-window -t "${SESSION}:0.2" -v
+tmux new-window   -t "${SESSION}" -n "ocsp+cms"
+tmux split-window -t "${SESSION}:1"   -h
+tmux split-window -t "${SESSION}:1.0" -v
+tmux split-window -t "${SESSION}:1.2" -v
+tmux new-window   -t "${SESSION}" -n "status"
+sleep 0.5
+
+send() { tmux send-keys -t "${SESSION}:${1}.${2}" "${3}" Enter; }
+
+send 0 0 "$(afl_cmd '-M master_cert' master fuzz_cert cert explore)"
+send 0 1 "$(afl_cmd '-S sec_cert'    cmplog fuzz_cert cert coe)"
+send 0 2 "$(afl_cmd '-M master_crl'  master fuzz_crl  crl  explore)"
+send 0 3 "$(afl_cmd '-S sec_crl'     cmplog fuzz_crl  crl  rare)"
+send 1 0 "$(afl_cmd '-M master_ocsp' master fuzz_ocsp ocsp explore)"
+send 1 1 "$(afl_cmd '-S sec_ocsp'    cmplog fuzz_ocsp ocsp coe)"
+send 1 2 "$(afl_cmd '-M master_cms'  master fuzz_cms  cms  explore)"
+send 1 3 "$(afl_cmd '-S sec_cms'     cmplog fuzz_cms  cms  lin)"
+send 2 0 "watch -n 15 'afl-whatsup ${OUTPUT} 2>/dev/null || echo waiting...'"
+
+ok "Session started. Attach: tmux attach -t ${SESSION}"