diff --git a/config/config.sample.php b/config/config.sample.php index b645121a59a4f..bdc4759a6f2c5 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -2980,13 +2980,19 @@ 'enable_lazy_objects' => true, /** - * Change the default certificates bundle used for trusting certificates. + * Override the default CA bundle used by Nextcloud to verify TLS certificates. * - * Nextcloud ships its own up-to-date certificates bundle, but in certain cases admins may wish to specify a different bundle, for example the one shipped by their distro. + * By default, Nextcloud uses its shipped CA bundle. You may instead configure a + * bundle provided by your operating-system distribution or organization, such as + * ``/etc/ssl/certs/ca-certificates.crt`` on Debian-derived systems. * - * Defaults to `\OC::$SERVERROOT . '/resources/config/ca-bundle.crt'`. + * The value must be the path to a readable local CA-bundle file. Nextcloud uses + * this bundle directly when no certificates have been uploaded, and includes it + * when generating a bundle containing uploaded certificates. + * + * Defaults to ``resources/config/ca-bundle.crt`` inside the Nextcloud installation directory. */ - 'default_certificates_bundle_path' => \OC::$SERVERROOT . '/resources/config/ca-bundle.crt', + 'default_certificates_bundle_path' => '/etc/ssl/certs/ca-certificates.crt', /** * OpenMetrics skipped exporters diff --git a/lib/private/Security/CertificateManager.php b/lib/private/Security/CertificateManager.php index ee9332e550fac..84054d9045bb1 100644 --- a/lib/private/Security/CertificateManager.php +++ b/lib/private/Security/CertificateManager.php @@ -15,9 +15,6 @@ use OCP\Security\ISecureRandom; use Psr\Log\LoggerInterface; -/** - * Manage trusted certificates for users - */ class CertificateManager implements ICertificateManager { private ?string $bundlePath = null; @@ -29,11 +26,6 @@ public function __construct( ) { } - /** - * Returns all certificates trusted by the user - * - * @return ICertificate[] - */ #[\Override] public function listCertificates(): array { if (!$this->config->getSystemValueBool('installed', false)) { @@ -67,6 +59,9 @@ public function listCertificates(): array { return $result; } + /** + * Check whether any uploaded certificates are present. + */ private function hasCertificates(): bool { if (!$this->config->getSystemValueBool('installed', false)) { return false; @@ -76,13 +71,14 @@ private function hasCertificates(): bool { if (!$this->view->is_dir($path)) { return false; } - $result = []; + $handle = $this->view->opendir($path); if (!is_resource($handle)) { return false; } while (false !== ($file = readdir($handle))) { if ($file !== '.' && $file !== '..') { + closedir($handle); return true; } } @@ -91,9 +87,13 @@ private function hasCertificates(): bool { } /** - * create the certificate bundle of all trusted certificated + * Rebuild the generated effective certificate bundle from: + * - uploaded certificates + * - the configured default CA bundle (defaults to Nextcloud's shipped CA bundle) + * + * The bundle is written atomically to /files_external/rootcerts.crt. */ - public function createCertificateBundle(): void { + private function createCertificateBundle(): void { $path = $this->getPathToCertificates(); $certs = $this->listCertificates(); @@ -102,14 +102,13 @@ public function createCertificateBundle(): void { } $defaultCertificates = file_get_contents($this->getDefaultCertificatesBundlePath()); - if (strlen($defaultCertificates) < 1024) { // sanity check to verify that we have some content for our bundle - // log as exception so we have a stacktrace - $e = new \Exception('Shipped ca-bundle is empty, refusing to create certificate bundle'); + if (!is_string($defaultCertificates) || strlen($defaultCertificates) < 1024) { + $e = new \Exception('Configured CA bundle is empty or unreadable, refusing to create certificate bundle'); $this->logger->error($e->getMessage(), ['exception' => $e]); return; } - $certPath = $path . 'rootcerts.crt'; + $certPath = $this->getCertificateBundle(); $tmpPath = $certPath . '.tmp' . $this->random->generate(10, ISecureRandom::CHAR_DIGITS); $fhCerts = $this->view->fopen($tmpPath, 'w'); @@ -117,38 +116,24 @@ public function createCertificateBundle(): void { throw new \RuntimeException('Unable to open file handler to create certificate bundle "' . $tmpPath . '".'); } - // Write user certificates + // Write uploaded certificates. foreach ($certs as $cert) { - $file = $path . '/uploads/' . $cert->getName(); + $file = $path . 'uploads/' . $cert->getName(); $data = $this->view->file_get_contents($file); - if (strpos($data, 'BEGIN CERTIFICATE')) { + if (is_string($data) && str_contains($data, 'BEGIN CERTIFICATE')) { fwrite($fhCerts, $data); fwrite($fhCerts, "\r\n"); } } - // Append the default certificates + // Append the configured default CA bundle. fwrite($fhCerts, $defaultCertificates); - // Append the system certificate bundle - $systemBundle = $this->getCertificateBundle(); - if ($systemBundle !== $certPath && $this->view->file_exists($systemBundle)) { - $systemCertificates = $this->view->file_get_contents($systemBundle); - fwrite($fhCerts, $systemCertificates); - } - fclose($fhCerts); $this->view->rename($tmpPath, $certPath); } - /** - * Save the certificate and re-generate the certificate bundle - * - * @param string $certificate the certificate data - * @param string $name the filename for the certificate - * @throws \Exception If the certificate could not get added - */ #[\Override] public function addCertificate(string $certificate, string $name): ICertificate { $path = $this->getPathToCertificates() . 'uploads/' . $name; @@ -161,19 +146,12 @@ public function addCertificate(string $certificate, string $name): ICertificate $this->view->mkdir($directory); } - try { - $certificateObject = new Certificate($certificate, $name); - $this->view->file_put_contents($path, $certificate); - $this->createCertificateBundle(); - return $certificateObject; - } catch (\Exception $e) { - throw $e; - } + $certificateObject = new Certificate($certificate, $name); + $this->view->file_put_contents($path, $certificate); + $this->createCertificateBundle(); + return $certificateObject; } - /** - * Remove the certificate and re-generate the certificate bundle - */ #[\Override] public function removeCertificate(string $name): bool { $path = $this->getPathToCertificates() . 'uploads/' . $name; @@ -192,18 +170,11 @@ public function removeCertificate(string $name): bool { return true; } - /** - * Get the path to the certificate bundle - */ #[\Override] public function getCertificateBundle(): string { return $this->getPathToCertificates() . 'rootcerts.crt'; } - /** - * Get the full local path to the certificate bundle - * @throws \Exception when getting bundle path fails - */ #[\Override] public function getAbsoluteBundlePath(): string { try { @@ -230,12 +201,20 @@ public function getAbsoluteBundlePath(): string { } } + /** + * Get the base path used to store uploaded certificates and the generated bundle. + * + * The uploaded certificates and generated bundle are stored under the + * files_external path for historical reasons, maintaining compatibility + * with pre-existing deployments. + */ private function getPathToCertificates(): string { return '/files_external/'; } /** - * Check if we need to re-bundle the certificates because one of the sources has updated + * Determine whether the generated bundle must be rebuilt because the source + * CA bundle has changed or the target bundle is missing. */ private function needsRebundling(): bool { $targetBundle = $this->getCertificateBundle(); @@ -248,7 +227,7 @@ private function needsRebundling(): bool { } /** - * get mtime of ca-bundle shipped by Nextcloud + * Return the modification time of the configured default CA bundle. */ protected function getFilemtimeOfCaBundle(): int { return filemtime($this->getDefaultCertificatesBundlePath()); diff --git a/lib/public/ICertificateManager.php b/lib/public/ICertificateManager.php index be4afc799fa63..c90e2f434656f 100644 --- a/lib/public/ICertificateManager.php +++ b/lib/public/ICertificateManager.php @@ -9,12 +9,18 @@ namespace OCP; /** - * Manage trusted certificates + * Manage uploaded trusted certificates and the CA bundle used by Nextcloud. + * + * Implementations provide access to uploaded trusted certificates and the + * certificate bundle consumed by HTTP clients and external storage integrations. + * * @since 8.0.0 */ interface ICertificateManager { /** - * Returns all certificates trusted by the system + * Returns all uploaded trusted certificates. + * + * This does not include certificates in the configured default CA bundle. * * @return \OCP\ICertificate[] * @since 8.0.0 @@ -22,23 +28,27 @@ interface ICertificateManager { public function listCertificates(): array; /** - * @param string $certificate the certificate data - * @param string $name the filename for the certificate + * Add a trusted certificate to the certificate store. + * + * @param string $certificate The certificate data + * @param string $name The filename for the certificate * @return \OCP\ICertificate - * @throws \Exception If the certificate could not get added + * @throws \Exception If the certificate could not be added * @since 8.0.0 - since 8.1.0 throws exception instead of returning false */ public function addCertificate(string $certificate, string $name): \OCP\ICertificate; /** - * @param string $name + * Remove a trusted certificate from the certificate store. + * + * @param string $name The filename for the certificate * @return bool * @since 8.0.0 */ public function removeCertificate(string $name): bool; /** - * Get the path to the certificate bundle + * Get the virtual filesystem path to the generated certificate bundle. * * @return string * @since 8.0.0 @@ -46,7 +56,10 @@ public function removeCertificate(string $name): bool; public function getCertificateBundle(): string; /** - * Get the full local path to the certificate bundle + * Get the full local path to the certificate bundle used by Nextcloud. + * + * If no uploaded certificates are configured, or if resolving the generated + * bundle fails, this falls back to returning the configured default CA bundle path. * * @return string * @since 9.0.0 @@ -54,7 +67,9 @@ public function getCertificateBundle(): string; public function getAbsoluteBundlePath(): string; /** - * Get the path of the default certificates bundle. + * Get the path to the configured default CA bundle. + * + * By default, this is Nextcloud's shipped CA bundle. * * @since 33.0.0 */