php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #80770 It is not possible to get client peer certificate with stream_socket_server
Submitted: 2021-02-18 16:11 UTC Modified: 2025-06-21 17:59 UTC
Votes:3
Avg. Score:3.7 ± 0.5
Reproduced:2 of 2 (100.0%)
Same Version:0 (0.0%)
Same OS:0 (0.0%)
From: mcmic@php.net Assigned: bukka (profile)
Status: Assigned Package: OpenSSL related
PHP Version: 7.3.27 OS:
Private report: No CVE-ID: None
Anyone can comment on a bug. Have a simpler test case? Does it work for you on a different platform? Let us know!
Just going to say 'Me too!'? Don't clutter the database with that please !
Your email address:
MUST BE VALID
Solve the problem:
33 + 28 = ?
Subscribe to this entry?

 
 [2021-02-18 16:11 UTC] mcmic@php.net
Description:
------------
It is not possible to get the client certificate when accepting TLS connections, without forcing the client to provide a (valid) certificate.

This forbids writing a fully compliant Gemini server in PHP, because it is not possible to accept client certificates without forcing them on all pages.

See https://www.openssl.org/docs/man1.0.2/man3/SSL_CTX_set_verify.html

Test script:
---------------
$context = stream_context_create(
    [
        'ssl' => [
            'allow_self_signed' => true,
            'SNI_enabled'       => true,
            'SNI_server_certs'  => ['example.com' => '/path/to/cert.pem'],
            'capture_peer_cert' => true,
        ]
    ]
);
$socket = stream_socket_server(
    'tcp://[::]:' . $port,
    $errno,
    $errstr,
    STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
    $context,
);

if ($socket === false) {
    throw new \Exception($errstr, $errno);
} else {
    while ($conn = stream_socket_accept($socket, -1, $peername)) {
        $tlsSuccess = stream_socket_enable_crypto(
            $conn,
            true,
            STREAM_CRYPTO_METHOD_TLS_SERVER
        );
        if ($tlsSuccess !== true) {
            fclose($conn);
            continue;
        }
        var_dump(stream_context_get_options($conn));
    }
}

Expected result:
----------------
Have the client certificate in 'peer_certificate' key of $conn, if the client sends one.

Actual result:
--------------
No 'peer_certificate' option.

If I set 'verify_peer' to true, it works, but then it is not possible for a client to connect without a certificate.

Patches

Pull Requests

Pull requests:

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2021-02-18 16:26 UTC] daverandom@php.net
Note that there is a test for this behaviour which is not marked as XFAIL, so I believe this functionality should work - although I cannot immediately see an issue with your sample code

https://heap.space/xref/php-src/ext/openssl/tests/capture_peer_cert_001.phpt?r=1fab01be#38
 [2021-02-18 16:26 UTC] daverandom@php.net
Ah no I think I see the problem, you need to pass $context instead of $conn to stream_context_get_options()
 [2021-02-18 16:32 UTC] mcmic@php.net
@daverandom:
The test you linked to is about getting the server certificate from the client, I want the other way around, get the client certificate from the server.
And without making the client certificate mandatory, I still need to accept certificate-less requests.
 [2021-02-18 16:34 UTC] mcmic@php.net
See section 4.3 of https://gemini.circumlunar.space/docs/specification.html for more information about client certificate use in Gemini protocol.
 [2021-02-18 17:13 UTC] daverandom@php.net
My apologies, you are correct and I have actually now remembered encountering the same limitation a few months ago. I do also have a (theoretical) work-around, which is to obtain the ClientHello message (and potentially other traffic) via stream_socket_recvfrom() and STREAM_PEEK before calling stream_socket_enable_crypto().

I think I even wrote a PoC partial implementation of it, I cannot find it at the moment but if I do I will make a gist and link it here.

As for a proper fix for the issue, I have touched this code in the (distant) past and I from what I remember it should be reasonably easy to optionally store a client certificate into the stream's context during the peer verification callback, though I think this would not work if peer verification was completely disabled. I'm not sure if this would be considered an acceptable limitation.
 [2021-02-18 17:13 UTC] daverandom@php.net
-Status: Open +Status: Verified
 [2022-12-31 05:09 UTC] marlynrasavong at gmail dot com
Hey it is not possible to get certificate without TLS. 
(https://www.benefitscal.biz/)github.com
 [2025-06-21 17:59 UTC] bukka@php.net
-Status: Verified +Status: Assigned -Assigned To: +Assigned To: bukka
 [2025-06-21 17:59 UTC] bukka@php.net
So this took me quite a lot of time to figure out. Well the issue is clearly because it is necessary to set verify_peer to true. But the problem was here:

> If I set 'verify_peer' to true, it works, but then it is not possible for a client to connect without a certificate.

The reason why connect failed is most likely because cafile was missing. But even if it was there, it wouldn't work for SNI. The problem was that cafile is not set in SNI context which I just fixed in https://github.com/php/php-src/pull/18893 .
 [2025-06-21 17:59 UTC] bukka@php.net
The following pull request has been associated:

Patch Name: Fix bug #80770: openssl cafile not used in SNI SSL_CTX
On GitHub:  https://github.com/php/php-src/pull/18893
Patch:      https://github.com/php/php-src/pull/18893.patch
 
PHP Copyright © 2001-2025 The PHP Group
All rights reserved.
Last updated: Tue Jul 01 02:01:36 2025 UTC