From 65c0d6b169feed3502d189649ec87ea55ad4bc51 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 2 Jul 2026 13:29:03 +0200 Subject: [PATCH 1/2] ocsptest: add generator for test/ocsptest.c PKI Add an ossl_test_tools ocsptest subpackage that generates the root CA, root key, and leaf certificate used by test/ocsptest.c, mirroring the crltest subpackage. The flat root -> leaf chain lets the root act as both the trust anchor and the authorized OCSP responder, and shipping the root key allows the C test to sign its own OCSP responses at run time. Assisted-by: Claude:claude-opus-4-8 --- test-tools/ossl_test_tools/cli.py | 3 +- .../ossl_test_tools/ocsptest/__init__.py | 35 ++++++ test-tools/ossl_test_tools/ocsptest/pki.py | 117 ++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 test-tools/ossl_test_tools/ocsptest/__init__.py create mode 100644 test-tools/ossl_test_tools/ocsptest/pki.py diff --git a/test-tools/ossl_test_tools/cli.py b/test-tools/ossl_test_tools/cli.py index 3c5dcca3..1133b186 100644 --- a/test-tools/ossl_test_tools/cli.py +++ b/test-tools/ossl_test_tools/cli.py @@ -14,7 +14,7 @@ import argparse -from . import crltest, pem +from . import crltest, ocsptest, pem def main(argv=None): @@ -25,6 +25,7 @@ def main(argv=None): sub = parser.add_subparsers(dest="cmd", required=True) crltest.register(sub) + ocsptest.register(sub) pem.register(sub) args = parser.parse_args(argv) diff --git a/test-tools/ossl_test_tools/ocsptest/__init__.py b/test-tools/ossl_test_tools/ocsptest/__init__.py new file mode 100644 index 00000000..140ed4a3 --- /dev/null +++ b/test-tools/ossl_test_tools/ocsptest/__init__.py @@ -0,0 +1,35 @@ +# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. +# +# Licensed under the Apache License 2.0 (the "License"). You may not use +# this file except in compliance with the License. You can obtain a copy +# in the file LICENSE in the source distribution or at +# https://www.openssl.org/source/license.html + +"""Generators for the artifacts in test/ocsptest.c. + +Each subcommand writes its outputs back into the C source file, replacing +the existing kXxx[] declarations by variable name. +""" + +from pathlib import Path + +from . import pki + + +def _all_cmd(args): + pki.build(args.source) + print(f"regenerated all OCSP artifacts in {args.source}") + + +def register(subparsers): + parent = subparsers.add_parser( + "ocsptest", + help="Regenerate artifacts in test/ocsptest.c.", + ) + sub = parent.add_subparsers(dest="ocsptest_cmd", required=True) + + pki.register(sub) + + p = sub.add_parser("all", help="Run all OCSP artifact generators.") + p.add_argument("--source", type=Path, required=True, help="Path to ocsptest.c") + p.set_defaults(func=_all_cmd) diff --git a/test-tools/ossl_test_tools/ocsptest/pki.py b/test-tools/ossl_test_tools/ocsptest/pki.py new file mode 100644 index 00000000..56602a10 --- /dev/null +++ b/test-tools/ossl_test_tools/ocsptest/pki.py @@ -0,0 +1,117 @@ +# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved. +# +# Licensed under the Apache License 2.0 (the "License"). You may not use +# this file except in compliance with the License. You can obtain a copy +# in the file LICENSE in the source distribution or at +# https://www.openssl.org/source/license.html + +"""PKI for test/ocsptest.c. + + kOcspTestRoot self-signed root CA, trust anchor and OCSP responder + kOcspTestRootKey the root CA private key, used to sign OCSP responses + kOcspTestLeaf leaf issued by the root CA + +The chain is flat (no intermediate) so the root is the leaf's issuer and +therefore its authorized OCSP responder. Validity windows are long because +the verification path checks OCSP response validity against the wall clock. +""" + +import datetime +from pathlib import Path + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes +from cryptography.x509.oid import ExtendedKeyUsageOID + +from .. import cert_util + +UTC = datetime.timezone.utc +NOT_BEFORE = datetime.datetime(2026, 1, 1, 0, 0, 0, tzinfo=UTC) +NOT_AFTER = datetime.datetime(2126, 1, 1, 0, 0, 0, tzinfo=UTC) + +ROOT_DN = cert_util.name( + "US", "California", "San Francisco", + "Example Corp", "Certificate Authority", + "Example Corp OCSP Test Root CA", +) +LEAF_DN = cert_util.name( + "US", "California", "San Francisco", + "Example Corp", "Web Services", + "ocsp-leaf.example.com", +) + + +def _ca_key_usage(): + return x509.KeyUsage( + digital_signature=True, content_commitment=False, + key_encipherment=False, data_encipherment=False, key_agreement=False, + key_cert_sign=True, crl_sign=True, + encipher_only=False, decipher_only=False) + + +def _leaf_key_usage(): + return x509.KeyUsage( + digital_signature=True, content_commitment=False, + key_encipherment=True, data_encipherment=False, key_agreement=False, + key_cert_sign=False, crl_sign=False, + encipher_only=False, decipher_only=False) + + +def build(source_path): + root_key = cert_util.new_rsa_key() + root = ( + x509.CertificateBuilder() + .subject_name(ROOT_DN) + .issuer_name(ROOT_DN) + .public_key(root_key.public_key()) + .serial_number(0x1) + .not_valid_before(NOT_BEFORE) + .not_valid_after(NOT_AFTER) + .add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True) + .add_extension(_ca_key_usage(), critical=True) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(root_key.public_key()), + critical=False) + .sign(private_key=root_key, algorithm=hashes.SHA256()) + ) + + leaf_key = cert_util.new_rsa_key() + leaf = ( + x509.CertificateBuilder() + .subject_name(LEAF_DN) + .issuer_name(ROOT_DN) + .public_key(leaf_key.public_key()) + .serial_number(0x1000) + .not_valid_before(NOT_BEFORE) + .not_valid_after(NOT_AFTER) + .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True) + .add_extension(_leaf_key_usage(), critical=True) + .add_extension( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False) + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(leaf_key.public_key()), + critical=False) + .add_extension(cert_util.akid_from_cert(root), critical=False) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("ocsp-leaf.example.com")]), + critical=False) + .sign(private_key=root_key, algorithm=hashes.SHA256()) + ) + + cert_util.update_cert_in_c(source_path, "kOcspTestRoot", root) + cert_util.update_key_in_c(source_path, "kOcspTestRootKey", root_key) + cert_util.update_cert_in_c(source_path, "kOcspTestLeaf", leaf) + + +def _cmd(args): + build(args.source) + print(f"updated kOcspTestRoot kOcspTestRootKey kOcspTestLeaf in {args.source}") + + +def register(sub): + p = sub.add_parser( + "pki", + help="Regenerate the root CA, root key, and leaf.", + ) + p.add_argument("--source", type=Path, required=True, help="Path to ocsptest.c") + p.set_defaults(func=_cmd) From a8c14234716b37c92313be1d85492a9fe46b2154 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 3 Jul 2026 17:51:48 +0200 Subject: [PATCH 2/2] test-tools: expand README to cover ocsptest and point at --help --- test-tools/README.md | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/test-tools/README.md b/test-tools/README.md index 5e206503..af4aeb73 100644 --- a/test-tools/README.md +++ b/test-tools/README.md @@ -1,7 +1,8 @@ # ossl-test-tools -Python tools for OpenSSL's C tests. Edits test artifacts (certs, CRLs, -keys) into the .c source files in place by variable name. +Python tools for OpenSSL's C tests. They generate test artifacts (certs, +CRLs, keys) and edit them into the `.c` source files in place, replacing +the existing `static const ... kName[]` declarations by variable name. ## Setup @@ -10,16 +11,31 @@ keys) into the .c source files in place by variable name. ## Use - uv run ossl-test-tools crltest all --source path/to/crltest.c - uv run ossl-test-tools crltest indirect --source path/to/crltest.c - uv run ossl-test-tools crltest alt-ta --source path/to/crltest.c - uv run ossl-test-tools crltest no-chain --source path/to/crltest.c +The first argument names the test to update. It matches the `.c` file it +generates artifacts for, e.g. `crltest` for `test/crltest.c` and +`ocsptest` for `test/ocsptest.c`. After that comes the generator +subcommand. Run `--help` at any level to see what is available and the +parameters each subcommand takes: -Each subcommand has its own `--help`. + uv run ossl-test-tools --help + uv run ossl-test-tools crltest --help + uv run ossl-test-tools ocsptest --help + +The generator subcommands take a `--source` pointing at the `.c` file to +rewrite, and `all` regenerates every artifact for that test: + + uv run ossl-test-tools crltest all --source path/to/crltest.c + uv run ossl-test-tools crltest indirect --source path/to/crltest.c + uv run ossl-test-tools ocsptest all --source path/to/ocsptest.c + +`pem-to-c` formats a PEM file as a C declaration and prints it to stdout, +for one-off pasting: + + uv run ossl-test-tools pem-to-c some.pem --name kSomething ## Adding a new variable -Add an empty placeholder to the .c file: +Add an empty placeholder to the `.c` file: static const char *kSomething[] = {}; @@ -32,17 +48,11 @@ and register it from `cli.py`. Two helpers worth knowing: -* `csource` — locate, read, and rewrite `static const ... kName[]` +* `csource` locates, reads, and rewrites `static const ... kName[]` arrays. PEM string arrays and hex byte arrays are both supported for reading; only PEM is written. -* `cert_util` — cert/CRL/key builders plus `cert_from_c` / +* `cert_util` provides cert/CRL/key builders plus `cert_from_c` / `update_cert_in_c` bridges. Per-tool constants (DNs, validity windows, serials) stay in the tool's own module. - -## pem-to-c - -Format a PEM file as a C declaration, for one-off pasting: - - uv run ossl-test-tools pem-to-c some.pem --name kSomething