Skip to content

Instantly share code, notes, and snippets.

@Doxylamin
Created December 10, 2025 10:11
Show Gist options
  • Select an option

  • Save Doxylamin/f25609ff994aca4cd2ee70b9416b427d to your computer and use it in GitHub Desktop.

Select an option

Save Doxylamin/f25609ff994aca4cd2ee70b9416b427d to your computer and use it in GitHub Desktop.
<?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