<?php
 declare(strict_types=1); namespace SpomkyLabs\Pki\X509\CertificationPath\PathBuilding; use SpomkyLabs\Pki\X509\Certificate\Certificate; use SpomkyLabs\Pki\X509\Certificate\CertificateBundle; use SpomkyLabs\Pki\X509\CertificationPath\CertificationPath; use SpomkyLabs\Pki\X509\CertificationPath\Exception\PathBuildingException; use function count; final class CertificationPathBuilder { private function __construct( private readonly CertificateBundle $trustList ) { } public static function create(CertificateBundle $trustList): self { return new self($trustList); } public function allPathsToTarget(Certificate $target, ?CertificateBundle $intermediate = null): array { $paths = $this->resolvePathsToTarget($target, $intermediate); return array_map(static fn ($certs) => CertificationPath::create(...$certs), $paths); } public function shortestPathToTarget( Certificate $target, ?CertificateBundle $intermediate = null ): CertificationPath { $paths = $this->allPathsToTarget($target, $intermediate); if (count($paths) === 0) { throw new PathBuildingException('No certification paths.'); } usort($paths, fn ($a, $b) => count($a) < count($b) ? -1 : 1); return reset($paths); } private function findIssuers(Certificate $target, CertificateBundle $bundle): array { $issuers = []; $issuer_name = $target->tbsCertificate() ->issuer(); $extensions = $target->tbsCertificate() ->extensions(); if ($extensions->hasAuthorityKeyIdentifier()) { $ext = $extensions->authorityKeyIdentifier(); if ($ext->hasKeyIdentifier()) { foreach ($bundle->allBySubjectKeyIdentifier($ext->keyIdentifier()) as $issuer) { if ($issuer->tbsCertificate()->subject()->equals($issuer_name)) { $issuers[] = $issuer; } } } } return $issuers; } private function resolvePathsToTarget(Certificate $target, ?CertificateBundle $intermediate = null): array { $paths = []; foreach ($this->findIssuers($target, $this->trustList) as $issuer) { if ($target->equals($issuer)) { $paths[] = [$target]; } else { $paths[] = [$issuer, $target]; } } if (isset($intermediate)) { foreach ($this->findIssuers($target, $intermediate) as $issuer) { if ($issuer->isSelfIssued()) { continue; } $subpaths = $this->resolvePathsToTarget($issuer, $intermediate); foreach ($subpaths as $path) { $paths[] = array_merge($path, [$target]); } } } return $paths; } } 