Skip to content

Commit

Permalink
Merge pull request #4427 from LibreSign/backport/4424/stable30
Browse files Browse the repository at this point in the history
[stable30] feat: add extracerts to generated cert
  • Loading branch information
vitormattos authored Jan 22, 2025
2 parents c61dcc8 + b1fe9c5 commit 5d6f5db
Show file tree
Hide file tree
Showing 7 changed files with 592 additions and 145 deletions.
4 changes: 4 additions & 0 deletions lib/Handler/CertificateEngine/CfsslHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ public function generateCertificate(): string {
$certKeys['private_key'],
[
'friendly_name' => $this->getFriendlyName(),
'extracerts' => [
$certKeys['certificate'],
$certKeys['certificate_request'],
],
],
);
}
Expand Down
61 changes: 46 additions & 15 deletions lib/Handler/CertificateEngine/OpenSslHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,11 @@ public function generateCertificate(): string {
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
$temporaryFile = $this->tempManager->getTemporaryFile('.cfg');
// More information about x509v3: https://www.openssl.org/docs/manmaster/man5/x509v3_config.html
file_put_contents($temporaryFile, <<<CONFIG
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment, keyCertSign
extendedKeyUsage = clientAuth, emailProtection
subjectAltName = {$this->getSubjectAltNames()}
authorityKeyIdentifier = keyid
subjectKeyIdentifier = hash
# certificatePolicies = <policyOID> CPS: http://url/with/policy/informations.pdf
CONFIG);

$csr = openssl_csr_new($this->getCsrNames(), $privateKey);

$x509 = openssl_csr_sign($csr, $rootCertificate, $rootPrivateKey, $this->expirity(), [
'config' => $temporaryFile,
'config' => $this->getFilenameToLeafCert(),
// This will set "basicConstraints" to CA:FALSE, the default is CA:TRUE
// The signer certificate is not a Certificate Authority
'x509_extensions' => 'v3_req',
Expand All @@ -93,15 +83,56 @@ public function generateCertificate(): string {
$privateKey,
[
'friendly_name' => $this->getFriendlyName(),
'extracerts' => [
$x509,
$rootCertificate,
],
],
);
}

private function getFilenameToLeafCert(): string {
$temporaryFile = $this->tempManager->getTemporaryFile('.cfg');
// More information about x509v3: https://www.openssl.org/docs/manmaster/man5/x509v3_config.html
$config = [
'v3_req' => [
'basicConstraints' => 'CA:FALSE',
'keyUsage' => 'digitalSignature, keyEncipherment, keyCertSign',
'extendedKeyUsage' => 'clientAuth, emailProtection',
'subjectAltName' => $this->getSubjectAltNames(),
'authorityKeyIdentifier' => 'keyid',
'subjectKeyIdentifier' => 'hash',
// @todo Implement a feature to define this PDF at Administration Settings
// 'certificatePolicies' => '<policyOID> CPS: http://url/with/policy/informations.pdf',
]
];
if (empty($config['v3_req']['subjectAltName'])) {
unset($config['v3_req']['subjectAltName']);
}
$config = $this->arrayToIni($config);
file_put_contents($temporaryFile, $config);
return $temporaryFile;
}

private function arrayToIni(array $config) {
$fileContent = '';
foreach ($config as $i => $v) {
if (is_array($v)) {
$fileContent .= "\n[$i]\n" . $this->arrayToIni($v);
} else {
$fileContent .= "$i = " . (str_contains($v, "\n") ? '"' . $v . '"' : $v) . "\n";
}
}
return $fileContent;
}

private function getSubjectAltNames(): string {
$hosts = $this->getHosts();
$altNames = [];
foreach ($hosts as $email) {
$altNames[] = 'email:' . $email;
foreach ($hosts as $host) {
if (filter_var($host, FILTER_VALIDATE_EMAIL)) {
$altNames[] = 'email:' . $host;
}
}
return implode(', ', $altNames);
}
Expand Down
71 changes: 52 additions & 19 deletions lib/Handler/CertificateEngine/OrderCertificatesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,70 @@

namespace OCA\Libresign\Handler\CertificateEngine;

use InvalidArgumentException;

trait OrderCertificatesTrait {
public function orderCertificates(array $certificates): array {
$ordered = [];
$map = [];
$this->validateCertificateStructure($certificates);
$remainingCerts = [];

$tree = current($certificates);
// Add the root cert at ordered list and collect the remaining certs
foreach ($certificates as $cert) {
if ($tree['subject'] === $cert['issuer']) {
$tree = $cert;
if (!$this->arrayDiffCanonicalized($cert['subject'], $cert['issuer'])) {
$ordered = [$cert];
continue;
}
$map[$cert['name']] = $cert;
$remainingCerts[$cert['name']] = $cert;
}

if (!$tree) {
if (!isset($ordered)) {
return $certificates;
}
unset($map[$tree['name']]);
$ordered[] = $tree;

$current = $tree;
while (!empty($map) && $current) {
if ($current['subject'] === $tree['issuer']) {
$ordered[] = $current;
$tree = $current;
unset($map[$current['name']]);
$current = reset($map);
continue;


while (!empty($remainingCerts)) {
$found = false;
foreach ($remainingCerts as $name => $cert) {
$first = reset($ordered);
if (!$this->arrayDiffCanonicalized($first['subject'], $cert['issuer'])) {
array_unshift($ordered, $cert);
unset($remainingCerts[$name]);
$found = true;
break;
}
}

if (!$found) {
throw new InvalidArgumentException('Certificate chain is incomplete or invalid.');
}
$current = next($map);
}

return $ordered;
}

private function validateCertificateStructure(array $certificates): void {
if (empty($certificates)) {
throw new InvalidArgumentException('Certificate list cannot be empty');
}

foreach ($certificates as $cert) {
if (!isset($cert['subject'], $cert['issuer'], $cert['name'])) {
throw new InvalidArgumentException(
'Invalid certificate structure. Certificate must have "subject", "issuer", and "name".'
);
}
}

$names = array_column($certificates, 'name');
if (count($names) !== count(array_unique($names))) {
throw new InvalidArgumentException('Duplicate certificate names detected');
}
}

private function arrayDiffCanonicalized(array $array1, array $array2): array {
sort($array1);
sort($array2);

return array_diff($array1, $array2);
}
}
Loading

0 comments on commit 5d6f5db

Please sign in to comment.