# RFC: openssl_get_channel_binding()

- Status: Under discussion
- Authors: Sjoerd Langkemper, sjoerd-php@linuxonly.nl
- Date: 2026-10-30
- Version: 0.2
- Implementation: https://github.com/php/php-src/pull/24035
- Original: https://wiki.php.net/rfc/openssl_get_channel_binding
- Page: https://externals.io/rfc/openssl_get_channel_binding
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

## Votes

### Add openssl_get_channel_binding() and Openssl\ChannelBindingType as outlined in the RFC? (closed)

Yes: 0 · No: 0 · Abstain: 0

## Discussions on the mailing list

- [\[RFC\] openssl_get_channel_binding](https://externals.io/message/132941.md) — started on 2026-10-27, 3 messages

---

## 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 [RFC 5802](https://www.rfc-editor.org/rfc/rfc5802) and [RFC 5801](https://www.rfc-editor.org/rfc/rfc5801). 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.

```php
<?php
 
$binding = openssl_get_channel_binding(
    $stream,
    Openssl\ChannelBindingType::TlsExporter,
);
 
// SCRAM transports the channel binding data as its base64-encoded "cb" property.
$cb = base64_encode($binding);
```

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 [php-src issue #16766](https://github.com/php/php-src/issues/16766).

## Proposal

Add the following function to ext/openssl, and the following exception class and enum to the existing `Openssl` namespace:

```php
<?php
 
namespace Openssl {
    class UnsupportedChannelBindingException extends OpensslException
    {
    }
 
    enum ChannelBindingType: string {
        case TlsUnique = 'tls-unique';
        case TlsServerEndPoint = 'tls-server-end-point';
        case TlsExporter = 'tls-exporter';
    }
}
 
namespace {
    /**
     * @param resource $stream
     */
    function openssl_get_channel_binding(
        $stream,
        Openssl\ChannelBindingType $channel_binding_type
    ): string {}
}
```

### 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 non-null `string`. Callers that need the SASL `cb` property must base64-encode it. It never returns `null`: when the requested type is not applicable to the connection, it throws `Openssl\UnsupportedChannelBindingException`. This happens for `tls-unique` over TLS 1.3, where that binding type is not defined, and for `tls-server-end-point` when no certificate is available, for example in a PSK handshake without a certificate.

### Changes from the original pull request

The originally published version of [pull request #24035](https://github.com/php/php-src/pull/24035) accepted a string as the second parameter, returned `?string`, and raised `Openssl\OpensslException` for a stream without transport encryption. The implementation has been updated to match this RFC: it takes an `Openssl\ChannelBindingType`, returns `string`, throws `Openssl\UnsupportedChannelBindingException` for inapplicable types, and throws `ValueError` for a stream without transport encryption.

## Supported channel binding types

| Type | Standard | Value and applicability |
|---|---|---|
| `tls-unique` | [RFC 5929 section 3](https://www.rfc-editor.org/rfc/rfc5929#section-3) | The Finished message content of the relevant handshake. Defined for TLS 1.2 and earlier, but not for TLS 1.3. Throws `Openssl\UnsupportedChannelBindingException` on TLS 1.3. |
| `tls-server-end-point` | [RFC 5929 section 4.1](https://www.rfc-editor.org/rfc/rfc5929#section-4.1) | A digest of the server certificate as it appears in the Certificate message. Requires a certificate. Throws `Openssl\UnsupportedChannelBindingException` when no certificate is available, for example in a PSK handshake without a certificate. |
| `tls-exporter` | [RFC 9266 section 4](https://www.rfc-editor.org/rfc/rfc9266#section-4), [RFC 5705](https://www.rfc-editor.org/rfc/rfc5705) | 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`; requesting it on a TLS 1.3 connection throws `Openssl\UnsupportedChannelBindingException`.

### 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, for example in a PSK handshake without a certificate, the function throws `Openssl\UnsupportedChannelBindingException`. Otherwise the binding is computed from the available certificate.

### 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 (no crypto enabled, or the handshake has not completed) | `ValueError` with the message `Stream does not have transport encryption enabled` |
| The requested type is not applicable to the connection | `Openssl\UnsupportedChannelBindingException`. This happens for `tls-unique` over TLS 1.3 and for `tls-server-end-point` when no certificate is available, for example in a PSK handshake without a certificate. |
| An OpenSSL call fails while computing the value | `Openssl\OpensslException` with the message `Failed to get channel binding data: <OpenSSL error>`, or `Failed to get channel binding data` if OpenSSL provides no error detail |

`Openssl\UnsupportedChannelBindingException` extends `Openssl\OpensslException`, so code that catches `Openssl\OpensslException` still catches all OpenSSL-related failures while code that specifically handles an inapplicable binding type can catch the subclass.

There is currently no `Openssl\OpensslError` class. This RFC intentionally does not add one. Calling the function on a stream without crypto enabled is an invalid argument value, which is what `ValueError` expresses. Introducing a new `Error` hierarchy for ext/openssl is a larger API decision that would affect more than this function, and none of the existing ext/openssl APIs currently use an `Error` subclass. A dedicated exception class for the inapplicable-type case is justified because callers are expected to handle it and choose another binding type; a dedicated class for the non-TLS case is not.

## 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. Unlike CPython, it throws an exception instead of returning `null` when the requested type is not applicable, so a successful call always returns usable binding data.

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

```php
<?php
 
$context = stream_context_create([
    'ssl' => [
        '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");
}
 
try {
    $binding = openssl_get_channel_binding(
        $stream,
        Openssl\ChannelBindingType::TlsUnique,
    );
} catch (Openssl\UnsupportedChannelBindingException) {
    // 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

```php
<?php
 
foreach (Openssl\ChannelBindingType::cases() as $type) {
    try {
        $binding = openssl_get_channel_binding($stream, $type);
    } catch (Openssl\UnsupportedChannelBindingException $e) {
        printf("%s: not applicable (%s)\n", $type->value, $e->getMessage());
        continue;
    }
 
    printf("%s: %s\n", $type->value, bin2hex($binding));
}
```

### Mapping a configured name to the enum

```php
<?php
 
$configured = 'tls-server-end-point';
 
try {
    $type = Openssl\ChannelBindingType::from($configured);
} catch (ValueError $e) {
    throw new InvalidArgumentException(
        "Unsupported channel binding type: $configured",
        previous: $e,
    );
}
 
$binding = openssl_get_channel_binding($stream, $type);
```

## 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. An `Openssl\UnsupportedChannelBindingException` means that no binding is available for that type; applications must not swallow it and continue authentication without a binding, and must not treat an unsupported 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 would fail. This is explicitly acceptable for the reserved `openssl_*` namespace; 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.
- The class name `Openssl\UnsupportedChannelBindingException` is new. It extends the existing `Openssl\OpensslException`. 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.

Other extensions that use a PHP stream backed by ext/openssl could in principle call the same internal helper and use channel binding for their own authentication protocols, but none currently does so. In practice there is little opportunity: PDO PostgreSQL performs TLS and channel binding inside libpq, so it cannot use the PHP stream implementation, and although mysqlnd uses PHP streams, MySQL and MariaDB do not define a channel binding mechanism. The new function therefore does not create an immediate integration point for existing extensions; it primarily exposes the data to userland.

### 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:

**Vote: Add openssl\_get\_channel\_binding() and Openssl\\ChannelBindingType as outlined in the RFC?** — Yes: 0 · No: 0 · Abstain: 0 (closed)

## Patches and Tests

The implementation is [pull request #24035](https://github.com/php/php-src/pull/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, and checks of `tls-server-end-point` against the certificate fingerprint. A TLS-PSK test verifies that `tls-server-end-point` throws `Openssl\UnsupportedChannelBindingException` when no certificate is available. The tests also cover the error conditions, including `ValueError` for a stream without transport encryption and the exception hierarchy. The implementation and tests match the API described in this RFC.

The implementation has also been tested manually against Prosody and PostgreSQL.

## Implementation

After the RFC is implemented, this section should contain:

1. the version(s) it was merged into
2. a link to the git commit(s)
3. a link to the PHP manual entry for the feature

## References

- Original issue: [php-src #16766](https://github.com/php/php-src/issues/16766)
- Pull request: [php-src #24035](https://github.com/php/php-src/pull/24035)
- RFC 5056: [On the Use of Channel Bindings to Secure Channels](https://www.rfc-editor.org/rfc/rfc5056)
- RFC 5929: [Channel Bindings for TLS](https://www.rfc-editor.org/rfc/rfc5929)
- RFC 9266: [Channel Bindings for TLS 1.3](https://www.rfc-editor.org/rfc/rfc9266)
- RFC 5705: [Keying Material Exporters for TLS](https://www.rfc-editor.org/rfc/rfc5705)
- RFC 5801: [Using GSS-API Mechanisms in SASL](https://www.rfc-editor.org/rfc/rfc5801)
- RFC 5802: [SCRAM: Salted Challenge Response Authentication Mechanism](https://www.rfc-editor.org/rfc/rfc5802)
- RFC 7677: [SCRAM-SHA-256 and SCRAM-SHA-256-PLUS](https://www.rfc-editor.org/rfc/rfc7677)
- IANA Channel-Binding Types registry: [IANA registry](https://www.iana.org/assignments/channel-binding-types/channel-binding-types.xhtml)
- CPython `ssl.SSLSocket.get_channel_binding()`: [Python documentation](https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.get_channel_binding)
- fabiang/sasl channel binding discussion: [fabiang/sasl #11](https://github.com/fabiang/sasl/issues/11)

## Rejected Features

- **String parameter.** An earlier version of the implementation accepted 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 `null` for an inapplicable type.** It would force every call site to check for `null` and would make a successful return ambiguous. `tls-unique` over TLS 1.3 and `tls-server-end-point` without a certificate are protocol-level incompatibilities, not silent successes. The dedicated `Openssl\UnsupportedChannelBindingException` lets callers explicitly choose another type.
- **Adding an `Openssl\OpensslError` class.** No such class exists today. A stream without crypto enabled is an invalid argument value, so the standard `ValueError` is sufficient. Introducing a new `Error` hierarchy is a broader API decision that should not be part of this RFC.
- **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.
- 0.2 (2026-10-30): Throw `Openssl\UnsupportedChannelBindingException` for inapplicable binding types instead of returning `null`; return `string` instead of `?string`; use `ValueError` for streams without transport encryption; document impact on existing extensions; update the implementation and tests to match.
