From c15cab7ed6bf2d3ce9009ca09c7c5f33b252860f Mon Sep 17 00:00:00 2001
From: Robin Appelman <icewind@owncloud.com>
Date: Tue, 22 Dec 2015 17:42:28 +0100
Subject: [PATCH] Allow admins to add system wide root certificates

---
 apps/files_external/js/settings.js            |  7 --
 lib/private/http/client/client.php            |  9 +-
 lib/private/security/certificatemanager.php   | 89 ++++++++++++++++---
 lib/private/server.php                        |  6 +-
 lib/public/icertificatemanager.php            | 12 ++-
 lib/public/iservercontainer.php               |  2 +-
 settings/admin.php                            | 14 +++
 settings/application.php                      |  4 +
 settings/controller/certificatecontroller.php | 71 +++++++++++----
 settings/js/certificates.js                   | 69 ++++++++++++++
 settings/js/personal.js                       | 67 --------------
 settings/personal.php                         | 19 ++--
 settings/routes.php                           |  2 +
 settings/templates/certificates.php           | 44 +++++++++
 settings/templates/personal.php               | 42 ---------
 tests/lib/security/certificatemanager.php     | 23 +++--
 .../controller/CertificateControllerTest.php  |  4 +
 17 files changed, 316 insertions(+), 168 deletions(-)
 create mode 100644 settings/js/certificates.js
 create mode 100644 settings/templates/certificates.php

diff --git a/apps/files_external/js/settings.js b/apps/files_external/js/settings.js
index 59cf55d6ad5..2f879e1c850 100644
--- a/apps/files_external/js/settings.js
+++ b/apps/files_external/js/settings.js
@@ -1210,13 +1210,6 @@ $(document).ready(function() {
 	});
 	mountConfigListView.loadStorages();
 
-	$('#sslCertificate').on('click', 'td.remove>img', function() {
-		var $tr = $(this).closest('tr');
-		$.post(OC.filePath('files_external', 'ajax', 'removeRootCertificate.php'), {cert: $tr.attr('id')});
-		$tr.remove();
-		return true;
-	});
-
 	// TODO: move this into its own View class
 	var $allowUserMounting = $('#allowUserMounting');
 	$allowUserMounting.bind('change', function() {
diff --git a/lib/private/http/client/client.php b/lib/private/http/client/client.php
index 5f298e1acd7..8cddfc3ae03 100644
--- a/lib/private/http/client/client.php
+++ b/lib/private/http/client/client.php
@@ -58,12 +58,11 @@ class Client implements IClient {
 	 * Sets the default options to the client
 	 */
 	private function setDefaultOptions() {
-		// Either use default bundle or the user bundle if nothing is specified
-		if($this->certificateManager->listCertificates() !== []) {
-			$dataDir = $this->config->getSystemValue('datadirectory');
-			$this->client->setDefaultOption('verify', $dataDir.'/'.$this->certificateManager->getCertificateBundle());
+		// Either use user bundle or the system bundle if nothing is specified
+		if ($this->certificateManager->listCertificates() !== []) {
+			$this->client->setDefaultOption('verify', $this->certificateManager->getAbsoluteBundlePath());
 		} else {
-			$this->client->setDefaultOption('verify', \OC::$SERVERROOT . '/resources/config/ca-bundle.crt');
+			$this->client->setDefaultOption('verify', $this->certificateManager->getAbsoluteBundlePath(null));
 		}
 
 		$this->client->setDefaultOption('headers/User-Agent', 'ownCloud Server Crawler');
diff --git a/lib/private/security/certificatemanager.php b/lib/private/security/certificatemanager.php
index ded81863a73..ce0d330c4b2 100644
--- a/lib/private/security/certificatemanager.php
+++ b/lib/private/security/certificatemanager.php
@@ -50,7 +50,7 @@ class CertificateManager implements ICertificateManager {
 
 	/**
 	 * @param string $uid
-	 * @param \OC\Files\View $view relative zu data/
+	 * @param \OC\Files\View $view relative to data/
 	 * @param IConfig $config
 	 */
 	public function __construct($uid, \OC\Files\View $view, IConfig $config) {
@@ -83,7 +83,8 @@ class CertificateManager implements ICertificateManager {
 			if ($file != '.' && $file != '..') {
 				try {
 					$result[] = new Certificate($this->view->file_get_contents($path . $file), $file);
-				} catch(\Exception $e) {}
+				} catch (\Exception $e) {
+				}
 			}
 		}
 		closedir($handle);
@@ -97,22 +98,34 @@ class CertificateManager implements ICertificateManager {
 		$path = $this->getPathToCertificates();
 		$certs = $this->listCertificates();
 
-		$fh_certs = $this->view->fopen($path . '/rootcerts.crt', 'w');
+		if (!$this->view->file_exists($path)) {
+			$this->view->mkdir($path);
+		}
+
+		$fhCerts = $this->view->fopen($path . '/rootcerts.crt', 'w');
 
 		// Write user certificates
 		foreach ($certs as $cert) {
 			$file = $path . '/uploads/' . $cert->getName();
 			$data = $this->view->file_get_contents($file);
 			if (strpos($data, 'BEGIN CERTIFICATE')) {
-				fwrite($fh_certs, $data);
-				fwrite($fh_certs, "\r\n");
+				fwrite($fhCerts, $data);
+				fwrite($fhCerts, "\r\n");
 			}
 		}
 
 		// Append the default certificates
 		$defaultCertificates = file_get_contents(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt');
-		fwrite($fh_certs, $defaultCertificates);
-		fclose($fh_certs);
+		fwrite($fhCerts, $defaultCertificates);
+
+		// Append the system certificate bundle
+		$systemBundle = $this->getCertificateBundle(null);
+		if ($this->view->file_exists($systemBundle)) {
+			$systemCertificates = $this->view->file_get_contents($systemBundle);
+			fwrite($fhCerts, $systemCertificates);
+		}
+
+		fclose($fhCerts);
 	}
 
 	/**
@@ -166,18 +179,72 @@ class CertificateManager implements ICertificateManager {
 	/**
 	 * Get the path to the certificate bundle for this user
 	 *
+	 * @param string $uid (optional) user to get the certificate bundle for, use `null` to get the system bundle
 	 * @return string
 	 */
-	public function getCertificateBundle() {
-		return $this->getPathToCertificates() . 'rootcerts.crt';
+	public function getCertificateBundle($uid = '') {
+		if ($uid === '') {
+			$uid = $this->uid;
+		}
+		return $this->getPathToCertificates($uid) . 'rootcerts.crt';
+	}
+
+	/**
+	 * Get the full local path to the certificate bundle for this user
+	 *
+	 * @param string $uid (optional) user to get the certificate bundle for, use `null` to get the system bundle
+	 * @return string
+	 */
+	public function getAbsoluteBundlePath($uid = '') {
+		if ($uid === '') {
+			$uid = $this->uid;
+		}
+		if ($this->needsRebundling($uid)) {
+			if (is_null($uid)) {
+				$manager = new CertificateManager(null, $this->view, $this->config);
+				$manager->createCertificateBundle();
+			} else {
+				$this->createCertificateBundle();
+			}
+		}
+		return $this->view->getLocalFile($this->getCertificateBundle($uid));
 	}
 
 	/**
+	 * @param string $uid (optional) user to get the certificate path for, use `null` to get the system path
 	 * @return string
 	 */
-	private function getPathToCertificates() {
-		$path = is_null($this->uid) ? '/files_external/' : '/' . $this->uid . '/files_external/';
+	private function getPathToCertificates($uid = '') {
+		if ($uid === '') {
+			$uid = $this->uid;
+		}
+		$path = is_null($uid) ? '/files_external/' : '/' . $uid . '/files_external/';
 
 		return $path;
 	}
+
+	/**
+	 * Check if we need to re-bundle the certificates because one of the sources has updated
+	 *
+	 * @param string $uid (optional) user to get the certificate path for, use `null` to get the system path
+	 * @return bool
+	 */
+	private function needsRebundling($uid = '') {
+		if ($uid === '') {
+			$uid = $this->uid;
+		}
+		$sourceMTimes = [filemtime(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt')];
+		$targetBundle = $this->getCertificateBundle($uid);
+		if (!$this->view->file_exists($targetBundle)) {
+			return true;
+		}
+		if (!is_null($uid)) { // also depend on the system bundle
+			$sourceBundles[] = $this->view->filemtime($this->getCertificateBundle(null));
+		}
+
+		$sourceMTime = array_reduce($sourceMTimes, function ($max, $mtime) {
+			return max($max, $mtime);
+		}, 0);
+		return $sourceMTime > $this->view->filemtime($targetBundle);
+	}
 }
diff --git a/lib/private/server.php b/lib/private/server.php
index ead9fa95db8..414f59af612 100644
--- a/lib/private/server.php
+++ b/lib/private/server.php
@@ -921,11 +921,11 @@ class Server extends ServerContainer implements IServerContainer {
 	/**
 	 * Get the certificate manager for the user
 	 *
-	 * @param string $userId (optional) if not specified the current loggedin user is used
+	 * @param string $userId (optional) if not specified the current loggedin user is used, use null to get the system certificate manager
 	 * @return \OCP\ICertificateManager | null if $uid is null and no user is logged in
 	 */
-	public function getCertificateManager($userId = null) {
-		if (is_null($userId)) {
+	public function getCertificateManager($userId = '') {
+		if ($userId === '') {
 			$userSession = $this->getUserSession();
 			$user = $userSession->getUser();
 			if (is_null($user)) {
diff --git a/lib/public/icertificatemanager.php b/lib/public/icertificatemanager.php
index b1a16d8b5ee..3872fdd765d 100644
--- a/lib/public/icertificatemanager.php
+++ b/lib/public/icertificatemanager.php
@@ -54,8 +54,18 @@ interface ICertificateManager {
 	/**
 	 * Get the path to the certificate bundle for this user
 	 *
+	 * @param string $uid (optional) user to get the certificate bundle for, use `null` to get the system bundle (since 9.0.0)
 	 * @return string
 	 * @since 8.0.0
 	 */
-	public function getCertificateBundle();
+	public function getCertificateBundle($uid = '');
+
+	/**
+	 * Get the full local path to the certificate bundle for this user
+	 *
+	 * @param string $uid (optional) user to get the certificate bundle for, use `null` to get the system bundle
+	 * @return string
+	 * @since 9.0.0
+	 */
+	public function getAbsoluteBundlePath($uid = '');
 }
diff --git a/lib/public/iservercontainer.php b/lib/public/iservercontainer.php
index 267e5dc4d31..e706750bb21 100644
--- a/lib/public/iservercontainer.php
+++ b/lib/public/iservercontainer.php
@@ -326,7 +326,7 @@ interface IServerContainer {
 	/**
 	 * Get the certificate manager for the user
 	 *
-	 * @param string $userId (optional) if not specified the current loggedin user is used
+	 * @param string $userId (optional) if not specified the current loggedin user is used, use null to get the system certificate manager
 	 * @return \OCP\ICertificateManager | null if $userId is null and no user is logged in
 	 * @since 8.0.0
 	 */
diff --git a/settings/admin.php b/settings/admin.php
index d484d6a1e48..7bd3760b6ce 100644
--- a/settings/admin.php
+++ b/settings/admin.php
@@ -38,6 +38,10 @@ OC_Util::checkAdminUser();
 $template = new OC_Template('settings', 'admin', 'user');
 $l = \OC::$server->getL10N('settings');
 
+OC_Util::addScript('settings', 'certificates');
+OC_Util::addScript('files', 'jquery.iframe-transport');
+OC_Util::addScript('files', 'jquery.fileupload');
+
 $showLog = (\OC::$server->getConfig()->getSystemValue('log_type', 'owncloud') === 'owncloud');
 $numEntriesToLoad = 3;
 $entries = OC_Log_Owncloud::getEntries($numEntriesToLoad + 1);
@@ -52,6 +56,8 @@ if($doesLogFileExist) {
 $config = \OC::$server->getConfig();
 $appConfig = \OC::$server->getAppConfig();
 $request = \OC::$server->getRequest();
+$certificateManager = \OC::$server->getCertificateManager(null);
+$urlGenerator = \OC::$server->getURLGenerator();
 
 // Should we display sendmail as an option?
 $template->assign('sendmail_is_available', (bool) \OC_Helper::findBinaryPath('sendmail'));
@@ -151,6 +157,14 @@ $template->assign('OutdatedCacheWarning', $outdatedCaches);
 
 // add hardcoded forms from the template
 $forms = OC_App::getForms('admin');
+
+$certificatesTemplate = new OC_Template('settings', 'certificates');
+$certificatesTemplate->assign('type', 'admin');
+$certificatesTemplate->assign('uploadRoute', 'settings.Certificate.addSystemRootCertificate');
+$certificatesTemplate->assign('certs', $certificateManager->listCertificates());
+$certificatesTemplate->assign('urlGenerator', $urlGenerator);
+$forms[] = $certificatesTemplate->fetchPage();
+
 $formsAndMore = array();
 if ($request->getServerProtocol()  !== 'https' || !OC_Util::isAnnotationsWorking() ||
 	$suggestedOverwriteCliUrl || !OC_Util::isSetLocaleWorking()  ||
diff --git a/settings/application.php b/settings/application.php
index 729e61b5925..1c562c62a84 100644
--- a/settings/application.php
+++ b/settings/application.php
@@ -107,6 +107,7 @@ class Application extends App {
 				$c->query('AppName'),
 				$c->query('Request'),
 				$c->query('CertificateManager'),
+				$c->query('SystemCertificateManager'),
 				$c->query('L10N'),
 				$c->query('IAppManager')
 			);
@@ -243,6 +244,9 @@ class Application extends App {
 		$container->registerService('CertificateManager', function(IContainer $c){
 			return $c->query('ServerContainer')->getCertificateManager();
 		});
+		$container->registerService('SystemCertificateManager', function (IContainer $c) {
+			return $c->query('ServerContainer')->getCertificateManager(null);
+		});
 		$container->registerService('Checker', function(IContainer $c) {
 			/** @var Server $server */
 			$server = $c->query('ServerContainer');
diff --git a/settings/controller/certificatecontroller.php b/settings/controller/certificatecontroller.php
index e360a1053c3..1c8dfe35556 100644
--- a/settings/controller/certificatecontroller.php
+++ b/settings/controller/certificatecontroller.php
@@ -36,7 +36,9 @@ use OCP\IRequest;
  */
 class CertificateController extends Controller {
 	/** @var ICertificateManager */
-	private $certificateManager;
+	private $userCertificateManager;
+	/** @var ICertificateManager  */
+	private $systemCertificateManager;
 	/** @var IL10N */
 	private $l10n;
 	/** @var IAppManager */
@@ -45,17 +47,20 @@ class CertificateController extends Controller {
 	/**
 	 * @param string $appName
 	 * @param IRequest $request
-	 * @param ICertificateManager $certificateManager
+	 * @param ICertificateManager $userCertificateManager
+	 * @param ICertificateManager $systemCertificateManager
 	 * @param IL10N $l10n
 	 * @param IAppManager $appManager
 	 */
 	public function __construct($appName,
 								IRequest $request,
-								ICertificateManager $certificateManager,
+								ICertificateManager $userCertificateManager,
+								ICertificateManager $systemCertificateManager,
 								IL10N $l10n,
 								IAppManager $appManager) {
 		parent::__construct($appName, $request);
-		$this->certificateManager = $certificateManager;
+		$this->userCertificateManager = $userCertificateManager;
+		$this->systemCertificateManager = $systemCertificateManager;
 		$this->l10n = $l10n;
 		$this->appManager = $appManager;
 	}
@@ -68,6 +73,16 @@ class CertificateController extends Controller {
 	 * @return array
 	 */
 	public function addPersonalRootCertificate() {
+		return $this->addCertificate($this->userCertificateManager);
+	}
+
+	/**
+	 * Add a new root certificate to a trust store
+	 *
+	 * @param ICertificateManager $certificateManager
+	 * @return array
+	 */
+	private function addCertificate(ICertificateManager $certificateManager) {
 		$headers = [];
 		if ($this->request->isUserAgent([\OC\AppFramework\Http\Request::USER_AGENT_IE_8])) {
 			// due to upload iframe workaround, need to set content-type to text/plain
@@ -79,23 +94,23 @@ class CertificateController extends Controller {
 		}
 
 		$file = $this->request->getUploadedFile('rootcert_import');
-		if(empty($file)) {
+		if (empty($file)) {
 			return new DataResponse(['message' => 'No file uploaded'], Http::STATUS_UNPROCESSABLE_ENTITY, $headers);
 		}
 
 		try {
-			$certificate = $this->certificateManager->addCertificate(file_get_contents($file['tmp_name']), $file['name']);
+			$certificate = $certificateManager->addCertificate(file_get_contents($file['tmp_name']), $file['name']);
 			return new DataResponse(
 				[
-				'name' => $certificate->getName(),
-				'commonName' => $certificate->getCommonName(),
-				'organization' => $certificate->getOrganization(),
-				'validFrom' => $certificate->getIssueDate()->getTimestamp(),
-				'validTill' => $certificate->getExpireDate()->getTimestamp(),
-				'validFromString' => $this->l10n->l('date', $certificate->getIssueDate()),
-				'validTillString' => $this->l10n->l('date', $certificate->getExpireDate()),
-				'issuer' => $certificate->getIssuerName(),
-				'issuerOrganization' => $certificate->getIssuerOrganization(),
+					'name' => $certificate->getName(),
+					'commonName' => $certificate->getCommonName(),
+					'organization' => $certificate->getOrganization(),
+					'validFrom' => $certificate->getIssueDate()->getTimestamp(),
+					'validTill' => $certificate->getExpireDate()->getTimestamp(),
+					'validFromString' => $this->l10n->l('date', $certificate->getIssueDate()),
+					'validTillString' => $this->l10n->l('date', $certificate->getExpireDate()),
+					'issuer' => $certificate->getIssuerName(),
+					'issuerOrganization' => $certificate->getIssuerOrganization(),
 				],
 				Http::STATUS_OK,
 				$headers
@@ -119,7 +134,7 @@ class CertificateController extends Controller {
 			return new DataResponse('Individual certificate management disabled', Http::STATUS_FORBIDDEN);
 		}
 
-		$this->certificateManager->removeCertificate($certificateIdentifier);
+		$this->userCertificateManager->removeCertificate($certificateIdentifier);
 		return new DataResponse();
 	}
 
@@ -140,4 +155,28 @@ class CertificateController extends Controller {
 		return false;
 	}
 
+	/**
+	 * Add a new personal root certificate to the system's trust store
+	 *
+	 * @return array
+	 */
+	public function addSystemRootCertificate() {
+		return $this->addCertificate($this->systemCertificateManager);
+	}
+
+	/**
+	 * Removes a personal root certificate from the users' trust store
+	 *
+	 * @param string $certificateIdentifier
+	 * @return DataResponse
+	 */
+	public function removeSystemRootCertificate($certificateIdentifier) {
+
+		if ($this->isCertificateImportAllowed() === false) {
+			return new DataResponse('Individual certificate management disabled', Http::STATUS_FORBIDDEN);
+		}
+
+		$this->systemCertificateManager->removeCertificate($certificateIdentifier);
+		return new DataResponse();
+	}
 }
diff --git a/settings/js/certificates.js b/settings/js/certificates.js
new file mode 100644
index 00000000000..9ce9f9aa8d8
--- /dev/null
+++ b/settings/js/certificates.js
@@ -0,0 +1,69 @@
+$(document).ready(function () {
+	var type = $('#sslCertificate').data('type');
+	$('#sslCertificate').on('click', 'td.remove', function () {
+		var row = $(this).parent();
+		$.ajax(OC.generateUrl('settings/' + type + '/certificate/{certificate}', {certificate: row.data('name')}), {
+			type: 'DELETE'
+		});
+		row.remove();
+
+		if ($('#sslCertificate > tbody > tr').length === 0) {
+			$('#sslCertificate').hide();
+		}
+		return true;
+	});
+
+	$('#sslCertificate tr > td').tipsy({gravity: 'n', live: true});
+
+	$('#rootcert_import').fileupload({
+		submit: function (e, data) {
+			data.formData = _.extend(data.formData || {}, {
+				requesttoken: OC.requestToken
+			});
+		},
+		success: function (data) {
+			if (typeof data === 'string') {
+				data = $.parseJSON(data);
+			} else if (data && data.length) {
+				// fetch response from iframe
+				data = $.parseJSON(data[0].body.innerText);
+			}
+			if (!data || typeof(data) === 'string') {
+				// IE8 iframe workaround comes here instead of fail()
+				OC.Notification.showTemporary(
+					t('settings', 'An error occurred. Please upload an ASCII-encoded PEM certificate.'));
+				return;
+			}
+			var issueDate = new Date(data.validFrom * 1000);
+			var expireDate = new Date(data.validTill * 1000);
+			var now = new Date();
+			var isExpired = !(issueDate <= now && now <= expireDate);
+
+			var row = $('<tr/>');
+			row.data('name', data.name);
+			row.addClass(isExpired ? 'expired' : 'valid');
+			row.append($('<td/>').attr('title', data.organization).text(data.commonName));
+			row.append($('<td/>').attr('title', t('core,', 'Valid until {date}', {date: data.validTillString}))
+				.text(data.validTillString));
+			row.append($('<td/>').attr('title', data.issuerOrganization).text(data.issuer));
+			row.append($('<td/>').addClass('remove').append(
+				$('<img/>').attr({
+					alt: t('core', 'Delete'),
+					title: t('core', 'Delete'),
+					src: OC.imagePath('core', 'actions/delete.svg')
+				}).addClass('action')
+			));
+
+			$('#sslCertificate tbody').append(row);
+			$('#sslCertificate').show();
+		},
+		fail: function () {
+			OC.Notification.showTemporary(
+				t('settings', 'An error occurred. Please upload an ASCII-encoded PEM certificate.'));
+		}
+	});
+
+	if ($('#sslCertificate > tbody > tr').length === 0) {
+		$('#sslCertificate').hide();
+	}
+});
diff --git a/settings/js/personal.js b/settings/js/personal.js
index da74f28d70c..3e1a0d7497b 100644
--- a/settings/js/personal.js
+++ b/settings/js/personal.js
@@ -339,73 +339,6 @@ $(document).ready(function () {
 		}
 	});
 
-	$('#sslCertificate').on('click', 'td.remove > img', function () {
-		var row = $(this).parent().parent();
-		$.ajax(OC.generateUrl('settings/personal/certificate/{certificate}', {certificate: row.data('name')}), {
-			type: 'DELETE'
-		});
-		row.remove();
-
-		if ($('#sslCertificate > tbody > tr').length === 0) {
-			$('#sslCertificate').hide();
-		}
-		return true;
-	});
-
-	$('#sslCertificate tr > td').tipsy({gravity: 'n', live: true});
-
-	$('#rootcert_import').fileupload({
-		submit: function(e, data) {
-			data.formData = _.extend(data.formData || {}, {
-				requesttoken: OC.requestToken
-			});
-		},
-		success: function (data) {
-			if (typeof data === 'string') {
-				data = $.parseJSON(data);
-			} else if (data && data.length) {
-				// fetch response from iframe
-				data = $.parseJSON(data[0].body.innerText);
-			}
-			if (!data || typeof(data) === 'string') {
-				// IE8 iframe workaround comes here instead of fail()
-				OC.Notification.showTemporary(
-					t('settings', 'An error occurred. Please upload an ASCII-encoded PEM certificate.'));
-				return;
-			}
-			var issueDate = new Date(data.validFrom * 1000);
-			var expireDate = new Date(data.validTill * 1000);
-			var now = new Date();
-			var isExpired = !(issueDate <= now && now <= expireDate);
-
-			var row = $('<tr/>');
-			row.data('name', data.name);
-			row.addClass(isExpired? 'expired': 'valid');
-			row.append($('<td/>').attr('title', data.organization).text(data.commonName));
-			row.append($('<td/>').attr('title', t('core,', 'Valid until {date}', {date: data.validTillString}))
-				.text(data.validTillString));
-			row.append($('<td/>').attr('title', data.issuerOrganization).text(data.issuer));
-			row.append($('<td/>').addClass('remove').append(
-				$('<img/>').attr({
-					alt: t('core', 'Delete'),
-					title: t('core', 'Delete'),
-					src: OC.imagePath('core', 'actions/delete.svg')
-				}).addClass('action')
-			));
-
-			$('#sslCertificate tbody').append(row);
-			$('#sslCertificate').show();
-		},
-		fail: function () {
-			OC.Notification.showTemporary(
-				t('settings', 'An error occurred. Please upload an ASCII-encoded PEM certificate.'));
-		}
-	});
-
-	if ($('#sslCertificate > tbody > tr').length === 0) {
-		$('#sslCertificate').hide();
-	}
-
 	// Load the big avatar
 	if (oc_config.enable_avatars) {
 		$('#avatar .avatardiv').avatar(OC.currentUser, 145);
diff --git a/settings/personal.php b/settings/personal.php
index c4e1c057bf3..54698fd6d54 100644
--- a/settings/personal.php
+++ b/settings/personal.php
@@ -43,6 +43,7 @@ $urlGenerator = \OC::$server->getURLGenerator();
 
 // Highlight navigation entry
 OC_Util::addScript( 'settings', 'personal' );
+OC_Util::addScript('settings', 'certificates');
 OC_Util::addStyle( 'settings', 'settings' );
 \OC_Util::addVendorScript('strengthify/jquery.strengthify');
 \OC_Util::addVendorStyle('strengthify/strengthify');
@@ -168,6 +169,17 @@ $formsAndMore[]= ['anchor' => 'clientsbox', 'section-name' => $l->t('Sync client
 
 $forms=OC_App::getForms('personal');
 
+
+// add bottom hardcoded forms from the template
+if ($enableCertImport) {
+	$certificatesTemplate = new OC_Template('settings', 'certificates');
+	$certificatesTemplate->assign('type', 'personal');
+	$certificatesTemplate->assign('uploadRoute', 'settings.Certificate.addPersonalRootCertificate');
+	$certificatesTemplate->assign('certs', $certificateManager->listCertificates());
+	$certificatesTemplate->assign('urlGenerator', $urlGenerator);
+	$forms[] = $certificatesTemplate->fetchPage();
+}
+
 $formsMap = array_map(function($form){
 	if (preg_match('%(<h2(?P<class>[^>]*)>.*?</h2>)%i', $form, $regs)) {
 		$sectionName = str_replace('<h2'.$regs['class'].'>', '', $regs[0]);
@@ -188,12 +200,5 @@ $formsMap = array_map(function($form){
 
 $formsAndMore = array_merge($formsAndMore, $formsMap);
 
-// add bottom hardcoded forms from the template
-if($enableCertImport) {
-	$formsAndMore[]= array( 'anchor' => 'ssl-root-certificates', 'section-name' => $l->t('SSL root certificates') );
-}
-
-
-
 $tmpl->assign('forms', $formsAndMore);
 $tmpl->printPage();
diff --git a/settings/routes.php b/settings/routes.php
index 6b6b0150168..0cc5e1eccab 100644
--- a/settings/routes.php
+++ b/settings/routes.php
@@ -57,6 +57,8 @@ $application->registerRoutes($this, [
 		['name' => 'CheckSetup#rescanFailedIntegrityCheck', 'url' => '/settings/integrity/rescan', 'verb' => 'GET'],
 		['name' => 'Certificate#addPersonalRootCertificate', 'url' => '/settings/personal/certificate', 'verb' => 'POST'],
 		['name' => 'Certificate#removePersonalRootCertificate', 'url' => '/settings/personal/certificate/{certificateIdentifier}', 'verb' => 'DELETE'],
+		['name' => 'Certificate#addSystemRootCertificate', 'url' => '/settings/admin/certificate', 'verb' => 'POST'],
+		['name' => 'Certificate#removeSystemRootCertificate', 'url' => '/settings/admin/certificate/{certificateIdentifier}', 'verb' => 'DELETE'],
 	]
 ]);
 
diff --git a/settings/templates/certificates.php b/settings/templates/certificates.php
new file mode 100644
index 00000000000..c1ccdcaef95
--- /dev/null
+++ b/settings/templates/certificates.php
@@ -0,0 +1,44 @@
+<div class="section">
+	<h2><?php p($l->t('SSL Root Certificates')); ?></h2>
+	<table id="sslCertificate" class="grid" data-type="<?php p($_['type']); ?>">
+		<thead>
+			<tr>
+				<th><?php p($l->t('Common Name')); ?></th>
+				<th><?php p($l->t('Valid until')); ?></th>
+				<th><?php p($l->t('Issued By')); ?></th>
+			</tr>
+		</thead>
+		<tbody>
+		<?php foreach ($_['certs'] as $rootCert): /**@var \OCP\ICertificate $rootCert */ ?>
+			<tr class="<?php echo ($rootCert->isExpired()) ? 'expired' : 'valid' ?>"
+				data-name="<?php p($rootCert->getName()) ?>">
+				<td class="rootCert"
+					title="<?php p($rootCert->getOrganization()) ?>">
+					<?php p($rootCert->getCommonName()) ?>
+				</td>
+				<td title="<?php p($l->t('Valid until %s', $l->l('date', $rootCert->getExpireDate()))) ?>">
+					<?php echo $l->l('date', $rootCert->getExpireDate()) ?>
+				</td>
+				<td title="<?php p($rootCert->getIssuerOrganization()) ?>">
+					<?php p($rootCert->getIssuerName()) ?>
+				</td>
+				<td <?php if ($rootCert != ''): ?>class="remove"
+					<?php else: ?>style="visibility:hidden;"
+					<?php endif; ?>><img alt="<?php p($l->t('Delete')); ?>"
+										 title="<?php p($l->t('Delete')); ?>"
+										 class="svg action"
+										 src="<?php print_unescaped(image_path('core', 'actions/delete.svg')); ?>"/>
+				</td>
+			</tr>
+		<?php endforeach; ?>
+		</tbody>
+	</table>
+	<form class="uploadButton" method="post"
+		  action="<?php p($_['urlGenerator']->linkToRoute($_['uploadRoute'])); ?>"
+		  target="certUploadFrame">
+		<label for="rootcert_import" class="inlineblock button"
+			   id="rootcert_import_button"><?php p($l->t('Import root certificate')); ?></label>
+		<input type="file" id="rootcert_import" name="rootcert_import"
+			   class="hiddenuploadfield">
+	</form>
+</div>
diff --git a/settings/templates/personal.php b/settings/templates/personal.php
index ce179ca8788..5bae01742b6 100644
--- a/settings/templates/personal.php
+++ b/settings/templates/personal.php
@@ -204,48 +204,6 @@ if($_['passwordChangeSupported']) {
 	<?php }
 };?>
 
-<?php if($_['showCertificates']) : ?>
-<div id="ssl-root-certificates" class="section">
-	<h2><?php p($l->t('SSL root certificates')); ?></h2>
-	<table id="sslCertificate" class="grid">
-		<thead>
-			<tr>
-			<th><?php p($l->t('Common Name')); ?></th>
-			<th><?php p($l->t('Valid until')); ?></th>
-			<th><?php p($l->t('Issued By')); ?></th>
-			<th></th>
-			</tr>
-		</thead>
-		<tbody>
-			<?php foreach ($_['certs'] as $rootCert): /**@var \OCP\ICertificate $rootCert*/ ?>
-				<tr class="<?php echo ($rootCert->isExpired()) ? 'expired' : 'valid' ?>" data-name="<?php p($rootCert->getName()) ?>">
-					<td class="rootCert" title="<?php p($rootCert->getOrganization())?>">
-						<?php p($rootCert->getCommonName()) ?>
-					</td>
-					<td title="<?php p($l->t('Valid until %s', $l->l('date', $rootCert->getExpireDate()))) ?>">
-						<?php echo $l->l('date', $rootCert->getExpireDate()) ?>
-					</td>
-					<td title="<?php p($rootCert->getIssuerOrganization()) ?>">
-						<?php p($rootCert->getIssuerName()) ?>
-					</td>
-					<td <?php if ($rootCert != ''): ?>class="remove"
-						<?php else: ?>style="visibility:hidden;"
-						<?php endif; ?>><img alt="<?php p($l->t('Delete')); ?>"
-											 title="<?php p($l->t('Delete')); ?>"
-											 class="svg action"
-											 src="<?php print_unescaped(image_path('core', 'actions/delete.svg')); ?>"/>
-					</td>
-				</tr>
-			<?php endforeach; ?>
-		</tbody>
-	</table>
-	<form class="uploadButton" method="post" action="<?php p($_['urlGenerator']->linkToRoute('settings.Certificate.addPersonalRootCertificate')); ?>" target="certUploadFrame">
-		<label for="rootcert_import" class="inlineblock button" id="rootcert_import_button"><?php p($l->t('Import root certificate')); ?></label>
-		<input type="file" id="rootcert_import" name="rootcert_import" class="hiddenuploadfield">
-	</form>
-</div>
-<?php endif; ?>
-
 <div class="section">
 	<h2><?php p($l->t('Version'));?></h2>
 	<p><a href="<?php print_unescaped($theme->getBaseUrl()); ?>" target="_blank"><?php p($theme->getTitle()); ?></a> <?php p(OC_Util::getHumanVersion()) ?></p>
diff --git a/tests/lib/security/certificatemanager.php b/tests/lib/security/certificatemanager.php
index f2e29cab18e..e9ccea39efe 100644
--- a/tests/lib/security/certificatemanager.php
+++ b/tests/lib/security/certificatemanager.php
@@ -14,6 +14,8 @@ use \OC\Security\CertificateManager;
  * @group DB
  */
 class CertificateManagerTest extends \Test\TestCase {
+	use \Test\Traits\UserTrait;
+	use \Test\Traits\MountProviderTrait;
 
 	/** @var CertificateManager */
 	private $certificateManager;
@@ -24,7 +26,10 @@ class CertificateManagerTest extends \Test\TestCase {
 		parent::setUp();
 
 		$this->username = $this->getUniqueID('', 20);
-		\OC::$server->getUserManager()->createUser($this->username, $this->getUniqueID('', 20));
+		$this->createUser($this->username, '');
+
+		$storage = new \OC\Files\Storage\Temporary();
+		$this->registerMount($this->username, $storage, '/' . $this->username . '/');
 
 		\OC_Util::tearDownFS();
 		\OC_User::setUserId('');
@@ -40,7 +45,9 @@ class CertificateManagerTest extends \Test\TestCase {
 
 	protected function tearDown() {
 		$user = \OC::$server->getUserManager()->get($this->username);
-		if ($user !== null) { $user->delete(); }
+		if ($user !== null) {
+			$user->delete();
+		}
 		parent::tearDown();
 	}
 
@@ -56,14 +63,14 @@ class CertificateManagerTest extends \Test\TestCase {
 		$this->assertSame(array(), $this->certificateManager->listCertificates());
 
 		// Add some certificates
-		$this->certificateManager->addCertificate(file_get_contents(__DIR__.'/../../data/certificates/goodCertificate.crt'), 'GoodCertificate');
+		$this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/goodCertificate.crt'), 'GoodCertificate');
 		$certificateStore = array();
-		$certificateStore[] =  new \OC\Security\Certificate(file_get_contents(__DIR__.'/../../data/certificates/goodCertificate.crt'), 'GoodCertificate');
+		$certificateStore[] = new \OC\Security\Certificate(file_get_contents(__DIR__ . '/../../data/certificates/goodCertificate.crt'), 'GoodCertificate');
 		$this->assertEqualsArrays($certificateStore, $this->certificateManager->listCertificates());
 
 		// Add another certificates
-		$this->certificateManager->addCertificate(file_get_contents(__DIR__.'/../../data/certificates/expiredCertificate.crt'), 'ExpiredCertificate');
-		$certificateStore[] =  new \OC\Security\Certificate(file_get_contents(__DIR__.'/../../data/certificates/expiredCertificate.crt'), 'ExpiredCertificate');
+		$this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'), 'ExpiredCertificate');
+		$certificateStore[] = new \OC\Security\Certificate(file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'), 'ExpiredCertificate');
 		$this->assertEqualsArrays($certificateStore, $this->certificateManager->listCertificates());
 	}
 
@@ -93,7 +100,7 @@ class CertificateManagerTest extends \Test\TestCase {
 	 * @param string $filename
 	 */
 	function testAddDangerousFile($filename) {
-		$this->certificateManager->addCertificate(file_get_contents(__DIR__.'/../../data/certificates/expiredCertificate.crt'), $filename);
+		$this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/expiredCertificate.crt'), $filename);
 	}
 
 	function testRemoveDangerousFile() {
@@ -101,7 +108,7 @@ class CertificateManagerTest extends \Test\TestCase {
 	}
 
 	function testRemoveExistingFile() {
-		$this->certificateManager->addCertificate(file_get_contents(__DIR__.'/../../data/certificates/goodCertificate.crt'), 'GoodCertificate');
+		$this->certificateManager->addCertificate(file_get_contents(__DIR__ . '/../../data/certificates/goodCertificate.crt'), 'GoodCertificate');
 		$this->assertTrue($this->certificateManager->removeCertificate('GoodCertificate'));
 	}
 
diff --git a/tests/settings/controller/CertificateControllerTest.php b/tests/settings/controller/CertificateControllerTest.php
index 023d7753cca..2fdbbb8b0ac 100644
--- a/tests/settings/controller/CertificateControllerTest.php
+++ b/tests/settings/controller/CertificateControllerTest.php
@@ -44,12 +44,15 @@ class CertificateControllerTest extends \Test\TestCase {
 	private $l10n;
 	/** @var IAppManager */
 	private $appManager;
+	/** @var  ICertificateManager */
+	private $systemCertificateManager;
 
 	public function setUp() {
 		parent::setUp();
 
 		$this->request = $this->getMock('\OCP\IRequest');
 		$this->certificateManager = $this->getMock('\OCP\ICertificateManager');
+		$this->systemCertificateManager = $this->getMock('\OCP\ICertificateManager');
 		$this->l10n = $this->getMock('\OCP\IL10N');
 		$this->appManager = $this->getMock('OCP\App\IAppManager');
 
@@ -59,6 +62,7 @@ class CertificateControllerTest extends \Test\TestCase {
 					'settings',
 					$this->request,
 					$this->certificateManager,
+					$this->systemCertificateManager,
 					$this->l10n,
 					$this->appManager
 				]
-- 
GitLab