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..a2b7490e 100644 --- a/src/ocsp/clu_ocsp.c +++ b/src/ocsp/clu_ocsp.c @@ -291,13 +291,176 @@ 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"; + 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)"; + } +} + +/* 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, + ocspCertStatusStr(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", + ocspRespStatusStr(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 +503,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.