From faf9ddfc29676b86621bdc3033e05d63df5c2a93 Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Mon, 15 Jan 2024 13:54:52 +0100 Subject: client certificate handling and workaround --- src/ClientCertificate.php | 28 ++++++++++++++++++++++++++++ src/Request.php | 29 +++++++++++++++++++++++++++-- src/Server.php | 38 +++++++++++++++++++++++++++++++++++--- 3 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/ClientCertificate.php diff --git a/src/ClientCertificate.php b/src/ClientCertificate.php new file mode 100644 index 0000000..d2b9530 --- /dev/null +++ b/src/ClientCertificate.php @@ -0,0 +1,28 @@ +certificate = $certificate; + } + + public function getObject(): \OpenSSLCertificate + { + return $this->certificate; + } + + public function getFingerprint(): string + { + return openssl_x509_fingerprint($this->certificate); + } + + public function parse(): array + { + return openssl_x509_parse($this->certificate); + } +} diff --git a/src/Request.php b/src/Request.php index ceb7dbb..82ce0d3 100644 --- a/src/Request.php +++ b/src/Request.php @@ -8,8 +8,9 @@ class Request protected string $host; protected string $path; protected array $query; + protected ?ClientCertificate $clientCertificate; - public function __construct(string $url) + public function __construct(string $url, ?ClientCertificate $clientCertificate = null) { $requestUrl = parse_url($url); @@ -29,6 +30,8 @@ class Request } } } + + $this->clientCertificate = $clientCertificate; } /** @@ -36,7 +39,17 @@ class Request */ public static function fromResource($resource): static { - return Request::fromString(fread($resource, 1024)); + $request = Request::fromString(fread($resource, 1024)); + + $clientCertificate = null; + $streamParams = stream_context_get_params($resource); + $peerCertificate = $streamParams['options']['ssl']['peer_certificate'] ?? null; + if ($peerCertificate) { + $clientCertificate = new ClientCertificate($peerCertificate); + $request->setClientCertificate($clientCertificate); + } + + return $request; } public static function fromString(string $string): static @@ -67,4 +80,16 @@ class Request return $this; } + + public function getClientCertificate(): ?ClientCertificate + { + return $this->clientCertificate; + } + + public function setClientCertificate(ClientCertificate $clientCertificate): self + { + $this->clientCertificate = $clientCertificate; + + return $this; + } } diff --git a/src/Server.php b/src/Server.php index 46c7dca..102c4fd 100644 --- a/src/Server.php +++ b/src/Server.php @@ -12,19 +12,29 @@ class Server 'passphrase' => null, ]; + protected array $options = []; + protected array $requestHandlers = []; /** * @param array $certificate * @param string $hostname + * @param array $options */ public function __construct( array $certificate, - string $hostname = 'localhost' + string $hostname = 'localhost', + array $options = [] ) { $this->certificate = $certificate; $this->hostname = $hostname; + + $this->options = array_replace_recursive([ + 'context' => [], + 'client_certificate_support_workaround' => false, # "support" until https://bugs.php.net/bug.php?id=80770 + 'client_certificate_support_workaround_timeout' => 3, + ], $options); } public function setCertificate(string $certificateFile, string $keyFile, string $passphrase = ''): static @@ -47,7 +57,7 @@ class Server public function listen(int $port = 1965): void { - $context = stream_context_create(options: [ + $contextOptions = array_replace_recursive([ 'ssl' => [ 'local_cert' => $this->certificate['file'], 'local_pk' => $this->certificate['key'], @@ -55,8 +65,11 @@ class Server 'allow_self_signed' => true, 'verify_peer' => false, + 'capture_peer_cert' => true, ], - ]); + ], $this->options['context']); + + $context = stream_context_create(options: $contextOptions); $socket = stream_socket_server( address: "tls://{$this->hostname}:{$port}", @@ -66,6 +79,11 @@ class Server $connections = []; while (true) { + if ($this->options['client_certificate_support_workaround']) { + stream_context_set_option($socket, 'ssl', 'verify_peer', true); + } + + echo "Listening for connections.\n"; $connection = stream_socket_accept( socket: $socket, timeout: empty($connections) ? -1 : 0, @@ -74,6 +92,20 @@ class Server if ($connection) { $connections[$peer] = $connection; + } else if ($this->options['client_certificate_support_workaround']) { + echo "Enabling client certificate \"support\" workaround.\n"; + echo "Listening without verifying peer.\n"; + + stream_context_set_option($socket, 'ssl', 'verify_peer', false); + $connection = stream_socket_accept( + socket: $socket, + timeout: $this->options['client_certificate_support_workaround_timeout'], + peer_name: $peer + ); + + if ($connection) { + $connections[$peer] = $connection; + } } if (count($connections) == 0) { -- cgit v1.2.3