Skip to content

Add stream_get_channel_binding - #24035

Draft
Sjord wants to merge 6 commits into
php:masterfrom
Sjord:stream_get_channel_binding
Draft

Sjord wants to merge 6 commits into
php:masterfrom
Sjord:stream_get_channel_binding

Conversation

@Sjord

@Sjord Sjord commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Related to #16766

The branch adds a new function to the openssl extension:

stream_get_channel_binding(resource $stream, string $channel_binding_type): ?string

It extracts TLS channel binding data from an established tls:// socket stream. This unblocks SASL channel binding (SCRAM-SHA-*-PLUS per RFC 5802 and RFC 5801) for clients written in PHP, which previously had no way to obtain the binding material that those mechanisms require.

The accepted binding types are the names from the IANA "Channel Binding" registry, matching the spelling and semantics used by CPython's _ssl.get_channel_binding().

Parameter Type Description
stream resource An open stream using an openssl transport (ssl:// / tls://). Must be in the connected/active state.
channel_binding_type string One of "tls-unique", "tls-server-end-point", "tls-exporter" (case-sensitive).

Return value

  • On success, the channel binding data as a binary string.
  • null when the requested type is not applicable to the connection
    (currently: "tls-unique" over TLS 1.3, where the type is not defined).

Errors / exceptions

Condition Exception
channel_binding_type is not one of the three known types ValueError (checked before the stream is inspected)
stream is not a stream resource TypeError
stream is a stream but has no active TLS transport RuntimeException — "Stream does not have transport encryption enabled"
An OpenSSL call fails while computing the value RuntimeException — "Failed to get channel binding data: <OpenSSL error>"

Supported channel binding types

Type Standard Value Length TLS applicability
tls-unique RFC 5929 §3 The TLS Finished message content (the "tls-unique" channel binding) variable (length of the Finished message, e.g. 12 for PRF-SHA-1, 32 for PRF-SHA-256) TLS 1.2 and earlier only; not applicable over TLS 1.3 → returns null
tls-server-end-point RFC 5929 §4.1 Digest of the server certificate (DER encoding) size of the chosen digest (32 for SHA-256-signed certs) All versions where the peer presents a certificate
tls-exporter RFC 9266 §4 / RFC 5705 32 octets derived from the TLS exporter with label EXPORTER-Channel-Binding and an empty context 32 Primarily TLS 1.3 (exporter is defined there); also computed on earlier versions where the exporter is available

Notes on semantics

  • tls-unique selection. In a full handshake both endpoints transmit a
    Finished message; in a resumed handshake only the server does. The
    implementation picks between the local SSL_get_finished() and
    SSL_get_peer_finished() using is_client ^ SSL_session_reused(), so that the
    client and the server always derive the same value. This mirrors the
    selection made by CPython's _ssl.get_channel_binding().
  • tls-server-end-point digest choice. The digest algorithm is taken from
    the certificate's own signature algorithm. If that algorithm is MD5 or SHA-1,
    or cannot be determined, SHA-256 is used instead (as mandated by
    RFC 5929 §4.1). For a SHA-256-signed certificate the result equals
    openssl_x509_fingerprint($cert, "sha256", true). On the client side the peer
    certificate is used; on the server side the local certificate is used.
  • tls-exporter. Computed with SSL_export_keying_material() using a 32
    octet output, the label EXPORTER-Channel-Binding and an empty context
    (RFC 9266 §4 / RFC 5705).

Example

Obtaining each binding on a connected client and using one as the SASL
cb property for a SCRAM-SHA-256-PLUS negotiation:

$ctx = stream_context_create([
    'ssl' => [
        'verify_peer' => true,
    ],
]);
$client = stream_socket_client(
    'ssl://example.com:587', $errno, $errstr, 30,
    STREAM_CLIENT_CONNECT, $ctx
);

// tls-exporter is the most broadly applicable choice (defined for TLS 1.3).
$cb = stream_get_channel_binding($client, 'tls-exporter');
if ($cb === null) {
    // e.g. tls-unique over TLS 1.3 — pick another type instead.
    $cb = stream_get_channel_binding($client, 'tls-server-end-point');
}

// $cb is the raw binding data; base64-encode it for the SCRAM "cb" property.
$scramCb = base64_encode($cb);

Comment thread ext/openssl/tests/stream_get_channel_binding_errors.phpt Outdated
Comment thread ext/openssl/xp_ssl.c Outdated
@Neustradamus

Copy link
Copy Markdown

@Sjord: Thanks a lot for your work about Channel Binding!

It will be not possible to backport to previous PHP versions too?

@TimWolla

TimWolla commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

The branch adds a new function to the openssl extension:

and

stream_get_channel_binding(resource $stream, string $channel_binding_type): ?string

If it's part of OpenSSL, it must have an openssl_ prefix or sit in the Openssl namespace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants