System: Trust: Certificates - work in progress for https://github.com/opnsense/core/issues/7248

Implement certificate actions, further optimize certificate store to limit code duplication.
This commit is contained in:
Ad Schellevis 2024-03-02 17:54:16 +01:00
parent b8bd667d64
commit 94952145b3
5 changed files with 219 additions and 87 deletions

View File

@ -48,7 +48,91 @@ class CertController extends ApiMutableModelControllerBase
if (empty((string)$node->refid)) {
$node->refid = uniqid();
}
throw new UserException((string)$node->refid, (string)$node->action);
$error = false;
if (!empty((string)$node->prv_payload)) {
/** private key manually offered */
$node->prv = base64_encode((string)$node->prv_payload);
}
switch ((string)$node->action) {
case 'internal':
$data = CertStore::createCert(
(string)$node->key_type,
(string)$node->lifetime,
$node->dn(),
(string)$node->digest,
(string)$node->caref,
(string)$node->cert_type,
$node->extns()
);
if (!empty($data['crt']) && !empty($data['prv'])) {
$node->crt = base64_encode($data['crt']);
if ((string)$node->private_key_location == 'local') {
/* return only in volatile storage */
$node->prv_payload = $data['prv'];
} else {
$node->prv= base64_encode($data['prv']);
}
} else {
$error = $data['error'] ?? '';
}
break;
case 'external':
$data = CertStore::createCert(
(string)$node->key_type,
(string)$node->lifetime,
$node->dn(),
(string)$node->digest,
false,
(string)$node->cert_type,
$node->extns()
);
if (!empty($data['csr'])) {
$node->csr = base64_encode($data['csr']);
} else {
$error = $data['error'] ?? '';
}
break;
case 'import':
if (CertStore::parseX509((string)$node->crt_payload) === false) {
$error = gettext('Invalid X509 certificate provided');
} else {
$node->crt = base64_encode((string)$node->crt_payload);
if (
!empty(trim((string)$node->prv_payload)) &&
openssl_pkey_get_private((string)$node->prv_payload) === false
) {
$error = gettext('Invalid private key provided');
}
}
break;
case 'import_csr':
if (CertStore::parseX509((string)$node->crt_payload) === false) {
$error = gettext('Invalid X509 certificate provided');
} else {
$node->crt = base64_encode((string)$node->crt_payload);
}
break;
case 'reissue':
$data = CertStore::reIssueCert(
(string)$node->key_type,
(string)$node->lifetime,
$node->dn(),
(string)$node->prv_payload,
(string)$node->digest,
(string)$node->caref,
(string)$node->cert_type,
$node->extns()
);
if (!empty($data['crt'])) {
$node->crt = base64_encode($data['crt']);
} else {
$error = $data['error'] ?? '';
}
break;
}
if ($error !== false) {
throw new UserException($error, "Certificate error");
}
}
public function searchAction()

View File

@ -7,20 +7,22 @@
<field>
<type>header</type>
<label>Key</label>
<style>action action_internal action_reissue action_internal</style>
<style>action action_internal action_reissue action_internal action_external</style>
</field>
<field>
<id>cert.cert_type</id>
<label>Type</label>
<type>dropdown</type>
<style>selectpicker action action_internal action_reissue</style>
</field>
<field>
<id>cert.private_key_location</id>
<label>Private key location</label>
<style>selectpicker action action_internal</style>
<type>dropdown</type>
</field>
<field>
<id>cert.key</id>
<id>cert.key_type</id>
<label>Key type</label>
<type>dropdown</type>
</field>
@ -35,10 +37,15 @@
<style>selectpicker action action_internal action_reissue</style>
<type>dropdown</type>
</field>
<field>
<id>cert.lifetime</id>
<label>Lifetime (days)</label>
<type>text</type>
</field>
<field>
<type>header</type>
<label>General</label>
<style>action action_internal action_reissue action_internal</style>
<style>action action_internal action_reissue action_internal action_external</style>
</field>
<field>
<id>cert.descr</id>
@ -84,12 +91,13 @@
<id>cert.ocsp_uri</id>
<label>OCSP uri</label>
<type>text</type>
<style>action action_internal action_reissue</style>
</field>
<field>
<type>header</type>
<label>Alternative Names</label>
<collapse>true</collapse>
<style>action action_internal action_reissue action_internal</style>
<style>action action_internal action_reissue action_internal action_external</style>
</field>
<field>
<id>cert.altnames_dns</id>
@ -131,6 +139,6 @@
<id>cert.csr_payload</id>
<label>Certificate signing request</label>
<type>textbox</type>
<style>selectpicker action action_internal action_reissue action_internal</style>
<style>selectpicker action action_import_csr</style>
</field>
</form>

View File

@ -184,6 +184,62 @@ class Store
return [];
}
/**
* create openssl options config including configuration file to use
* @param string $keylen_curve rsa key length or elliptic curve name to use
* @param string $digest_alg digest algorithm
* @param string $x509_extensions openssl section to use
* @param array $extns template fragments to replace in openssl.cnf
* @return array needed for certificate generation, caller is responsible for removing ['filename'] after use
*/
private static function _createSSLOptions($keylen_curve, $digest_alg, $x509_extensions = 'usr_cert', $extns = [])
{
// define temp filename to use for openssl.cnf and add extensions values to it
$configFilename = tempnam(sys_get_temp_dir(), 'ssl');
$template = file_get_contents('/usr/local/etc/ssl/opnsense.cnf');
foreach (array_keys($extns) as $extnTag) {
$template_extn = $extnTag . ' = ' . str_replace(array("\r", "\n"), '', $extns[$extnTag]);
// Overwrite the placeholders for this property
$template = str_replace('###OPNsense:' . $extnTag . '###', $template_extn, $template);
}
file_put_contents($configFilename, $template);
$args = [
'config' => $configFilename,
'x509_extensions' => $x509_extensions,
'digest_alg' => $digest_alg,
'encrypt_key' => false
];
if (is_numeric($keylen_curve)) {
$args['private_key_type'] = OPENSSL_KEYTYPE_RSA;
$args['private_key_bits'] = (int)$keylen_curve;
} else {
$args['private_key_type'] = OPENSSL_KEYTYPE_EC;
$args['curve_name'] = $keylen_curve;
}
return $args;
}
/**
* @param array to add 'error' result to when openssl_error_string returns data
*/
private static function _addSSLErrors(&$arr)
{
$data = '';
while ($ssl_err = openssl_error_string()) {
$data .= " " . $ssl_err;
}
if (!empty(trim($data))) {
if (!isset($arr['error'])) {
$arr['error'] = $data;
} else {
$arr['error'] .= ("\n" . $data);
}
}
}
/**
* Sign a certificate, when signed by a CA, make sure to serialize the config after doing so,
* it's the callers responsibility to update the serial number administration of the supplied CA.
@ -208,39 +264,56 @@ class Store
$extns = []
) {
$old_err_level = error_reporting(0); /* prevent openssl error from going to stderr/stdout */
// handle parameters which can only be set via the configuration file
$config_filename = create_temp_openssl_config($extns);
$args = [
'config' => $config_filename,
'x509_extensions' => $x509_extensions,
'digest_alg' => $digest_alg,
'encrypt_key' => false
];
if (is_numeric($keylen_curve)) {
$args['private_key_type'] = OPENSSL_KEYTYPE_RSA;
$args['private_key_bits'] = (int)$keylen_curve;
} else {
$args['private_key_type'] = OPENSSL_KEYTYPE_EC;
$args['curve_name'] = $keylen_curve;
}
$tmp = self::_signCert($csr, $caref, $lifetime, $args);
if (!empty($tmp['crt'])) {
$result = ['crt' => $tmp['crt']];
} else {
$result = $tmp;
}
$args = self::_createSSLOptions($keylen_curve, $digest_alg, $x509_extensions, $extns);
$result = self::_signCert($csr, $caref, $lifetime, $args);
/* something went wrong, return openssl error */
if (empty($result) || !empty($result['error'])) {
$result['error'] = $result['error'] ?? '';
while ($ssl_err = openssl_error_string()) {
$result['error'] .= " " . $ssl_err;
}
}
self::_addSSLErrors($result);
// remove tempfile (template)
@unlink($config_filename);
@unlink($args['filename']);
error_reporting($old_err_level);
return $result;
}
/**
* re-issue a certificate, when signed by a CA, make sure to serialize the config after doing so,
* it's the callers responsibility to update the serial number administration of the supplied CA.
* A call to \OPNsense\Core\Config::getInstance()->save(); would persist the new serial.
*
* @param string $keylen_curve rsa key length or elliptic curve name to use
* @param int $lifetime in number of days
* @param array $dn subject to use
* @param string $prv private key
* @param string $digest_alg digest algorithm
* @param string $caref key to certificate authority
* @param string $x509_extensions openssl section to use
* @param array $extns template fragments to replace in openssl.cnf
* @return array containing generated certificate or returned errors
*/
public static function reIssueCert(
$keylen_curve,
$lifetime,
$dn,
$prv,
$digest_alg,
$caref,
$x509_extensions = 'usr_cert',
$extns = []
) {
$old_err_level = error_reporting(0); /* prevent openssl error from going to stderr/stdout */
$args = self::_createSSLOptions($keylen_curve, $digest_alg, $x509_extensions, $extns);
$csr = openssl_csr_new($dn, $prv, $args);
if ($csr !== false) {
$result = self::_signCert($csr, $caref, $lifetime, $args);
} else {
$result = [];
}
self::_addSSLErrors($result);
// remove tempfile (template)
@unlink($args['filename']);
error_reporting($old_err_level);
return $result;
}
@ -270,22 +343,7 @@ class Store
) {
$result = [];
$old_err_level = error_reporting(0); /* prevent openssl error from going to stderr/stdout */
// handle parameters which can only be set via the configuration file
$config_filename = create_temp_openssl_config($extns);
$args = [
'config' => $config_filename,
'x509_extensions' => $x509_extensions,
'digest_alg' => $digest_alg,
'encrypt_key' => false
];
if (is_numeric($keylen_curve)) {
$args['private_key_type'] = OPENSSL_KEYTYPE_RSA;
$args['private_key_bits'] = (int)$keylen_curve;
} else {
$args['private_key_type'] = OPENSSL_KEYTYPE_EC;
$args['curve_name'] = $keylen_curve;
}
$args = self::_createSSLOptions($keylen_curve, $digest_alg, $x509_extensions, $extns);
// generate a new key pair
$res_key = openssl_pkey_new($args);
@ -293,7 +351,7 @@ class Store
$res_csr = openssl_csr_new($dn, $res_key, $args);
if ($res_csr !== false) {
if ($caref !== false) {
$tmp = self::_signCert($res_csr, $caref !== null ? $caref : $res_key, $lifetime, $args);
$tmp = self::_signCert($res_csr, !empty($caref) ? $caref : $res_key, $lifetime, $args);
if (!empty($tmp['crt'])) {
$result = ['caref' => $caref, 'crt' => $tmp['crt'], 'prv' => $str_key];
} else {
@ -307,16 +365,10 @@ class Store
}
}
}
/* something went wrong, return openssl error */
if (empty($result) || !empty($result['error'])) {
$result['error'] = $result['error'] ?? '';
while ($ssl_err = openssl_error_string()) {
$result['error'] .= " " . $ssl_err;
}
}
self::_addSSLErrors($result);
// remove tempfile (template)
@unlink($config_filename);
@unlink($args['filename']);
error_reporting($old_err_level);
return $result;
}
@ -412,26 +464,4 @@ class Store
}
return implode("\n", $chain);
}
/**
* Create a temporary config file, to help with calls that require properties that can only be set via the config file.
*
* @param $dn
* @return string The name of the temporary config file.
*/
public static function createTempOpenSSLconfig($extns = [])
{
// define temp filename to use for openssl.cnf and add extensions values to it
$configFilename = tempnam(sys_get_temp_dir(), 'ssl');
$template = file_get_contents('/usr/local/etc/ssl/opnsense.cnf');
foreach (array_keys($extns) as $extnTag) {
$template_extn = $extnTag . ' = ' . str_replace(array("\r", "\n"), '', $extns[$extnTag]);
// Overwrite the placeholders for this property
$template = str_replace('###OPNsense:' . $extnTag . '###', $template_extn, $template);
}
file_put_contents($configFilename, $template);
return $configFilename;
}
}

View File

@ -24,7 +24,7 @@
<reissue>Reissue and replace certificate (does not restart services)</reissue>
</OptionValues>
</action>
<key type="OptionField" volatile="true">
<key_type type="OptionField" volatile="true">
<Required>Y</Required>
<Default>2048</Default>
<OptionValues>
@ -38,7 +38,7 @@
<secp384r1>Elliptic Curve secp384r1</secp384r1>
<secp521r1>Elliptic Curve secp521r1</secp521r1>
</OptionValues>
</key>
</key_type>
<digest type="OptionField" volatile="true">
<Required>Y</Required>
<Default>sha256</Default>
@ -60,9 +60,13 @@
<v3_ca>Certificate Authority</v3_ca>
</OptionValues>
</cert_type>
<lifetime type="IntegerField" volatile="true">
<Required>Y</Required>
<Default>397</Default>
</lifetime>
<private_key_location type="OptionField" volatile="true">
<Required>Y</Required>
<Default>usr_cert</Default>
<Default>firewall</Default>
<OptionValues>
<firewall>Save on this firewall</firewall>
<local>Download and do not save</local>
@ -85,7 +89,9 @@
<Default>NL</Default>
<Required>Y</Required>
</country>
<email type="EmailField"/>
<email type="TextField" volatile="true">
<Mask>/^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/</Mask>
</email>
<commonname type="TextField" volatile="true">
<Mask>/^[^\x00-\x08\x0b\x0c\x0e-\x1f\n]*$/</Mask>
</commonname>

View File

@ -115,11 +115,15 @@
target.show();
}
});
/* expand PEM section */
/* expand/collapse PEM section */
if (['import', 'import_csr'].includes($(this).val())) {
if ($(".pem_section > table > tbody > tr:eq(0) > td:eq(0)").is(':hidden')) {
$(".pem_section > table > thead").click();
}
} else {
if (!$(".pem_section > table > tbody > tr:eq(0) > td:eq(0)").is(':hidden')) {
$(".pem_section > table > thead").click();
}
}
});