Merge pull request #4449 from fraenki/acme_trust

security/acme-client: migrate to Trust MVC
This commit is contained in:
Frank Wall
2025-01-06 21:26:15 +01:00
committed by GitHub
6 changed files with 91 additions and 185 deletions
+2
View File
@@ -18,10 +18,12 @@ Added:
Changed:
* Convert Synology deploy hook variables to uppercase (#4286)
* Migrate to MVC Trust storage
Fixed:
* SFTP/SSH automation results in fatal PHP error (#4363)
* Typo in INWX password field name
* Certs not fully functional after import into Trust storage (#4401)
4.6
@@ -1,7 +1,7 @@
<?php
/*
* Copyright (C) 2020-2024 Frank Wall
* Copyright (C) 2020-2025 Frank Wall
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -28,10 +28,12 @@
namespace OPNsense\AcmeClient;
// Load legacy functions
require_once("certs.inc"); // used in import()
require_once("script/load_phalcon.php");
use OPNsense\Core\Config;
use OPNsense\Trust\Ca;
use OPNsense\Trust\Cert;
use OPNsense\Trust\Store as CertStore;
use OPNsense\AcmeClient\LeAccount;
use OPNsense\AcmeClient\LeAutomationFactory;
use OPNsense\AcmeClient\LeValidationFactory;
@@ -143,61 +145,54 @@ class LeCertificate extends LeCommon
// Read contents from CA file
$ca_content = @file_get_contents($this->cert_chain_file);
if ($ca_content != false) {
$ca_subject = cert_get_subject($ca_content, false);
$ca_serial = cert_get_serial($ca_content, false);
$ca_cn = LeUtils::local_cert_get_cn($ca_content, false);
$ca_issuer = cert_get_issuer($ca_content, false);
$ca_purpose = cert_get_purpose($ca_content, false);
$ca_details = CertStore::parseX509($ca_content);
$ca_subject = $ca_details['name'];
$ca_serial = $ca_details['serialNumber'];
$ca_cn = $ca_details['commonname'];
$ca_issuer = implode(",", $ca_details['issuer']);
} else {
LeUtils::log_error('unable to read CA certificate content from file');
Config::getInstance()->unlock();
return false;
}
// Prepare CA for import in Cert Manager
// Prepare CA
$caModel = new Ca();
$ca = array();
$ca['crt'] = base64_encode($ca_content);
$ca['refid'] = uniqid();
$ca['descr'] = (string)$ca_cn . ' (ACME Client)';
$ca_found = false;
// Check if CA was previously imported
foreach (Config::getInstance()->object()->ca as $cacrt) {
$cacrt_subject = cert_get_subject($cacrt->crt, true);
$cacrt_issuer = cert_get_issuer($cacrt->crt, true);
foreach ($caModel->ca->iterateItems() as $cacrt) {
$cacrt_content = base64_decode((string)$cacrt->crt);
$cacrt_details = CertStore::parseX509($cacrt_content);
$cacrt_subject = $cacrt_details['name'];
$cacrt_issuer = implode(",", $cacrt_details['issuer']);
if (($ca_subject === $cacrt_subject) and ($ca_issuer === $cacrt_issuer)) {
// Use old refid instead of generating a new one
$ca['refid'] = (string)$cacrt->refid;
// Update existing CA
$cacrt->descr = $ca['descr'];
$ca_found = true;
break;
}
}
// Collect required CA information
$ca_cn = LeUtils::local_cert_get_cn($ca_content, false);
$ca['descr'] = (string)$ca_cn . ' (ACME Client)';
// Prepare CA for import
LeUtils::local_ca_import($ca, $ca_content);
// Check if CA was found in config
if ($ca_found == true) {
// Update existing CA
foreach (Config::getInstance()->object()->ca as $cacrt) {
if ((string)$cacrt->refid == $ca['refid']) {
$cacrt->crt = $ca['crt'];
$cacrt->descr = $ca['descr'];
break;
}
}
} else {
// Create new CA
LeUtils::log("importing ACME CA: {$ca_cn}");
$newca = Config::getInstance()->object()->addChild('ca');
// Create new CA
if ($ca_found == false) {
LeUtils::log("imported ACME CA: {$ca_cn} ({$ca['refid']})");
$newca = $caModel->ca->Add();
foreach (array_keys($ca) as $cacfg) {
$newca->addChild($cacfg, (string)$ca[$cacfg]);
$newca->$cacfg = (string)$ca[$cacfg];
}
$newca->crt = base64_encode($ca_content);
}
// Serialize to config and save
$caModel->serializeToConfig();
Config::getInstance()->save();
/**
* Step 2: import certificate
*/
@@ -205,11 +200,11 @@ class LeCertificate extends LeCommon
// Read contents from certificate file
$cert_content = @file_get_contents($this->cert_file);
if ($cert_content != false) {
$cert_subject = cert_get_subject($cert_content, false);
$cert_serial = cert_get_serial($cert_content, false);
$cert_cn = LeUtils::local_cert_get_cn($cert_content, false);
$cert_issuer = cert_get_issuer($cert_content, false);
$cert_purpose = cert_get_purpose($cert_content, false);
$cert_details = CertStore::parseX509($cert_content);
$cert_subject = $cert_details['name'];
$cert_serial = $cert_details['serialNumber'];
$cert_cn = $cert_details['commonname'];
$cert_issuer = implode(",", $cert_details['issuer']);
} else {
LeUtils::log_error('unable to read certificate content from file');
Config::getInstance()->unlock();
@@ -217,34 +212,6 @@ class LeCertificate extends LeCommon
return false;
}
// Prepare certificate for import in Cert Manager
$cert = array();
$cert_refid = uniqid();
$cert['refid'] = $cert_refid;
$cert['caref'] = (string)$ca['refid'];
$import_log_message = 'imported';
$cert_found = false;
// Check if cert was previously imported
if (!empty((string)$this->config->certRefId)) {
// Check if the previously imported certificate can still be found
foreach (Config::getInstance()->object()->cert as $cfgCert) {
// Check if IDs match
if ((string)$this->config->certRefId == (string)$cfgCert->refid) {
$cert_found = true;
break;
}
}
// Existing cert?
if ($cert_found) {
// Use old refid instead of generating a new one
$cert_refid = (string)$this->config->certRefId;
$import_log_message = 'updated';
}
} else {
// Not found. Just import as new cert.
}
// Read private key
$key_content = @file_get_contents($this->cert_key_file);
if ($key_content == false) {
@@ -254,28 +221,39 @@ class LeCertificate extends LeCommon
return false;
}
// Collect required cert information
$cert_cn = LeUtils::local_cert_get_cn($cert_content, false);
$cert['descr'] = (string)$cert_cn . ' (ACME Client)';
$cert['refid'] = $cert_refid;
// Prepare certificate for import
cert_import($cert, $cert_content, $key_content);
// Overwrite caref in order to use the correct CA (GH #2550).
// This is required because cert_import() uses lookup_ca_by_subject()
// to find a matching CA. If multiple CAs are using the same name, the
// first CA wins, but it may still be the wrong CA.
// Prepare certificate
$certModel = new Cert();
$cert = array();
$cert['refid'] = uniqid();
$cert['caref'] = (string)$ca['refid'];
$cert['descr'] = (string)$cert_cn . ' (ACME Client)';
$import_log_message = 'imported';
$cert_found = false;
// Check if cert was previously imported.
// Otherwise just import as new cert.
if (!empty((string)$this->config->certRefId)) {
// Check if the previously imported certificate can still be found
foreach ($certModel->cert->iterateItems() as $cfgCert) {
// Check if IDs match
if ((string)$this->config->certRefId == (string)$cfgCert->refid) {
// Use old refid instead of generating a new one
$cert['refid'] = (string)$cfgCert->refid;
$import_log_message = 'updated';
$cert_found = true;
break;
}
}
}
// Check if cert was found in config
if ($cert_found == true) {
// Update existing cert
foreach (Config::getInstance()->object()->cert as $cfgCert) {
foreach ($certModel->cert->iterateItems() as $cfgCert) {
if ((string)$cfgCert->refid == $cert['refid']) {
$cfgCert->crt = $cert['crt'];
$cfgCert->prv = $cert['prv'];
$cfgCert->descr = $cert['descr'];
$cfgCert->crt = base64_encode($cert_content);
$cfgCert->prv = base64_encode($key_content);
// Update CA ref, because it may be signed by a different CA.
$cfgCert->caref = $cert['caref'];
break;
@@ -283,27 +261,32 @@ class LeCertificate extends LeCommon
}
} else {
// Create new cert
$newcert = Config::getInstance()->object()->addChild('cert');
$newcert = $certModel->cert->Add();
foreach (array_keys($cert) as $certcfg) {
$newcert->addChild($certcfg, (string)$cert[$certcfg]);
$newcert->$certcfg = (string)$cert[$certcfg];
}
$newcert->crt = base64_encode($cert_content);
$newcert->prv = base64_encode($key_content);
}
LeUtils::log("{$import_log_message} ACME X.509 certificate: {$cert_cn}");
LeUtils::log("{$import_log_message} ACME X.509 certificate: {$cert_cn} ({$cert['refid']})");
// Serialize to config and save
// Skip validation because the current in-memory model may not
// know about the CA item that was just created.
$certModel->serializeToConfig(false,true);
Config::getInstance()->save();
/**
* Step 3: update configuration
*/
// Add refid to certObj
$this->config->certRefId = $cert_refid;
// Set update/create time
// Update Acme cert config
$this->config->certRefId = $cert['refid'];
$this->config->lastUpdate = time();
// Serialize to config and save
$this->model->serializeToConfig();
Config::getInstance()->save();
// Reload to get most recent config
Config::getInstance()->forceReload();
$this->loadConfig(self::CONFIG_PATH, $this->uuid);
@@ -402,12 +385,12 @@ class LeCertificate extends LeCommon
return false;
}
// Run referenced automations.
$this->runAutomations();
// Update cert status.
$this->setStatus(200);
// Run referenced automations.
$this->runAutomations();
return true;
}
@@ -1,7 +1,7 @@
<?php
/*
* Copyright (C) 2017-2024 Frank Wall
* Copyright (C) 2017-2025 Frank Wall
* Copyright (C) 2015 Deciso B.V.
* Copyright (C) 2010 Jim Pingle <jimp@pfsense.org>
* Copyright (C) 2008 Shrew Soft Inc. <mgrooms@shrew.net>
@@ -70,86 +70,6 @@ class LeUtils
return vsprintf($format, $args);
}
// Copied from system_camanager.php.
public static function local_ca_import(&$ca, $str, $key = "", $serial = 0)
{
// Get config object.
$config = Config::getInstance()->object();
$ca['crt'] = base64_encode($str);
if (!empty($key)) {
$ca['prv'] = base64_encode($key);
}
if (!empty($serial)) {
$ca['serial'] = $serial;
}
$subject = cert_get_subject($str, false);
$issuer = cert_get_issuer($str, false);
// Find my issuer unless self-signed
if ($issuer != $subject) {
$issuer_crt =& lookup_ca_by_subject($issuer);
if ($issuer_crt) {
$ca['caref'] = $issuer_crt['refid'];
}
}
/* Correct if child certificate was loaded first */
if (is_array($config['ca'])) {
foreach ($config['ca'] as & $oca) {
$issuer = cert_get_issuer($oca['crt']);
if ($ca['refid'] != $oca['refid'] && $issuer == $subject) {
$oca['caref'] = $ca['refid'];
}
}
}
if (is_array($config['cert'])) {
foreach ($config['cert'] as & $cert) {
$issuer = cert_get_issuer($cert['crt']);
if ($issuer == $subject) {
$cert['caref'] = $ca['refid'];
}
}
}
return true;
}
// copied from certs.inc
public static function local_cert_get_cn($crt, $decode = true)
{
$sub = self::local_cert_get_subject_array($crt, $decode);
if (is_array($sub)) {
foreach ($sub as $s) {
if (strtoupper($s['a']) == "CN") {
return $s['v'];
}
}
}
return "";
}
// copied from certs.inc
public static function local_cert_get_subject_array($str_crt, $decode = true)
{
if ($decode) {
$str_crt = base64_decode($str_crt);
}
$inf_crt = openssl_x509_parse($str_crt);
$components = $inf_crt['subject'];
if (!is_array($components)) {
return;
}
$subject_array = array();
foreach ($components as $a => $v) {
$subject_array[] = array('a' => $a, 'v' => $v);
}
return $subject_array;
}
/**
* log runtime information
*/
@@ -2,7 +2,7 @@
<?php
/*
* Copyright (C) 2020-2021 Frank Wall
* Copyright (C) 2020-2025 Frank Wall
* Copyright (C) 2019 Juergen Kellerer
* All rights reserved.
*
@@ -30,7 +30,6 @@
// Import legacy code
@include_once('config.inc');
@include_once('certs.inc');
@include_once('util.inc');
// Import classes
@@ -94,7 +94,6 @@ const EXITCODE_ERROR_UNKNOWN_COMMAND = 254;
// Optional imports
@include_once("config.inc");
@include_once("certs.inc");
@include_once("util.inc");
// Optional autoloader (for local dev environment)
@@ -2,6 +2,7 @@
<?php
/*
* Copyright (C) 2025 Frank Wall
* Copyright (C) 2019 Juergen Kellerer
* All rights reserved.
*
@@ -124,8 +125,8 @@ const EXITCODE_ERROR_UNKNOWN_COMMAND = 255;
// Optional imports
@include_once("config.inc");
@include_once("certs.inc");
@include_once("util.inc");
require_once("script/load_phalcon.php");
// Optional autoloader (for local dev environment)
if (!function_exists("log_error")) {
@@ -135,6 +136,8 @@ if (!function_exists("log_error")) {
}
// Importing classes
use OPNsense\Trust\Cert;
use OPNsense\Trust\Store as CertStore;
use OPNsense\AcmeClient\SftpUploader;
use OPNsense\AcmeClient\SftpClient;
use OPNsense\AcmeClient\SSHKeys;
@@ -519,17 +522,17 @@ function findCertificates(array $certificate_ids_or_names, $load_content = true)
function exportCertificates(array $cert_refids): array
{
$result = [];
$config = OPNsense\Core\Config::getInstance()->object();
foreach ($config->cert as $cert) {
$certModel = new Cert();
foreach ($certModel->cert->iterateItems() as $cert) {
$refid = (string)$cert->refid;
$item = [];
if (in_array($refid, $cert_refids)) {
$item["cert"] = str_replace(["\n\n", "\r"], ["\n", ""], base64_decode($cert->crt));
$item["key"] = str_replace(["\n\n", "\r"], ["\n", ""], base64_decode($cert->prv));
$_tmp = CertStore::getCertificate($refid);
$item["cert"] = $_tmp["crt"];
$item["key"] = $_tmp["prv"];
// check if a CA is linked
if (!empty((string)$cert->caref)) {
$cert = (array)$cert;
$item["ca"] = ca_chain($cert);
$item["ca"] = $_tmp["ca"];;
// combine files to export a fullchain.pem
$item["fullchain"] = $item["cert"] . $item["ca"];
}