From 64634d43ffbc6cc0e46c5669faa193e5d6a943a8 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 14 Oct 2026 13:10:42 +0000 Subject: [PATCH 1/2] ocsp: match openssl ocsp client output and exit status Print the certificate status on stdout the way openssl ocsp does: ": good|revoked|unknown" followed by This Update, Next Update and, for revoked certificates, Reason and Revocation Time. Exit 0 for any verified status, including revoked and unknown, and print openssl's "Responder Error" line when the responder returns an error status. Previously the status line went to stderr and an unknown status was reported as an error. The client drives the exchange with the native API: wc_InitOcspRequest and wc_EncodeOcspRequest build the request, EmbedOcspLookup sends it and wolfSSL_CertManagerCheckOCSPResponse verifies the response and returns the matched CertStatus, which supplies the status details. Dates are formatted from wc_GetDateAsCalendarTime. F-12945 --- manpages/wolfssl-ocsp.1 | 7 ++ src/ocsp/clu_ocsp.c | 245 ++++++++++++++++++++++++++++++++++------ tests/ocsp/ocsp-test.py | 84 ++++++++++++-- 3 files changed, 293 insertions(+), 43 deletions(-) diff --git a/manpages/wolfssl-ocsp.1 b/manpages/wolfssl-ocsp.1 index 40d0035b..03842a72 100644 --- a/manpages/wolfssl-ocsp.1 +++ b/manpages/wolfssl-ocsp.1 @@ -49,6 +49,13 @@ both \-cert and \-issuer to complete a check. -nrequest n stop the responder after processing n requests (0 = .br unlimited). +.SH EXIT STATUS +In client mode the exit status is 0 whenever a verified response was +received, whatever status it reports, as with openssl ocsp. The status +(good, revoked or unknown) and its update and revocation times are printed +on standard output in the same format as openssl ocsp. An unreachable +responder, a malformed or unverifiable response, or a responder error +status exits non-zero. .SH EXAMPLES Check a certificate's status against the responder named in its AIA: .RS diff --git a/src/ocsp/clu_ocsp.c b/src/ocsp/clu_ocsp.c index dd807d3c..39f3bf3a 100644 --- a/src/ocsp/clu_ocsp.c +++ b/src/ocsp/clu_ocsp.c @@ -291,13 +291,136 @@ static void wolfCLU_OcspHelp(void) WOLFCLU_LOG(WOLFCLU_L0, " "); } +/* CRLReason names as printed by openssl ocsp */ +static const char* ocspCrlReasonStr(int reason) +{ + switch (reason) { + case WC_CRL_REASON_UNSPECIFIED: + return "unspecified"; + case WC_CRL_REASON_KEY_COMPROMISE: + return "keyCompromise"; + case WC_CRL_REASON_CA_COMPROMISE: + return "cACompromise"; + case WC_CRL_REASON_AFFILIATION_CHANGED: + return "affiliationChanged"; + case WC_CRL_REASON_SUPERSEDED: + return "superseded"; + case WC_CRL_REASON_CESSATION_OF_OPERATION: + return "cessationOfOperation"; + case WC_CRL_REASON_CERTIFICATE_HOLD: + return "certificateHold"; + case WC_CRL_REASON_REMOVE_FROM_CRL: + return "removeFromCRL"; + default: + return "(UNKNOWN)"; + } +} + +/* Print an ASN.1 time in the same form as openssl ocsp */ +static void ocspPrintTime(WOLFSSL_BIO* bio, const char* label, + const byte* date, byte format) +{ + static const char* const month[] = { "Jan", "Feb", "Mar", "Apr", "May", + "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + struct tm t; + + if (wc_GetDateAsCalendarTime(date, MAX_DATE_SIZE, format, &t) != 0 || + t.tm_mon < 0 || t.tm_mon > 11) { + wolfSSL_BIO_printf(bio, "\t%s: Bad time value\n", label); + return; + } + wolfSSL_BIO_printf(bio, "\t%s: %s %2d %02d:%02d:%02d %d GMT\n", label, + month[t.tm_mon], t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, + t.tm_year + 1900); +} + +static WOLFSSL_BIO* ocspStdoutBio(void) +{ + WOLFSSL_BIO* bio = wolfSSL_BIO_new(wolfSSL_BIO_s_file()); + + if (bio != NULL && + wolfSSL_BIO_set_fp(bio, stdout, BIO_NOCLOSE) != WOLFSSL_SUCCESS) { + wolfSSL_BIO_free(bio); + bio = NULL; + } + return bio; +} + +/* Print the status summary like openssl ocsp */ +static int ocspPrintStatus(const char* name, const CertStatus* cs) +{ + WOLFSSL_BIO* bio = ocspStdoutBio(); + + if (bio == NULL) { + return WOLFCLU_FATAL_ERROR; + } + wolfSSL_BIO_printf(bio, "%s: %s\n", name, + wolfSSL_OCSP_cert_status_str(cs->status)); + ocspPrintTime(bio, "This Update", cs->thisDate, cs->thisDateFormat); + if (cs->nextDate[0] != '\0') { + ocspPrintTime(bio, "Next Update", cs->nextDate, cs->nextDateFormat); + } + if (cs->status == CERT_REVOKED) { + wolfSSL_BIO_printf(bio, "\tReason: %s\n", + ocspCrlReasonStr(cs->revocationReason)); + ocspPrintTime(bio, "Revocation Time", cs->revocationDate, + ASN_GENERALIZED_TIME); + } + wolfSSL_BIO_free(bio); + return WOLFCLU_SUCCESS; +} + +/* Print openssl's "Responder Error" line for an OCSP error status. + * Returns 1 when the response was an error status. */ +static int ocspPrintResponderError(const byte* p, int sz) +{ + int idx = 0; + int status; + WOLFSSL_BIO* bio; + + /* OCSPResponse ::= SEQUENCE { responseStatus ENUMERATED, ... } */ + if (p == NULL || sz < 5 || p[idx++] != (ASN_SEQUENCE | ASN_CONSTRUCTED)) { + return 0; + } + if (p[idx] & ASN_LONG_LENGTH) { + idx += (p[idx] & 0x7f) + 1; + } + else { + idx++; + } + if (idx + 3 > sz || p[idx] != ASN_ENUMERATED || p[idx + 1] != 1) { + return 0; + } + status = p[idx + 2]; + if (status == OCSP_SUCCESSFUL) { + return 0; + } + + bio = ocspStdoutBio(); + if (bio != NULL) { + wolfSSL_BIO_printf(bio, "Responder Error: %s (%d)\n", + wolfSSL_OCSP_response_status_str(status), status); + wolfSSL_BIO_free(bio); + } + return 1; +} + static int ocspClient(OcspClientConfig* config) { - WOLFSSL_CERT_MANAGER* cm = NULL; - byte* der = NULL; + WOLFSSL_CERT_MANAGER* cm = NULL; + OcspRequest* req = NULL; + byte* der = NULL; + byte* reqBuf = NULL; + byte* resp = NULL; + const char* url = NULL; + DecodedCert cert; + CertStatus status; int derSz; - int ocspFlags = 0; - int ret = WOLFCLU_SUCCESS; + int reqSz = 0; + int respSz = 0; + int urlSz = 0; + int certInit = 0; + int ret = WOLFCLU_SUCCESS; if (config->cert == NULL) { wolfCLU_LogError("Client mode requires -cert"); @@ -340,52 +463,110 @@ static int ocspClient(OcspClientConfig* config) } } - /* Build OCSP flags */ - if (config->noNonce) - ocspFlags |= WOLFSSL_OCSP_NO_NONCE; - if (config->url != NULL) - ocspFlags |= WOLFSSL_OCSP_URL_OVERRIDE; - - /* Enable OCSP in the CertManager */ + /* Needed by wolfSSL_CertManagerCheckOCSPResponse */ if (ret == WOLFCLU_SUCCESS) { - if (wolfSSL_CertManagerEnableOCSP(cm, ocspFlags) - != WOLFSSL_SUCCESS) { + if (wolfSSL_CertManagerEnableOCSP(cm, 0) != WOLFSSL_SUCCESS) { wolfCLU_LogError("Failed to enable OCSP"); ret = WOLFCLU_FATAL_ERROR; } } - /* Set override URL if provided */ - if (ret == WOLFCLU_SUCCESS && config->url != NULL) { - if (wolfSSL_CertManagerSetOCSPOverrideURL(cm, config->url) - != WOLFSSL_SUCCESS) { - wolfCLU_LogError("Failed to set OCSP override URL"); + /* Parse the certificate against its issuer, as + * wolfSSL_CertManagerCheckOCSP does, to fill in the CertID fields */ + if (ret == WOLFCLU_SUCCESS) { + int err; + + wc_InitDecodedCert(&cert, der, (word32)derSz, HEAP_HINT); + certInit = 1; + err = wc_ParseCert(&cert, CERT_TYPE, VERIFY_OCSP, cm); + if (err != 0) { + wolfCLU_LogError("Failed to verify %s (err %d: %s)", config->cert, + err, wolfSSL_ERR_reason_error_string(err)); + ret = WOLFCLU_FATAL_ERROR; + } + } + + /* Responder URL from -url or the certificate's AIA extension */ + if (ret == WOLFCLU_SUCCESS) { + if (config->url != NULL) { + url = config->url; + urlSz = (int)XSTRLEN(url); + } + else if (cert.extAuthInfo != NULL && cert.extAuthInfoSz > 0) { + url = (const char*)cert.extAuthInfo; + urlSz = cert.extAuthInfoSz; + } + else { + wolfCLU_LogError("No OCSP responder URL in %s, use -url", + config->cert); + ret = WOLFCLU_FATAL_ERROR; + } + } + + if (ret == WOLFCLU_SUCCESS) { + req = wc_OcspRequest_new(HEAP_HINT); + if (req == NULL || wc_InitOcspRequest(req, &cert, + (byte)(config->noNonce == 0), HEAP_HINT) != 0) { + wolfCLU_LogError("Failed to create OCSP request"); + ret = WOLFCLU_FATAL_ERROR; + } + } + + if (ret == WOLFCLU_SUCCESS) { + reqSz = wc_EncodeOcspRequest(req, NULL, 0); + if (reqSz > 0) { + reqBuf = (byte*)XMALLOC((size_t)reqSz, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER); + } + if (reqBuf == NULL || + wc_EncodeOcspRequest(req, reqBuf, (word32)reqSz) != reqSz) { + wolfCLU_LogError("Failed to encode OCSP request"); ret = WOLFCLU_FATAL_ERROR; } } - /* Perform the OCSP check – wolfSSL handles networking and HTTP */ + /* HTTP POST to the responder */ if (ret == WOLFCLU_SUCCESS) { - int ocspRet = wolfSSL_CertManagerCheckOCSP(cm, der, derSz); - if (ocspRet == WOLFSSL_SUCCESS) { - WOLFCLU_LOG(WOLFCLU_L0, "%s: good", config->cert); + respSz = EmbedOcspLookup(NULL, url, urlSz, reqBuf, reqSz, &resp); + if (respSz <= 0 || resp == NULL) { + wolfCLU_LogError("Error querying OCSP responder"); + ret = WOLFCLU_FATAL_ERROR; } - else if (ocspRet == OCSP_CERT_REVOKED) { - /* Certificate is revoked - report it but return success. - * OpenSSL returns exit code 0 for revoked certs because the OCSP - * transaction itself succeeded. The revocation status is in output. */ - WOLFCLU_LOG(WOLFCLU_L0, "%s: revoked", config->cert); - /* ret remains WOLFCLU_SUCCESS */ + } + + if (ret == WOLFCLU_SUCCESS && ocspPrintResponderError(resp, respSz)) { + ret = WOLFCLU_FATAL_ERROR; + } + + /* Verify the response and get the status of the certificate */ + if (ret == WOLFCLU_SUCCESS) { + int ocspRet; + + XMEMSET(&status, 0, sizeof(status)); + ocspRet = wolfSSL_CertManagerCheckOCSPResponse(cm, resp, respSz, NULL, + &status, NULL, req); + if (ocspRet == WOLFSSL_SUCCESS || ocspRet == OCSP_CERT_REVOKED || + ocspRet == OCSP_CERT_UNKNOWN) { + /* Like openssl ocsp, print the status on stdout and exit 0 + * whatever the status is. */ + WOLFCLU_LOG(WOLFCLU_L0, "Response verify OK"); + ret = ocspPrintStatus(config->cert, &status); } else { - /* Other OCSP errors (network, malformed response, etc.) */ - wolfCLU_LogError("OCSP check failed for %s (err %d: %s)", - config->cert, ocspRet, - wolfSSL_ERR_reason_error_string(ocspRet)); + wolfCLU_LogError("Response Verify Failure: %s (%d)", + wolfSSL_ERR_reason_error_string(ocspRet), ocspRet); ret = WOLFCLU_FATAL_ERROR; } } + if (resp != NULL) { + EmbedOcspRespFree(NULL, resp); + } + XFREE(reqBuf, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + wc_OcspRequest_free(req); + if (certInit) { + wc_FreeDecodedCert(&cert); + } wolfSSL_CertManagerFree(cm); XFREE(der, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); return ret; diff --git a/tests/ocsp/ocsp-test.py b/tests/ocsp/ocsp-test.py index 077be417..09cadaf3 100644 --- a/tests/ocsp/ocsp-test.py +++ b/tests/ocsp/ocsp-test.py @@ -32,6 +32,14 @@ "/CN=www.wolfssl.com/emailAddress=info@wolfssl.com\n" ) +# 'E' (expired) is neither valid nor revoked: the wolfssl responder +# answers unknown for it. +INDEX_UNKNOWN = ( + "E\t991231235959Z\t\t01\tunknown\t" + "/C=US/ST=Montana/L=Bozeman/O=wolfSSL/OU=Support" + "/CN=www.wolfssl.com/emailAddress=info@wolfssl.com\n" +) + def _ocsp_supported(binary): """Check if the given binary supports OCSP.""" @@ -171,27 +179,33 @@ def _query(self, cert, extra_args=None): args.extend(extra_args) return _run_client(self.CLIENT_BIN, self.PORT, args) + def _assert_status(self, rc, out, status): + """Like openssl ocsp: exit 0 for any verified status, and print + ': ' followed by the update and revocation times.""" + self.assertEqual(rc, 0, f"expected exit 0 for {status}: {out}") + self.assertIn(f": {status}", out, f"expected ': {status}': {out}") + self.assertIn("This Update:", out, out) + if status == "revoked": + self.assertIn("Revocation Time:", out, out) + # -- Positive tests -- def test_01_basic_check(self): self._start_responder(INDEX_VALID) rc, out = self._query("server-cert.pem") - self.assertEqual(rc, 0, f"basic OCSP check failed: {out}") - self.assertIn("good", out.lower()) + self._assert_status(rc, out, "good") def test_02_no_nonce(self): self._start_responder(INDEX_VALID) rc, out = self._query("server-cert.pem", ["-no_nonce"]) - self.assertEqual(rc, 0, f"no_nonce check failed: {out}") - self.assertIn("good", out.lower()) + self._assert_status(rc, out, "good") # -- Revoked cert -- def test_03_revoked_cert(self): self._start_responder(INDEX_VALID + INDEX_REVOKED) rc, out = self._query("server-revoked-cert.pem") - self.assertRegex(out, re.compile("revoked", re.IGNORECASE), - "expected 'revoked' in output") + self._assert_status(rc, out, "revoked") # -- Missing parameters -- @@ -259,8 +273,7 @@ def test_08_delegated_responder(self): rsigner=os.path.join(CERTS_DIR, "ocsp-responder-cert.pem"), rkey=os.path.join(CERTS_DIR, "ocsp-responder-key.pem")) rc, out = self._query("server-cert.pem") - self.assertEqual(rc, 0, f"delegated responder failed: {out}") - self.assertIn("good", out.lower()) + self._assert_status(rc, out, "good") def test_09_delegated_no_nonce(self): self._start_responder( @@ -268,8 +281,7 @@ def test_09_delegated_no_nonce(self): rsigner=os.path.join(CERTS_DIR, "ocsp-responder-cert.pem"), rkey=os.path.join(CERTS_DIR, "ocsp-responder-key.pem")) rc, out = self._query("server-cert.pem", ["-no_nonce"]) - self.assertEqual(rc, 0, f"delegated no_nonce failed: {out}") - self.assertIn("good", out.lower()) + self._assert_status(rc, out, "good") def test_10_delegated_revoked(self): self._start_responder( @@ -277,7 +289,7 @@ def test_10_delegated_revoked(self): rsigner=os.path.join(CERTS_DIR, "ocsp-responder-cert.pem"), rkey=os.path.join(CERTS_DIR, "ocsp-responder-key.pem")) rc, out = self._query("server-revoked-cert.pem") - self.assertRegex(out, re.compile("revoked", re.IGNORECASE)) + self._assert_status(rc, out, "revoked") # -- Unreachable responder -- @@ -306,6 +318,56 @@ def test_12_graceful_shutdown(self): log = resp.read_log() self.assertIn("wolfssl exiting gracefully", log) + # -- Unknown status -- + + def test_13_unknown_status(self): + if self.RESPONDER_BIN == WOLFSSL_BIN: + index = INDEX_UNKNOWN + else: + # openssl answers unknown for a serial missing from the index + index = INDEX_REVOKED + self._start_responder(index) + rc, out = self._query("server-cert.pem") + self._assert_status(rc, out, "unknown") + + # -- Responder error status -- + + def test_14_responder_error(self): + if self.RESPONDER_BIN != WOLFSSL_BIN: + self.skipTest("openssl answers unknown for an unknown issuer") + # ca-ecc is not served by the responder: it replies unauthorized + self._start_responder(INDEX_VALID) + cmd = [ + self.CLIENT_BIN, "ocsp", + "-issuer", os.path.join(CERTS_DIR, "ca-ecc-cert.pem"), + "-CAfile", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-url", f"http://127.0.0.1:{self.PORT}", + "-cert", os.path.join(CERTS_DIR, "server-ecc.pem"), + ] + r = subprocess.run(cmd, capture_output=True, text=True, + stdin=subprocess.DEVNULL, timeout=30) + self.assertNotEqual(r.returncode, 0, r.stdout + r.stderr) + self.assertIn("Responder Error: unauthorized", r.stdout, + r.stdout + r.stderr) + + # -- Output streams -- + + def test_15_status_on_stdout(self): + self._start_responder(INDEX_VALID) + cmd = [ + self.CLIENT_BIN, "ocsp", + "-issuer", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-CAfile", os.path.join(CERTS_DIR, "ca-cert.pem"), + "-url", f"http://127.0.0.1:{self.PORT}", + "-cert", os.path.join(CERTS_DIR, "server-cert.pem"), + ] + r = subprocess.run(cmd, capture_output=True, text=True, + stdin=subprocess.DEVNULL, timeout=30) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + self.assertIn(": good", r.stdout, r.stdout) + self.assertIn("This Update:", r.stdout, r.stdout) + self.assertIn("Response verify OK", r.stderr, r.stderr) + # Concrete test classes for each client/responder combination. # Each gets a dynamically assigned port in setUpClass to avoid conflicts. From 7093a36653e44a684fa5468bbe11538d345c6435 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Thu, 15 Oct 2026 05:28:24 +0000 Subject: [PATCH 2/2] Address review: map OCSP status strings locally, add missing CRL reasons --- src/ocsp/clu_ocsp.c | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/ocsp/clu_ocsp.c b/src/ocsp/clu_ocsp.c index 39f3bf3a..a2b7490e 100644 --- a/src/ocsp/clu_ocsp.c +++ b/src/ocsp/clu_ocsp.c @@ -311,6 +311,46 @@ static const char* ocspCrlReasonStr(int reason) return "certificateHold"; case WC_CRL_REASON_REMOVE_FROM_CRL: return "removeFromCRL"; + case WC_CRL_REASON_PRIVILEGE_WITHDRAWN: + return "privilegeWithdrawn"; + case WC_CRL_REASON_AA_COMPROMISE: + return "aACompromise"; + default: + return "(UNKNOWN)"; + } +} + +/* Certificate status names as printed by openssl ocsp */ +static const char* ocspCertStatusStr(int status) +{ + switch (status) { + case CERT_GOOD: + return "good"; + case CERT_REVOKED: + return "revoked"; + case CERT_UNKNOWN: + return "unknown"; + default: + return "(UNKNOWN)"; + } +} + +/* OCSP response status names as printed by openssl ocsp */ +static const char* ocspRespStatusStr(int status) +{ + switch (status) { + case OCSP_SUCCESSFUL: + return "successful"; + case OCSP_MALFORMED_REQUEST: + return "malformedrequest"; + case OCSP_INTERNAL_ERROR: + return "internalerror"; + case OCSP_TRY_LATER: + return "trylater"; + case OCSP_SIG_REQUIRED: + return "sigrequired"; + case OCSP_UNAUTHORIZED: + return "unauthorized"; default: return "(UNKNOWN)"; } @@ -355,7 +395,7 @@ static int ocspPrintStatus(const char* name, const CertStatus* cs) return WOLFCLU_FATAL_ERROR; } wolfSSL_BIO_printf(bio, "%s: %s\n", name, - wolfSSL_OCSP_cert_status_str(cs->status)); + ocspCertStatusStr(cs->status)); ocspPrintTime(bio, "This Update", cs->thisDate, cs->thisDateFormat); if (cs->nextDate[0] != '\0') { ocspPrintTime(bio, "Next Update", cs->nextDate, cs->nextDateFormat); @@ -399,7 +439,7 @@ static int ocspPrintResponderError(const byte* p, int sz) bio = ocspStdoutBio(); if (bio != NULL) { wolfSSL_BIO_printf(bio, "Responder Error: %s (%d)\n", - wolfSSL_OCSP_response_status_str(status), status); + ocspRespStatusStr(status), status); wolfSSL_BIO_free(bio); } return 1;