diff --git a/lam/HISTORY b/lam/HISTORY
index 0c26fa6e..6af2f2f1 100644
--- a/lam/HISTORY
+++ b/lam/HISTORY
@@ -1,4 +1,5 @@
September 2013 4.3
+ - Custom SSL CA certificates can be setup in LAM main configuration
- LAM Pro:
-> PPolicy: check password history for password reuse
-> Custom fields: read-only fields for admin interface and file upload for binary data
diff --git a/lam/help/help.inc b/lam/help/help.inc
index 9c40f27b..863e8787 100644
--- a/lam/help/help.inc
+++ b/lam/help/help.inc
@@ -80,6 +80,8 @@ $helpArray = array (
_("Example").
":
".
_("dc=yourcompany,dc=com")),
+ "204" => array ("Headline" => _("SSL certificate"),
+ "Text" => _("This is only needed for TLS/SSL connections. By default, LAM will use the certificate authorities installed on your system. If you have a private CA in your company you can upload your CA certificates here and override the system certificates.")),
"206" => array ("Headline" => _("List attributes"),
"Text" => _("This is the list of attributes to show in the account list. The entries can either be predefined values, \"#attribute\", or individual ones, \"attribute:description\". Several entries are separated by semicolons.") .
"
" .
diff --git a/lam/lib/account.inc b/lam/lib/account.inc
index f1204c35..356e563b 100644
--- a/lam/lib/account.inc
+++ b/lam/lib/account.inc
@@ -1077,4 +1077,39 @@ function getRandomNumber() {
return mt_rand();
}
+/**
+ * Connects to the LDAP server and extracts the certificates.
+ *
+ * @param String $server server name
+ * @param String $port server port
+ * @return mixed false on error and certificate if extracted successfully
+ */
+function getLDAPSSLCertificate($server, $port) {
+ $stream = @stream_context_create(array("ssl" => array("capture_peer_cert_chain" => true)));
+ if (!$stream) {
+ return false;
+ }
+ $client = @stream_socket_client('ssl://' . $server . ':' . $port, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $stream);
+ if (!$client) {
+ return false;
+ }
+ $context = stream_context_get_params($client);
+ if (!isset($context['options']['ssl']['peer_certificate_chain'])) {
+ return false;
+ }
+ $finalPEM = '';
+ for ($i = 0; $i < sizeof($context['options']['ssl']['peer_certificate_chain']); $i++) {
+ $cert = $context['options']['ssl']['peer_certificate_chain'][$i];
+ $pemData = null;
+ $pemResult = @openssl_x509_export($cert, $pemData);
+ if ($pemResult) {
+ $finalPEM .= $pemData;
+ }
+ else {
+ return false;
+ }
+ }
+ return $finalPEM;
+}
+
?>
diff --git a/lam/lib/checkEnvironment.inc b/lam/lib/checkEnvironment.inc
index 98105187..1186ac93 100644
--- a/lam/lib/checkEnvironment.inc
+++ b/lam/lib/checkEnvironment.inc
@@ -70,6 +70,10 @@ if (! function_exists('ldap_search')) {
if (! function_exists('gettext') || !function_exists('_')) {
$criticalErrors[] = array("ERROR", "Your PHP has no gettext support!", "Please install gettext for PHP.");
}
+// check if PHP has openssl support
+if (! function_exists('openssl_x509_parse')) {
+ $criticalErrors[] = array("ERROR", "Your PHP has no openssl support!", "Please install openssl for PHP.");
+}
// check if PHP has XML support
if (! function_exists('utf8_decode')) {
$criticalErrors[] = array("ERROR", "Your PHP has no XML support!", "Please install the XML extension for PHP.");
diff --git a/lam/lib/config.inc b/lam/lib/config.inc
index b17c0b7a..cba2e190 100644
--- a/lam/lib/config.inc
+++ b/lam/lib/config.inc
@@ -37,6 +37,20 @@ include_once("modules.inc");
/** Used to get type information. */
include_once("types.inc");
+/**
+ * Sets the environment variables for custom SSL CA certificates.
+ */
+function setSSLCaCert() {
+ // set SSL certificate if set
+ if (isset($_SESSION['cfgMain'])) {
+ $sslCaPath = $_SESSION['cfgMain']->getSSLCaCertPath();
+ if ($sslCaPath != null) {
+ putenv('LDAPTLS_CACERT=' . $sslCaPath);
+ putenv('TLS_CACERT=' . $sslCaPath);
+ }
+ }
+}
+
/**
* Sets language settings for automatic translation
*/
@@ -233,13 +247,13 @@ class LAMConfig {
/** line separator */
const LINE_SEPARATOR = '+::+';
-
+
/** Server address (e.g. ldap://127.0.0.1:389) */
private $ServerURL;
/** enables/disables TLS encryption */
private $useTLS;
-
+
/** Array of string: users with admin rights */
private $Admins;
@@ -545,15 +559,16 @@ class LAMConfig {
}
}
$file = @fopen($conffile, "w");
+ $saveResult = LAMConfig::SAVE_OK;
if ($file) {
for ($i = 0; $i < sizeof($file_array); $i++) fputs($file, $file_array[$i]);
fclose($file);
- @chmod ($conffile, 0600);
- return LAMConfig::SAVE_OK;
+ @chmod($conffile, 0600);
}
else {
- return LAMConfig::SAVE_FAIL;
+ $saveResult = LAMConfig::SAVE_FAIL;
}
+ return $saveResult;
}
}
@@ -1326,7 +1341,7 @@ class LAMConfig {
public function setLamProMailText($lamProMailText) {
$this->lamProMailText = implode(LAMConfig::LINE_SEPARATOR, explode("\r\n", $lamProMailText));
}
-
+
}
@@ -1376,6 +1391,12 @@ class LAMCfgMain {
/** path to config file */
private $conffile;
+ /** uploaded SSL certificate that is stored to disk on save() */
+ private $uploadedSSLCaCert = null;
+
+ /** SSL certificate should be deleted on save() */
+ private $delSSLCaCert = false;
+
/** list of data fields to save in config file */
private $settings = array("password", "default", "sessionTimeout",
"logLevel", "logDestination", "allowedHosts", "passwordMinLength",
@@ -1473,6 +1494,27 @@ class LAMCfgMain {
else {
StatusMessage("ERROR", "", _("Cannot open config file!") . " (" . $this->conffile . ")");
}
+ // store SSL certificate
+ if ($this->uploadedSSLCaCert != null) {
+ $sslPath = $this->getInternalSSLCaCertFileName();
+ $file = @fopen($sslPath, "w");
+ if ($file) {
+ fputs($file, $this->uploadedSSLCaCert);
+ fclose($file);
+ @chmod($sslPath, 0600);
+ }
+ else {
+ StatusMessage("ERROR", _("Cannot write certificate file. Please check the permissions of config/serverCerts.pem."));
+ }
+ }
+ // delete SSL certificate
+ if ($this->delSSLCaCert === true) {
+ $sslPath = $this->getInternalSSLCaCertFileName();
+ $result = @unlink($sslPath);
+ if (!$result) {
+ StatusMessage("ERROR", _("Cannot write certificate file. Please check the permissions of config/serverCerts.pem."));
+ }
+ }
}
/**
@@ -1537,6 +1579,187 @@ class LAMCfgMain {
return file_exists($this->conffile);
}
+ /**
+ * Returns the path to the SSL CA certificate file that overrides the system certificates.
+ *
+ * @return String path to certificate file or null if certificate is not overridden
+ */
+ public function getSSLCaCertPath() {
+ $path = $this->getInternalSSLCaCertFileName();
+ if (file_exists($path)) {
+ return $path;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the file name that will be used internally to store the CA file.
+ *
+ * @return String file name
+ */
+ private function getInternalSSLCaCertFileName() {
+ return dirname(__FILE__) . '/../config/serverCerts.pem';
+ }
+
+ /**
+ * Uploads a new SSL CA cert.
+ *
+ * @param String $cert file content in DER/PEM format
+ * @return mixed TRUE if format is correct, error message if file is not accepted
+ */
+ public function uploadSSLCaCert($cert) {
+ if (strpos($cert, '-----BEGIN CERTIFICATE-----') !== 0) {
+ $pem = @chunk_split(@base64_encode($cert), 64, "\n");
+ $cert = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
+ }
+ $pemData = @openssl_x509_parse($cert);
+ if ($pemData === false) {
+ return _('Please provide a file in DER or PEM format.');
+ }
+ $existingCerts = $this->getSSLCaCertificateContent();
+ if (!empty($existingCerts)) {
+ // merge with existing certificates
+ $existingList = $this->splitSSLCaCertificateContent($existingCerts);
+ $newList = $this->splitSSLCaCertificateContent($cert);
+ $this->uploadedSSLCaCert = implode("\n", array_unique(array_merge($existingList, $newList)));
+ }
+ else {
+ $this->uploadedSSLCaCert = $cert;
+ }
+ $this->delSSLCaCert = false;
+ return true;
+ }
+
+ /**
+ * Returns the name of a temporary file in tmp that contains the SSL certificate.
+ * The file contains either the stored data in serverCerts or the uploaded data.
+ *
+ * @return String file name or null if no certificate was set
+ */
+ public function getSSLCaCertTempFileName() {
+ if ($this->delSSLCaCert) {
+ return null;
+ }
+ // get certificate data
+ $content = $this->getSSLCaCertificateContent();
+ if ($content == null) {
+ return null;
+ }
+ // write to temp file
+ $fileName = time() . getRandomNumber() . '.pem';
+ $path = dirname(__FILE__) . '/../tmp/' . $fileName;
+ $handle = @fopen($path, "wb");
+ @chmod($path, 0600);
+ if ($handle) {
+ $content = fputs($handle, $content);
+ fclose($handle);
+ }
+ else {
+ return null;
+ }
+ return $fileName;
+ }
+
+ /**
+ * Marks a single or all SSL CA certificate files for deletion.
+ * The changes take effect on save().
+ *
+ * @param int $index certificate index, null deletes all certificates (default: null)
+ */
+ public function deleteSSLCaCert($index = null) {
+ if ($index == null) {
+ // delete all
+ $this->delSSLCaCert = true;
+ return;
+ }
+ $content = $this->getSSLCaCertificateContent();
+ $list = $this->splitSSLCaCertificateContent($content);
+ unset($list[$index]);
+ if (sizeof($list) < 1) {
+ $this->delSSLCaCert = true;
+ $this->uploadedSSLCaCert = null;
+ }
+ else {
+ $this->uploadedSSLCaCert = implode("\n", $list);
+ }
+ }
+
+ /**
+ * Returns a list of all CA certificates.
+ *
+ * @return array list of certificates as output of openssl_x509_parse()
+ */
+ public function getSSLCaCertificates() {
+ if ($this->delSSLCaCert) {
+ return array();
+ }
+ $content = $this->getSSLCaCertificateContent();
+ if (empty($content)) {
+ return array();
+ }
+ $list = $this->splitSSLCaCertificateContent($content);
+ for ($i = 0; $i < sizeof($list); $i++) {
+ $list[$i] = @openssl_x509_parse($list[$i]);
+ }
+ return $list;
+ }
+
+ /**
+ * Returns the content of the certificate file or uploaded data.
+ *
+ * @return String null or certificate content
+ */
+ private function getSSLCaCertificateContent() {
+ $content = null;
+ if ($this->delSSLCaCert) {
+ return null;
+ }
+ if ($this->uploadedSSLCaCert != null) {
+ $content = $this->uploadedSSLCaCert;
+ }
+ elseif ($this->getSSLCaCertPath() != null) {
+ $path = $this->getSSLCaCertPath();
+ $handle = @fopen($path, "r");
+ if ($handle) {
+ $content = fread($handle, 10000000);
+ fclose($handle);
+ }
+ }
+ return $content;
+ }
+
+ /**
+ * Splits the certificate content into single PEM data chunks.
+ *
+ * @param String $content PEM file content
+ * @return array one element for each certificate chunk
+ */
+ private function splitSSLCaCertificateContent($content) {
+ if (empty($content)) {
+ return array();
+ }
+ $content = str_replace("\n\n", "\n", $content);
+ if (empty($content)) {
+ return array();
+ }
+ if (!(strpos($content, '-----BEGIN CERTIFICATE-----') === 0)) {
+ return array();
+ }
+ $lines = explode("\n", $content);
+ $list = array();
+ $pos = -1;
+ foreach ($lines as $line) {
+ if (strpos($line, '-----BEGIN CERTIFICATE-----') === 0) {
+ $pos++;
+ }
+ if (!isset($list[$pos])) {
+ $list[$pos] = '';
+ }
+ $list[$pos] .= $line . "\n";
+ }
+ return $list;
+ }
+
}
?>
diff --git a/lam/lib/modules/inetOrgPerson.inc b/lam/lib/modules/inetOrgPerson.inc
index d9a8945e..fd5bbe02 100644
--- a/lam/lib/modules/inetOrgPerson.inc
+++ b/lam/lib/modules/inetOrgPerson.inc
@@ -2261,7 +2261,9 @@ class inetOrgPerson extends baseModule implements passwordService {
$table->colspan = 10;
for ($i = 0; $i < sizeof($this->attributes['userCertificate;binary']); $i++) {
$filename = 'userCertificate' . getRandomNumber() . '.der';
- $out = @fopen(dirname(__FILE__) . '/../../tmp/' . $filename, "wb");
+ $pathOut = dirname(__FILE__) . '/../../tmp/' . $filename;
+ $out = @fopen($pathOut, "wb");
+ @chmod($pathOut, 0600);
fwrite($out, $this->attributes['userCertificate;binary'][$i]);
fclose ($out);
$path = '../../tmp/' . $filename;
diff --git a/lam/lib/security.inc b/lam/lib/security.inc
index c7d91385..f1f597ab 100644
--- a/lam/lib/security.inc
+++ b/lam/lib/security.inc
@@ -87,6 +87,7 @@ function startSecureSession($redirectToLogin = true, $initSecureData = false) {
else {
return false;
}
+ setSSLCaCert();
return true;
}
diff --git a/lam/templates/config/confmain.php b/lam/templates/config/confmain.php
index 24834590..72a19c14 100644
--- a/lam/templates/config/confmain.php
+++ b/lam/templates/config/confmain.php
@@ -186,7 +186,7 @@ if (sizeof($errorsToDisplay) > 0) {
}
// display formular
-echo ("