Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
346edab
test: S03.08 in-process TLS integration harness with real libssl
DavidCozens Apr 21, 2026
2cda618
test: S03.08 integration tests for TLS handshake rejection paths
DavidCozens Apr 21, 2026
321fbc5
fix: S03.08 block handshake when SSL_set1_host fails
DavidCozens Apr 21, 2026
e993666
fix: S03.08 block handshake when SNI hostname setup fails
DavidCozens Apr 21, 2026
a287592
fix: S03.08 abort Open when SSL_CTX_new returns NULL
DavidCozens Apr 21, 2026
9dbb645
fix: S03.08 abort Open when SSL_new returns NULL
DavidCozens Apr 21, 2026
c7ddb15
fix: S03.08 abort Open when SSL_CTX_load_verify_locations fails
DavidCozens Apr 21, 2026
6214b75
fix: S03.08 abort Open when TLS 1.2 floor cannot be set
DavidCozens Apr 21, 2026
45ee8a0
fix: S03.08 free BIO_METHOD on Close
DavidCozens Apr 22, 2026
c43a59f
fix: S03.08 BIO_METHOD freed exactly once across Close/Destroy
DavidCozens Apr 22, 2026
7b2f6b3
fix: S03.08 SSL freed exactly once across Close/Destroy
DavidCozens Apr 22, 2026
d8d9860
fix: S03.08 abort Open when BIO_meth_new returns NULL
DavidCozens Apr 22, 2026
c637f08
fix: S03.08 abort Open when BIO_new returns NULL and free BIO_METHOD
DavidCozens Apr 22, 2026
9e34a2d
feat: S03.08 forward SolidSyslogTlsStreamConfig.cipherList to libssl
DavidCozens Apr 22, 2026
7ddb34d
fix: S03.08 abort Open when cipher list is rejected by libssl
DavidCozens Apr 22, 2026
063bd0c
test: S03.08 integration test for cipher list rejection
DavidCozens Apr 22, 2026
78a2f52
docs: S03.08 document SL4 TLS hardening in iec62443.md
DavidCozens Apr 22, 2026
af27c49
docs: update DEVLOG for S03.08 TLS hardening session
DavidCozens Apr 22, 2026
a476ec1
chore: S03.08 satisfy clang-format and clang-tidy on new TLS code
DavidCozens Apr 22, 2026
db553c9
chore: S03.08 address CodeRabbit review on PR #183
DavidCozens Apr 22, 2026
a707a6b
refactor: extract ConfigureExpectedHostname from Open
DavidCozens Apr 22, 2026
fcf238b
refactor: extract AttachTransportBio from Open
DavidCozens Apr 22, 2026
6887bf2
refactor: extract ReleaseHandshakeState from Close and Destroy
DavidCozens Apr 22, 2026
a85ca00
refactor: single-exit Open via && chain of named steps
DavidCozens Apr 22, 2026
a36d9b2
refactor: single-exit CreateSslContext with named configurers
DavidCozens Apr 22, 2026
896072c
refactor: split CreateTransportBioMethod out of CreateTransportBio
DavidCozens Apr 22, 2026
79dadcd
refactor: single-exit TransportBioCtrl
DavidCozens Apr 22, 2026
53ad1ea
refactor: order functions top-down, depth-first from callers
DavidCozens Apr 22, 2026
152e3bb
refactor: extract ReleaseSsl, ReleaseBioMethod, ReleaseSslContext
DavidCozens Apr 22, 2026
5755cce
refactor: vtable is a static const, not per-instance assignment
DavidCozens Apr 22, 2026
913ec4e
refactor: prefix all TU-local functions with TlsStream_
DavidCozens Apr 22, 2026
9703ab8
refactor: mark all TU-local static functions static inline
DavidCozens Apr 22, 2026
69b5fdf
fix: S03.08 OpenSslFake stores BIO data per-instance
DavidCozens Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,47 @@ jobs:
retention-days: 1
compression-level: 0

openssl-integration:
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
container:
image: ghcr.io/davidcozens/cpputest:sha-18f19e1
options: --user root
env:
GIT_CONFIG_COUNT: 1
GIT_CONFIG_KEY_0: safe.directory
GIT_CONFIG_VALUE_0: '*'

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Configure
run: cmake --preset debug

- name: Build integration tests
run: cmake --build --preset debug --target OpenSslIntegrationTests

- name: Run integration tests
run: cd build/debug && ./Tests/OpenSslIntegration/OpenSslIntegrationTests -v -ojunit -k OpenSslIntegrationTests

- name: Test Report
uses: dorny/test-reporter@v3
if: success() || failure()
with:
name: Test Results (OpenSSL Integration)
path: build/debug/cpputest_*.xml
reporter: java-junit

- name: Upload JUnit XML
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: junit-openssl-integration
path: build/debug/cpputest_*.xml
retention-days: 1

clang-build-and-test:
runs-on: ubuntu-latest
permissions:
Expand Down Expand Up @@ -508,7 +549,7 @@ jobs:

quality-summary:
if: always() && github.event_name == 'pull_request'
needs: [build-and-test, clang-build-and-test, sanitize, coverage, tidy, cppcheck, format, bdd, windows-build-and-test, bdd-windows]
needs: [build-and-test, clang-build-and-test, sanitize, coverage, tidy, cppcheck, format, bdd, windows-build-and-test, bdd-windows, openssl-integration]
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -563,6 +604,11 @@ jobs:
"name": "Unit Tests (Sanitize)",
"pattern": "**/junit-sanitize/cpputest_*.xml"
},
{
"id": "junit",
"name": "Integration Tests (OpenSSL)",
"pattern": "**/junit-openssl-integration/cpputest_*.xml"
},
{
"id": "junit",
"name": "BDD Tests (Linux)",
Expand Down
9 changes: 7 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,13 @@ endif()
# OpenSSL.
find_package(OpenSSL QUIET)
option(SOLIDSYSLOG_OPENSSL "Include OpenSSL-based TLS support" ${OpenSSL_FOUND})
if(SOLIDSYSLOG_OPENSSL AND NOT OpenSSL_FOUND)
message(FATAL_ERROR "SOLIDSYSLOG_OPENSSL=ON but OpenSSL was not found; install OpenSSL or pass -DSOLIDSYSLOG_OPENSSL=OFF")
if(SOLIDSYSLOG_OPENSSL)
if(NOT OpenSSL_FOUND)
message(FATAL_ERROR "SOLIDSYSLOG_OPENSSL=ON but OpenSSL was not found; install OpenSSL or pass -DSOLIDSYSLOG_OPENSSL=OFF")
endif()
if(OPENSSL_VERSION VERSION_LESS 3.0)
message(FATAL_ERROR "SOLIDSYSLOG_OPENSSL requires OpenSSL 3.0 or later; found ${OPENSSL_VERSION}. The integration harness uses EVP_RSA_gen (3.0+).")
endif()
endif()

add_subdirectory(Core/Source)
Expand Down
112 changes: 112 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -841,3 +841,115 @@ S3.9 / S3.10 / S3.11 as placeholder issues under E3.
because it rides on the already-reliable auto-load. Worth recording:
separating "conventions that must be read" from "conventions the tool
auto-loads" is a real gap, and signposting from CLAUDE.md bridges it.

## 2026-04-21/22 — S03.08 TLS hardening: cert validation, cipher pinning, lifecycle

### What went well

- **Planning turn before coding paid off.** Before any C, I laid out a
phase plan and six API-shape surprises — integration harness must
live in its own binary (fake interposes libssl), cert generation
programmatic not fixtures, cipher policy at caller not library, BIO
lifecycle per-Open, the `OpenSslFake` per-BIO aliasing issue. The
user corrected one call (BDD scenarios for negative cases →
integration harness instead, much faster feedback), parked three
(cipher shape, BIO lifecycle, `SSL_VERIFY_FAIL_IF_NO_PEER_CERT`),
and greenlit the rest. Every surprise came up once, got a decision,
then didn't recur.

- **Composable test-support trio.** `TlsTestCert` + `TlsTestServer` +
`BioPairStream` under `Tests/OpenSslIntegration/` each do one thing;
new scenarios are a single test body with a cert config and an
assertion. The fourth scenario (unsupported cipher) was a
seven-line addition. This is the scaffolding paying for itself
instantly, not in six months.

- **Pump-on-read pattern.** `BioPairStream::Read` drives the server
side cooperatively when the BIO pair is empty, so
`SolidSyslogTlsStream`'s synchronous `SSL_connect` works unchanged
over non-blocking in-memory BIOs — no threads in the harness, no
production changes to accommodate testing.

### What didn't go well / process refinements

- **BDD-first plan got reversed.** My Phase 1 was "BDD pending
scenarios for SL4 failure modes." User pushed back: integration
harness covers it faster with no PO-visibility loss. Should have
surfaced this trade-off *myself* in the opening surprises list
rather than defaulting to BDD. Memory entry
`feedback_integration_over_bdd.md` captures the pattern: for
crypto/TLS failure modes, integration harness beats BDD.

- **Phase 2 / Phase 4 coupling I didn't spot up front.** Plan
separated "return-value checks" (Phase 2) from "resource lifecycle"
(Phase 4). In practice, every new early-return path made a
pre-existing leak worse until `Destroy` covered partial state.
Ended up interleaving — not a real problem but the phase plan was
cleaner than reality.

- **Commit count.** 17 commits on the branch is a lot of tight red/green
pairs. Each is a clear atomic behaviour change and reads well in
`git log --oneline`, but the PR-review surface is larger than a
single "TLS hardening" story arguably needed. Next time, consider
batching several return-value checks into one commit when they
share a test shape.

- **Characterisation vs red/green mismatch.** Three of the integration
tests went green on first write — OpenSSL defaults already rejected
expired / wrong-hostname / wrong-CA. Under strict "only write
production code driven by a failing test" this feels off, but the
tests still earn their keep as regression protection. I flagged
this explicitly rather than pretending it was red/green, and the
user accepted the framing. Calling out "this is characterisation,
not TDD" is probably the right move whenever it happens.

- **One stray production branch.** The `if (ctx == NULL) return NULL;`
inside `CreateSslContext` wasn't strictly driven by a failing test —
it was the natural implementation alongside the `OpenReturnsFalseWhenCtxNewFails`
fix. User reminded me mid-session to "only write a new branch of code
when you have a failing test." Tightened up for the rest of the session.

### Decisions captured this session

- **Integration test executable is its own CI job** (option C of A/B/C)
— runs in parallel with `build-and-test`, self-contained configure
+ build of only `OpenSslIntegrationTests`. Clang and sanitize
presets don't run it; unit tests remain the coverage engine.
- **Cipher list is caller policy.** `SolidSyslogTlsStreamConfig.cipherList`
is forwarded to `SSL_CTX_set_cipher_list` if non-NULL; library ships
no SL4 default. `docs/iec62443.md` names
`ECDHE+AESGCM:ECDHE+CHACHA20` as a reasonable starting point, not a
baked-in list.
- **BIO_METHOD lifecycle is per-Open.** Symmetric with SSL/SSL_CTX —
each Open allocates a fresh set, each Close releases. Destroy
covers partial-init state idempotently.
- **Coverage stays unit-test-only.** Integration tests are supplementary
evidence of real-libssl acceptance, not the coverage engine. Memory
entry `project_coverage_integration.md` records the trigger for
revisiting (multi-platform, or a real-libssl-only code path).

### Deferred

- **`OpenSslFake` per-BIO data refactor.** Listed as S03.08 acceptance
but no production code path or test requires distinct BIO data yet
(the library creates exactly one BIO per Open). Forward-looking
for mTLS (S03.09) and future cases. Suggest a short follow-up
story rather than doing it here without a test that fails without it.
- **TLS 1.3 cipher suite control.** `SSL_CTX_set_cipher_list` governs
TLS 1.2 only; TLS 1.3 uses `SSL_CTX_set_ciphersuites`. Out of the
ticket's acceptance. Follow-up.
- **`max_proto_version` control.** Library pins min but not max. No
current need; might emerge with an SL4 policy that mandates TLS 1.3.
- **Formal Planned → Available promotion** for TLS in
`docs/iec62443.md` — S03.11 scope. This session added the SL4
substrate narrative without touching the headline status.

### Open questions for the blog source

- **When is characterisation a legitimate TDD outcome?** Three tests
went green on first write because OpenSSL already did the right
thing. Keeping them is evidence and regression protection; rejecting
them would be dogmatic. The useful lens: if the test protects against
a realistic future regression, it's load-bearing — TDD discipline is
about "production code driven by tests", not "every test must have
been red at some point."
1 change: 1 addition & 0 deletions Platform/OpenSsl/Interface/SolidSyslogTlsStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ EXTERN_C_BEGIN
struct SolidSyslogStream* transport; /* underlying byte stream — caller owns */
const char* caBundlePath; /* PEM file of trust anchors */
const char* serverName; /* SNI + cert hostname check; NULL to skip */
const char* cipherList; /* TLS 1.2 cipher list; NULL = OpenSSL default */
};

struct SolidSyslogStream* SolidSyslogTlsStream_Create(const struct SolidSyslogTlsStreamConfig* config);
Expand Down
Loading
Loading