Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
92.24% |
202 / 219 |
|
52.94% |
9 / 17 |
CRAP | |
0.00% |
0 / 1 |
| CertificateSignersMergeService | |
92.24% |
202 / 219 |
|
52.94% |
9 / 17 |
160.52 | |
0.00% |
0 / 1 |
| merge | |
100.00% |
39 / 39 |
|
100.00% |
1 / 1 |
8 | |||
| isTechnicalTimestampEntry | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
9.00 | |||
| getSingleContractSignerIndex | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
6 | |||
| hydrateTimestampOnly | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| resolveCertSignerUid | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
12.34 | |||
| resolveTargetIndex | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
| ensureSignerSlotExists | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| hydrateSignerFromCertData | |
85.71% |
24 / 28 |
|
0.00% |
0 / 1 |
16.75 | |||
| assignSignerUid | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
5.39 | |||
| assignSignerDisplayName | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
7 | |||
| hasContractSigners | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
8.30 | |||
| buildSignerIndexMap | |
60.00% |
6 / 10 |
|
0.00% |
0 / 1 |
10.14 | |||
| findMatchingSignerIndex | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
| nextAvailableSignerIndex | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
| processChainData | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
11 | |||
| enrichSignerWithCertificateValidation | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
27 | |||
| tryMatchWithExistingSigners | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
18 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | /** |
| 5 | * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors |
| 6 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 7 | */ |
| 8 | |
| 9 | namespace OCA\Libresign\Service\File; |
| 10 | |
| 11 | use DateTime; |
| 12 | use DateTimeInterface; |
| 13 | |
| 14 | class CertificateSignersMergeService { |
| 15 | /** |
| 16 | * @param callable(array, string): (?string) $resolveUid |
| 17 | * @param callable(string, string): string $buildIdentifier |
| 18 | * @param callable(string): (?string) $lookupAccountDisplayName |
| 19 | */ |
| 20 | public function merge( |
| 21 | \stdClass $fileData, |
| 22 | array $certData, |
| 23 | string $host, |
| 24 | string $signedStatusText, |
| 25 | callable $resolveUid, |
| 26 | callable $buildIdentifier, |
| 27 | callable $lookupAccountDisplayName, |
| 28 | ): void { |
| 29 | $existingSigners = $fileData->signers ?? []; |
| 30 | $hasContractSigners = $this->hasContractSigners($existingSigners); |
| 31 | $indexMap = $this->buildSignerIndexMap($existingSigners, $buildIdentifier); |
| 32 | $usedIndexes = []; |
| 33 | $lastMatchedSignerIndex = null; |
| 34 | $singleContractSignerIndex = $this->getSingleContractSignerIndex($existingSigners); |
| 35 | |
| 36 | foreach ($certData as $index => $signer) { |
| 37 | $resolvedUid = $this->resolveCertSignerUid($signer, $existingSigners, $host, $resolveUid); |
| 38 | $matchedIndex = $this->findMatchingSignerIndex($indexMap, $resolvedUid, $signer); |
| 39 | $targetIndex = $this->resolveTargetIndex( |
| 40 | $matchedIndex, |
| 41 | $existingSigners, |
| 42 | $usedIndexes, |
| 43 | $hasContractSigners, |
| 44 | $index, |
| 45 | ); |
| 46 | if ($targetIndex === null) { |
| 47 | $timestampTargetIndex = $lastMatchedSignerIndex ?? $singleContractSignerIndex; |
| 48 | if ($timestampTargetIndex !== null |
| 49 | && isset($fileData->signers[$timestampTargetIndex]) |
| 50 | && $this->isTechnicalTimestampEntry($signer) |
| 51 | ) { |
| 52 | $this->hydrateTimestampOnly($fileData->signers[$timestampTargetIndex], $signer); |
| 53 | } |
| 54 | continue; |
| 55 | } |
| 56 | |
| 57 | $isLibreSignMatch = $matchedIndex !== null && isset($existingSigners[$matchedIndex]->signRequestId); |
| 58 | $usedIndexes[$targetIndex] = true; |
| 59 | |
| 60 | $this->ensureSignerSlotExists($fileData, $targetIndex); |
| 61 | $this->hydrateSignerFromCertData( |
| 62 | $fileData->signers[$targetIndex], |
| 63 | $signer, |
| 64 | $resolvedUid, |
| 65 | $isLibreSignMatch, |
| 66 | $host, |
| 67 | $signedStatusText, |
| 68 | $resolveUid, |
| 69 | $lookupAccountDisplayName, |
| 70 | ); |
| 71 | |
| 72 | if (isset($fileData->signers[$targetIndex]->uid)) { |
| 73 | $indexMap[strtolower((string)$fileData->signers[$targetIndex]->uid)] = $targetIndex; |
| 74 | } |
| 75 | |
| 76 | $lastMatchedSignerIndex = $targetIndex; |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | private function isTechnicalTimestampEntry(array $signer): bool { |
| 81 | if (!isset($signer['timestamp']) || !is_array($signer['timestamp'])) { |
| 82 | return false; |
| 83 | } |
| 84 | |
| 85 | if (isset($signer['uid']) && is_string($signer['uid']) && $signer['uid'] !== '') { |
| 86 | return false; |
| 87 | } |
| 88 | |
| 89 | $subjectUid = $signer['chain'][0]['subject']['UID'] ?? null; |
| 90 | if (is_string($subjectUid) && $subjectUid !== '') { |
| 91 | return false; |
| 92 | } |
| 93 | |
| 94 | return true; |
| 95 | } |
| 96 | |
| 97 | private function getSingleContractSignerIndex(array $signers): ?int { |
| 98 | $contractIndexes = []; |
| 99 | foreach ($signers as $index => $signer) { |
| 100 | if (is_object($signer) && isset($signer->signRequestId) && is_numeric($signer->signRequestId)) { |
| 101 | $contractIndexes[] = $index; |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | if (count($contractIndexes) === 1) { |
| 106 | return $contractIndexes[0]; |
| 107 | } |
| 108 | |
| 109 | return null; |
| 110 | } |
| 111 | |
| 112 | private function hydrateTimestampOnly(\stdClass $targetSigner, array $signer): void { |
| 113 | $targetSigner->timestamp = $signer['timestamp']; |
| 114 | if (isset($signer['timestamp']['genTime']) && $signer['timestamp']['genTime'] instanceof DateTimeInterface) { |
| 115 | $targetSigner->timestamp['genTime'] = $signer['timestamp']['genTime']->format(DateTimeInterface::ATOM); |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * @param callable(array, string): (?string) $resolveUid |
| 121 | */ |
| 122 | private function resolveCertSignerUid(array $signer, array $existingSigners, string $host, callable $resolveUid): ?string { |
| 123 | if (!isset($signer['chain'][0]) || !is_array($signer['chain'][0])) { |
| 124 | return is_string($signer['uid'] ?? null) ? $signer['uid'] : null; |
| 125 | } |
| 126 | |
| 127 | $resolvedUid = $this->tryMatchWithExistingSigners($signer['chain'][0], $existingSigners, $host, $resolveUid); |
| 128 | if ($resolvedUid) { |
| 129 | return $resolvedUid; |
| 130 | } |
| 131 | |
| 132 | $isLibreSignCert = isset($signer['chain'][0]['isLibreSignRootCA']) |
| 133 | && $signer['chain'][0]['isLibreSignRootCA'] === true; |
| 134 | if ($isLibreSignCert) { |
| 135 | $certUid = $signer['chain'][0]['subject']['UID'] ?? null; |
| 136 | if (!is_string($certUid) || $certUid === '') { |
| 137 | return null; |
| 138 | } |
| 139 | return str_contains($certUid, ':') ? $certUid : 'account:' . $certUid; |
| 140 | } |
| 141 | |
| 142 | if (is_string($signer['uid'] ?? null) && $signer['uid'] !== '') { |
| 143 | return $signer['uid']; |
| 144 | } |
| 145 | |
| 146 | return $resolveUid($signer['chain'][0], $host); |
| 147 | } |
| 148 | |
| 149 | private function resolveTargetIndex( |
| 150 | ?int $matchedIndex, |
| 151 | array $existingSigners, |
| 152 | array $usedIndexes, |
| 153 | bool $hasContractSigners, |
| 154 | int $defaultIndex, |
| 155 | ): ?int { |
| 156 | if ($matchedIndex !== null) { |
| 157 | return $matchedIndex; |
| 158 | } |
| 159 | |
| 160 | if ($hasContractSigners) { |
| 161 | return null; |
| 162 | } |
| 163 | |
| 164 | if (empty($existingSigners)) { |
| 165 | return $defaultIndex; |
| 166 | } |
| 167 | |
| 168 | return $this->nextAvailableSignerIndex($existingSigners, $usedIndexes); |
| 169 | } |
| 170 | |
| 171 | private function ensureSignerSlotExists(\stdClass $fileData, int $targetIndex): void { |
| 172 | if (!isset($fileData->signers[$targetIndex])) { |
| 173 | $fileData->signers[$targetIndex] = new \stdClass(); |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | /** |
| 178 | * @param callable(array, string): (?string) $resolveUid |
| 179 | * @param callable(string): (?string) $lookupAccountDisplayName |
| 180 | */ |
| 181 | private function hydrateSignerFromCertData( |
| 182 | \stdClass $targetSigner, |
| 183 | array $signer, |
| 184 | ?string $resolvedUid, |
| 185 | bool $isLibreSignMatch, |
| 186 | string $host, |
| 187 | string $signedStatusText, |
| 188 | callable $resolveUid, |
| 189 | callable $lookupAccountDisplayName, |
| 190 | ): void { |
| 191 | $preservedDisplayName = $isLibreSignMatch && isset($targetSigner->displayName) |
| 192 | ? $targetSigner->displayName |
| 193 | : null; |
| 194 | |
| 195 | $targetSigner->status = 2; |
| 196 | $targetSigner->statusText = $signedStatusText; |
| 197 | |
| 198 | if (isset($signer['timestamp'])) { |
| 199 | $targetSigner->timestamp = $signer['timestamp']; |
| 200 | if (isset($signer['timestamp']['genTime']) && $signer['timestamp']['genTime'] instanceof DateTimeInterface) { |
| 201 | $targetSigner->timestamp['genTime'] = $signer['timestamp']['genTime']->format(DateTimeInterface::ATOM); |
| 202 | } |
| 203 | } |
| 204 | if (isset($signer['signingTime']) && $signer['signingTime'] instanceof DateTimeInterface) { |
| 205 | $targetSigner->signingTime = $signer['signingTime']; |
| 206 | $targetSigner->signed = $signer['signingTime']->format(DateTimeInterface::ATOM); |
| 207 | } |
| 208 | if (isset($signer['docmdp'])) { |
| 209 | $targetSigner->docmdp = $signer['docmdp']; |
| 210 | } |
| 211 | if (isset($signer['docmdp_validation'])) { |
| 212 | $targetSigner->docmdp_validation = $signer['docmdp_validation']; |
| 213 | } |
| 214 | if (isset($signer['modifications'])) { |
| 215 | $targetSigner->modifications = $signer['modifications']; |
| 216 | } |
| 217 | if (isset($signer['modification_validation'])) { |
| 218 | $targetSigner->modification_validation = $signer['modification_validation']; |
| 219 | } |
| 220 | |
| 221 | if (isset($signer['chain']) && is_array($signer['chain'])) { |
| 222 | $this->processChainData($targetSigner, $signer['chain']); |
| 223 | } |
| 224 | |
| 225 | $this->assignSignerUid($targetSigner, $signer, $resolvedUid, $host, $resolveUid); |
| 226 | |
| 227 | if (isset($signer['signDate'])) { |
| 228 | $targetSigner->signDate = $signer['signDate']; |
| 229 | } |
| 230 | if (isset($signer['type'])) { |
| 231 | $targetSigner->type = $signer['type']; |
| 232 | } |
| 233 | |
| 234 | $this->assignSignerDisplayName($targetSigner, $signer, $preservedDisplayName, $lookupAccountDisplayName); |
| 235 | } |
| 236 | |
| 237 | /** |
| 238 | * @param callable(array, string): (?string) $resolveUid |
| 239 | */ |
| 240 | private function assignSignerUid(\stdClass $targetSigner, array $signer, ?string $resolvedUid, string $host, callable $resolveUid): void { |
| 241 | if (isset($signer['uid'])) { |
| 242 | $targetSigner->uid = $signer['uid']; |
| 243 | return; |
| 244 | } |
| 245 | |
| 246 | if ($resolvedUid) { |
| 247 | $targetSigner->uid = $resolvedUid; |
| 248 | return; |
| 249 | } |
| 250 | |
| 251 | if (isset($signer['chain'][0]) && is_array($signer['chain'][0])) { |
| 252 | $targetSigner->uid = $resolveUid($signer['chain'][0], $host); |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * @param callable(string): (?string) $lookupAccountDisplayName |
| 258 | */ |
| 259 | private function assignSignerDisplayName(\stdClass $targetSigner, array $signer, ?string $preservedDisplayName, callable $lookupAccountDisplayName): void { |
| 260 | if ($preservedDisplayName) { |
| 261 | $targetSigner->displayName = $preservedDisplayName; |
| 262 | return; |
| 263 | } |
| 264 | |
| 265 | if (isset($targetSigner->uid) && str_starts_with($targetSigner->uid, 'account:')) { |
| 266 | $accountId = substr($targetSigner->uid, strlen('account:')); |
| 267 | $displayName = $lookupAccountDisplayName($accountId); |
| 268 | $targetSigner->displayName = $displayName ?: $accountId; |
| 269 | return; |
| 270 | } |
| 271 | |
| 272 | if (!isset($targetSigner->displayName) && isset($signer['chain'][0])) { |
| 273 | $targetSigner->displayName = $signer['chain'][0]['name'] ?? ($signer['chain'][0]['subject']['CN'] ?? ''); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | private function hasContractSigners(array $signers): bool { |
| 278 | foreach ($signers as $signer) { |
| 279 | if (is_object($signer) && isset($signer->signRequestId) && is_numeric($signer->signRequestId)) { |
| 280 | return true; |
| 281 | } |
| 282 | if (is_array($signer) && isset($signer['signRequestId']) && is_numeric($signer['signRequestId'])) { |
| 283 | return true; |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | return false; |
| 288 | } |
| 289 | |
| 290 | /** |
| 291 | * @param callable(string, string): string $buildIdentifier |
| 292 | */ |
| 293 | private function buildSignerIndexMap(array $signers, callable $buildIdentifier): array { |
| 294 | $map = []; |
| 295 | foreach ($signers as $index => $signer) { |
| 296 | if (isset($signer->uid)) { |
| 297 | $map[strtolower((string)$signer->uid)] = $index; |
| 298 | } |
| 299 | if (!empty($signer->identifyMethods)) { |
| 300 | foreach ($signer->identifyMethods as $identifyMethod) { |
| 301 | if (isset($identifyMethod['method']) && isset($identifyMethod['value'])) { |
| 302 | $identifier = $buildIdentifier($identifyMethod['method'], $identifyMethod['value']); |
| 303 | $map[strtolower($identifier)] = $index; |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | } |
| 308 | return $map; |
| 309 | } |
| 310 | |
| 311 | private function findMatchingSignerIndex(array $indexMap, ?string $resolvedUid, array $certSigner): ?int { |
| 312 | $identifiers = []; |
| 313 | if ($resolvedUid) { |
| 314 | $identifiers[] = strtolower($resolvedUid); |
| 315 | } |
| 316 | if (!empty($certSigner['uid'])) { |
| 317 | $identifiers[] = strtolower((string)$certSigner['uid']); |
| 318 | } |
| 319 | foreach ($identifiers as $identifier) { |
| 320 | if (isset($indexMap[$identifier])) { |
| 321 | return $indexMap[$identifier]; |
| 322 | } |
| 323 | } |
| 324 | return null; |
| 325 | } |
| 326 | |
| 327 | private function nextAvailableSignerIndex(array $existingSigners, array $usedIndexes): int { |
| 328 | $index = count($existingSigners); |
| 329 | while (isset($existingSigners[$index]) || isset($usedIndexes[$index])) { |
| 330 | $index++; |
| 331 | } |
| 332 | return $index; |
| 333 | } |
| 334 | |
| 335 | private function processChainData(\stdClass $signer, array $chain): void { |
| 336 | $signer->chain = []; |
| 337 | |
| 338 | foreach ($chain as $chainIndex => $chainItem) { |
| 339 | $chainArr = $chainItem; |
| 340 | |
| 341 | if (isset($chainItem['validFrom_time_t']) && is_numeric($chainItem['validFrom_time_t'])) { |
| 342 | $chainArr['valid_from'] = (new DateTime('@' . $chainItem['validFrom_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM); |
| 343 | } |
| 344 | if (isset($chainItem['validTo_time_t']) && is_numeric($chainItem['validTo_time_t'])) { |
| 345 | $chainArr['valid_to'] = (new DateTime('@' . $chainItem['validTo_time_t'], new \DateTimeZone('UTC')))->format(DateTimeInterface::ATOM); |
| 346 | } |
| 347 | |
| 348 | $chainArr['displayName'] = $chainArr['name'] ?? ($chainArr['subject']['CN'] ?? ''); |
| 349 | $signer->chain[$chainIndex] = $chainArr; |
| 350 | } |
| 351 | |
| 352 | if (isset($chain[0])) { |
| 353 | $this->enrichSignerWithCertificateValidation($signer, $chain[0]); |
| 354 | } |
| 355 | |
| 356 | // Promote the end-entity (leaf) certificate validity dates to the signer root so the |
| 357 | // validation UI and the SignerDetail API contract can read them from a stable location. |
| 358 | // The chain still carries the full per-certificate dates for the certificate-chain view. |
| 359 | if (isset($signer->chain[0]['valid_from']) && !isset($signer->valid_from)) { |
| 360 | $signer->valid_from = $signer->chain[0]['valid_from']; |
| 361 | } |
| 362 | if (isset($signer->chain[0]['valid_to']) && !isset($signer->valid_to)) { |
| 363 | $signer->valid_to = $signer->chain[0]['valid_to']; |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | private function enrichSignerWithCertificateValidation(\stdClass $signer, array $endEntityCert): void { |
| 368 | if (isset($endEntityCert['name']) && !isset($signer->name)) { |
| 369 | $signer->name = $endEntityCert['name']; |
| 370 | } |
| 371 | if (isset($endEntityCert['hash']) && !isset($signer->hash)) { |
| 372 | $signer->hash = $endEntityCert['hash']; |
| 373 | } |
| 374 | if (isset($endEntityCert['serialNumber']) && !isset($signer->serialNumber)) { |
| 375 | $signer->serialNumber = $endEntityCert['serialNumber']; |
| 376 | } |
| 377 | if (isset($endEntityCert['serialNumberHex']) && !isset($signer->serialNumberHex)) { |
| 378 | $signer->serialNumberHex = $endEntityCert['serialNumberHex']; |
| 379 | } |
| 380 | if (isset($endEntityCert['signatureTypeSN']) && !isset($signer->signatureTypeSN)) { |
| 381 | $signer->signatureTypeSN = $endEntityCert['signatureTypeSN']; |
| 382 | } |
| 383 | |
| 384 | if (isset($endEntityCert['subject']) && !isset($signer->subject)) { |
| 385 | $signer->subject = $endEntityCert['subject']; |
| 386 | } |
| 387 | |
| 388 | if (isset($endEntityCert['crl_urls']) && !isset($signer->crl_urls)) { |
| 389 | $signer->crl_urls = $endEntityCert['crl_urls']; |
| 390 | } |
| 391 | if (isset($endEntityCert['crl_validation']) && !isset($signer->crl_validation)) { |
| 392 | $signer->crl_validation = $endEntityCert['crl_validation']; |
| 393 | } |
| 394 | if (isset($endEntityCert['crl_revoked_at']) && !isset($signer->crl_revoked_at)) { |
| 395 | $signer->crl_revoked_at = $endEntityCert['crl_revoked_at']; |
| 396 | } |
| 397 | |
| 398 | if (isset($endEntityCert['signature_validation']) && !isset($signer->signature_validation)) { |
| 399 | $signer->signature_validation = $endEntityCert['signature_validation']; |
| 400 | } |
| 401 | |
| 402 | if (isset($endEntityCert['isLibreSignRootCA']) && !isset($signer->isLibreSignRootCA)) { |
| 403 | $signer->isLibreSignRootCA = $endEntityCert['isLibreSignRootCA']; |
| 404 | } |
| 405 | |
| 406 | if (isset($endEntityCert['covers_entire_document']) && !isset($signer->covers_entire_document)) { |
| 407 | $signer->covers_entire_document = $endEntityCert['covers_entire_document']; |
| 408 | } |
| 409 | |
| 410 | if ( |
| 411 | isset($endEntityCert['document_modification_state']) |
| 412 | && !isset($signer->document_modification_state) |
| 413 | ) { |
| 414 | $signer->document_modification_state |
| 415 | = $endEntityCert['document_modification_state']; |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | /** |
| 420 | * @param callable(array, string): (?string) $resolveUid |
| 421 | */ |
| 422 | private function tryMatchWithExistingSigners(array $certData, array $existingSigners, string $host, callable $resolveUid): ?string { |
| 423 | if (empty($existingSigners)) { |
| 424 | return null; |
| 425 | } |
| 426 | |
| 427 | $certSerialNumber = $certData['serialNumber'] ?? null; |
| 428 | $certSerialNumberHex = $certData['serialNumberHex'] ?? null; |
| 429 | $certHash = $certData['hash'] ?? null; |
| 430 | |
| 431 | if (!$certSerialNumber && !$certSerialNumberHex && !$certHash) { |
| 432 | return null; |
| 433 | } |
| 434 | |
| 435 | foreach ($existingSigners as $signer) { |
| 436 | if (!isset($signer->metadata) || !is_array($signer->metadata)) { |
| 437 | continue; |
| 438 | } |
| 439 | |
| 440 | $certInfo = $signer->metadata['certificate_info'] ?? null; |
| 441 | if (!is_array($certInfo)) { |
| 442 | continue; |
| 443 | } |
| 444 | |
| 445 | if ($certSerialNumber && isset($certInfo['serialNumber']) && $certSerialNumber === $certInfo['serialNumber']) { |
| 446 | return $signer->uid ?? $resolveUid($certData, $host); |
| 447 | } |
| 448 | |
| 449 | if ($certSerialNumberHex && isset($certInfo['serialNumberHex']) && $certSerialNumberHex === $certInfo['serialNumberHex']) { |
| 450 | return $signer->uid ?? $resolveUid($certData, $host); |
| 451 | } |
| 452 | |
| 453 | if ($certHash && isset($certInfo['hash']) && $certHash === $certInfo['hash']) { |
| 454 | return $signer->uid ?? $resolveUid($certData, $host); |
| 455 | } |
| 456 | } |
| 457 | |
| 458 | return null; |
| 459 | } |
| 460 | } |