YUME Internet freedom by FixCraft
On this page

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

  1. 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.
  2. Sanitize and review the fixture. Record exact binary hashes and preserve measured distributions instead of inventing stable timing constants.
  3. Add a new fixture directory and a new registry entry. Never rewrite the evidence behind an existing profile ID.
  4. Fill in openssl_selection for 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_suites Offered suites, in order. Split across SSL_CTX_set_ciphersuites (TLS 1.3) and SSL_CTX_set_cipher_list (TLS 1.2); both preserve order.
    signature_algorithms Offered schemes, in order. JA4 hashes these in order, so the sequence is load-bearing, not just the set.
    supported_groups Offered groups, in order.
    key_share_groups Subset of supported_groups that must carry a key_share. OpenSSL generates a share only for groups whose name carries a * prefix in SSL_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 in supported_groups.
    cert_compression RFC 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-chrome151 therefore requires Brotli and fails closed without it; openssl-diagnostic may log its declared degradation.
    extensions The target extension set. Not an emission order: see below.
    injected_extensions Extensions OpenSSL will not emit itself, each with a body shape. In openssl-diagnostic, "GREASE" allocates a type when the SSL_CTX is configured. openssl-chrome151 skips those two registrations because the patched library supplies distinct first/last values per connection.
    alps_protocols Body for the injected 0x44cd extension.
    ech_grease_lengths Permitted total lengths for the injected 0xfe0d GREASE ECH body, taken from the capture.
    no_encrypt_then_mac Suppresses 0x0016, which OpenSSL offers by default and browsers generally do not.
    status_request "ocsp" emits 0x0005, which add_custom_ext cannot because OpenSSL owns that number internally.
    min_version / max_version The 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_version The version the handshake must actually end on. Keeps a browser-shaped offer from becoming a carrier downgrade; enforced in handshake_with_timeout and fails closed.

    Stock extension emission order is not configurable. The openssl-diagnostic gate therefore targets JA4 and pins its fixed order as a negative control. openssl-chrome151 serializes 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. ChromeNativeWireTests checks 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.

  5. Fill in cover_server_tls, the cover server’s own cipher preference: tls13_cipher_suites in its order and tls12_cipher_list as its OpenSSL cipher string. yumed applies both over the client’s order, as a server with Node’s default honorCipherOrder does. The generator refuses a TLS 1.3 order that does not pick the suite the captured ServerHello chose from the captured ClientHello.
  6. Record the stock diagnostic gap in known_tls_divergence and let the tests pin it. scripts/generate_transport_profiles.py compares the four declared set fields against the capture, and tests/test_yume_native_tls_wire.py re-derives those plus the ec_point_formats and key_share geometry 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.
  7. Regenerate and validate:

    python3 scripts/generate_transport_profiles.py
    python3 scripts/generate_transport_profiles.py --check
    python3 tests/test_project_metadata.py
    
  8. 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.