[ 'capture_peer_cert' => true, 'verify_peer' => false, 'verify_peer_name' => false, 'SNI_enabled' => true, 'peer_name' => $host, ]]); $errno = 0; $errstr = ''; // @ — a refused/timed-out/unresolved endpoint is a normal result here, not a PHP warning. $client = @stream_socket_client( 'ssl://'.$host.':'.$port, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $ctx, ); if (! $client) { return ['ok' => false, 'error' => $errstr !== '' ? $errstr : 'connection failed']; } $params = stream_context_get_params($client); fclose($client); $cert = $params['options']['ssl']['peer_certificate'] ?? null; if ($cert === null) { return ['ok' => false, 'error' => 'no certificate presented']; } return ['ok' => true] + $this->certInfo($cert); } /** * Extract subject/issuer/expiry from an openssl cert (resource or PEM). Split out so it is unit- * testable against a generated cert without a live network connection. * * @param \OpenSSLCertificate|string $cert * @return array{subject:string, issuer:string, expiresAt:?int, daysLeft:?int} */ public function certInfo($cert): array { $parsed = openssl_x509_parse($cert) ?: []; $expiresAt = isset($parsed['validTo_time_t']) ? (int) $parsed['validTo_time_t'] : null; return [ 'subject' => (string) ($parsed['subject']['CN'] ?? ($parsed['subject']['O'] ?? '—')), 'issuer' => (string) ($parsed['issuer']['CN'] ?? ($parsed['issuer']['O'] ?? '—')), 'expiresAt' => $expiresAt, 'daysLeft' => $expiresAt !== null ? (int) floor(($expiresAt - time()) / 86400) : null, ]; } /** expired · critical (<7d) · warning (<30d) · ok — mapped to the status triad in the UI. */ public function status(?int $daysLeft): string { if ($daysLeft === null) { return 'unknown'; } return match (true) { $daysLeft < 0 => 'expired', $daysLeft < 7 => 'critical', $daysLeft < 30 => 'warning', default => 'ok', }; } }