[ 'capture_peer_cert' => true, 'SNI_enabled' => true, 'peer_name' => $hostname, 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $client = @stream_socket_client( 'ssl://'.$hostname.':443', $errorNumber, $errorMessage, $timeoutSeconds, STREAM_CLIENT_CONNECT, $context, ); if ($client === false) { return [ 'ok' => false, 'expires_at' => null, 'issuer' => null, // Die Meldung von PHP ist lang und trägt Dateipfade. Was zählt, // ist die erste Zeile — sie unterscheidet „kein Zertifikat" von // „Name löst nicht auf", und das sind zwei ganz verschiedene // Aufgaben für den Betreiber. 'error' => $this->firstLine($errorMessage ?: 'kein Verbindungsaufbau'), ]; } $params = stream_context_get_params($client); fclose($client); $certificate = $params['options']['ssl']['peer_certificate'] ?? null; if ($certificate === null) { return ['ok' => false, 'expires_at' => null, 'issuer' => null, 'error' => 'kein Zertifikat vorgelegt']; } $parsed = openssl_x509_parse($certificate); if ($parsed === false || ! isset($parsed['validTo_time_t'])) { return ['ok' => false, 'expires_at' => null, 'issuer' => null, 'error' => 'Zertifikat nicht lesbar']; } return [ 'ok' => true, 'expires_at' => Carbon::createFromTimestampUTC($parsed['validTo_time_t']), 'issuer' => $parsed['issuer']['O'] ?? ($parsed['issuer']['CN'] ?? null), 'error' => null, ]; } private function firstLine(string $message): string { $line = trim((string) preg_split('/\R/', $message)[0]); // Der PHP-Vorspann („stream_socket_client(): ") sagt nichts über die // Sache und frisst die halbe Spalte. $line = (string) preg_replace('/^stream_socket_client\(\):\s*/', '', $line); return mb_substr($line, 0, 190); } }