Skip to content

S03.08: Certificate validation + cipher hardening (SL4: SR 3.1 / CR 3.9) #172

Description

@DavidCozens

Parent epic: #5

Goal

Harden SolidSyslogTlsStream beyond the walking-skeleton defaults: make hostname/SAN verification explicit and failure-checked, set an explicit cipher-suite floor, plug the mid-Open resource leaks that the walking skeleton knowingly left behind, and stand up an in-process integration harness so SL4 failure modes can be unit-tested against real libssl.

Problem

S03.07 (#165) landed an OpenSSL-backed TLS stream that relies on OpenSSL defaults for hostname verification and cipher selection. The walking-skeleton scope explicitly said "Explicit validation, hostname/SAN, cipher hardening … arrive in later stories." Review of PR #170 surfaced concrete items that belong here:

  1. Unchecked hostname setup return values — MITM risk. SSL_set_tlsext_host_name() and SSL_set1_host() are called without checking their return values; if either fails the handshake proceeds without hostname verification. See PR #170 comment.
  2. BIO_METHOD lifecycle. CreateTransportBio() allocates a fresh BIO_METHOD via BIO_meth_new() that is never freed. Either hoist to module-scope one-time init or pair with BIO_meth_free() in Destroy. SSL is also not nulled in Close; Destroy does not fully clean up. Flagged in PR #170 review.
  3. Mid-Open error cleanup. Already acknowledged during S03.07 review — if CreateSslContext, SSL_new, SSL_set_bio, or SSL_set1_host fails partway through Open, allocated SSL / SSL_CTX / BIO are leaked.
  4. OpenSslFake BIO data aliasing. BIO_set_data / BIO_get_data currently share a single global lastSetDataArg across all BIOs. Fine for the walking skeleton (one read BIO + one write BIO, same transport payload) but any test that needs two BIOs with different payloads will alias silently. Flagged in PR #170 review.
  5. Unit tests cannot reach libssl-internal failure modes. Tests/SolidSyslogTlsStreamTest.cpp runs against OpenSslFake, which can observe API shape (e.g. "was SSL_set1_host called?") but cannot model expired certs, wrong CN/SAN, wrong CA, or cipher-list rejection — the very failure modes SL4 verification hinges on. BDD can catch these in principle but is slow (~25 s full run) and awkward to drive into specific failure states (requires misconfiguring syslog-ng or rotating certs live).

Scope

Certificate + hostname validation

  • Check return values of every OpenSSL call in Open; on any failure, release what was allocated so far and fail Open cleanly.
  • Treat SNI / hostname setup failure as Open failure — no silent bypass.
  • Explicit SSL_VERIFY_PEER; confirm whether SSL_VERIFY_FAIL_IF_NO_PEER_CERT applies (client-side default is adequate but pin it).
  • SAN-aware hostname verification where OpenSSL defaults need supplementing for SL4.

Cipher hardening

  • TLS 1.2 floor (confirm OpenSSL default matches on every target).
  • Explicit cipher list matching IEC 62443 SL4 expectations; ADR if deviating from OpenSSL defaults.

Resource lifecycle

  • BIO_meth_free wired — either module-scope one-time init or per-Open pair.
  • SSL nulled in Close; full cleanup in Destroy (CTX, SSL, BIO_METHOD).

In-process TLS integration harness (scaffolding)

Stand up a real-libssl loopback harness before the cert-validation tests land, so failure-mode coverage can run in the unit test suite rather than BDD:

  • BIO_new_bio_pair creates two connected in-memory BIOs.
  • SolidSyslogTlsStream runs as client on one side; the test stands up a real libssl server (SSL_CTX + cert-generation helpers) on the other.
  • Full handshake in-process — no sockets, no containers.
  • Per-test server rigging lets each scenario supply its own certs/ciphers: expired cert, wrong CN/SAN, wrong CA, missing client cert (for S03.09), unsupported cipher.

Scope note: the harness is explicitly OpenSSL-specific — it exercises Platform/OpenSsl/. If future epics add mbedTLS / BearSSL / wolfSSL, each gets its own harness under its own Platform tier. The harness must not try to be portable.

Cost estimate: ~150 lines of scaffolding (server-side SSL_CTX, cert-generation helpers, BIO-pair plumbing) for the first test; subsequent tests are one function call.

Test support

  • OpenSslFake.c — per-BIO BIO_set_data / BIO_get_data storage so existing fake-based tests can exercise multiple BIOs without aliasing.

Acceptance

  • Every OpenSSL call in Open is return-value-checked; partial-init failures release all resources.
  • Hostname-setup failure (SSL_set_tlsext_host_name or SSL_set1_host) blocks the handshake.
  • BIO_METHOD is freed (no leak at process exit).
  • Close nulls the SSL pointer; Destroy performs full cleanup.
  • OpenSslFake stores BIO data per-instance.
  • In-process TLS integration harness lives under Tests/ (landing as the first commit of the story), runs against real libssl, and covers at least: expired server cert, wrong CN/SAN, wrong CA, unsupported cipher.
  • All presets green; coverage stays at 100% on Platform/OpenSsl.
  • IEC 62443 SL4 mapping for hostname verification + cipher list captured in docs/iec62443.md.

Out of scope

  • Mutual TLS (client certs) — S03.09. (Harness scaffolded here will be reused.)
  • Certificate rotation — S03.10.
  • CRL / OCSP revocation — deferred; OpenSSL defaults apply, ADR under this story if non-default.
  • TLS handshake-failure audit callback — folds into E12 (E12: Error Handling #31).

Metadata

Metadata

Assignees

No one assigned

    Labels

    storyStory issue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions