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

* add certificate purpose according to rfc3280
* add info button for raw (readable) certificate and csr output
This commit is contained in:
Ad Schellevis 2024-03-03 15:40:49 +01:00
parent dbd80f33f5
commit 875fb5d8b3
4 changed files with 141 additions and 5 deletions

View File

@ -141,7 +141,9 @@ class CertController extends ApiMutableModelControllerBase
$filter_funct = function ($record) use ($carefs) {
return empty($carefs) || array_intersect(explode(',', $record->caref), $carefs);
};
return $this->searchBase('cert', ['descr', 'caref', 'name', 'valid_from', 'valid_to'], null, $filter_funct);
return $this->searchBase(
'cert', ['descr', 'caref', 'rfc3280_purpose', 'name', 'valid_from', 'valid_to'], null, $filter_funct
);
}
public function getAction($uuid = null)
@ -179,6 +181,19 @@ class CertController extends ApiMutableModelControllerBase
return [];
}
public function rawDumpAction($uuid)
{
$payload = $this->getBase('cert', 'cert', $uuid);
if (!empty($payload['cert'])) {
if (!empty($payload['cert']['crt_payload'])) {
return CertStore::dumpX509($payload['cert']['crt_payload']);
} elseif (!empty($payload['cert']['csr_payload'])) {
return CertStore::dumpCSR($payload['cert']['csr_payload']);
}
}
return [];
}
public function caListAction()
{
$result = [];

View File

@ -446,11 +446,97 @@ class Store
$result[$target] = implode('\n', $values);
}
}
/* Extract certificate purpose */
$purpose = [];
foreach (['basicConstraints', 'extendedKeyUsage', 'keyUsage', 'authorityInfoAccess'] as $ext) {
$purpose[$ext] = [];
if (!empty($crt['extensions'][$ext])) {
foreach (explode(",", $crt['extensions'][$ext]) as $item) {
$purpose[$ext][] = trim($item);
}
}
}
// rfc3280 purpose definitions
$result['rfc3280_purpose'] = '';
if (
in_array('TLS Web Server Authentication', $purpose['extendedKeyUsage']) &&
in_array('Digital Signature', $purpose['keyUsage']) && (
in_array('Key Encipherment', $purpose['keyUsage']) ||
in_array('Key Agreement', $purpose['keyUsage'])
)
) {
$result['rfc3280_purpose'] = 'id-kp-serverAuth';
} elseif (
in_array('TLS Web Client Authentication', $purpose['extendedKeyUsage']) &&
in_array('Digital Signature', $purpose['keyUsage'])
) {
$result['rfc3280_purpose'] = 'id-kp-clientAuth';
} elseif (
in_array('OCSP Signing', $purpose['extendedKeyUsage']) &&
in_array('Digital Signature', $purpose['keyUsage'])
) {
$result['rfc3280_purpose'] = 'id-kp-OCSPSigning';
}
//
return $result;
}
return false;
}
/**
* @param string $cert certificate
* @return array [stdout|stderr]
*/
public static function dumpX509($cert)
{
$result = [];
$process = proc_open(
'/usr/local/bin/openssl x509 -fingerprint -sha256 -text',
[["pipe", "r"], ["pipe", "w"], ["pipe", "w"]],
$pipes
);
if (is_resource($process)) {
fwrite($pipes[0], $cert);
fclose($pipes[0]);
$result['stdout'] = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$result['stderr'] = stream_get_contents($pipes[2]);
fclose($pipes[2]);
proc_close($process);
}
return $result;
}
/**
* @param string $csr CSR
* @return array [stdout|stderr]
*/
public static function dumpCSR($csr)
{
$result = [];
$process = proc_open(
'/usr/local/bin/openssl req -text -noout',
[["pipe", "r"], ["pipe", "w"], ["pipe", "w"]],
$pipes
);
if (is_resource($process)) {
fwrite($pipes[0], $csr);
fclose($pipes[0]);
$result['stdout'] = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$result['stderr'] = stream_get_contents($pipes[2]);
fclose($pipes[2]);
proc_close($process);
}
return $result;
}
/**
* Extract certificate chain
* @param string $caref reference number

View File

@ -111,6 +111,7 @@
<crt_payload type="TextField" volatile="true"/>
<csr_payload type="TextField" volatile="true"/>
<prv_payload type="TextField" volatile="true"/>
<rfc3280_purpose type="TextField" volatile="true"/>
<name type="TextField" volatile="true"/>
<valid_from type="TextField" volatile="true"/>

View File

@ -41,6 +41,26 @@
}
return request;
}
},
commands: {
raw_dump: {
method: function(event){
let uuid = $(this).data("row-id") !== undefined ? $(this).data("row-id") : '';
ajaxGet('/api/trust/cert/raw_dump/' + uuid, {}, function(data, status){
if (data.stdout) {
BootstrapDialog.show({
title: "{{ lang._('Certificate info') }}",
type:BootstrapDialog.TYPE_INFO,
message: $("<div/>").text(data.stdout).html(),
cssClass: 'monospace-dialog',
});
}
});
},
classname: 'fa fa-fw fa-info-circle',
title: "{{ lang._('show certificate info') }}",
sequence: 10
}
}
});
grid_cert.on("loaded.rs.jquery.bootgrid", function (e){
@ -126,13 +146,26 @@
}
}
});
});
</script>
<style>
.monospace-dialog {
font-family: monospace;
white-space: pre;
}
.monospace-dialog > .modal-dialog {
width:70% !important;
}
.modal-body {
max-height: calc(100vh - 210px);
overflow-y: auto;
}
</style>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#cert">{{ lang._('Certificates') }}</a></li>
</ul>
@ -151,10 +184,11 @@
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="descr" data-width="15em" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="caref" data-width="15em" data-type="string">{{ lang._('Issuer') }}</th>
<th data-column-id="rfc3280_purpose" data-width="10em" data-type="string">{{ lang._('Purpose') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Name') }}</th>
<th data-column-id="valid_from" data-width="10em" data-type="datetime">{{ lang._('Valid from') }}</th>
<th data-column-id="valid_to" data-width="10em" data-type="datetime">{{ lang._('Valid to') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
<th data-column-id="commands" data-width="9em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>