Transport profile architecture
YUME separates its captured transport identity from whatever browser is
currently installed on the host. Browser auto-updates must not silently change
wire behavior. The profile ID is not a YTP/1 field and enters no key or
admission proof, but it selects the exact TLS, HTTP/2 and cover geometry that
yume and yumed present. Changing its meaning in place would change what
observers see without a new capture behind it. The relay channel’s ratchet
binds the string chrome151-node24-v1 as a fixed label, which does not follow
the active profile.
Sources of truth
| Surface | Role |
|---|---|
config/transport_profiles.json |
Registered profiles and the active profile |
| Registry-selected fixture manifest | Exact browser, OS, cover runtime, binaries, hashes, and evidence provenance |
| Registry-selected HTTP/2 profile | Capture-derived HTTP/2, HTTP, WebSocket, and shaping values |
| Registry-selected TLS profile and candidates | Normalized ordered TLS first-flight acceptance profile and samples |
src/stealth/transport_profiles.inc |
Generated immutable C++ registry; never edit manually |
Each entry names its own artifact files; shared generator, CMake, test, install,
and release logic does not depend on Chrome-specific filenames.
scripts/generate_transport_profiles.py validates bounded schemas, fixture and
artifact path containment, unique profile and alias identities, required
evidence fields, and header geometry. It requires the registry’s active ID to
equal kEvidenceProfile in src/common/version.hpp, then generates the C++
registry consumed through cover_profile::active(). TLS/HTTP/H2 consumers
contain no browser-version branches.
The H2 carrier currently requires exactly two assets and the
captured stream sequence 1/3/5/7/9 (priming, CSS, JavaScript, extended CONNECT
and the favicon request Chrome sends once the WebSocket is open). The
generator takes the favicon request’s headers and priority from the stream-9
entry of the fixture’s observations.
The generator rejects any other geometry instead of admitting metadata that
H2Carrier cannot execute. Generalizing that carrier is a separate reviewed
change.
The generated files are committed so production builds do not acquire a Python
code-generation dependency. CI and release preflight run the generator in
--check mode and reject stale output.
The matched Gate B browser workload is separately frozen in
tools/cover-node/workload-v1.json. The direct HTTP/2 capture target and
classifier-input validator consume that file. It defines reference page assets
and the transfer/control/idle contract, not a second transport identity and not
the installed production HTTP/1 cover backend. Changing it requires new
matched captures and must not silently rewrite the existing
chrome151-node24-v1 evidence claim.
yume --outer-carrier-evidence PATH writes the YUME arm’s behavior report
from its first carrier’s OuterCarrierTrace when the run ends
(runtime/outer_carrier_evidence.*). The capture script for the YUME arm,
tools/cover-node/capture_yume151_runs.sh, starts yumed and yume from
one exact-commit release bundle with a kit that
scripts/yume_carrier_workload.py configure-kit prepared, puts the TLS wire
relay between them, and has that script drive the workload through yume’s
SOCKS5 port: 64 16-KiB messages to an echo target behind yumed, echoed
byte for byte, then the stream closes, the tunnel idles 42 seconds and a
SIGTERM closes yume. Each run keeps behavior.json, tls-wire.json and
the driver’s workload.json. yume carries whatever its applications send,
so the driver’s result, not the carrier report, binds the application
volume. The browser arm must use the kit’s server certificate and key so both
arms terminate with the same certificate. The behavior summary comes from
bounded live carrier events, and the registry only validates and redacts
expected metadata, never fabricating observed events. The observer reports
the actual outer WebSocket geometry, so the classifier may correctly return
DRIFT. The stable classifier projection keeps ordered request and WebSocket
lifecycle.
The YUME carrier reproduces normal Chrome’s stream-9 favicon request, while the
server’s PING-before-first-fragment relationship is compared even though the
YUME server does not reproduce it.
The transport and dependency registries are source/build metadata and are not installed as runtime examples. The transport registry contains repository-relative evidence paths; install rules copy only the active, flattened evidence files that installed diagnostics consume.
Adding or refreshing a browser profile
- Capture several normal-browser sessions in an isolated profile using exact browser, OS, server runtime, certificate/SNI/ALPN, and workload conditions. Keep raw NetLogs and PCAPs private.
- Sanitize and review the fixture. Record exact binary hashes and preserve measured distributions instead of inventing stable timing constants.
- Add a new fixture directory and a new registry entry. Never rewrite the evidence behind an existing profile ID.
-
Fill in
openssl_selectionfor the native backend. This lets the in-process OpenSSL path track the browser, and it is deliberately data rather than code so a new browser needs no C++ change:Key Meaning cipher_suitesOffered suites, in order. Split across SSL_CTX_set_ciphersuites(TLS 1.3) andSSL_CTX_set_cipher_list(TLS 1.2); both preserve order.signature_algorithmsOffered schemes, in order. JA4 hashes these in order, so the sequence is load-bearing, not just the set. supported_groupsOffered groups, in order. key_share_groupsSubset of supported_groupsthat must carry akey_share. OpenSSL generates a share only for groups whose name carries a*prefix inSSL_CTX_set1_groups_list, and defaults to the first group alone. A browser offering a hybrid and a classical share needs both named here. Every entry must appear insupported_groups.cert_compressionRFC 8879 algorithms for compress_certificate(0x001b), in preference order, by name (zlib,brotli,zstd). OpenSSL can only advertise an algorithm compiled into the library.openssl-chrome151therefore requires Brotli and fails closed without it;openssl-diagnosticmay log its declared degradation.extensionsThe target extension set. Not an emission order: see below. injected_extensionsExtensions OpenSSL will not emit itself, each with a body shape. In openssl-diagnostic,"GREASE"allocates a type when theSSL_CTXis configured.openssl-chrome151skips those two registrations because the patched library supplies distinct first/last values per connection.alps_protocolsBody for the injected 0x44cdextension.ech_grease_lengthsPermitted total lengths for the injected 0xfe0dGREASE ECH body, taken from the capture.no_encrypt_then_macSuppresses 0x0016, which OpenSSL offers by default and browsers generally do not.status_request"ocsp"emits0x0005, whichadd_custom_extcannot because OpenSSL owns that number internally.min_version/max_versionThe offered range. Browser-shaped profiles usually offer TLS 1.2 through 1.3. Offering only 1.3 silently drops the TLS 1.2 half of the cipher list and extension 0xff01.require_negotiated_versionThe version the handshake must actually end on. Keeps a browser-shaped offer from becoming a carrier downgrade; enforced in handshake_with_timeoutand fails closed.Stock extension emission order is not configurable. The
openssl-diagnosticgate therefore targets JA4 and pins its fixed order as a negative control.openssl-chrome151serializes each movable custom and built-in extension as an independent block and shuffles those blocks per SSL object. Padding remains second-to-last and PSK remains last; their constructors run on the final packet so padding length and the TLS 1.3 binder transcript stay correct.One further property has no registry key. Stock OpenSSL offers all three
ec_point_formats; the native Chrome patch emits only uncompressed ([0]) when its default-off control is enabled.The native emitted-byte gate groups the patch-only behavior into six pinned structure rows: per-connection GREASE, first/last GREASE extension placement, per-connection middle-extension shuffling, uncompressed-only point format, both captured non-GREASE key shares, and Brotli-only certificate compression.
ChromeNativeWireTestschecks those rows across 12 fresh SSL objects on one context, plus exact capture-selected fields and JA4. This is a ClientHello structure gate, not a six-test-method count or a whole-session parity claim. - Fill in
cover_server_tls, the cover server’s own cipher preference:tls13_cipher_suitesin its order andtls12_cipher_listas its OpenSSL cipher string.yumedapplies both over the client’s order, as a server with Node’s defaulthonorCipherOrderdoes. The generator refuses a TLS 1.3 order that does not pick the suite the captured ServerHello chose from the captured ClientHello. - Record the stock diagnostic gap in
known_tls_divergenceand let the tests pin it.scripts/generate_transport_profiles.pycompares the four declared set fields against the capture, andtests/test_yume_native_tls_wire.pyre-derives those plus theec_point_formatsandkey_sharegeometry from the bytes the production backend actually emits, and asserts that JA3 still fails to match. Both fail if the gap widens, so a divergence cannot be introduced silently. Closing all four set fields buys an exact JA4 and nothing wider, because sets are all JA4 hashes. -
Regenerate and validate:
python3 scripts/generate_transport_profiles.py python3 scripts/generate_transport_profiles.py --check python3 tests/test_project_metadata.py - Select the new profile ID in a deliberate profile change, update its registry, fixture tests and documentation, then run same-session capture, matched performance, sanitizer, classifier/active-probe, soak, packaging, and independent-review gates. A profile refresh does not change YTP/1, its key schedule or the relay channel’s frozen labels. A separate protocol change updates its own contract and vectors.
The registry can hold several immutable profiles, but the current transport
activates exactly the profile named by src/common/version.hpp. Runtime
selection of another profile is not implemented. Adding it would need an
explicit interface and evidence for each profile.
Installed-browser updates
An operating-system Chrome update does not require rewriting YUME or downgrading the daily browser. It only means that installed Chrome cannot be used as evidence for the older frozen profile. Reproduction should use an isolated, checksum-verified browser artifact or container/VM; a newly captured version becomes a new profile ID after its evidence passes.
Do not download a browser during a normal YUME connection or build. Do not silently map an unknown/current browser to the closest registered profile. Unsupported profiles fail closed.
BaseFWX dependency metadata
BaseFWX remains pinned for reproducible cryptographic builds, but its repository,
exact revision, and minimum compatible version now live once in
config/dependencies.json. CMake, local build scripts, CI, CodeQL, and release
preflight read that manifest through scripts/yume_dependencies.py; they must
not copy those values into another workflow.
The manifest accepts only an exact lowercase 40-hex commit for each dependency.
This removes update choreography without weakening reproducibility: updating BaseFWX is one reviewed manifest change followed by its own compatibility, security, ABI, and build validation, not an unbounded floating dependency.