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

This commit is contained in:
Ad Schellevis 2024-03-12 18:28:42 +01:00
parent 672e8ba9e1
commit 73344dfa79
4 changed files with 273 additions and 7 deletions

View File

@ -39,6 +39,16 @@ use OPNsense\Trust\Store as CertStore;
*/
class CrlController extends ApiControllerBase
{
private static $status_codes = [
'0' => 'unspecified',
'1' => 'keyCompromise',
'2' => 'cACompromise',
'3' => 'affiliationChanged',
'4' => 'superseded',
'5' => 'cessationOfOperation',
'6' => 'certificateHold'
];
public function searchAction()
{
$this->sessionClose();
@ -55,6 +65,9 @@ class CrlController extends ApiControllerBase
return $this->searchRecordsetBase(array_values($items));
}
/**
* fetch (a new) revocation list for a given autority.
*/
public function getAction($caref)
{
if ($this->request->isGet() && !empty($caref)) {
@ -67,7 +80,7 @@ class CrlController extends ApiControllerBase
}
}
if ($found) {
$result = ['caref' => $caref];
$result = ['caref' => $caref, 'descr' => ''];
foreach ($config->crl as $node) {
if ((string)$node->caref == $caref) {
$result['descr'] = (string)$node->descr;
@ -76,21 +89,153 @@ class CrlController extends ApiControllerBase
$certs = [];
foreach ($config->cert as $node) {
if ((string)$node->caref == $caref) {
$certs[(string)$node->refid] = (string)255;
$certs[(string)$node->refid] = [
'code' => null,
'descr' => (string)$node->descr
];
}
}
$crlmethod = 'internal';
foreach ($config->crl as $node) {
if ((string)$node->caref == $caref) {
foreach ($node->cert as $cert) {
if (!empty((string)$cert->refid)) {
$certs[(string)$cert->refid] = (string)$cert->reason;
$certs[(string)$cert->refid] = [
'code' => (string)$cert->reason == '-1' ? '0' : (string)$cert->reason,
'descr' => (string)$cert->descr
];
}
}
$crlmethod = (string)$node->crlmethod;
$result['text'] = !empty((string)$node->text) ? base64_decode((string)$node->text) : '';
}
}
$result['crlmethod'] = [
'internal' => [
'value' => gettext('Internal'),
'selected' => $crlmethod == 'internal' ? '1' : '0'
],
'existing' => [
'value' => gettext('Import existing'),
'selected' => $crlmethod == 'existing' ? '1' : '0'
],
];
for ($i=0 ; $i <= 6; $i++) {
$code = (string)$i;
$result['revoked_reason_' . $code] = [];
foreach ($certs as $ref => $data) {
$result['revoked_reason_' . $code][$ref] = [
'value' => $data['descr'],
'selected' => $data['code'] === $code ? '1' : '0'
];
}
}
return ['crl' => $result, 'certs' => $certs];
return ['crl' => $result];
}
return ['caref' => '', 'descr' => ''];
}
}
/**
* set crl for a certificate authority, mimicking standard model operations
* (which we can not use due to the nested structure of the CRL's)
*/
public function setAction($caref)
{
if ($this->request->isPost() && !empty($caref)) {
Config::getInstance()->lock();
$config = Config::getInstance()->object();
$payload = $_POST['crl'] ?? [];
$validations = [];
if (!in_array($payload['crlmethod'], ['internal', 'existing'])) {
$validations['crl.crlmethod'] = [sprintf(gettext('Invalid method %s'), $payload['crlmethod'])];
}
if (!preg_match('/^(.){1,255}$/', $payload['descr'] ?? '')) {
$validations['crl.descr'] = gettext('Description should be a string between 1 and 255 characters.');
}
$found = false;
foreach ($config->ca as $node) {
if ((string)$node->refid == $caref) {
$found = true;
break;
}
}
if (!$found) {
$validations['crl.caref'] = gettext('Certificate does not seem to exist');
}
if (!empty($validations)) {
return ['status' => 'failed', 'validations' => $validations];
} else {
$revoked_refs = [];
for ($i=0 ; $i <= 6; $i++) {
$fieldname = 'revoked_reason_' . (string)$i;
foreach (explode(',', $payload[$fieldname] ?? '') as $refid) {
if (!empty($refid)) {
$revoked_refs[$refid] = (string)$i;
}
}
}
$crl = null;
foreach ($config->crl as $node) {
if ((string)$node->caref == $caref) {
if ($crl !== null) {
/* When duplicate CRL's exist, remove all but the first */
$dom = dom_import_simplexml($node);
$dom->parentNode->removeChild($dom);
} else {
$crl = $node;
}
}
}
$last_crl = null;
if ($crl === null) {
$last_crl = current($config->xpath('//opnsense/crl[last()]'));
if ($last_crl) {
$crl = simplexml_load_string('<crl/>');
} else {
$crl = $config->addChild('crl');
}
}
$crl->caref = (string)$caref;
$crl->descr = (string)$payload['descr'];
foreach ($crl->cert as $cert) {
if (!isset($revoked_refs[(string)$cert->refid])) {
$dom = dom_import_simplexml($cert);
$dom->parentNode->removeChild($dom);
} else {
$cert->reason = $revoked_refs[(string)$cert->refid];
unset($revoked_refs[(string)$cert->refid]);
}
}
foreach ($config->cert as $cert) {
if (isset($revoked_refs[(string)$cert->refid])) {
$tmp = $crl->addChild('cert');
$tmp->refid = (string)$cert->refid;
$tmp->descr = (string)$cert->descr;
$tmp->caref = (string)$cert->caref;
$tmp->crt = (string)$cert->crt;
$tmp->prv = (string)$cert->prv;
$tmp->revoke_time = (string)time();
$tmp->reason = $revoked_refs[(string)$cert->refid];
}
}
if ($last_crl) {
/* insert new item after last crl */
$target = dom_import_simplexml($last_crl);
$insert = $target->ownerDocument->importNode(dom_import_simplexml($crl), true);
if ($target->nextSibling) {
$target->parentNode->insertBefore($insert, $target->nextSibling);
} else {
$target->parentNode->appendChild($insert);
}
}
Config::getInstance()->save();
return ['status' => 'saved'];
}
}
return ['status' => 'failed'];
}
}

View File

@ -32,6 +32,7 @@ class CrlController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->formDialogCrl = $this->getForm("dialogCrl");
$this->view->pick('OPNsense/Trust/crl');
}
}

View File

@ -0,0 +1,70 @@
<form>
<field>
<id>crl.crlmethod</id>
<label>Method</label>
<type>dropdown</type>
</field>
<field>
<id>crl.caref</id>
<label>Caref</label>
<type>info</type>
</field>
<field>
<id>crl.descr</id>
<label>Description</label>
<type>text</type>
</field>
<field>
<id>crl.text</id>
<label>CRL data</label>
<type>textbox</type>
<style>crlmethod crlmethod_existing</style>
</field>
<field>
<type>header</type>
<label>Revocations per type</label>
<style>crlmethod crlmethod_internal</style>
</field>
<field>
<id>crl.revoked_reason_0</id>
<label>Unspecified</label>
<type>select_multiple</type>
<style>selectpicker revoked</style>
</field>
<field>
<id>crl.revoked_reason_1</id>
<label>Key Compromise</label>
<type>select_multiple</type>
<style>selectpicker revoked</style>
</field>
<field>
<id>crl.revoked_reason_2</id>
<label>CA Compromise</label>
<type>select_multiple</type>
<style>selectpicker revoked</style>
</field>
<field>
<id>crl.revoked_reason_3</id>
<label>Affiliation Changed</label>
<type>select_multiple</type>
<style>selectpicker revoked</style>
</field>
<field>
<id>crl.revoked_reason_4</id>
<label>Superseded</label>
<type>select_multiple</type>
<style>selectpicker revoked</style>
</field>
<field>
<id>crl.revoked_reason_5</id>
<label>Cessation of Operation</label>
<type>select_multiple</type>
<style>selectpicker revoked</style>
</field>
<field>
<id>crl.revoked_reason_6</id>
<label>Certificate Hold</label>
<type>select_multiple</type>
<style>selectpicker revoked</style>
</field>
</form>

View File

@ -39,12 +39,51 @@
$(this).html($("#crl\\.descr").val() !== '' ? $("#crl\\.descr").val() : '-');
$(this).show();
});
$("#DialogCrl").change(function(){
if ($("#edit_crl").is(':visible')) {
$("#tab_crls").click();
$("#DialogCrl").hide();
}
});
$("#crl\\.crlmethod").change(function(event){
let this_action = $(this).val();
$(".crlmethod").each(function(){
let target = null;
if ($(this)[0].tagName == 'DIV') {
target = $(this)
} else {
target = $(this).closest("tr");
}
target.hide();
if ($(this).hasClass('crlmethod_' + this_action)) {
target.show();
}
});
});
/* when revoking a certificate, make sure we only add the same cert in one group */
$(".revoked").change(function(event){
event.preventDefault();
let values = $(this).val();
let that = $(this).attr('id');
$("select.revoked").each(function(){
if ($(this).attr('id') !== that) {
let target = $(this);
values.forEach(function(refid){
target.find('option[value="'+refid+'"]').prop("selected", false);
});
target.selectpicker('refresh');
}
});
})
});
</script>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#cert">{{ lang._('Index') }}</a></li>
<li class="active"><a data-toggle="tab" id="tab_crls" href="#cert">{{ lang._('Index') }}</a></li>
<li><a data-toggle="tab" href="#edit_crl" id="DialogCrl" style="display: none;"> </a></li>
</ul>
<div class="tab-content content-box">
@ -73,8 +112,19 @@
</div>
<div id="edit_crl" class="tab-pane fade in">
<form id="frm_DialogCrl">
<input type="text" class="form-control" size="50" id="crl.caref">
<input type="text" class="form-control" size="50" id="crl.descr">
{{ partial("layout_partials/base_form",['fields':formDialogCrl,'id':'DialogCrl'])}}
</form>
<table class="table table-condensed">
<tbody>
<tr>
<td>
<button class="btn btn-primary" id="btn_DialogCrl_save" type="button">
<b>{{ lang._('Apply') }}</b>
<i id="DialogCrl_progress" class=""></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>