From 0a6e437a48d57b4b6fc061e1a2e2b844124a9933 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Fri, 7 Jul 2023 17:01:05 +0200 Subject: [PATCH] Use `SAN` as cert name if `CN` isn't pressent --- library/X509/CertificateUtils.php | 105 ++++++++++++++++++------------ 1 file changed, 62 insertions(+), 43 deletions(-) diff --git a/library/X509/CertificateUtils.php b/library/X509/CertificateUtils.php index 81a06374..b4665323 100644 --- a/library/X509/CertificateUtils.php +++ b/library/X509/CertificateUtils.php @@ -5,6 +5,7 @@ namespace Icinga\Module\X509; use Exception; +use Generator; use Icinga\Application\Logger; use Icinga\File\Storage\TemporaryLocalFileStorage; use Icinga\Module\X509\Model\X509Certificate; @@ -16,6 +17,7 @@ use ipl\Sql\Expression; use ipl\Sql\Select; use ipl\Stdlib\Filter; +use Traversable; class CertificateUtils { @@ -123,15 +125,14 @@ public static function duration($seconds) * * If the given DN contains a CN, the CN is returned. Else, the DN is returned as string. * - * @param array $dn + * @param array $dn * * @return string The CN if it exists or the full DN as string */ - private static function shortNameFromDN(array $dn) + private static function shortNameFromDN(array $dn): string { if (isset($dn['CN'])) { - $cn = (array) $dn['CN']; - return $cn[0]; + return ((array) $dn['CN'])[0]; } else { $result = []; foreach ($dn as $key => $value) { @@ -151,16 +152,31 @@ private static function shortNameFromDN(array $dn) /** * Split the given Subject Alternative Names into key-value pairs * - * @param string $sans + * @param string $sans * - * @return \Generator + * @return Generator */ - private static function splitSANs($sans) + private static function splitSANs(string $sans): Generator { preg_match_all('/(?:^|, )([^:]+):/', $sans, $keys); - $values = preg_split('/(^|, )[^:]+:\s*/', $sans); - for ($i = 0; $i < count($keys[1]); $i++) { - yield [$keys[1][$i], $values[$i + 1]]; + $orderedKeys = $keys[1]; + usort($orderedKeys, function (string $a, string $b) { + $order = ['DNS' => 0, 'rfc822Name' => 1, 'URI' => 2, 'IP Address' => 3, 'email' => 4]; + if (! isset($order[$a])) { + return 1; + } + + if (! isset($order[$b])) { + return -1; + } + + return $order[$a] <=> $order[$b]; + }); + + $keys = array_flip($keys[1]); + $values = preg_split('/(^|, )[^:]+:\s*/', $sans, -1, PREG_SPLIT_NO_EMPTY); + for ($i = 0; $i < count($keys); $i++) { + yield [$orderedKeys[$i], $values[$keys[$orderedKeys[$i]]]]; } } @@ -169,7 +185,7 @@ private static function splitSANs($sans) * * @param string $file Path to the bundle * - * @return \Generator + * @return Generator */ public static function parseBundle($file) { @@ -232,11 +248,18 @@ public static function findOrInsertCert(Connection $db, $cert) $pubkey = openssl_pkey_get_details(openssl_pkey_get_public($cert)); $signature = explode('-', $certInfo['signatureTypeSN']); + $sans = static::splitSANs($certInfo['extensions']['subjectAltName'] ?? ''); + if (! isset($certInfo['subject']['CN']) && $sans->valid()) { + $subject = $sans->current()[1]; + } else { + $subject = static::shortNameFromDN($certInfo['subject']); + } + // TODO: https://github.com/Icinga/ipl-orm/pull/78 $db->insert( 'x509_certificate', [ - 'subject' => CertificateUtils::shortNameFromDN($certInfo['subject']), + 'subject' => $subject, 'subject_hash' => $dbTool->marshalBinary($subjectHash), 'issuer' => CertificateUtils::shortNameFromDN($certInfo['issuer']), 'issuer_hash' => $dbTool->marshalBinary($issuerHash), @@ -258,53 +281,49 @@ public static function findOrInsertCert(Connection $db, $cert) $certId = $db->lastInsertId(); - CertificateUtils::insertSANs($db, $certId, $certInfo); + CertificateUtils::insertSANs($db, $certId, $sans); return [$certId, $issuerHash]; } - private static function insertSANs($db, $certId, array $certInfo) + private static function insertSANs($db, $certId, Traversable $sans): void { $dbTool = new DbTool($db); + foreach ($sans as $san) { + list($type, $value) = $san; - if (isset($certInfo['extensions']['subjectAltName'])) { - foreach (CertificateUtils::splitSANs($certInfo['extensions']['subjectAltName']) as $san) { - list($type, $value) = $san; - - $hash = hash('sha256', sprintf('%s=%s', $type, $value), true); - - $row = X509CertificateSubjectAltName::on($db); - $row->columns([new Expression('1')]); + $hash = hash('sha256', sprintf('%s=%s', $type, $value), true); - $filter = Filter::all( - Filter::equal('certificate_id', $certId), - Filter::equal('hash', $hash) - ); + $row = X509CertificateSubjectAltName::on($db); + $row->columns([new Expression('1')]); - $row->filter($filter); + $filter = Filter::all( + Filter::equal('certificate_id', $certId), + Filter::equal('hash', $hash) + ); - // Ignore duplicate SANs - if ($row->execute()->hasResult()) { - continue; - } + $row->filter($filter); - // TODO: https://github.com/Icinga/ipl-orm/pull/78 - $db->insert( - 'x509_certificate_subject_alt_name', - [ - 'certificate_id' => $certId, - 'hash' => $dbTool->marshalBinary($hash), - 'type' => $type, - 'value' => $value, - 'ctime' => new Expression('UNIX_TIMESTAMP() * 1000') - ] - ); + // Ignore duplicate SANs + if ($row->execute()->hasResult()) { + continue; } + + // TODO: https://github.com/Icinga/ipl-orm/pull/78 + $db->insert( + 'x509_certificate_subject_alt_name', + [ + 'certificate_id' => $certId, + 'hash' => $dbTool->marshalBinary($hash), + 'type' => $type, + 'value' => $value, + 'ctime' => new Expression('UNIX_TIMESTAMP() * 1000') + ] + ); } } - private static function findOrInsertDn($db, $certInfo, $type) { $dbTool = new DbTool($db);