Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
86.76% |
59 / 68 |
|
87.50% |
7 / 8 |
CRAP | |
0.00% |
0 / 1 |
| SubjectAlternativeNameService | |
86.76% |
59 / 68 |
|
87.50% |
7 / 8 |
30.95 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| parseFromCertificate | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| parse | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
| build | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| buildForHosts | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| resolveUid | |
70.97% |
22 / 31 |
|
0.00% |
0 / 1 |
13.96 | |||
| extractEmail | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
| extractEmailFromCertificate | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| 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; |
| 10 | |
| 11 | use OCP\IUserManager; |
| 12 | |
| 13 | /** |
| 14 | * Service to handle Subject Alternative Name (SAN) according to RFC 5280 Section 4.2.1.6 |
| 15 | */ |
| 16 | class SubjectAlternativeNameService { |
| 17 | public function __construct( |
| 18 | private IUserManager $userManager, |
| 19 | ) { |
| 20 | } |
| 21 | |
| 22 | public function parseFromCertificate(array $certificateData): ?array { |
| 23 | if (empty($certificateData['extensions']['subjectAltName'])) { |
| 24 | return null; |
| 25 | } |
| 26 | |
| 27 | $subjectAltName = $certificateData['extensions']['subjectAltName']; |
| 28 | if (is_array($subjectAltName)) { |
| 29 | $subjectAltName = $subjectAltName[0]; |
| 30 | } |
| 31 | |
| 32 | return $this->parse((string)$subjectAltName); |
| 33 | } |
| 34 | |
| 35 | protected function parse(string $subjectAltName): ?array { |
| 36 | $pattern = '/^(?<method>' . implode('|', IdentifyMethodService::IDENTIFY_METHODS) . '):(?<value>.+)$/'; |
| 37 | |
| 38 | if (preg_match($pattern, $subjectAltName, $matches)) { |
| 39 | return [ |
| 40 | 'method' => $matches['method'], |
| 41 | 'value' => $matches['value'], |
| 42 | ]; |
| 43 | } |
| 44 | |
| 45 | if (filter_var($subjectAltName, FILTER_VALIDATE_EMAIL)) { |
| 46 | return [ |
| 47 | 'method' => IdentifyMethodService::IDENTIFY_EMAIL, |
| 48 | 'value' => $subjectAltName, |
| 49 | ]; |
| 50 | } |
| 51 | |
| 52 | return null; |
| 53 | } |
| 54 | |
| 55 | public function build(string $method, string $value): string { |
| 56 | return $method . ':' . $value; |
| 57 | } |
| 58 | |
| 59 | public function buildForHosts(array $hosts): string { |
| 60 | $altNames = []; |
| 61 | foreach ($hosts as $host) { |
| 62 | if (filter_var($host, FILTER_VALIDATE_EMAIL)) { |
| 63 | $altNames[] = $this->build(IdentifyMethodService::IDENTIFY_EMAIL, $host); |
| 64 | } |
| 65 | } |
| 66 | return implode(', ', $altNames); |
| 67 | } |
| 68 | |
| 69 | public function resolveUid(array $certificateData, string $host): ?string { |
| 70 | if (!empty($certificateData['subject']['CN'])) { |
| 71 | $cn = $certificateData['subject']['CN']; |
| 72 | if (is_array($cn)) { |
| 73 | $cn = $cn[0]; |
| 74 | } |
| 75 | $pattern = '/^(?<method>' . implode('|', IdentifyMethodService::IDENTIFY_METHODS) . '):(?<value>.*), /'; |
| 76 | if (preg_match($pattern, (string)$cn, $matches)) { |
| 77 | return $matches['method'] . ':' . $matches['value']; |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | $parsed = $this->parseFromCertificate($certificateData); |
| 82 | if (!$parsed) { |
| 83 | return null; |
| 84 | } |
| 85 | |
| 86 | $method = $parsed['method']; |
| 87 | $value = $parsed['value']; |
| 88 | |
| 89 | if (in_array($method, [IdentifyMethodService::IDENTIFY_EMAIL, IdentifyMethodService::IDENTIFY_ACCOUNT], true)) { |
| 90 | if (str_ends_with((string)$value, $host)) { |
| 91 | $uid = str_replace('@' . $host, '', $value); |
| 92 | $user = $this->userManager->get($uid); |
| 93 | if ($user) { |
| 94 | return IdentifyMethodService::IDENTIFY_ACCOUNT . ':' . $uid; |
| 95 | } |
| 96 | |
| 97 | $users = $this->userManager->getByEmail($value); |
| 98 | if (!empty($users)) { |
| 99 | $user = current($users); |
| 100 | return IdentifyMethodService::IDENTIFY_ACCOUNT . ':' . $user->getUID(); |
| 101 | } |
| 102 | |
| 103 | return IdentifyMethodService::IDENTIFY_EMAIL . ':' . $value; |
| 104 | } |
| 105 | |
| 106 | $users = $this->userManager->getByEmail($value); |
| 107 | if (!empty($users)) { |
| 108 | $user = current($users); |
| 109 | return IdentifyMethodService::IDENTIFY_ACCOUNT . ':' . $user->getUID(); |
| 110 | } |
| 111 | |
| 112 | $user = $this->userManager->get($value); |
| 113 | if ($user) { |
| 114 | return IdentifyMethodService::IDENTIFY_ACCOUNT . ':' . $user->getUID(); |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | return $this->build($method, $value); |
| 119 | } |
| 120 | |
| 121 | private function extractEmail(string $subjectAltName): ?string { |
| 122 | if (preg_match('/(?:email:)+(?<email>[^\s,]+)/', $subjectAltName, $matches)) { |
| 123 | if (filter_var($matches['email'], FILTER_VALIDATE_EMAIL)) { |
| 124 | return $matches['email']; |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | if (filter_var($subjectAltName, FILTER_VALIDATE_EMAIL)) { |
| 129 | return $subjectAltName; |
| 130 | } |
| 131 | |
| 132 | return null; |
| 133 | } |
| 134 | |
| 135 | public function extractEmailFromCertificate(array $certificateData): ?string { |
| 136 | if (empty($certificateData['extensions']['subjectAltName'])) { |
| 137 | return null; |
| 138 | } |
| 139 | |
| 140 | $subjectAltName = $certificateData['extensions']['subjectAltName']; |
| 141 | if (is_array($subjectAltName)) { |
| 142 | $subjectAltName = $subjectAltName[0]; |
| 143 | } |
| 144 | |
| 145 | return $this->extractEmail((string)$subjectAltName); |
| 146 | } |
| 147 | } |