Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions config/config.sample.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 32 additions & 53 deletions lib/private/Security/CertificateManager.php
Comment thread
provokateurin marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand All @@ -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();

Expand All @@ -102,53 +102,38 @@ 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');

if (!is_resource($fhCerts)) {
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;
Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -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());
Expand Down
33 changes: 24 additions & 9 deletions lib/public/ICertificateManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,67 @@
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
*/
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
*/
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
*/
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
*/
Expand Down
Loading