Commit eac5d9fb authored by Thomas Müller's avatar Thomas Müller
Browse files

Merge pull request #21336 from owncloud/system-root-certs

Allow admins to add system wide root certificates
parents 2493cfed 0d0377eb
......@@ -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() {
......
......@@ -853,6 +853,11 @@ $CONFIG = array(
'config' => '/absolute/location/of/openssl.cnf',
),
/**
* Allow the configuration of system wide trusted certificates
*/
'enable_certificate_management' => false,
/**
* Memory caching backend configuration
*
......
......@@ -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');
......
......@@ -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);
}
}
......@@ -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)) {
......
......@@ -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 = '');
}
......@@ -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
*/
......
......@@ -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'));
......@@ -152,6 +158,16 @@ $template->assign('OutdatedCacheWarning', $outdatedCaches);
// add hardcoded forms from the template
$forms = OC_App::getForms('admin');
if ($config->getSystemValue('enable_certificate_management', false)) {
$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() ||
......
......@@ -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');
......
......@@ -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();
}
}
$(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();
}
});
......@@ -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);
......
......@@ -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();
......@@ -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'],
[