|
9 | 9 | namespace OCA\Libresign\Handler\CertificateEngine; |
10 | 10 |
|
11 | 11 | use OCA\Libresign\AppInfo\Application; |
| 12 | +use OCA\Libresign\Enum\CrlValidationStatus; |
12 | 13 | use OCA\Libresign\Exception\EmptyCertificateException; |
13 | 14 | use OCA\Libresign\Exception\InvalidPasswordException; |
14 | 15 | use OCA\Libresign\Exception\LibresignException; |
15 | 16 | use OCA\Libresign\Helper\ConfigureCheckHelper; |
16 | 17 | use OCA\Libresign\Helper\MagicGetterSetterTrait; |
17 | 18 | use OCA\Libresign\Service\CaIdentifierService; |
18 | 19 | use OCA\Libresign\Service\CertificatePolicyService; |
| 20 | +use OCA\Libresign\Service\Crl\CrlRevocationChecker; |
19 | 21 | use OCP\Files\AppData\IAppDataFactory; |
20 | 22 | use OCP\Files\IAppData; |
21 | 23 | use OCP\Files\SimpleFS\ISimpleFolder; |
@@ -82,6 +84,7 @@ public function __construct( |
82 | 84 | protected IURLGenerator $urlGenerator, |
83 | 85 | protected CaIdentifierService $caIdentifierService, |
84 | 86 | protected LoggerInterface $logger, |
| 87 | + private CrlRevocationChecker $crlRevocationChecker, |
85 | 88 | ) { |
86 | 89 | $this->appData = $appDataFactory->get('libresign'); |
87 | 90 | } |
@@ -180,19 +183,17 @@ private function parseX509(string $x509): array { |
180 | 183 |
|
181 | 184 | private function addCrlValidationInfo(array &$certData, string $certPem): void { |
182 | 185 | if (isset($certData['extensions']['crlDistributionPoints'])) { |
183 | | - $crlDistributionPoints = $certData['extensions']['crlDistributionPoints']; |
184 | | - |
185 | | - preg_match_all('/URI:([^\s,\n]+)/', $crlDistributionPoints, $matches); |
| 186 | + preg_match_all('/URI:([^\s,\n]+)/', $certData['extensions']['crlDistributionPoints'], $matches); |
186 | 187 | $extractedUrls = $matches[1] ?? []; |
187 | 188 |
|
188 | 189 | $certData['crl_urls'] = $extractedUrls; |
189 | | - $crlDetails = $this->validateCrlFromUrlsWithDetails($extractedUrls, $certPem); |
| 190 | + $crlDetails = $this->crlRevocationChecker->validate($extractedUrls, $certPem); |
190 | 191 | $certData['crl_validation'] = $crlDetails['status']; |
191 | 192 | if (!empty($crlDetails['revoked_at'])) { |
192 | 193 | $certData['crl_revoked_at'] = $crlDetails['revoked_at']; |
193 | 194 | } |
194 | 195 | } else { |
195 | | - $certData['crl_validation'] = 'missing'; |
| 196 | + $certData['crl_validation'] = CrlValidationStatus::MISSING; |
196 | 197 | $certData['crl_urls'] = []; |
197 | 198 | } |
198 | 199 | } |
@@ -788,244 +789,6 @@ protected function getCrlDistributionUrl(): string { |
788 | 789 | ]); |
789 | 790 | } |
790 | 791 |
|
791 | | - private function validateCrlFromUrls(array $crlUrls, string $certPem): string { |
792 | | - $details = $this->validateCrlFromUrlsWithDetails($crlUrls, $certPem); |
793 | | - return $details['status']; |
794 | | - } |
795 | | - |
796 | | - private function validateCrlFromUrlsWithDetails(array $crlUrls, string $certPem): array { |
797 | | - if (empty($crlUrls)) { |
798 | | - return ['status' => 'no_urls']; |
799 | | - } |
800 | | - |
801 | | - $accessibleUrls = 0; |
802 | | - foreach ($crlUrls as $crlUrl) { |
803 | | - try { |
804 | | - $validationResult = $this->downloadAndValidateCrlWithDetails($crlUrl, $certPem); |
805 | | - if ($validationResult['status'] === 'valid') { |
806 | | - return $validationResult; |
807 | | - } |
808 | | - if ($validationResult['status'] === 'revoked') { |
809 | | - return $validationResult; |
810 | | - } |
811 | | - $accessibleUrls++; |
812 | | - } catch (\Exception $e) { |
813 | | - continue; |
814 | | - } |
815 | | - } |
816 | | - |
817 | | - if ($accessibleUrls === 0) { |
818 | | - return ['status' => 'urls_inaccessible']; |
819 | | - } |
820 | | - |
821 | | - return ['status' => 'validation_failed']; |
822 | | - } |
823 | | - |
824 | | - private function downloadAndValidateCrl(string $crlUrl, string $certPem): string { |
825 | | - try { |
826 | | - if ($this->isLocalCrlUrl($crlUrl)) { |
827 | | - $crlContent = $this->generateLocalCrl($crlUrl); |
828 | | - } else { |
829 | | - $crlContent = $this->downloadCrlContent($crlUrl); |
830 | | - } |
831 | | - |
832 | | - if (!$crlContent) { |
833 | | - throw new \Exception('Failed to get CRL content'); |
834 | | - } |
835 | | - |
836 | | - return $this->checkCertificateInCrl($certPem, $crlContent); |
837 | | - |
838 | | - } catch (\Exception $e) { |
839 | | - return 'validation_error'; |
840 | | - } |
841 | | - } |
842 | | - |
843 | | - private function downloadAndValidateCrlWithDetails(string $crlUrl, string $certPem): array { |
844 | | - try { |
845 | | - if ($this->isLocalCrlUrl($crlUrl)) { |
846 | | - $crlContent = $this->generateLocalCrl($crlUrl); |
847 | | - } else { |
848 | | - $crlContent = $this->downloadCrlContent($crlUrl); |
849 | | - } |
850 | | - |
851 | | - if (!$crlContent) { |
852 | | - throw new \Exception('Failed to get CRL content'); |
853 | | - } |
854 | | - |
855 | | - return $this->checkCertificateInCrlWithDetails($certPem, $crlContent); |
856 | | - |
857 | | - } catch (\Exception $e) { |
858 | | - return ['status' => 'validation_error']; |
859 | | - } |
860 | | - } |
861 | | - |
862 | | - private function isLocalCrlUrl(string $url): bool { |
863 | | - $host = parse_url($url, PHP_URL_HOST); |
864 | | - if (!$host) { |
865 | | - return false; |
866 | | - } |
867 | | - |
868 | | - $trustedDomains = $this->config->getSystemValue('trusted_domains', []); |
869 | | - |
870 | | - return in_array($host, $trustedDomains, true); |
871 | | - } |
872 | | - |
873 | | - private function generateLocalCrl(string $crlUrl): ?string { |
874 | | - try { |
875 | | - $templateUrl = $this->urlGenerator->linkToRouteAbsolute('libresign.crl.getRevocationList', [ |
876 | | - 'instanceId' => 'INSTANCEID', |
877 | | - 'generation' => 999999, |
878 | | - 'engineType' => 'ENGINETYPE', |
879 | | - ]); |
880 | | - |
881 | | - $patternUrl = str_replace('INSTANCEID', '([^/_]+)', $templateUrl); |
882 | | - $patternUrl = str_replace('999999', '(\d+)', $patternUrl); |
883 | | - $patternUrl = str_replace('ENGINETYPE', '([^/_]+)', $patternUrl); |
884 | | - |
885 | | - $escapedPattern = str_replace([':', '/', '.'], ['\:', '\/', '\.'], $patternUrl); |
886 | | - |
887 | | - $escapedPattern = str_replace('\/apps\/', '(?:\/index\.php)?\/apps\/', $escapedPattern); |
888 | | - |
889 | | - $pattern = '/^' . $escapedPattern . '$/'; |
890 | | - if (preg_match($pattern, $crlUrl, $matches)) { |
891 | | - $instanceId = $matches[1]; |
892 | | - $generation = (int)$matches[2]; |
893 | | - $engineType = $matches[3]; |
894 | | - |
895 | | - /** @var \OCA\Libresign\Service\Crl\CrlService */ |
896 | | - $crlService = \OC::$server->get(\OCA\Libresign\Service\Crl\CrlService::class); |
897 | | - |
898 | | - $crlData = $crlService->generateCrlDer($instanceId, $generation, $engineType); |
899 | | - |
900 | | - return $crlData; |
901 | | - } |
902 | | - |
903 | | - $this->logger->debug('CRL URL does not match expected pattern', ['url' => $crlUrl, 'pattern' => $pattern]); |
904 | | - return null; |
905 | | - } catch (\Exception $e) { |
906 | | - $this->logger->warning('Failed to generate local CRL: ' . $e->getMessage()); |
907 | | - return null; |
908 | | - } |
909 | | - } |
910 | | - |
911 | | - private function downloadCrlContent(string $url): ?string { |
912 | | - if (!filter_var($url, FILTER_VALIDATE_URL) || !in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'])) { |
913 | | - return null; |
914 | | - } |
915 | | - |
916 | | - $context = stream_context_create([ |
917 | | - 'http' => [ |
918 | | - 'timeout' => 30, |
919 | | - 'user_agent' => 'LibreSign/1.0 CRL Validator', |
920 | | - 'follow_location' => 1, |
921 | | - 'max_redirects' => 3, |
922 | | - ] |
923 | | - ]); |
924 | | - |
925 | | - $content = @file_get_contents($url, false, $context); |
926 | | - return $content !== false ? $content : null; |
927 | | - } |
928 | | - |
929 | | - private function isSerialNumberInCrl(string $crlText, string $serialNumber): bool { |
930 | | - $normalizedSerial = strtoupper($serialNumber); |
931 | | - $normalizedSerial = ltrim($normalizedSerial, '0') ?: '0'; |
932 | | - |
933 | | - return preg_match('/Serial Number: 0*' . preg_quote($normalizedSerial, '/') . '/', $crlText) === 1; |
934 | | - } |
935 | | - |
936 | | - private function checkCertificateInCrl(string $certPem, string $crlContent): string { |
937 | | - try { |
938 | | - $certResource = openssl_x509_read($certPem); |
939 | | - if (!$certResource) { |
940 | | - return 'validation_error'; |
941 | | - } |
942 | | - |
943 | | - $certData = openssl_x509_parse($certResource); |
944 | | - if (!isset($certData['serialNumber'])) { |
945 | | - return 'validation_error'; |
946 | | - } |
947 | | - |
948 | | - return $this->checkCertificateInCrlWithDetails($certPem, $crlContent)['status']; |
949 | | - |
950 | | - } catch (\Exception $e) { |
951 | | - return 'validation_error'; |
952 | | - } |
953 | | - } |
954 | | - |
955 | | - private function checkCertificateInCrlWithDetails(string $certPem, string $crlContent): array { |
956 | | - try { |
957 | | - $certResource = openssl_x509_read($certPem); |
958 | | - if (!$certResource) { |
959 | | - return ['status' => 'validation_error']; |
960 | | - } |
961 | | - |
962 | | - $certData = openssl_x509_parse($certResource); |
963 | | - if (!isset($certData['serialNumber'])) { |
964 | | - return ['status' => 'validation_error']; |
965 | | - } |
966 | | - |
967 | | - $tempCrlFile = $this->tempManager->getTemporaryFile('.crl'); |
968 | | - file_put_contents($tempCrlFile, $crlContent); |
969 | | - |
970 | | - try { |
971 | | - $crlTextCmd = sprintf( |
972 | | - 'openssl crl -in %s -inform DER -text -noout', |
973 | | - escapeshellarg($tempCrlFile) |
974 | | - ); |
975 | | - |
976 | | - exec($crlTextCmd, $output, $exitCode); |
977 | | - |
978 | | - if ($exitCode !== 0) { |
979 | | - return ['status' => 'validation_error']; |
980 | | - } |
981 | | - |
982 | | - $crlText = implode("\n", $output); |
983 | | - $serialCandidates = [$certData['serialNumber']]; |
984 | | - if (!empty($certData['serialNumberHex'])) { |
985 | | - $serialCandidates[] = $certData['serialNumberHex']; |
986 | | - } |
987 | | - |
988 | | - foreach ($serialCandidates as $serial) { |
989 | | - if ($this->isSerialNumberInCrl($crlText, $serial)) { |
990 | | - $revokedAt = $this->extractRevocationDateFromCrlText($crlText, $serialCandidates); |
991 | | - return array_filter([ |
992 | | - 'status' => 'revoked', |
993 | | - 'revoked_at' => $revokedAt, |
994 | | - ]); |
995 | | - } |
996 | | - } |
997 | | - |
998 | | - return ['status' => 'valid']; |
999 | | - |
1000 | | - } finally { |
1001 | | - if (file_exists($tempCrlFile)) { |
1002 | | - unlink($tempCrlFile); |
1003 | | - } |
1004 | | - } |
1005 | | - |
1006 | | - } catch (\Exception $e) { |
1007 | | - return ['status' => 'validation_error']; |
1008 | | - } |
1009 | | - } |
1010 | | - |
1011 | | - private function extractRevocationDateFromCrlText(string $crlText, array $serialNumbers): ?string { |
1012 | | - foreach ($serialNumbers as $serial) { |
1013 | | - $normalizedSerial = strtoupper(ltrim((string)$serial, '0')) ?: '0'; |
1014 | | - $pattern = '/Serial Number:\s*0*' . preg_quote($normalizedSerial, '/') . '\s*\R\s*Revocation Date:\s*([^\r\n]+)/i'; |
1015 | | - if (preg_match($pattern, $crlText, $matches) !== 1) { |
1016 | | - continue; |
1017 | | - } |
1018 | | - $dateText = trim($matches[1]); |
1019 | | - try { |
1020 | | - $date = new \DateTimeImmutable($dateText, new \DateTimeZone('UTC')); |
1021 | | - return $date->setTimezone(new \DateTimeZone('UTC'))->format(\DateTimeInterface::ATOM); |
1022 | | - } catch (\Exception $e) { |
1023 | | - continue; |
1024 | | - } |
1025 | | - } |
1026 | | - return null; |
1027 | | - } |
1028 | | - |
1029 | 792 | #[\Override] |
1030 | 793 | public function generateCrlDer(array $revokedCertificates, string $instanceId, int $generation, int $crlNumber): string { |
1031 | 794 | $configPath = $this->getConfigPathByParams($instanceId, $generation); |
|
0 commit comments