Skip to content

Add openssl_get_channel_binding - #24035

Open
Sjord wants to merge 12 commits into
php:masterfrom
Sjord:stream_get_channel_binding
Open

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

Conversation

@Sjord

@Sjord Sjord commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

PHP RFC: openssl_get_channel_binding()

Related to #16766

The branch adds a new function to the openssl extension:

openssl_get_channel_binding(
    resource $stream,
    Openssl\ChannelBindingType $channel_binding_type
): ?string

The binding type is selected with a string-backed enum:

namespace Openssl {
    enum ChannelBindingType: string {
        case TlsUnique = 'tls-unique';
        case TlsServerEndPoint = 'tls-server-end-point';
        case TlsExporter = 'tls-exporter';
    }
}

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 enum case values are the names from the IANA "Channel Binding" registry. The tls-unique selection semantics follow CPython's _ssl.get_channel_binding(); this proposal additionally supports the other two TLS channel binding types.

Parameter Type Description
stream resource An open stream using an openssl transport (ssl:// / tls://). Must be in the connected/active state.
channel_binding_type Openssl\ChannelBindingType One of TlsUnique, TlsServerEndPoint, or TlsExporter. A string such as "tls-unique" is not accepted.

Return value

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

Errors / exceptions

Condition Exception
channel_binding_type is not an Openssl\ChannelBindingType TypeError
stream is not a stream resource TypeError
stream is a stream but has no active TLS transport Openssl\OpensslException — "Stream does not have transport encryption enabled"
An OpenSSL call fails while computing the value Openssl\OpensslException — "Failed to get channel binding data: <OpenSSL error>"

Code that stores the binding type as a string can map it with Openssl\ChannelBindingType::from($name), which raises ValueError for an unknown name. The enum backing values, like the IANA names, are case-sensitive.

Supported channel binding types

Enum case Standard Value Length TLS applicability
TlsUnique (tls-unique) RFC 5929 §3 The TLS Finished message content (the "tls-unique" channel binding) length of the Finished message TLS 1.2 and earlier only; not applicable over TLS 1.3 → returns null
TlsServerEndPoint (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
TlsExporter (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 a binding on a connected client and using it as the SASL
cb property for a SCRAM-SHA-256-PLUS negotiation:

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

$cb = openssl_get_channel_binding(
    $client,
    Openssl\ChannelBindingType::TlsUnique,
);
if ($cb === null) {
    // tls-unique is not defined for TLS 1.3; use tls-exporter instead.
    $cb = openssl_get_channel_binding(
        $client,
        Openssl\ChannelBindingType::TlsExporter,
    );
}

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

Comment thread ext/openssl/xp_ssl.c Resolved
@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.

@Sjord Sjord changed the title Add stream_get_channel_binding Add openssl_get_channel_binding Oct 5, 2026
@Sjord

Sjord commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

I tested this against Prosody and Postgres, which gave me some confidence that this works at least in some situations.

@Sjord
Sjord marked this pull request as ready for review October 7, 2026 07:19
@Sjord

Sjord commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

@iluuu1994 Could you take a look at this?

Is it OK to keep the strings ("tls-unique", "tls-server-end-point", "tls-exporter") or do these need to become enums or constants?

Comment thread ext/openssl/xp_ssl.c Resolved
Comment thread ext/openssl/php_openssl.h Resolved
Comment thread ext/openssl/openssl.c Resolved
Comment thread ext/openssl/xp_ssl.c Resolved
Comment thread ext/openssl/openssl.c Resolved
Comment thread ext/openssl/php_openssl.h Resolved
Comment thread ext/openssl/php_openssl.h Resolved
@Sjord

Sjord commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for reviewing, @TimWolla

@Neustradamus

Copy link
Copy Markdown

After the merging in master branch, it will be nice to backport it in other PHP branches starting 8.2.x.

The goal is to have a better security with PHP...

Several developers can not test it because it is not for PHP 8.2... :/

@iluuu1994

Copy link
Copy Markdown
Member

New features don't get backported to stable branches, especially not the ones in security-maintenance. Otherwise, what's the point of having different branches at all?

@Neustradamus

Copy link
Copy Markdown

@iluuu1994: But this PR adds an important security feature...

It will be nice to add in previous branches to have more security...

@Neustradamus

Copy link
Copy Markdown

@LamentXU123, @shivammathur, @ndossche: What do you think?

Security is very important and this feature is needed by a lot of people and projects.

Thanks in advance.

@iluuu1994

Copy link
Copy Markdown
Member

This is disallowed according to our policy.

https://github.com/php/policies/blob/main/release-process.rst

Patch Version Number

  • New features MUST NOT be added.

@Neustradamus

Copy link
Copy Markdown

So PHP 8.6.0 has not been released yet, it can be added into?

@iluuu1994

Copy link
Copy Markdown
Member

No, because 8.6 has reached feature freeze. Anything that conflicts with our policy would need some RFC to overrule the policy.

@TimWolla

Copy link
Copy Markdown
Member

this feature is needed by a lot of people and projects.

[citation needed]

@Neustradamus

Copy link
Copy Markdown

@TimWolla: A lot of projects have been linked here previously ^

Movim XMPP Social Network (creator @edhelas) can be added too:

There is this biggest ticket too:

@TimWolla

Copy link
Copy Markdown
Member

A lot of projects have been linked here previously ^

I do not see how the links support that claim. A barely two-digit number of projects is not “a lot”.

And if this feature was actually so important that it is needed right now, I wonder why no one, including yourself, bothered to implement it in the past 7 years (or whenever the first issue was created).

@Neustradamus

Copy link
Copy Markdown

@TimWolla: @fabiang has added the support in the SASL PHP Library linked to this 24035 PHP PR, more details here:

More details about PHP softwares/projects here:

2025-02-03: About PHP, SCRAM has been added in, people wait for -PLUS variants:

  • PEAR Auth_SASL/Auth_SASL2/Net_SMTP (used by a lot of projects like Roundcube etc.)
  • SnappyMail
  • SquirelMail
  • Horde

For example, some tickets about it:

@Sjord

Sjord commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

I changed the channel_binding_type to a enum. I also wrote an RFC: PHP RFC: openssl_get_channel_binding()

Comment thread ext/openssl/xp_ssl.c
return PHP_OPENSSL_CHANNEL_BINDING_RESULT_ERROR;
}
*out = zend_string_init((char *)buf, 32, 0);
return *out != NULL ? PHP_OPENSSL_CHANNEL_BINDING_RESULT_OK : PHP_OPENSSL_CHANNEL_BINDING_RESULT_ERROR;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zend_string_init() guarantees a non-NULL return (only zend_string_finit() can fail).

Comment thread ext/openssl/xp_ssl.c
(const unsigned char *)"", 0, 1) != 1) {
return PHP_OPENSSL_CHANNEL_BINDING_RESULT_ERROR;
}
*out = zend_string_init((char *)buf, 32, 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*out = zend_string_init((char *)buf, 32, 0);
*out = zend_string_init((char *)buf, 32, false);

Comment thread ext/openssl/xp_ssl.c
unsigned char buf[EVP_MAX_MD_SIZE];
size_t len = 0;
php_openssl_netstream_data_t *sslsock;
SSL *ssl;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move down declarations as far as possible (applies everywhere).

Comment thread ext/openssl/xp_ssl.c
* stream PHP_OPENSSL_CHANNEL_BINDING_RESULT_NOT_TLS is returned, and if an OpenSSL call fails
* PHP_OPENSSL_CHANNEL_BINDING_RESULT_ERROR is returned; in both of these cases *out is set to NULL.
*/
php_openssl_channel_binding_result php_openssl_netstream_get_channel_binding( /* {{{ */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
php_openssl_channel_binding_result php_openssl_netstream_get_channel_binding( /* {{{ */
php_openssl_channel_binding_result php_openssl_netstream_get_channel_binding(

These are not used in new code.

Comment thread ext/openssl/xp_ssl.c
return *out != NULL ? PHP_OPENSSL_CHANNEL_BINDING_RESULT_OK : PHP_OPENSSL_CHANNEL_BINDING_RESULT_ERROR;

default:
return PHP_OPENSSL_CHANNEL_BINDING_RESULT_ERROR;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZEND_UNREACHABLE()

@Sjord
Sjord force-pushed the stream_get_channel_binding branch from da9073f to 035b582 Compare October 30, 2026 12:35
@Sjord
Sjord force-pushed the stream_get_channel_binding branch from 035b582 to 81098e1 Compare October 30, 2026 13:32
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.

4 participants