====== PHP RFC: openssl_get_channel_binding() ====== * Version: 0.1 * Date: 2026-10-27 * Author: Sjoerd Langkemper, sjoerd-php@linuxonly.nl * Status: Draft * Implementation: https://github.com/php/php-src/pull/24035 * Discussion thread: tbd * Voting thread: tbd ===== Introduction ===== This RFC proposes adding ''openssl_get_channel_binding()'', a function in the OpenSSL extension that returns the TLS channel binding data for a connected TLS stream, together with the ''Openssl\ChannelBindingType'' enum that selects one of the three supported binding types. Channel binding ties an application-layer authentication exchange to the underlying TLS connection. This prevents an attacker who can relay a TLS connection from relaying the authentication messages as well. SASL uses channel binding in the SCRAM-SHA-*-PLUS mechanisms defined by [[https://www.rfc-editor.org/rfc/rfc5802|RFC 5802]] and [[https://www.rfc-editor.org/rfc/rfc5801|RFC 5801]]. PHP userland libraries already implement SCRAM, but they cannot implement the -PLUS variants because PHP exposes no way to obtain the binding material from a TLS stream. The missing API affects userland libraries and applications such as PEAR Net_SMTP / Auth_SASL / Auth_SASL2, Roundcube, SnappyMail, SquirrelMail, Horde, and others that currently support only the non-PLUS SCRAM mechanisms. The original request is [[https://github.com/php/php-src/issues/16766|php-src issue #16766]]. ===== Proposal ===== Add the following function to ext/openssl, and the following enum to the existing ''Openssl'' namespace: ==== Parameters ==== * ''$stream'' is a connected stream that uses an OpenSSL transport, such as ''ssl://'', ''tls://'', or a version-specific transport such as ''tlsv1.2://''. The TLS handshake must have completed. The function does not accept a stream context, because a context can create multiple connections and the channel binding is a property of one specific connection. * ''$channel_binding_type'' selects the binding type. It is required; there is no default, because the correct type is determined by the application protocol and by the SASL mechanism being used. The function may be called on either the client or the server stream. It does not perform a handshake itself. ==== Return value ==== The function returns the raw, binary channel binding data as a string. Callers that need the SASL ''cb'' property must base64-encode it. The function returns ''null'' when the requested type is not applicable to the connection. Currently this happens only for ''tls-unique'' over TLS 1.3, where that binding type is not defined. ==== Difference from the current pull request ==== The implementation in [[https://github.com/php/php-src/pull/24035|pull request #24035]] currently accepts a string as the second parameter and raises ''ValueError'' for an unknown value. This RFC proposes an enum instead. The pull request will be updated to the signature above before the vote. ===== Supported channel binding types ===== ^ Type ^ Standard ^ Value and applicability ^ | ''tls-unique'' | [[https://www.rfc-editor.org/rfc/rfc5929#section-3|RFC 5929 section 3]] | The Finished message content of the relevant handshake. Defined for TLS 1.2 and earlier, but not for TLS 1.3. Returns ''null'' on TLS 1.3. | | ''tls-server-end-point'' | [[https://www.rfc-editor.org/rfc/rfc5929#section-4.1|RFC 5929 section 4.1]] | A digest of the server certificate as it appears in the Certificate message. Applicable whenever a certificate is available. | | ''tls-exporter'' | [[https://www.rfc-editor.org/rfc/rfc9266#section-4|RFC 9266 section 4]], [[https://www.rfc-editor.org/rfc/rfc5705|RFC 5705]] | 32 octets from the TLS exporter, using the label ''EXPORTER-Channel-Binding'' and an empty context. Defined for TLS 1.3 and also available on earlier TLS versions when the exporter is available. | ===== Semantics ===== ==== tls-unique ==== The value is the Finished message content of the most recent handshake, not the TLS record containing it. In a full handshake the client sends the first Finished message; in a resumed handshake the server sends the first one. The implementation selects between the local and peer Finished message using the same rule as CPython's ''ssl.SSLSocket.get_channel_binding()'', so both endpoints derive the same value. The value can change after a TLS renegotiation. Applications using ''tls-unique'' must not renegotiate during authentication, as required by RFC 5929 section 3.1. TLS 1.3 does not use renegotiation and does not define ''tls-unique''. ==== tls-server-end-point ==== 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 required by RFC 5929 section 4.1. For a SHA-256-signed certificate the result is identical to ''openssl_x509_fingerprint($cert, "sha256", true)''. On the client side the peer certificate is used. On the server side the local certificate is used. If no certificate is available, the function throws an exception. ==== tls-exporter ==== The value is produced with OpenSSL's ''SSL_export_keying_material()'' using a 32-octet output, the label ''EXPORTER-Channel-Binding'', and an empty context. This is the TLS 1.3 channel binding type from RFC 9266; it can also be computed on TLS 1.2 when OpenSSL supports the exporter for the connection. ===== Errors and exceptions ===== ^ Condition ^ Exception ^ | ''$stream'' is not a stream resource | ''TypeError'' | | ''$channel_binding_type'' is not an ''Openssl\ChannelBindingType'' | ''TypeError''. A string such as ''"tls-unique"'' is not accepted. | | ''$stream'' is a stream, but it has no active OpenSSL TLS transport | ''Openssl\OpensslException'' with the message ''Stream does not have transport encryption enabled'' | | An OpenSSL call fails while computing the value | ''Openssl\OpensslException'' with the message ''Failed to get channel binding data: '' | Because the second parameter is an enum, the function itself no longer has an unknown-type error. Code that obtains the name from configuration or from a protocol message can map it with ''Openssl\ChannelBindingType::from()'', which raises ''ValueError'' for an unknown name, or with ''tryFrom()'' for a non-throwing variant. The backing values are the exact IANA names and, like those names, ''from()'' is case-sensitive. ===== Why an enum instead of a string ===== This is a closed set: PHP implements exactly three computations, and each corresponds to an RFC-defined type. A string parameter suggests that arbitrary IANA names, or names implemented by another TLS library, can be passed, which is not the case. The enum makes the supported set explicit. * A typo or a case mismatch becomes a type error at the call site instead of a runtime ''ValueError'' inside the function. * IDEs, static analysers, and language servers can offer completion and check the argument type. * The implementation no longer needs a hand-written string parser and its associated error message. * The backing values preserve interoperability: ''$type->value'' is the IANA name, so it can be forwarded to SASL or other protocol code unchanged. * Recent PHP APIs use enums for comparable closed sets, for example ''ReflectionProperty::hasHook(PropertyHookType $type)'', ''Dom\Element::insertAdjacentElement(AdjacentPosition $where, ...)'', and ''Io\Poll\Event''. The enum is the only accepted type; this RFC does not propose ''Openssl\ChannelBindingType|string''. A union would preserve the typo problem and add a second code path. It would only be justified for backward compatibility, and this function has no prior released API. The IANA registry also contains ''tls-unique-for-telnet''. That type is specific to Telnet and is not used by SASL, so this RFC intentionally does not expose it. It can be added as a new enum case if a concrete use case appears. ===== Prior art ===== CPython's ''ssl.SSLSocket.get_channel_binding()'' returns the binding data as bytes, or ''None'' when no data is available (for example, ''tls-unique'' over TLS 1.3). It currently implements only ''tls-unique'', selected by its IANA name string. This RFC keeps the same names as the enum backing values, but makes the parameter type-safe and adds the other two TLS types. OpenSSL exposes the underlying primitives through ''SSL_get_finished()'', ''SSL_get_peer_finished()'', ''SSL_export_keying_material()'', and its certificate digest functions. PHP does not expose these through the stream API, so applications currently need FFI or a custom extension to obtain the data. ===== Examples ===== ==== Obtaining the binding data for SASL ==== [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $stream = stream_socket_client( 'tls://example.com:465', $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context, ); if ($stream === false) { throw new RuntimeException("Connection failed: $errstr"); } $binding = openssl_get_channel_binding( $stream, Openssl\ChannelBindingType::TlsUnique, ); if ($binding === null) { // tls-unique is not defined for TLS 1.3; use tls-exporter instead. $binding = openssl_get_channel_binding( $stream, Openssl\ChannelBindingType::TlsExporter, ); } $cb = base64_encode($binding); ==== Inspecting all supported types ==== value, $binding === null ? 'not applicable' : bin2hex($binding), ); } ==== Mapping a configured name to the enum ==== ===== Security considerations ===== Channel binding data is not a secret: RFC 5929 section 10 explicitly allows authentication mechanisms to disclose it. The value is nevertheless security-relevant, because it is what prevents a relay attack. Applications must use the same type that the negotiated SASL mechanism expects and must not treat a missing binding as an authenticated connection. ''tls-exporter'' is the appropriate choice for TLS 1.3. ''tls-unique'' is only meaningful for TLS 1.2 and earlier. ''tls-server-end-point'' is useful when the application needs a value that does not change after renegotiation and when a server certificate is available. ===== Backward Incompatible Changes ===== * The global function name ''openssl_get_channel_binding'' is new. Userland code that declares a function with the same name without a ''function_exists()'' guard would fail with "Cannot redeclare function openssl_get_channel_binding()". No existing conflicts are known. * The class name ''Openssl\ChannelBindingType'' is new. The ''Openssl'' namespace is already used by ext/openssl for ''Openssl\OpensslException'', ''Openssl\Psk'', and ''Openssl\Session''. Userland code that declares the same fully qualified class name would conflict when ext/openssl is loaded. No existing conflicts are known. * No existing functions, classes, constants, or INI settings are changed. ===== Proposed PHP Version(s) ===== Next minor version (PHP 8.7). This is a new feature and is not eligible for backporting to a stable branch under the release policy. ===== RFC Impact ===== ==== To the Ecosystem ==== IDEs, language servers, and static analysers need to add the new function and enum to their stubs. Userland SASL libraries can use ''function_exists('openssl_get_channel_binding')'' to detect support and ''Openssl\ChannelBindingType::from()'' to map a negotiated or configured binding type. The function cannot be fully polyfilled in userland because the binding data is only available from the TLS implementation. ==== To Existing Extensions ==== Only ext/openssl is affected. No existing extension APIs or data structures change. The function is available only when ext/openssl is compiled and loaded. ==== To SAPIs ==== None. The CLI, the built-in web server, embedded PHP, and other SAPIs behave the same. ===== Open Issues ===== None at this time. ===== Future Scope ===== * Support for ''tls-unique-for-telnet'', if a concrete use case appears. It is not part of this proposal because it is Telnet-specific. * Support for TLS connections created by other extensions or by a future common TLS abstraction. This RFC intentionally implements the function in ext/openssl, because the current stream API does not expose the required TLS state in a backend-independent way. * Additional IANA channel binding types, if they are registered and applicable to PHP streams. New values can be added as enum cases. ===== Voting Choices ===== Primary Vote requiring a 2/3 majority to accept the RFC: * Yes * No * Abstain ===== Patches and Tests ===== The proof-of-concept implementation is [[https://github.com/php/php-src/pull/24035|pull request #24035]]. It contains PHPT tests for TLS 1.2 and TLS 1.3, including checks that the client and server derive the same ''tls-unique'' and ''tls-exporter'' values, checks of ''tls-server-end-point'' against the certificate fingerprint, and tests for the error conditions. The PR will be updated for the enum parameter. The implementation has also been tested manually against Prosody and PostgreSQL. ===== Implementation ===== After the RFC is implemented, this section should contain: - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature ===== References ===== * Original issue: [[https://github.com/php/php-src/issues/16766|php-src #16766]] * Pull request: [[https://github.com/php/php-src/pull/24035|php-src #24035]] * RFC 5056: [[https://www.rfc-editor.org/rfc/rfc5056|On the Use of Channel Bindings to Secure Channels]] * RFC 5929: [[https://www.rfc-editor.org/rfc/rfc5929|Channel Bindings for TLS]] * RFC 9266: [[https://www.rfc-editor.org/rfc/rfc9266|Channel Bindings for TLS 1.3]] * RFC 5705: [[https://www.rfc-editor.org/rfc/rfc5705|Keying Material Exporters for TLS]] * RFC 5801: [[https://www.rfc-editor.org/rfc/rfc5801|Using GSS-API Mechanisms in SASL]] * RFC 5802: [[https://www.rfc-editor.org/rfc/rfc5802|SCRAM: Salted Challenge Response Authentication Mechanism]] * RFC 7677: [[https://www.rfc-editor.org/rfc/rfc7677|SCRAM-SHA-256 and SCRAM-SHA-256-PLUS]] * IANA Channel-Binding Types registry: [[https://www.iana.org/assignments/channel-binding-types/channel-binding-types.xhtml|IANA registry]] * CPython ''ssl.SSLSocket.get_channel_binding()'': [[https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.get_channel_binding|Python documentation]] * fabiang/sasl channel binding discussion: [[https://github.com/fabiang/sasl/issues/11|fabiang/sasl #11]] ===== Rejected Features ===== * **String parameter.** The current pull request accepts one of the strings ''"tls-unique"'', ''"tls-server-end-point"'', or ''"tls-exporter"''. This RFC uses an enum instead, for the reasons described under "Why an enum instead of a string". * **''Openssl\ChannelBindingType|string''.** A union would accept both forms, but it would reintroduce the possibility of a typo and require a second parsing path. It is only needed for backward compatibility, which does not apply to a new API. * **Integer constants.** Global constants such as ''OPENSSL_CHANNEL_BINDING_TLS_UNIQUE'' would be less discoverable and would not give the parameter a precise type. * **Accepting a stream context.** The original issue suggested accepting either a stream or a stream context. A context can create multiple connections, so it does not identify one channel. The function therefore requires a connected stream. * **Returning an exception for an inapplicable type.** ''tls-unique'' over TLS 1.3 is a normal protocol combination, not an error. Returning ''null'' lets callers select another type without catching an exception. * **Supporting ''tls-unique-for-telnet''.** It is registered by IANA but is specific to Telnet and is not used by SASL. * **Naming the function ''stream_get_channel_binding()''.** The implementation requires ext/openssl internals and cannot work for arbitrary streams, so the OpenSSL prefix is more accurate. This also follows the naming feedback on the pull request. * **The spelling ''tls-server-endpoint''.** The IANA name is ''tls-server-end-point''. ===== Changelog ===== * 0.1 (2026-10-27): Initial draft.