Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.76% covered (warning)
86.76%
59 / 68
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SubjectAlternativeNameService
86.76% covered (warning)
86.76%
59 / 68
87.50% covered (warning)
87.50%
7 / 8
30.95
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseFromCertificate
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 parse
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 build
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildForHosts
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 resolveUid
70.97% covered (warning)
70.97%
22 / 31
0.00% covered (danger)
0.00%
0 / 1
13.96
 extractEmail
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 extractEmailFromCertificate
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service;
10
11use OCP\IUserManager;
12
13/**
14 * Service to handle Subject Alternative Name (SAN) according to RFC 5280 Section 4.2.1.6
15 */
16class 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}