Created
December 10, 2025 10:11
-
-
Save Doxylamin/f25609ff994aca4cd2ee70b9416b427d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Import SSL certificates from a pre-determined place on the filesystem. | |
| * Once imported, set them for use in the GUI | |
| */ | |
| if (empty($argc)) { | |
| echo "Only accessible from the CLI.\r\n"; | |
| die(1); | |
| } | |
| if ($argc != 4) { | |
| echo "Usage: php " . $argv[0] . " /path/to/certificate.crt /path/to/private/key.pem cert_hostname.domain.tld\r\n"; | |
| die(1); | |
| } | |
| require_once "config.inc"; | |
| require_once "certs.inc"; | |
| require_once "util.inc"; | |
| require_once "filter.inc"; | |
| $certificate = trim(file_get_contents($argv[1])); | |
| $key = trim(file_get_contents($argv[2])); | |
| $hostname = trim($argv[3]); | |
| // small helper for hostname ↔ pattern (supports wildcard like *.exampe.com) | |
| function hostname_matches_pattern(string $hostname, string $pattern): bool | |
| { | |
| // exact match (case-insensitive) | |
| if (strcasecmp($hostname, $pattern) === 0) { | |
| return true; | |
| } | |
| // wildcard: *.example.com | |
| if (substr($pattern, 0, 2) === '*.') { | |
| $suffix = substr($pattern, 1); // ".example.com" | |
| if ($suffix === '') { | |
| return false; | |
| } | |
| // hostname must end with suffix, e.g. "firewall.example.com" | |
| if (substr($hostname, -strlen($suffix)) === $suffix) { | |
| // and have at least one label before it | |
| return (substr_count($hostname, '.') > substr_count($suffix, '.')); | |
| } | |
| } | |
| return false; | |
| } | |
| function cert_matches_hostname(string $certificate, string $hostname): bool | |
| { | |
| $res = @openssl_x509_read($certificate); | |
| if ($res === false) { | |
| return false; | |
| } | |
| $parsed = @openssl_x509_parse($res); | |
| if ($parsed === false) { | |
| return false; | |
| } | |
| $candidates = []; | |
| // CN aus dem Subject | |
| if (!empty($parsed['subject']['CN'])) { | |
| $candidates[] = $parsed['subject']['CN']; | |
| } | |
| // SANs (subjectAltName) auswerten | |
| if (!empty($parsed['extensions']['subjectAltName'])) { | |
| $sanParts = explode(',', $parsed['extensions']['subjectAltName']); | |
| foreach ($sanParts as $san) { | |
| $san = trim($san); | |
| if (stripos($san, 'DNS:') === 0) { | |
| $dns = trim(substr($san, 4)); | |
| if ($dns !== '') { | |
| $candidates[] = $dns; | |
| } | |
| } | |
| } | |
| } | |
| if (empty($candidates)) { | |
| return false; | |
| } | |
| foreach ($candidates as $pattern) { | |
| if (hostname_matches_pattern($hostname, $pattern)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Do some quick verification of the certificate, similar to what the GUI does | |
| if (empty($certificate)) { | |
| echo "The certificate is empty.\r\n"; | |
| die(1); | |
| } | |
| if (!strstr($certificate, "BEGIN CERTIFICATE") || !strstr($certificate, "END CERTIFICATE")) { | |
| echo "This certificate does not appear to be valid.\r\nOr: cert and privkey args switched?\r\n"; | |
| die(1); | |
| } | |
| // Verification that the certificate matches | |
| if (empty($key)) { | |
| echo "The key is empty.\r\n"; | |
| die(1); | |
| } | |
| // NEW: use proper hostname ↔ CN/SAN check (supports wildcard) | |
| if (!cert_matches_hostname($certificate, $hostname)) { | |
| $subject = cert_get_subject($certificate, false); | |
| echo "The certificate subject/SAN does not match the hostname $hostname.\r\n"; | |
| echo "Subject: " . $subject . "\r\n"; | |
| die(1); | |
| } | |
| // Issuer check – optional / etwas entspannter. | |
| // Deine alte Version war: | |
| // if (trim(cert_get_issuer($certificate, false)) != "O=Let's Encrypt, CN=R3, C=US") { ... } | |
| $issuer = trim(cert_get_issuer($certificate, false)); | |
| if (strpos($issuer, "Let's Encrypt") === false) { | |
| echo "The certificate issuer does not look like a Let's Encrypt certificate.\r\n"; | |
| echo $issuer . "\r\n"; | |
| // Wenn du hier NICHT abbrechen willst, kommentiere das nächste 'die(1);' aus. | |
| // die(1); | |
| } | |
| $cert = array(); | |
| $cert['refid'] = uniqid(); | |
| $cert['descr'] = "Certificate added to opnsense through " . $argv[0] . " on " . date("Y/m/d"); | |
| cert_import($cert, $certificate, $key); | |
| // Set up the existing certificate store | |
| // Copied from system_certmanager.php | |
| if (!is_array($config['ca'])) { | |
| $config['ca'] = array(); | |
| } | |
| $a_ca =& $config['ca']; | |
| if (!is_array($config['cert'])) { | |
| $config['cert'] = array(); | |
| } | |
| $a_cert =& $config['cert']; | |
| $internal_ca_count = 0; | |
| foreach ($a_ca as $ca) { | |
| if ($ca['prv']) { | |
| $internal_ca_count++; | |
| } | |
| } | |
| // Check if the certificate we just parsed is already imported (we'll check the certificate portion) | |
| foreach ($a_cert as $existing_cert) { | |
| if ($existing_cert['crt'] === $cert['crt']) { | |
| echo "The certificate is already imported.\r\n"; | |
| die(); // exit with a valid error code, as this is intended behaviour | |
| } | |
| } | |
| // Append the final certificate | |
| $a_cert[] = $cert; | |
| // Write out the updated configuration | |
| write_config(); | |
| // Assuming that all worked, we now need to set the new certificate for use in the GUI | |
| $config['system']['webgui']['ssl-certref'] = $cert['refid']; | |
| write_config(); | |
| log_error('Web GUI configuration has changed. Restarting now.'); | |
| configd_run('webgui restart 2', true); | |
| echo "Completed! New certificate installed.\r\n"; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment