Skip to content

Commit 5eb9dca

Browse files
committed
tests: add OCSP responder CertID issuerKeyHash binding test
Adds resp_certid_keyhash_mismatch — a forged response signed by the legitimate ocsp-responder whose CertID pairs the legitimate root CA's issuerNameHash with the imposter root CA's issuerKeyHash. The new test_ocsp_responder_keyhash_binding asserts wolfSSL_OCSP_basic_verify rejects it, exercising the fix that requires both halves of the CertID to match the responder's issuer.
1 parent 89d94db commit 5eb9dca

5 files changed

Lines changed: 345 additions & 0 deletions

File tree

tests/api.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37520,6 +37520,7 @@ TEST_CASE testCases[] = {
3752037520
TEST_DECL(test_wolfSSL_inject),
3752137521
TEST_DECL(test_ocsp_status_callback),
3752237522
TEST_DECL(test_ocsp_basic_verify),
37523+
TEST_DECL(test_ocsp_responder_keyhash_binding),
3752337524
TEST_DECL(test_ocsp_response_parsing),
3752437525
TEST_DECL(test_ocsp_certid_enc_dec),
3752537526
TEST_DECL(test_ocsp_certid_dup),

tests/api/create_ocsp_test_blobs.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,23 @@ def create_response(rd: dict) -> rfc6960.OCSPResponse:
266266
for entry in rd.get('responses', []):
267267
if entry.get('certificate'):
268268
sr = single_response_from_cert(entry['certificate'], entry['status'])
269+
elif entry.get('name_cert') and entry.get('key_cert'):
270+
# Forge a CertID where issuerNameHash and issuerKeyHash are taken
271+
# from different certificates. Used to test that responder
272+
# authorization is bound to BOTH halves of the CertID.
273+
name_der = cert_pem_to_der(entry['name_cert'])
274+
name_cert, _ = decode(bytes(name_der), asn1Spec=rfc6960.Certificate())
275+
key_der = cert_pem_to_der(entry['key_cert'])
276+
key_cert, _ = decode(bytes(key_der), asn1Spec=rfc6960.Certificate())
277+
issuer_name_hash = sha1(encode(get_name(name_cert))).digest()
278+
issuer_key_hash = sha1(get_key(key_cert).asOctets()).digest()
279+
cid = cert_id_from_hash(issuer_name_hash, issuer_key_hash,
280+
entry['serial'])
281+
sr = rfc6960.SingleResponse().clone()
282+
sr.setComponentByName('certID', cid)
283+
sr['certStatus'] = cert_status(entry['status'])
284+
sr['thisUpdate'] = useful.GeneralizedTime().fromDateTime(
285+
datetime.now() - timedelta(days=1))
269286
else:
270287
sr = single_response(entry['issuer_cert'], entry['serial'], entry['status'])
271288
responses.append(sr)
@@ -480,6 +497,28 @@ def create_bad_response(rd: dict) -> bytes:
480497
'responder_key': WOLFSSL_OCSP_CERT_PATH + '../ca-key.pem',
481498
'name': 'resp_server_cert_unknown'
482499
},
500+
{
501+
# Forged response: CertID's issuerNameHash points at the legitimate
502+
# root CA, but issuerKeyHash points at the imposter root CA (same
503+
# DN, different key). Signed by the legitimate ocsp-responder so
504+
# the response signature alone verifies. Used to confirm that
505+
# responder authorization rejects mismatched CertID halves.
506+
'response_status': 0,
507+
'signature_algorithm': signature_algorithm(),
508+
'certs_path': [WOLFSSL_OCSP_CERT_PATH + 'ocsp-responder-cert.pem'],
509+
'responder_by_name': True,
510+
'responses': [
511+
{
512+
'name_cert': WOLFSSL_OCSP_CERT_PATH + 'root-ca-cert.pem',
513+
'key_cert': WOLFSSL_OCSP_CERT_PATH +
514+
'imposter-root-ca-cert.pem',
515+
'serial': 0x01,
516+
'status': CERT_GOOD
517+
}
518+
],
519+
'responder_key': WOLFSSL_OCSP_CERT_PATH + 'ocsp-responder-key.pem',
520+
'name': 'resp_certid_keyhash_mismatch'
521+
},
483522
]
484523

485524
with open('./tests/api/test_ocsp_test_blobs.h', 'w') as f:
@@ -517,6 +556,7 @@ def create_bad_response(rd: dict) -> bytes:
517556
add_certificate(WOLFSSL_OCSP_CERT_PATH + '../ca-cert.pem', f)
518557
add_certificate(WOLFSSL_OCSP_CERT_PATH + '../server-cert.pem', f)
519558
add_certificate(WOLFSSL_OCSP_CERT_PATH + 'intermediate1-ca-cert.pem', f)
559+
add_certificate(WOLFSSL_OCSP_CERT_PATH + 'imposter-root-ca-cert.pem', f)
520560
br = create_bad_response({
521561
'response_status': 0,
522562
'responder_by_key': True,

tests/api/test_ocsp.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,48 @@ int test_ocsp_basic_verify(void)
382382
}
383383
#endif /* HAVE_OCSP && (OPENSSL_ALL || OPENSSL_EXTRA) */
384384

385+
#if defined(HAVE_OCSP) && (defined(OPENSSL_ALL) || defined(OPENSSL_EXTRA)) && \
386+
!defined(NO_RSA) && !defined(WOLFSSL_NO_OCSP_ISSUER_CHECK)
387+
/* Verify that OCSP responder authorization is bound to BOTH halves of the
388+
* CertID (issuerNameHash AND issuerKeyHash). The forged response in
389+
* resp_certid_keyhash_mismatch was signed by the legitimate ocsp-responder
390+
* (so the response signature itself verifies) but its CertID pairs the
391+
* legitimate root CA's name hash with the imposter root CA's key hash. With
392+
* a name-only authorization check the response would be incorrectly
393+
* accepted; the CertID-bound check must reject it. */
394+
int test_ocsp_responder_keyhash_binding(void)
395+
{
396+
EXPECT_DECLS;
397+
WOLF_STACK_OF(WOLFSSL_X509)* certs = NULL;
398+
WOLFSSL_X509_STORE* store = NULL;
399+
const unsigned char* ptr = NULL;
400+
OcspResponse* response = NULL;
401+
402+
ExpectIntEQ(test_ocsp_create_x509store(&store, root_ca_cert_pem,
403+
sizeof(root_ca_cert_pem)),
404+
TEST_SUCCESS);
405+
ExpectIntEQ(test_create_stack_of_x509(&certs, ocsp_responder_cert_pem,
406+
sizeof(ocsp_responder_cert_pem)),
407+
TEST_SUCCESS);
408+
409+
ptr = (const unsigned char*)resp_certid_keyhash_mismatch;
410+
ExpectNotNull(response = wolfSSL_d2i_OCSP_RESPONSE(NULL, &ptr,
411+
sizeof(resp_certid_keyhash_mismatch)));
412+
ExpectIntNE(wolfSSL_OCSP_basic_verify(response, certs, store, 0),
413+
WOLFSSL_SUCCESS);
414+
415+
wolfSSL_OCSP_RESPONSE_free(response);
416+
wolfSSL_sk_X509_pop_free(certs, wolfSSL_X509_free);
417+
wolfSSL_X509_STORE_free(store);
418+
return EXPECT_RESULT();
419+
}
420+
#else
421+
int test_ocsp_responder_keyhash_binding(void)
422+
{
423+
return TEST_SKIPPED;
424+
}
425+
#endif
426+
385427
#if defined(HAVE_OCSP) && defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) && \
386428
defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(WOLFSSL_NO_TLS12) && \
387429
defined(OPENSSL_ALL) && !defined(WOLFSSL_SMALL_CERT_VERIFY)

tests/api/test_ocsp.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ int test_ocsp_certid_enc_dec(void);
2626
int test_ocsp_certid_dup(void);
2727
int test_ocsp_status_callback(void);
2828
int test_ocsp_basic_verify(void);
29+
int test_ocsp_responder_keyhash_binding(void);
2930
int test_ocsp_response_parsing(void);
3031
int test_ocsp_tls_cert_cb(void);
3132
int test_ocsp_cert_unknown_crl_fallback(void);

0 commit comments

Comments
 (0)