Rework TLS to OpenSSL-native style (memory BIOs, explicit status codes)#8002
Conversation
Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com>
Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com>
Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com>
|
@copilot check previous CI runs, find out what failed and fix it. |
Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com>
Investigated the failing runs (ACI SNP Genoa/Milan + VMSS Virtual C): all hung in Root cause: in the new memory-BIO design, Fix (3a8... commit): drain outbound with |
OpenSSL requires the thread-local error queue to be empty before SSL_get_error is used. Clear stale errors at each TLS operation boundary so unrelated crypto validation failures cannot close healthy sessions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Diagnosed and fixed the remaining The invalid X.509 policy check leaves ASN.1 entries on OpenSSL's thread-local error queue. Because The fix clears stale errors before handshake/read/write/shutdown operations and adds a regression test that reproduces the misclassification. I also merged the latest |
There was a problem hiding this comment.
Pull request overview
This PR modernises CCF’s TLS stack to follow OpenSSL-native patterns now that the MbedTLS compatibility layer is gone. It replaces the callback-based I/O emulation with in-memory BIOs and makes TLS APIs return explicit OpenSSL SSL_ERROR_* status codes (with byte counts via out-params), simplifying control flow in TLSSession and related components.
Changes:
- Replace BIO callbacks with explicit in-memory BIO pump APIs (
Context::recv/pending_write/send) and updateTLSSessionto drive them. - Split status and byte counts for
handshake/read/write/close, removing the previous negative/overloaded return scheme (keepingTLS_ERR_X509_VERIFYas a sentinel). - Update unit tests and docs to reflect the new memory-BIO design and status-code contract.
Custom instructions used:
.github/copilot-instructions.md.github/instructions/reviewing.instructions.md
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tls/context.h | Switch Context to memory BIOs and explicit OpenSSL-style status codes. |
| src/enclave/tls_session.h | Update session drive loop to feed/drain BIOs and switch on SSL_ERROR_* results. |
| src/tls/plaintext_server.h | Adapt the non-TLS passthrough server to the new BIO-driven virtual interface. |
| src/tls/tls.h | Remove legacy error macros; keep TLS_ERR_X509_VERIFY sentinel. |
| src/tls/test/main.cpp | Rework TLS tests to a single-threaded “BIO pump” model; add stale-error coverage. |
| src/tls/README.md | Update TLS design documentation to describe the new OpenSSL-native approach. |
| doc/architecture/tls_internals.rst | Update architecture docs to describe BIO-driven I/O and status-code contract. |
Comments suppressed due to low confidence (1)
src/tls/tls.h:14
- tls.h uses INT_MIN for TLS_ERR_X509_VERIFY but doesn’t include . Relying on transitive includes is brittle; include (or switch to std::numeric_limits::min()) so this header is self-contained.
// Specific error to flag a certificate validation failure during the handshake.
// This is not emitted by OpenSSL itself (its handshake failures surface as
// SSL_ERROR_SSL), but is set by Context so the caller can distinguish an
// authentication failure from a generic error. We use a bogus negative value
// that won't match any OpenSSL SSL_ERROR_* code.
#define TLS_ERR_X509_VERIFY INT_MIN
#include "ccf/crypto/openssl/openssl_wrappers.h"
#include <string>
Use size_t-safe BIO APIs and include SSL statuses in error diagnostics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove obsolete error-string setup, use the non-owning peer certificate accessor, and drop the unreachable certificate lookup retry while covering exact peer certificate extraction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DescriptionComparing this branch (#8002) against the trend of the last 30 Each chart plots every benchmark as an axis, with values normalized so 100 is the EWMA baseline of recent Axis labels show this branch's value and its difference from the main EWMA baseline, where 0% is on the baseline. They are coloured green where this branch improves on the baseline, red where it regresses, and grey where the difference is within one std dev of the baseline (within noise). Higher is better for throughput and rate, lower for latency and memory. Throughput (tx/s)---
config:
radar:
width: 620
height: 620
marginTop: 90
marginRight: 220
marginBottom: 60
marginLeft: 220
axisLabelFactor: 1.12
curveTension: 0.08
theme: base
themeCSS: |
.radarCurve-0{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-1{fill:color-mix(in srgb, #62B5E5 40%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-2{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-3{fill:var(--color-canvas-default,var(--bgColor-default,#fff))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-4{stroke-width:2px!important}
.radarAxisLabel,.radarTitle{fill:var(--color-fg-default,var(--fgColor-default,#111827))!important;color:var(--color-fg-default,var(--fgColor-default,#111827))!important}
.radarAxisLabel:nth-of-type(1){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(2){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(3){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(4){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(5){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(6){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(7){fill:#E5484D!important}
themeVariables:
cScale0: "#62B5E5"
cScale1: "#62B5E5"
cScale2: "#62B5E5"
cScale3: "#62B5E5"
cScale4: "#008FD3"
radar:
axisColor: "#9CA3AF"
graticuleColor: "#E5E7EB"
graticuleOpacity: 0
axisStrokeWidth: 1
curveOpacity: 0
---
radar-beta
axis b0["Basic: 58,700 tx/s ▼ 11%"]
axis b1["Basic Blocking: 979 tx/s ▼ 3%"]
axis b2["Basic JS: 4,071 tx/s ▼ 13%"]
axis b3["Basic Multi-Threaded: 74,476 tx/s ▼ 13%"]
axis b4["Historical Queries: 178,329 tx/s ▼ 13%"]
axis b5["Logging: 51,553 tx/s ▼ 15%"]
axis b6["Logging JWT: 8,893 tx/s ▼ 12%"]
curve stddev2_high["main EWMA + 2 std dev"]{106.99, 104.13, 106.91, 106.49, 112.29, 108.58, 107.05}
curve stddev1_high["main EWMA + 1 std dev"]{103.49, 102.06, 103.46, 103.25, 106.14, 104.29, 103.52}
curve stddev1_low["main EWMA - 1 std dev"]{96.51, 97.94, 96.54, 96.75, 93.86, 95.71, 96.48}
curve stddev2_low["main EWMA - 2 std dev"]{93.01, 95.87, 93.09, 93.51, 87.71, 91.42, 92.95}
curve branch["#8002"]{88.77, 97.27, 86.92, 86.87, 87.22, 85.25, 88.07}
graticule polygon
max 122
min 75
ticks 0
showLegend false
Latency (ms)---
config:
radar:
width: 620
height: 620
marginTop: 90
marginRight: 220
marginBottom: 60
marginLeft: 220
axisLabelFactor: 1.12
curveTension: 0.08
theme: base
themeCSS: |
.radarCurve-0{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-1{fill:color-mix(in srgb, #62B5E5 40%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-2{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-3{fill:var(--color-canvas-default,var(--bgColor-default,#fff))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-4{stroke-width:2px!important}
.radarAxisLabel,.radarTitle{fill:var(--color-fg-default,var(--fgColor-default,#111827))!important;color:var(--color-fg-default,var(--fgColor-default,#111827))!important}
.radarAxisLabel:nth-of-type(1){fill:#808A94!important}
.radarAxisLabel:nth-of-type(2){fill:#808A94!important}
.radarAxisLabel:nth-of-type(3){fill:#E5484D!important}
themeVariables:
cScale0: "#62B5E5"
cScale1: "#62B5E5"
cScale2: "#62B5E5"
cScale3: "#62B5E5"
cScale4: "#008FD3"
radar:
axisColor: "#9CA3AF"
graticuleColor: "#E5E7EB"
graticuleOpacity: 0
axisStrokeWidth: 1
curveOpacity: 0
---
radar-beta
axis b0["Commit Latency 16ms: 5.19 ms ▬ -5%"]
axis b1["Commit Latency 1ms: 1.71 ms ▬ -3%"]
axis b2["Commit Latency 256ms: 210 ms ▲ 1%"]
curve stddev2_high["main EWMA + 2 std dev"]{150.41, 107.32, 101.11}
curve stddev1_high["main EWMA + 1 std dev"]{125.21, 103.66, 100.55}
curve stddev1_low["main EWMA - 1 std dev"]{74.79, 96.34, 99.45}
curve stddev2_low["main EWMA - 2 std dev"]{49.59, 92.68, 98.89}
curve branch["#8002"]{94.56, 96.96, 101.46}
graticule polygon
max 186
min 14
ticks 0
showLegend false
Memory (bytes)---
config:
radar:
width: 620
height: 620
marginTop: 90
marginRight: 220
marginBottom: 60
marginLeft: 220
axisLabelFactor: 1.12
curveTension: 0.08
theme: base
themeCSS: |
.radarCurve-0{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-1{fill:color-mix(in srgb, #62B5E5 40%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-2{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-3{fill:var(--color-canvas-default,var(--bgColor-default,#fff))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-4{stroke-width:2px!important}
.radarAxisLabel,.radarTitle{fill:var(--color-fg-default,var(--fgColor-default,#111827))!important;color:var(--color-fg-default,var(--fgColor-default,#111827))!important}
.radarAxisLabel:nth-of-type(1){fill:#808A94!important}
.radarAxisLabel:nth-of-type(2){fill:#808A94!important}
.radarAxisLabel:nth-of-type(3){fill:#808A94!important}
.radarAxisLabel:nth-of-type(4){fill:#808A94!important}
.radarAxisLabel:nth-of-type(5){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(6){fill:#808A94!important}
themeVariables:
cScale0: "#62B5E5"
cScale1: "#62B5E5"
cScale2: "#62B5E5"
cScale3: "#62B5E5"
cScale4: "#008FD3"
radar:
axisColor: "#9CA3AF"
graticuleColor: "#E5E7EB"
graticuleOpacity: 0
axisStrokeWidth: 1
curveOpacity: 0
---
radar-beta
axis b0["Basic: 88.7 MiB ▬ +1%"]
axis b1["Basic Blocking: 71.2 MiB ▬ 0%"]
axis b2["Basic JS: 70.5 MiB ▬ 0%"]
axis b3["Basic Multi-Threaded: 89.7 MiB ▬ 0%"]
axis b4["Logging: 76.9 MiB ▲ 2%"]
axis b5["Logging JWT: 67.7 MiB ▬ 0%"]
curve stddev2_high["main EWMA + 2 std dev"]{107.12, 100.72, 103.81, 102.17, 102.10, 101.21}
curve stddev1_high["main EWMA + 1 std dev"]{103.56, 100.36, 101.91, 101.08, 101.05, 100.60}
curve stddev1_low["main EWMA - 1 std dev"]{96.44, 99.64, 98.09, 98.92, 98.95, 99.40}
curve stddev2_low["main EWMA - 2 std dev"]{92.88, 99.28, 96.19, 97.83, 97.90, 98.79}
curve branch["#8002"]{101.13, 100.32, 99.82, 100.38, 101.80, 99.54}
graticule polygon
max 113
min 87
ticks 0
showLegend false
Rate (ops/s)---
config:
radar:
width: 620
height: 620
marginTop: 90
marginRight: 220
marginBottom: 60
marginLeft: 220
axisLabelFactor: 1.12
curveTension: 0.08
theme: base
themeCSS: |
.radarCurve-0{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-1{fill:color-mix(in srgb, #62B5E5 40%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-2{fill:color-mix(in srgb, #62B5E5 13%, var(--color-canvas-default,var(--bgColor-default,#fff)))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-3{fill:var(--color-canvas-default,var(--bgColor-default,#fff))!important;fill-opacity:1!important;stroke:none!important;stroke-width:0!important}
.radarCurve-4{stroke-width:2px!important}
.radarAxisLabel,.radarTitle{fill:var(--color-fg-default,var(--fgColor-default,#111827))!important;color:var(--color-fg-default,var(--fgColor-default,#111827))!important}
.radarAxisLabel:nth-of-type(1){fill:#808A94!important}
.radarAxisLabel:nth-of-type(2){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(3){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(4){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(5){fill:#E5484D!important}
.radarAxisLabel:nth-of-type(6){fill:#E5484D!important}
themeVariables:
cScale0: "#62B5E5"
cScale1: "#62B5E5"
cScale2: "#62B5E5"
cScale3: "#62B5E5"
cScale4: "#008FD3"
radar:
axisColor: "#9CA3AF"
graticuleColor: "#E5E7EB"
graticuleOpacity: 0
axisStrokeWidth: 1
curveOpacity: 0
---
radar-beta
axis b0["CHAMP get: 32,909,114 ops/s ▬ -9%"]
axis b1["CHAMP put: 4,676,792 ops/s ▼ 11%"]
axis b2["KV deserialisation: 1,367,802 ops/s ▼ 13%"]
axis b3["KV serialisation: 1,246,572 ops/s ▼ 13%"]
axis b4["KV snapshot deserialis...: 3,642 ops/s ▼ 12%"]
axis b5["KV snapshot serialisation: 3,978 ops/s ▼ 12%"]
curve stddev2_high["main EWMA + 2 std dev"]{119.55, 116.06, 104.02, 103.64, 103.85, 114.01}
curve stddev1_high["main EWMA + 1 std dev"]{109.77, 108.03, 102.01, 101.82, 101.93, 107.00}
curve stddev1_low["main EWMA - 1 std dev"]{90.23, 91.97, 97.99, 98.18, 98.07, 93.00}
curve stddev2_low["main EWMA - 2 std dev"]{80.45, 83.94, 95.98, 96.36, 96.15, 85.99}
curve branch["#8002"]{90.92, 88.82, 86.77, 87.03, 87.86, 88.31}
graticule polygon
max 134
min 66
ticks 0
showLegend false
|
Now that MbedTLS is gone from the tree, the TLS layer no longer needs to emulate its style. This reworks
ccf::tls::ContextandTLSSessionto use OpenSSL idioms directly, removing the callback machinery and the "negative return" overloading. Net ~330 fewer lines.Changes
context.h):set_bio()attaches plainBIO_s_memread/write BIOs to theSSL.TLSSessionfeeds inbound ciphertext viarecv()and drains outbound ciphertext viapending_write()/send()after each SSL operation. No moresend_callback/recv_callbackindirection.context.h,tls.h):handshake/read/write/closereturn0on success or an OpenSSLSSL_ERROR_*code (fromSSL_get_error); bytes transferred are reported through an out-param. Removes the negative-return hack. OnlyTLS_ERR_X509_VERIFYremains as a CCF-specific sentinel to distinguish a handshake cert-verification failure (reported by OpenSSL as a genericSSL_ERROR_SSL) so callers can treat it as an auth failure.context.h): each SSL operation clears unrelated thread-local errors before calling OpenSSL, as required for reliableSSL_get_error()classification.tls_session.h):read/flush/do_handshake/closeswitch onSSL_ERROR_WANT_READ/WANT_WRITE/ZERO_RETURN/etc. instead of inspecting negated/overloaded returns. Outbound ciphertext is drained intopending_outand retried on ring-buffer backpressure. Deletes the obsoletehandle_send/handle_recv/*_callback_openssl/write_somepaths.PlaintextServer(plaintext_server.h): updated to the new virtual signatures over its own pair of memory BIOs.tls/test/main.cpp): rewritten from a threaded socketpair + callback BIOs to a single-threaded BIO pump; all existing cases preserved. Added coverage for stale errors from unrelated OpenSSL operations, which must be cleared before calls interpreted bySSL_get_error().tls/README.md,architecture/tls_internals.rst): describe the memory-BIO design and status-code contract; drop the MbedTLS-emulation and "future pure-OpenSSL" sections.Read path, before/after
Notes for reviewers
peer_cert_ok()onSSL_ERROR_SSL) is the one place where OpenSSL's status is reinterpreted.tls_test, andprogrammability_and_jwtpass locally.scripts/ci-checks.shand all required PR CI jobs are green.