mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-10-24 04:09:07 +03:00
The ClientHello builder wrote the renegotiation_info extension type as a conditional whose two branches were the same constant, which is a constant expression some -Wextra builds reject and which hid what 0xFF01 is. It is TLSX_RENEGOTIATION_INFO; use that. The CRL and OCSP skip stubs are guarded on their feature macro plus certs plus not-WOLFCRYPT_ONLY. Their printed messages already said so; the #else comments still named only the feature macro, so a reader chasing a skip saw the wrong reason. No coverage change: tls_core 810/1748 and revocation 28/47 + 31/48 both re-measured identical, gates green.
662 lines
26 KiB
C
662 lines
26 KiB
C
/* test_ocsp_whitebox.c -- MC/DC white-box driver for src/ocsp.c
|
|
*
|
|
* Copyright (C) 2006-2026 wolfSSL Inc.
|
|
*
|
|
* This file is part of wolfSSL.
|
|
*
|
|
* wolfSSL is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* wolfSSL is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
/* WHY A WHITE-BOX FOR THIS FILE.
|
|
*
|
|
* src/ocsp.c measured 7 of 47 conditions at intake. The `ocsp` group runs 3 of
|
|
* its 8 tests on the campaign option list -- the rest are gated on the OpenSSL
|
|
* compat layer this option list excludes as a build fact -- so most of the file
|
|
* is never entered from tests/api at all.
|
|
*
|
|
* The conditions that remain are lookup-table comparisons inside file-static
|
|
* helpers: matching a cached OCSP entry by issuer hash, and matching a status
|
|
* by serial. A caller coming through the public API always presents a
|
|
* consistent (issuerHash, serial) pair derived from a real DecodedCert, so the
|
|
* "same length, different content" and "different hash" cases -- exactly the
|
|
* ones an attacker controls -- have no independence pair from outside.
|
|
*
|
|
* Rules, same as the sibling drivers:
|
|
* - options.h FIRST, before any other wolfSSL header, or the smoke build
|
|
* compiles this with the feature macros undefined and it silently becomes
|
|
* a no-op that still exits 0.
|
|
* - main() ALWAYS returns 0; a non-zero exit discards the whole variant.
|
|
* - Every rejecting vector has its accepting partner in THIS binary.
|
|
* - Bail paths print, so "covered nothing" is distinguishable from
|
|
* "had nothing to say".
|
|
*/
|
|
|
|
#include <wolfssl/options.h>
|
|
|
|
#include <src/ocsp.c>
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#if defined(HAVE_OCSP) && !defined(WOLFCRYPT_ONLY) && !defined(NO_CERTS)
|
|
|
|
static int g_checks;
|
|
#define WB_NOTE(what) do { g_checks++; (void)(what); } while (0)
|
|
|
|
/* ------------------------------------------------ FindStatus / entry :243 */
|
|
/* `if (XMEMCMP((*entry)->issuerHash, request->issuerHash, ...) == 0 && ...)`
|
|
*
|
|
* Both operands need a pair, and the second is only reachable when the first
|
|
* matches. Three vectors: issuer hash differing (operand 0 false), issuer hash
|
|
* matching with the second discriminator differing (operand 0 true, operand 1
|
|
* false), and both matching (the accepting partner).
|
|
*
|
|
* The public path builds request and entry from the same certificate, so
|
|
* outside this binary the two hashes are equal by construction and operand 0
|
|
* has no false case at all. */
|
|
/* GetOcspEntry() prepends a freshly allocated OcspEntry when it finds no
|
|
* match, making it the new head. The driver then re-points ocspList at its own
|
|
* stack node for the next vector, which drops that pointer on the floor. Free
|
|
* every heap node ahead of the stack node before doing so. */
|
|
static void wb_free_prepended(WOLFSSL_OCSP* ocsp, OcspEntry* stackNode)
|
|
{
|
|
void* heap = (ocsp->cm != NULL) ? ocsp->cm->heap : NULL;
|
|
OcspEntry* e = ocsp->ocspList;
|
|
|
|
/* Same heap derivation FreeOcspEntry's own caller in src/ocsp.c uses. */
|
|
while (e != NULL && e != stackNode) {
|
|
OcspEntry* next = e->next;
|
|
|
|
FreeOcspEntry(e, heap);
|
|
XFREE(e, heap, DYNAMIC_TYPE_OCSP_ENTRY);
|
|
e = next;
|
|
}
|
|
ocsp->ocspList = stackNode;
|
|
}
|
|
|
|
static void wb_entry_match(WOLFSSL_OCSP* ocsp)
|
|
{
|
|
OcspRequest req;
|
|
OcspEntry seeded;
|
|
OcspEntry* found = NULL;
|
|
|
|
/* Seed the cache with one entry so the loop body executes. GetOcspEntry
|
|
* walks ocsp->ocspList and compares each node against the request; with an
|
|
* empty list the loop never runs and the comparison is never evaluated,
|
|
* which is exactly why driving this from the public API proves nothing. */
|
|
XMEMSET(&seeded, 0, sizeof(seeded));
|
|
XMEMSET(seeded.issuerHash, 0xAA, OCSP_DIGEST_SIZE);
|
|
XMEMSET(seeded.issuerKeyHash, 0xCC, OCSP_DIGEST_SIZE);
|
|
seeded.next = NULL;
|
|
ocsp->ocspList = &seeded;
|
|
|
|
/* Vector 1: issuer hash differs -> operand 0 false, operand 1 not reached.
|
|
* Pairs with vector 3. */
|
|
XMEMSET(&req, 0, sizeof(req));
|
|
XMEMSET(req.issuerHash, 0xBB, OCSP_DIGEST_SIZE);
|
|
XMEMSET(req.issuerKeyHash, 0xCC, OCSP_DIGEST_SIZE);
|
|
WB_NOTE(GetOcspEntry(ocsp, &req, &found));
|
|
/* No match, so GetOcspEntry PREPENDED a heap entry and made it the head.
|
|
* Free it here: the next vector overwrites ocspList, which would otherwise
|
|
* lose the pointer, and the teardown sets the list to NULL rather than
|
|
* walking it (it cannot walk it -- the seeded node is on the stack). */
|
|
wb_free_prepended(ocsp, &seeded);
|
|
|
|
/* Vector 2: issuer hash equal, key hash differs -> operand 0 true,
|
|
* operand 1 false. Pairs with vector 3 on operand 1. */
|
|
ocsp->ocspList = &seeded;
|
|
seeded.next = NULL;
|
|
XMEMSET(req.issuerHash, 0xAA, OCSP_DIGEST_SIZE);
|
|
XMEMSET(req.issuerKeyHash, 0xDD, OCSP_DIGEST_SIZE);
|
|
found = NULL;
|
|
WB_NOTE(GetOcspEntry(ocsp, &req, &found));
|
|
wb_free_prepended(ocsp, &seeded);
|
|
|
|
/* Vector 3: both equal -> the accepting partner that completes both pairs. */
|
|
ocsp->ocspList = &seeded;
|
|
seeded.next = NULL;
|
|
XMEMSET(req.issuerKeyHash, 0xCC, OCSP_DIGEST_SIZE);
|
|
found = NULL;
|
|
WB_NOTE(GetOcspEntry(ocsp, &req, &found));
|
|
|
|
/* Vector 3 matched, so nothing was prepended; the head is still the stack
|
|
* node. Detach it before the CertManager frees the list, or the teardown
|
|
* walks into this frame. */
|
|
ocsp->ocspList = NULL;
|
|
}
|
|
|
|
|
|
/* ---------------------------------------------- CheckOcspResponder :625-644 */
|
|
/* `if (bs == NULL || subjectNameHash == NULL || issuerNameHash == NULL)` and
|
|
* the two responder-identity chains
|
|
* subjectKeyHash != NULL && XMEMCMP(subjectNameHash, single->issuerHash)
|
|
* && XMEMCMP(subjectKeyHash, single->issuerKeyHash)
|
|
* issuerKeyHash != NULL && XMEMCMP(issuerNameHash, ...) && ...
|
|
*
|
|
* This one is genuinely hermetic: it takes an OcspResponse and four raw
|
|
* hashes, walks bs->single, and compares bytes. Nothing is stored, so the
|
|
* response can be a local -- unlike GetOcspEntry, which links what it is
|
|
* given into ocsp->ocspList, a list the library allocates and frees. A stack
|
|
* fixture there crashed; here there is no ownership at all.
|
|
*
|
|
* Each vector below breaks the chain at a DIFFERENT operand, and the last one
|
|
* matches on every field so the earlier ones have an accepting partner.
|
|
* A real response is always self-consistent, which is why the mismatch cases
|
|
* have no independence pair from outside. */
|
|
static void wb_check_responder(void)
|
|
{
|
|
OcspResponse bs;
|
|
OcspEntry single;
|
|
byte subjName[OCSP_DIGEST_SIZE];
|
|
byte issuName[OCSP_DIGEST_SIZE];
|
|
byte subjKey[KEYID_SIZE];
|
|
byte issuKey[KEYID_SIZE];
|
|
byte other[OCSP_DIGEST_SIZE];
|
|
|
|
XMEMSET(&bs, 0, sizeof(bs));
|
|
XMEMSET(&single, 0, sizeof(single));
|
|
XMEMSET(subjName, 0x11, sizeof(subjName));
|
|
XMEMSET(issuName, 0x22, sizeof(issuName));
|
|
XMEMSET(subjKey, 0x33, sizeof(subjKey));
|
|
XMEMSET(issuKey, 0x44, sizeof(issuKey));
|
|
XMEMSET(other, 0x99, sizeof(other));
|
|
|
|
/* the response's single entry is signed by the subject */
|
|
XMEMCPY(single.issuerHash, subjName, OCSP_DIGEST_SIZE);
|
|
XMEMCPY(single.issuerKeyHash, subjKey, KEYID_SIZE);
|
|
single.next = NULL;
|
|
bs.single = &single;
|
|
|
|
/* :625, one vector per operand */
|
|
WB_NOTE(CheckOcspResponder(NULL, subjName, subjKey, 0, issuName, issuKey));
|
|
WB_NOTE(CheckOcspResponder(&bs, NULL, subjKey, 0, issuName, issuKey));
|
|
WB_NOTE(CheckOcspResponder(&bs, subjName, subjKey, 0, NULL, issuKey));
|
|
|
|
/* :631 operand 0 false -- no subject key hash offered */
|
|
WB_NOTE(CheckOcspResponder(&bs, subjName, NULL, 0, issuName, issuKey));
|
|
/* :631 operand 1 false -- subject name does not match the single entry */
|
|
WB_NOTE(CheckOcspResponder(&bs, other, subjKey, 0, issuName, issuKey));
|
|
/* :631 operand 2 false -- name matches, key does not */
|
|
WB_NOTE(CheckOcspResponder(&bs, subjName, other, 0, issuName, issuKey));
|
|
/* :631 all true -- the accepting partner for the three above */
|
|
WB_NOTE(CheckOcspResponder(&bs, subjName, subjKey, 0, issuName, issuKey));
|
|
|
|
/* the delegated-responder arm: reached only when the subject chain fails
|
|
* AND the OCSP-signing usage bit is set. Re-point the single entry at the
|
|
* issuer so that chain can succeed. */
|
|
XMEMCPY(single.issuerHash, issuName, OCSP_DIGEST_SIZE);
|
|
XMEMCPY(single.issuerKeyHash, issuKey, KEYID_SIZE);
|
|
|
|
/* :640 operand 0 false -- no issuer key hash offered */
|
|
WB_NOTE(CheckOcspResponder(&bs, subjName, subjKey, EXTKEYUSE_OCSP_SIGN,
|
|
issuName, NULL));
|
|
/* :640 operand 2 false -- issuer name matches, key does not */
|
|
WB_NOTE(CheckOcspResponder(&bs, subjName, subjKey, EXTKEYUSE_OCSP_SIGN,
|
|
issuName, other));
|
|
/* :640 all true -- accepting partner */
|
|
WB_NOTE(CheckOcspResponder(&bs, subjName, subjKey, EXTKEYUSE_OCSP_SIGN,
|
|
issuName, issuKey));
|
|
|
|
bs.single = NULL;
|
|
}
|
|
|
|
|
|
/* --------------------------------------------------- CheckOcspRequest :484+
|
|
|
|
* This is where an OCSP check leaves the library: pick a responder URL, hand
|
|
* the encoded request to the application's transport callback, and interpret
|
|
* whatever comes back. From tests/api it is entered only when a real responder
|
|
* is configured, which no unit test does -- so the URL selection, the callback
|
|
* result handling and the response-free hook are all uncovered.
|
|
*
|
|
* None of it needs a network. CbOCSPIO is
|
|
* int (*)(void* ctx, const char* url, int urlSz,
|
|
* unsigned char** response)
|
|
* so a callback that hands back a static buffer is a complete responder for
|
|
* the purposes of this function, and it can return the results a real one
|
|
* cannot be made to produce on demand: a negative error, a zero-length body,
|
|
* a NULL response with a positive length. That is the point of mocking the
|
|
* consumed interface rather than the transport.
|
|
*/
|
|
|
|
static byte g_ocspResp[64];
|
|
static int g_ioResult; /* what the mock returns */
|
|
static int g_ioNullResponse; /* return a positive size but no buffer */
|
|
static int g_ioCalls;
|
|
static int g_freeCalls;
|
|
|
|
static int wb_ocsp_io(void* ctx, const char* url, int urlSz,
|
|
unsigned char* request, int requestSz,
|
|
unsigned char** response)
|
|
{
|
|
(void)ctx; (void)url; (void)urlSz; (void)request; (void)requestSz;
|
|
g_ioCalls++;
|
|
if (response != NULL)
|
|
*response = g_ioNullResponse ? NULL : g_ocspResp;
|
|
return g_ioResult;
|
|
}
|
|
|
|
static void wb_ocsp_respfree(void* ctx, unsigned char* response)
|
|
{
|
|
(void)ctx; (void)response;
|
|
g_freeCalls++;
|
|
}
|
|
|
|
static void wb_check_request(WOLFSSL_CERT_MANAGER* cm)
|
|
{
|
|
OcspRequest req;
|
|
byte serial[8];
|
|
byte url[] = "http://ocsp.example.com/";
|
|
size_t i;
|
|
|
|
/* the two operands of the argument guard at :504 */
|
|
WB_NOTE(CheckOcspRequest(NULL, NULL, NULL, NULL));
|
|
WB_NOTE(CheckOcspRequest(cm->ocsp, NULL, NULL, NULL));
|
|
|
|
XMEMSET(g_ocspResp, 0, sizeof(g_ocspResp));
|
|
g_ocspResp[0] = 0x30;
|
|
g_ocspResp[1] = 0x02;
|
|
|
|
/* Each row is one operand of the URL-selection, callback-result and
|
|
* response-free decisions. `haveUrl` drives
|
|
* `ocspRequest->urlSz != 0 && ocspRequest->url != NULL`, which a request
|
|
* built from a certificate's AIA extension always satisfies. */
|
|
{
|
|
static const struct { int haveUrl; int urlSz; int ioResult;
|
|
int nullResp; int installIo; int installFree;
|
|
const char* what; } rows[] = {
|
|
{ 1, 24, 8, 0, 1, 1, "url and a responder that answers" },
|
|
{ 1, 24, 8, 0, 1, 0, "answered, no free callback" },
|
|
{ 1, 24, 0, 0, 1, 1, "responder returns zero bytes" },
|
|
{ 1, 24, -1, 0, 1, 1, "responder returns an error" },
|
|
{ 1, 24, 8, 1, 1, 1, "positive length, NULL buffer" },
|
|
{ 1, 24, 8, 0, 0, 0, "no responder callback installed" },
|
|
{ 1, 0, 8, 0, 1, 1, "url present, length zero" },
|
|
{ 0, 24, 8, 0, 1, 1, "length present, url NULL" },
|
|
{ 0, 0, 8, 0, 1, 1, "no url at all" },
|
|
};
|
|
|
|
for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) {
|
|
XMEMSET(&req, 0, sizeof(req));
|
|
XMEMSET(serial, 0x5A, sizeof(serial));
|
|
req.serial = serial;
|
|
req.serialSz = (int)sizeof(serial);
|
|
XMEMSET(req.issuerHash, 0xAA, OCSP_DIGEST_SIZE);
|
|
XMEMSET(req.issuerKeyHash, 0xCC, OCSP_DIGEST_SIZE);
|
|
req.url = rows[i].haveUrl ? url : NULL;
|
|
req.urlSz = rows[i].urlSz;
|
|
|
|
g_ioResult = rows[i].ioResult;
|
|
g_ioNullResponse = rows[i].nullResp;
|
|
(void)wolfSSL_CertManagerSetOCSP_Cb(cm,
|
|
rows[i].installIo ? wb_ocsp_io : NULL,
|
|
rows[i].installFree ? wb_ocsp_respfree : NULL,
|
|
NULL);
|
|
|
|
WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, NULL));
|
|
|
|
/* GetOcspEntry appends a heap entry when it finds no match; drop
|
|
* the list between rows so each row starts from an empty cache
|
|
* and the "found" and "not found" arms both get exercised. */
|
|
}
|
|
}
|
|
|
|
/* `if (cm != NULL && cm->ocspFailIfNotSupported)` at :596 -- the policy
|
|
* flag that decides whether an unreachable responder is fatal. */
|
|
cm->ocspFailIfNotSupported = 1;
|
|
XMEMSET(&req, 0, sizeof(req));
|
|
req.url = url; req.urlSz = 24;
|
|
XMEMSET(req.issuerHash, 0xAB, OCSP_DIGEST_SIZE);
|
|
g_ioResult = -1;
|
|
(void)wolfSSL_CertManagerSetOCSP_Cb(cm, wb_ocsp_io, wb_ocsp_respfree, NULL);
|
|
WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, NULL));
|
|
cm->ocspFailIfNotSupported = 0;
|
|
WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, NULL));
|
|
|
|
(void)wolfSSL_CertManagerSetOCSP_Cb(cm, NULL, NULL, NULL);
|
|
}
|
|
|
|
/* ------------------------------------------------------ CheckOcspResponse */
|
|
/* `if (newStatus == NULL || newSingle == NULL || ocspResponse == NULL)` and
|
|
* the multi-response and response-buffer arms. The bytes are the interesting
|
|
* input: a caller cannot ask a real responder for a truncated DER. */
|
|
static void wb_check_response(WOLFSSL_CERT_MANAGER* cm)
|
|
{
|
|
OcspRequest req;
|
|
byte junk[16];
|
|
size_t i;
|
|
|
|
static const struct { int sz; const char* what; } rows[] = {
|
|
{ 0, "zero-length response" },
|
|
{ 1, "one byte" },
|
|
{ 16, "sixteen bytes of nothing" },
|
|
{ -1, "negative length" },
|
|
};
|
|
|
|
XMEMSET(junk, 0x30, sizeof(junk));
|
|
for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) {
|
|
XMEMSET(&req, 0, sizeof(req));
|
|
XMEMSET(req.issuerHash, 0xAA, OCSP_DIGEST_SIZE);
|
|
req.serialSz = 4;
|
|
WB_NOTE(CheckOcspResponse(cm->ocsp, junk, rows[i].sz, NULL, NULL,
|
|
NULL, &req, NULL, NULL));
|
|
}
|
|
WB_NOTE(CheckOcspResponse(cm->ocsp, NULL, 0, NULL, NULL, NULL, NULL, NULL,
|
|
NULL));
|
|
}
|
|
|
|
|
|
/* --------------------------------------------------------- FreeOcspEntry
|
|
|
|
* `if (entry == NULL || !entry->ownStatus)` -- the teardown path only ever
|
|
* reaches this with a real entry that owns its status list, because that is
|
|
* the only kind the parser builds. An entry whose status list is borrowed
|
|
* (ownStatus == 0) is what the multi-response path produces, and freeing one
|
|
* must be a no-op rather than a double free.
|
|
*
|
|
* Both vectors are stack objects and neither is linked into ocsp->ocspList,
|
|
* so nothing here can be reached by the CertManager teardown. */
|
|
static void wb_free_ocsp_entry(WOLFSSL_OCSP* ocsp)
|
|
{
|
|
void* heap = (ocsp->cm != NULL) ? ocsp->cm->heap : NULL;
|
|
OcspEntry borrowed;
|
|
|
|
/* operand 0: no entry at all */
|
|
FreeOcspEntry(NULL, heap);
|
|
g_checks++;
|
|
|
|
/* operand 1: an entry that does not own its status list. Left empty, so
|
|
* the early return is the whole behaviour under test. */
|
|
XMEMSET(&borrowed, 0, sizeof(borrowed));
|
|
borrowed.ownStatus = 0;
|
|
borrowed.status = NULL;
|
|
borrowed.next = NULL;
|
|
FreeOcspEntry(&borrowed, heap);
|
|
g_checks++;
|
|
|
|
/* the accepting partner: owns its (empty) list, so the loop is entered
|
|
* and finds nothing to free */
|
|
XMEMSET(&borrowed, 0, sizeof(borrowed));
|
|
borrowed.ownStatus = 1;
|
|
borrowed.status = NULL;
|
|
borrowed.next = NULL;
|
|
FreeOcspEntry(&borrowed, heap);
|
|
g_checks++;
|
|
}
|
|
|
|
/* ------------------------------------------- CheckOcspRequest ioCtx :527
|
|
|
|
* `ioCtx = (ssl && ssl->ocspIOCtx != NULL) ? ssl->ocspIOCtx : ocsp->cm->ocspIOCtx`
|
|
*
|
|
* Both operands. Every in-tree caller either passes no ssl at all or passes
|
|
* one whose ocspIOCtx was set at configuration time, so "an ssl with no
|
|
* per-connection IO context" -- the case that falls back to the manager's --
|
|
* is not produced by any existing test. */
|
|
static void wb_check_request_ioctx(WOLFSSL_CERT_MANAGER* cm)
|
|
{
|
|
WOLFSSL_CTX* ctx = NULL;
|
|
WOLFSSL* ssl = NULL;
|
|
OcspRequest req;
|
|
byte serial[8];
|
|
byte url[] = "http://ocsp.example.com/";
|
|
static byte ioCtxMarker;
|
|
|
|
ctx = wolfSSL_CTX_new(wolfSSLv23_client_method());
|
|
if (ctx == NULL)
|
|
return;
|
|
ssl = wolfSSL_new(ctx);
|
|
|
|
XMEMSET(&req, 0, sizeof(req));
|
|
XMEMSET(serial, 0x5A, sizeof(serial));
|
|
req.serial = serial;
|
|
req.serialSz = (int)sizeof(serial);
|
|
XMEMSET(req.issuerHash, 0xA1, OCSP_DIGEST_SIZE);
|
|
req.url = url;
|
|
req.urlSz = (int)sizeof(url) - 1;
|
|
|
|
/* operand 0 false: no ssl, so the manager's context is used */
|
|
WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, NULL));
|
|
|
|
if (ssl != NULL) {
|
|
/* operand 0 true, operand 1 false: an ssl with no per-connection
|
|
* IO context -- falls back to the manager's */
|
|
ssl->ocspIOCtx = NULL;
|
|
XMEMSET(req.issuerHash, 0xA2, OCSP_DIGEST_SIZE);
|
|
WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, ssl));
|
|
|
|
/* both true: the connection's own context wins */
|
|
ssl->ocspIOCtx = &ioCtxMarker;
|
|
XMEMSET(req.issuerHash, 0xA3, OCSP_DIGEST_SIZE);
|
|
WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, ssl));
|
|
ssl->ocspIOCtx = NULL;
|
|
}
|
|
|
|
wolfSSL_free(ssl);
|
|
wolfSSL_CTX_free(ctx);
|
|
}
|
|
|
|
|
|
/* --------------------------------------------------------- GetOcspStatus
|
|
|
|
* The cache lookup: walk this entry's status list for one matching the
|
|
* request's serial, then decide whether the cached answer is still usable.
|
|
*
|
|
* (*status)->serialSz == request->serialSz && !XMEMCMP(serial, ...)
|
|
* responseBuffer && *status && !(*status)->rawOcspResponse
|
|
* XVALIDATE_DATE(thisDate) && nextDate[0] != 0 && XVALIDATE_DATE(nextDate)
|
|
*
|
|
* A cache populated by the parser always holds self-consistent entries, so
|
|
* "same length, different serial", "cached but with no stored response body"
|
|
* and "cached with a date that no longer validates" are states the library
|
|
* produces only after time passes or a responder misbehaves. Built by hand
|
|
* here.
|
|
*
|
|
* Ownership: GetOcspStatus READS entry->status and writes *status; it stores
|
|
* nothing, so stack objects are correct. The entry is never linked into
|
|
* ocsp->ocspList, so teardown cannot reach this frame. */
|
|
static void wb_get_ocsp_status(WOLFSSL_OCSP* ocsp)
|
|
{
|
|
OcspRequest req;
|
|
OcspEntry entry;
|
|
CertStatus st;
|
|
CertStatus* found = NULL;
|
|
buffer respBuf;
|
|
byte serial[8];
|
|
byte raw[4];
|
|
size_t i;
|
|
|
|
static const struct { int stSz; byte stByte; int reqSz; byte reqByte;
|
|
int haveRaw; int haveBuf; int thisFmt;
|
|
int nextSet; const char* what; } rows[] = {
|
|
/* serial match/mismatch by length and by content */
|
|
{ 8, 0xA1, 8, 0xA1, 1, 1, ASN_UTC_TIME, 1, "match, cached, dated" },
|
|
{ 8, 0xA1, 8, 0xB2, 1, 1, ASN_UTC_TIME, 1, "same length, other serial" },
|
|
{ 4, 0xA1, 8, 0xA1, 1, 1, ASN_UTC_TIME, 1, "shorter cached serial" },
|
|
{ 8, 0xA1, 4, 0xA1, 1, 1, ASN_UTC_TIME, 1, "shorter request serial" },
|
|
/* cached entry with no stored response body: forces a refetch */
|
|
{ 8, 0xA1, 8, 0xA1, 0, 1, ASN_UTC_TIME, 1, "no raw response, buffer" },
|
|
{ 8, 0xA1, 8, 0xA1, 0, 0, ASN_UTC_TIME, 1, "no raw response, no buffer" },
|
|
{ 8, 0xA1, 8, 0xA1, 1, 0, ASN_UTC_TIME, 1, "raw response, no buffer" },
|
|
/* date shapes: unset format, and a nextDate that was never written */
|
|
{ 8, 0xA1, 8, 0xA1, 1, 1, 0, 1, "no date format" },
|
|
{ 8, 0xA1, 8, 0xA1, 1, 1, ASN_UTC_TIME, 0, "no nextDate" },
|
|
{ 8, 0xA1, 8, 0xA1, 1, 1, ASN_GENERALIZED_TIME, 1, "generalized time" },
|
|
};
|
|
|
|
XMEMSET(raw, 0x30, sizeof(raw));
|
|
|
|
for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) {
|
|
XMEMSET(&req, 0, sizeof(req));
|
|
XMEMSET(&entry, 0, sizeof(entry));
|
|
XMEMSET(&st, 0, sizeof(st));
|
|
XMEMSET(&respBuf, 0, sizeof(respBuf));
|
|
XMEMSET(serial, rows[i].reqByte, sizeof(serial));
|
|
|
|
st.serialSz = rows[i].stSz;
|
|
XMEMSET(st.serial, rows[i].stByte, sizeof(st.serial));
|
|
st.rawOcspResponse = rows[i].haveRaw ? raw : NULL;
|
|
st.rawOcspResponseSz = rows[i].haveRaw ? (word32)sizeof(raw) : 0;
|
|
st.thisDateFormat = (byte)rows[i].thisFmt;
|
|
st.nextDateFormat = (byte)rows[i].thisFmt;
|
|
if (rows[i].nextSet)
|
|
st.nextDate[0] = 0x30;
|
|
st.next = NULL;
|
|
entry.status = &st;
|
|
entry.next = NULL;
|
|
|
|
req.serial = serial;
|
|
req.serialSz = rows[i].reqSz;
|
|
|
|
found = NULL;
|
|
WB_NOTE(GetOcspStatus(ocsp, &req, &entry, &found,
|
|
rows[i].haveBuf ? &respBuf : NULL, NULL));
|
|
XFREE(respBuf.buffer, NULL, DYNAMIC_TYPE_TMP_BUFFER);
|
|
respBuf.buffer = NULL;
|
|
}
|
|
|
|
/* an entry whose status list is empty: the loop body never runs */
|
|
XMEMSET(&req, 0, sizeof(req));
|
|
XMEMSET(&entry, 0, sizeof(entry));
|
|
req.serial = serial;
|
|
req.serialSz = (int)sizeof(serial);
|
|
found = NULL;
|
|
WB_NOTE(GetOcspStatus(ocsp, &req, &entry, &found, NULL, NULL));
|
|
}
|
|
|
|
/* --------------------------------- CheckOcspRequest: responder replies
|
|
|
|
* The remaining guards read what the responder callback gave back:
|
|
*
|
|
* url != NULL && url[0] != '\0' a URL that is present but empty
|
|
* requestSz > 0 && ocsp->cm->ocspIOCb no transport installed
|
|
* responseSz >= 0 && response a size with no buffer, and back
|
|
* response != NULL && ocsp->cm->ocspRespFreeCb no free hook installed
|
|
*
|
|
* A real responder cannot be asked for "a positive length and a NULL buffer",
|
|
* nor for the two negative sentinels the caller maps onto WANT_READ and
|
|
* HTTP_TIMEOUT. The mock returns whatever the vector chose, which is the
|
|
* whole point. */
|
|
static byte g_replyBody[48];
|
|
|
|
static int wb_reply_io(void* ctx, const char* url, int urlSz,
|
|
unsigned char* request, int requestSz,
|
|
unsigned char** response)
|
|
{
|
|
(void)ctx; (void)url; (void)urlSz; (void)request; (void)requestSz;
|
|
if (response != NULL)
|
|
*response = g_ioNullResponse ? NULL : g_replyBody;
|
|
return g_ioResult;
|
|
}
|
|
|
|
static void wb_responder_replies(WOLFSSL_CERT_MANAGER* cm)
|
|
{
|
|
OcspRequest req;
|
|
byte serial[8];
|
|
byte url[] = "http://ocsp.example.com/";
|
|
byte emptyUrl[] = "";
|
|
size_t i;
|
|
|
|
static const struct { int result; int nullResp; int io; int freeCb;
|
|
int useUrl; const char* what; } rows[] = {
|
|
{ 16, 0, 1, 1, 1, "a body, both hooks" },
|
|
{ 16, 0, 1, 0, 1, "a body, no free hook" },
|
|
{ 16, 1, 1, 1, 1, "positive length, NULL buffer" },
|
|
{ 0, 0, 1, 1, 1, "zero-length reply" },
|
|
{ -1, 0, 1, 1, 1, "generic transport error" },
|
|
{ 16, 0, 0, 1, 1, "no transport installed" },
|
|
{ 16, 0, 1, 1, 2, "url present but empty" },
|
|
{ 16, 0, 1, 1, 0, "no url at all" },
|
|
{ WOLFSSL_CBIO_ERR_WANT_READ, 0, 1, 1, 1, "responder would block" },
|
|
{ WOLFSSL_CBIO_ERR_TIMEOUT, 0, 1, 1, 1, "responder timed out" },
|
|
};
|
|
|
|
XMEMSET(g_replyBody, 0, sizeof(g_replyBody));
|
|
g_replyBody[0] = 0x30;
|
|
g_replyBody[1] = 0x02;
|
|
|
|
for (i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) {
|
|
XMEMSET(&req, 0, sizeof(req));
|
|
XMEMSET(serial, (byte)(0x40 + i), sizeof(serial));
|
|
req.serial = serial;
|
|
req.serialSz = (int)sizeof(serial);
|
|
/* a distinct issuer per row, so each row misses the cache and
|
|
* actually reaches the transport */
|
|
XMEMSET(req.issuerHash, (byte)(0x70 + i), OCSP_DIGEST_SIZE);
|
|
if (rows[i].useUrl == 1) {
|
|
req.url = url; req.urlSz = (int)sizeof(url) - 1;
|
|
}
|
|
else if (rows[i].useUrl == 2) {
|
|
req.url = emptyUrl; req.urlSz = 0;
|
|
}
|
|
|
|
g_ioResult = rows[i].result;
|
|
g_ioNullResponse = rows[i].nullResp;
|
|
(void)wolfSSL_CertManagerSetOCSP_Cb(cm,
|
|
rows[i].io ? wb_reply_io : NULL,
|
|
rows[i].freeCb ? wb_ocsp_respfree : NULL, NULL);
|
|
|
|
WB_NOTE(CheckOcspRequest(cm->ocsp, &req, NULL, NULL));
|
|
}
|
|
(void)wolfSSL_CertManagerSetOCSP_Cb(cm, NULL, NULL, NULL);
|
|
}
|
|
|
|
/* ---------------------------------------------------------- main */
|
|
|
|
int main(void)
|
|
{
|
|
WOLFSSL_CERT_MANAGER* cm = NULL;
|
|
|
|
if (wolfSSL_Init() != WOLFSSL_SUCCESS) {
|
|
printf("ocsp white-box: wolfSSL_Init failed\n");
|
|
goto done;
|
|
}
|
|
cm = wolfSSL_CertManagerNew();
|
|
if (cm == NULL) {
|
|
printf("ocsp white-box: CertManagerNew failed\n");
|
|
goto done;
|
|
}
|
|
if (wolfSSL_CertManagerEnableOCSP(cm, 0) != WOLFSSL_SUCCESS) {
|
|
printf("ocsp white-box: EnableOCSP failed\n");
|
|
goto done;
|
|
}
|
|
|
|
wb_entry_match(cm->ocsp);
|
|
wb_check_responder();
|
|
wb_check_request(cm);
|
|
wb_check_response(cm);
|
|
wb_free_ocsp_entry(cm->ocsp);
|
|
wb_check_request_ioctx(cm);
|
|
wb_get_ocsp_status(cm->ocsp);
|
|
wb_responder_replies(cm);
|
|
|
|
printf("ocsp white-box: %d vectors driven\n", g_checks);
|
|
|
|
done:
|
|
if (cm != NULL)
|
|
wolfSSL_CertManagerFree(cm);
|
|
wolfSSL_Cleanup();
|
|
return 0; /* always 0: a non-zero exit discards the variant */
|
|
}
|
|
|
|
#else /* not (HAVE_OCSP && certs && !WOLFCRYPT_ONLY) */
|
|
|
|
int main(void)
|
|
{
|
|
printf("ocsp white-box: skipped (needs HAVE_OCSP, certs, and not WOLFCRYPT_ONLY)\n");
|
|
return 0;
|
|
}
|
|
|
|
#endif
|