From d35787079f975c3c9821c035893de06ffb10b4a0 Mon Sep 17 00:00:00 2001
From: Olav Morken <olav.morken@uninett.no>
Date: Thu, 15 Jul 2010 10:29:51 +0000
Subject: [PATCH] New authentication module: authX509

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@2421 44740490-163a-0410-bde0-09ae8108e29a
---
 dictionaries/errors.definition.json           |  18 ++
 dictionaries/errors.translation.json          |  18 ++
 docs/simplesamlphp-idp.txt                    |   3 +
 modules/authX509/default-disable              |   0
 .../dictionaries/X509error.definition.json    |   8 +
 .../dictionaries/X509error.translation.json   |   8 +
 modules/authX509/docs/authX509.txt            | 106 ++++++++
 .../authX509/lib/Auth/Source/X509userCert.php | 233 ++++++++++++++++++
 modules/authX509/templates/X509error.php      |  42 ++++
 modules/ldap/lib/ConfigHelper.php             |  49 +++-
 10 files changed, 483 insertions(+), 2 deletions(-)
 create mode 100644 modules/authX509/default-disable
 create mode 100644 modules/authX509/dictionaries/X509error.definition.json
 create mode 100644 modules/authX509/dictionaries/X509error.translation.json
 create mode 100644 modules/authX509/docs/authX509.txt
 create mode 100644 modules/authX509/lib/Auth/Source/X509userCert.php
 create mode 100644 modules/authX509/templates/X509error.php

diff --git a/dictionaries/errors.definition.json b/dictionaries/errors.definition.json
index 7e10e8c87..5d62394ff 100644
--- a/dictionaries/errors.definition.json
+++ b/dictionaries/errors.definition.json
@@ -235,5 +235,23 @@
 	},
 	"descr_RESPONSESTATUSNOSUCCESS": {
 		"en": "The Identity Provider responded with an error. (The status code in the SAML Response was not success)"
+	},
+	"title_NOCERT": {
+		"en": "No certificate"
+	},
+	"descr_NOCERT": {
+		"en": "Authentication failed: your browser did not send any certificate"
+	},
+	"title_INVALIDCERT": {
+		"en": "Invalid certificate"
+	},
+	"descr_INVALIDCERT": {
+		"en": "Authentication failed: the certificate your browser sent is invalid or cannot be read"
+	},
+	"title_UNKNOWNCERT": {
+		"en": "Unknown certificate"
+	},
+	"descr_UNKNOWNCERT": {
+		"en": "Authentication failed: the certificate your browser sent is unknown"
 	}
 }
diff --git a/dictionaries/errors.translation.json b/dictionaries/errors.translation.json
index 20f0d0100..a89cee59a 100644
--- a/dictionaries/errors.translation.json
+++ b/dictionaries/errors.translation.json
@@ -1433,5 +1433,23 @@
 		"pt": "O Fornecedor de Identidade respondeu com um erro. (A resposta SAML cont\u00e9m um c\u00f3digo de insucesso)",
 		"fr": "Le fournisseur d'identit\u00e9 a renvoy\u00e9 une erreur (le code de statut de la r\u00e9ponse SAML n'indiquait pas le succ\u00e8s)",
 		"hr": "Sustav jedinstvene autentikacije poslao je odgovor koji sadr\u017ei informaciju o pojavi gre\u0161ke. (\u0160ifra statusa dostavljena u SAML odgovoru ne odgovara \u0161ifri uspje\u0161no obra\u0111enog zahtjeva)"
+	},
+	"title_NOCERT": {
+		"fr": "Aucun certificat pr\u00e9sent\u00e9"
+	},
+	"descr_NOCERT": {
+		"fr": "Echec de l'authentification: votre navigateur n'a pas pr\u00e9sent\u00e9 de certificat"
+	},
+	"title_INVALIDCERT": {
+		"fr": "Certificat invalide"
+	},
+	"descr_INVALIDCERT": {
+		"fr": "Echec de l'authentification: le certificat pr\u00e9sent\u00e9 par votre navigateur est invalide ou illisible"
+	},
+	"title_UNKNOWNCERT": {
+		"fr": "Certificat inconnu"
+	},
+	"descr_UNKNOWNCERT": {
+		"fr": "Echec de l'authentification: le certificat pr\u00e9sent\u00e9 par votre navigateur n'est pas connu"
 	}
 }
diff --git a/docs/simplesamlphp-idp.txt b/docs/simplesamlphp-idp.txt
index eedc96578..805a8cbb5 100644
--- a/docs/simplesamlphp-idp.txt
+++ b/docs/simplesamlphp-idp.txt
@@ -28,6 +28,9 @@ Authentication module
 
 The next step is to configure the way users authenticate on your IdP. Various modules in the `modules/` directory provides methods for authenticating your users. This is an overview of those that are included in the simpleSAMLphp distribution:
 
+[`authX509:authX509userCert`](./authX509:authX509)
+: Authenticate against a LDAP database with a SSL client certificate.
+
 `exampleauth:UserPass`
 : Authenticate against a list of usernames and passwords.
 
diff --git a/modules/authX509/default-disable b/modules/authX509/default-disable
new file mode 100644
index 000000000..e69de29bb
diff --git a/modules/authX509/dictionaries/X509error.definition.json b/modules/authX509/dictionaries/X509error.definition.json
new file mode 100644
index 000000000..9bd4897b9
--- /dev/null
+++ b/modules/authX509/dictionaries/X509error.definition.json
@@ -0,0 +1,8 @@
+{
+	"certificate_header": {
+		"en": "X509 certificate authentication"
+	},
+	"certificate_text": {
+		"en": "X509 certificate authentication is required to access this service."
+	}
+}
diff --git a/modules/authX509/dictionaries/X509error.translation.json b/modules/authX509/dictionaries/X509error.translation.json
new file mode 100644
index 000000000..09b7b45d9
--- /dev/null
+++ b/modules/authX509/dictionaries/X509error.translation.json
@@ -0,0 +1,8 @@
+{
+	"certificate_header": {
+		"fr": "Authentification par certificat X509"
+	},
+	"certificate_text": {
+		"fr": "Un certificat X509 est requis pour acc\u00e9der \u00e0 ce service."
+	}
+}
diff --git a/modules/authX509/docs/authX509.txt b/modules/authX509/docs/authX509.txt
new file mode 100644
index 000000000..0364cf296
--- /dev/null
+++ b/modules/authX509/docs/authX509.txt
@@ -0,0 +1,106 @@
+Using the X509 authentication source with simpleSAMLphp
+=======================================================
+
+The authX509 module provide X509 authentication with certificate
+validation. For now there is only one authentication source:
+
+* authX509userCert Validate against LDAP userCertificate attribute
+
+More validation schemes (OCSP, CRL, local list) might be added later.
+
+Configuring Apache
+------------------
+
+This module assumes that the server requests a client certificate, and
+stores it in the environment variable SSL_CLIENT_CERT. This can be achieved
+with such a configuration:
+
+    SSLEngine on
+    SSLCertificateFile /etc/openssl/certs/server.crt
+    SSLCertificateKeyFile /etc/openssl/private/server.key
+    SSLCACertificateFile /etc/openssl/certs/ca.crt
+    SSLVerifyClient require
+    SSLVerifyDepth 2
+    SSLOptions +ExportCertData
+
+Note that SSLVerifyClient can be set to optional if you want to support
+both certificate and plain login authentication at the same time (more on
+this later).
+
+If your server or your client (or both!) have TLS renegociation disabled
+as a workaround for CVE-2009-3555, then the configuration directive above
+must not appear in a <Directory>, <Location>, or in a name-based
+<VirtualHost>. You can only use them server-wide, or in <VirtualHost>
+with different IP address/port combinaisons.
+
+
+Setting up the authX509 module
+------------------------------
+
+The first thing you need to do is to enable the cas module:
+
+    touch modules/authX509/enable
+
+Then you must add it as an authentication source. Here is an
+example authsource.php
+
+    'x509' => array(
+        'authX509:authX509userCert',
+        'hostname' => 'ldaps://ldap.example.net',
+        'enable_tls' => FALSE,
+        'attributes' => array("cn", "uid", "mail", "ou", "sn"),
+        'search.enable' => TRUE,
+        'search.attributes' => array('uid', 'mail'),
+        'search.base' => 'dc=example,dc=net',
+        'x509attributes' => array('UID' => 'uid'),
+        'ldapusercert' => array('userCertificate;binary'),
+    ),
+
+The configuration is the same as for the LDAP module, except for
+two options:
+
+* x509attributes is used to map a certificate subject attribute to
+                 an LDAP attribute. It is used to find the certificate
+                 owner in LDAP from the certificate subject. If multiple
+                 mappings are provided, any mappping will match (this
+                 is a logical OR). Default is array('UID' => 'uid')
+* ldapusercert   the LDAP attribute in which the user certificate will
+                 be found. Default is userCertificate;binary
+
+
+Uploading certificate in LDAP
+-----------------------------
+
+Certificate are usually stored in LDAP as DER, in binary. Here is
+how to convert from PEM to DER:
+
+    openssl x509 -in cert.pem -inform PEM -outform DER -out cert.der
+
+Here is some LDIF to upload the certificate in the directory:
+
+    dn: uid=jdoe,dc=example,dc=net
+    changetype: modify
+    add: userCertificate;binary
+    userCertificate;binary:< file:///path/to/cert.der
+
+
+Supporting both certificate and login authentications
+=====================================================
+
+In your Apache configuration, set SSLVerifyClient to optional. Then you
+can hack your metadata/saml20-idp-hosted.php file that way:
+
+    $auth_source = empty($_SERVER['SSL_CLIENT_CERT']) ? 'ldap' : 'x509';
+    $metadata = array(
+        '__DYNAMIC:1__' => array(
+            'host'          =>      '__DEFAULT__',
+            'privatekey'    =>      'server.key',
+            'certificate'   =>      'server.crt',
+            'auth'          =>       $auth_source,
+            'authority'     =>      'login',
+            'userid.attribute' =>   'uid',
+            'logouttype'    =>      'iframe',
+            'AttributeNameFormat' =>
+                            'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
+    )
+
diff --git a/modules/authX509/lib/Auth/Source/X509userCert.php b/modules/authX509/lib/Auth/Source/X509userCert.php
new file mode 100644
index 000000000..3ddd7e411
--- /dev/null
+++ b/modules/authX509/lib/Auth/Source/X509userCert.php
@@ -0,0 +1,233 @@
+<?php
+
+/**
+ * This class implements x509 certificate authentication with
+ * certificate validation against an LDAP directory.
+ *
+ * @author Emmanuel Dreyfus <manu@netbsd.org>
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_authX509_Auth_Source_X509userCert extends SimpleSAML_Auth_Source {
+
+	/**
+	 * x509 attributes to use from the certificate
+	 * for searching the user in the LDAP directory.
+	 */
+	private $x509attributes = array('UID' => 'uid');
+
+
+	/**
+	 * LDAP attribute containing the user certificate
+	 */
+	private $ldapusercert = array('userCertificate;binary');
+
+
+	/**
+	 * LDAPConfigHelper object
+	 */
+	private $ldapcf;
+
+
+	/**
+	 * Constructor for this authentication source.
+	 *
+	 * All subclasses who implement their own constructor must call this
+	 * constructor before using $config for anything.
+	 *
+	 * @param array $info  Information about this authentication source.
+	 * @param array &$config  Configuration for this authentication source.
+	 */
+	public function __construct($info, &$config) {
+		assert('is_array($info)');
+		assert('is_array($config)');
+
+		if (isset($config['authX509:x509attributes']))
+			$this->x509attributes =
+				$config['authX509:x509attributes'];
+
+		if (isset($config['authX509:ldapusercert']))
+			$this->ldapusercert =
+				$config['authX509:ldapusercert'];
+
+		parent::__construct($info, $config);
+
+		$this->ldapcf = new sspmod_ldap_ConfigHelper($config,
+			'Authentication source ' . var_export($this->authId, TRUE));
+
+		return;
+	}
+
+
+	/**
+	 * Convert certificate from PEM to DER
+	 *
+	 * @param array $pem_data  PEM-encoded certificate
+	 */
+	private function pem2der($pem_data) {
+		$begin = "CERTIFICATE-----";
+		$end   = "-----END";
+		$pem_data = substr($pem_data,
+			strpos($pem_data, $begin)+strlen($begin));
+		$pem_data = substr($pem_data, 0, strpos($pem_data, $end));
+		$der = base64_decode($pem_data);
+		return $der;
+	}
+
+
+	/**
+	 * Convert certificate from DER to PEM
+	 *
+	 * @param array $der_data  DER-encoded certificate
+	 */
+	private function der2pem($der_data) {
+		$pem = chunk_split(base64_encode($der_data), 64, "\n");
+		$pem = "-----BEGIN CERTIFICATE-----\n".$pem.
+			"-----END CERTIFICATE-----\n";
+		return $pem;
+	}
+
+
+	/**
+	 * Finish a failed authentication.
+	 *
+	 * This function can be overloaded by a child authentication
+	 * class that wish to perform some operations on failure
+	 *
+	 * @param array &$state  Information about the current authentication.
+	 */
+	public function authFailed(&$state) {
+		$id = SimpleSAML_Auth_State::saveState($state, self::STAGEID);
+		$params = array('AuthState' => $id);
+		$config = SimpleSAML_Configuration::getInstance();
+
+		$t = new SimpleSAML_XHTML_Template($config,
+			'authX509:X509error.php');
+		$t->data['stateparams'] = $params;
+		$t->data['errorcode'] = $state['authX509.error'];
+
+		$t->show();
+		exit();
+	}
+
+
+	/**
+	 * Validate certificate and login
+	 *
+	 * This function try to validate the certificate.
+	 * On success, the user is logged in without going through
+	 * o login page.
+	 * On failure, The authX509:X509error.php template is
+	 * loaded.
+	 *
+	 * @param array &$state  Information about the current authentication.
+	 */
+	public function authenticate(&$state) {
+		assert('is_array($state)');
+		$ldapcf = $this->ldapcf;
+
+		if (!isset($_SERVER['SSL_CLIENT_CERT']) ||
+		    ($_SERVER['SSL_CLIENT_CERT'] == '')) {
+			$state['authX509.error'] = "NOCERT";
+			$this->authFailed($state);
+			assert('FALSE'); /* NOTREACHED */
+			return;
+		}
+
+		$client_cert = $_SERVER['SSL_CLIENT_CERT'];
+		$client_cert_data = openssl_x509_parse($client_cert);
+		if ($client_cert_data == FALSE) {
+			SimpleSAML_Logger::error('authX509: invalid cert');
+			$state['authX509.error'] = "INVALIDCERT";
+			$this->authFailed($state);
+
+			assert('FALSE'); /* NOTREACHED */
+			return;
+		}
+
+		$dn = FALSE;
+		foreach ($this->x509attributes as $x509_attr => $ldap_attr) {
+			/* value is scalar */
+			$value = $client_cert_data['subject'][$x509_attr];
+			SimpleSAML_Logger::info('authX509: cert '.
+			                        $x509_attr.' = '.$value);
+			$dn = $ldapcf->searchfordn($ldap_attr, $value, TRUE);
+			if ($dn !== FALSE)
+				break;
+		}
+
+		if ($dn === FALSE) {
+			SimpleSAML_Logger::error('authX509: cert has '.
+			                         'no matching user in LDAP');
+			$state['authX509.error'] = "UNKNOWNCERT";
+			$this->authFailed($state);
+
+			assert('FALSE'); /* NOTREACHED */
+			return;
+		}
+
+		$ldap_certs = $ldapcf->getAttributes($dn, $this->ldapusercert);
+		if ($ldap_certs === FALSE) {
+			SimpleSAML_Logger::error('authX509: no certificate '.
+			                         'found in LDAP for dn='.$dn);
+			$state['authX509.error'] = "UNKNOWNCERT";
+			$this->authFailed($state);
+
+			assert('FALSE'); /* NOTREACHED */
+			return;
+		}
+
+
+		$merged_ldapcerts = array();
+		foreach ($this->ldapusercert as $attr)
+			$merged_ldapcerts = array_merge($merged_ldapcerts,
+			                                $ldap_certs[$attr]);
+		$ldap_certs = $merged_ldapcerts;
+
+		foreach ($ldap_certs as $ldap_cert) {
+			$pem = $this->der2pem($ldap_cert);
+			$ldap_cert_data = openssl_x509_parse($pem);
+			if($ldap_cert_data == FALSE) {
+				SimpleSAML_Logger::error('authX509: cert in '.
+				                         'LDAP in invalid for '.
+				                         'dn = '.$dn);
+				continue;
+			}
+
+			if ($ldap_cert_data === $client_cert_data) {
+				$attributes = $ldapcf->getAttributes($dn);
+				assert('is_array($attributes)');
+				$state['Attributes'] = $attributes;
+				$this->authSuccesful($state);
+
+				assert('FALSE'); /* NOTREACHED */
+				return;
+			}
+		}
+
+		SimpleSAML_Logger::error('authX509: no matching cert in '.
+		                         'LDAP for dn = '.$dn);
+		$state['authX509.error'] = "UNKNOWNCERT";
+		$this->authFailed($state);
+
+		assert('FALSE'); /* NOTREACHED */
+		return;
+	}
+
+
+	/**
+	 * Finish a succesfull authentication.
+	 *
+	 * This function can be overloaded by a child authentication
+	 * class that wish to perform some operations after login.
+	 *
+	 * @param array &$state  Information about the current authentication.
+	 */
+	public function authSuccesful(&$state) {
+		SimpleSAML_Auth_Source::completeAuth($state);
+
+		assert('FALSE'); /* NOTREACHED */
+		return;
+	}
+
+}
diff --git a/modules/authX509/templates/X509error.php b/modules/authX509/templates/X509error.php
new file mode 100644
index 000000000..cded8b864
--- /dev/null
+++ b/modules/authX509/templates/X509error.php
@@ -0,0 +1,42 @@
+<?php
+$this->data['header'] = $this->t('{authX509:X509error:certificate_header}');
+
+$this->includeAtTemplateBase('includes/header.php');
+
+?>
+
+<?php
+if ($this->data['errorcode'] !== NULL) {
+?>
+	<div style="border-left: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8; background: #f5f5f5">
+		<img src="/<?php echo $this->data['baseurlpath']; ?>resources/icons/experience/gtk-dialog-error.48x48.png" style="float: left; margin: 15px " />
+		<h2><?php echo $this->t('{login:error_header}'); ?></h2>
+		<p><b><?php echo $this->t('{errors:title_' . $this->data['errorcode'] . '}'); ?></b></p>
+		<p><?php echo $this->t('{errors:descr_' . $this->data['errorcode'] . '}'); ?></p>
+	</div>
+<?php
+}
+?>
+	<h2 style="break: both"><?php echo $this->t('{authX509:X509error:certificate_header}'); ?></h2>
+
+	<p><?php echo $this->t('{authX509:X509error:certificate_text}'); ?></p>
+
+	<a href="<?php echo SimpleSAML_Utilities::selfURL(); ?>">
+		<?php echo $this->t('{login:login_button}'); ?>
+	</a>
+
+<?php
+
+if(!empty($this->data['links'])) {
+	echo '<ul class="links" style="margin-top: 2em">';
+	foreach($this->data['links'] AS $l) {
+		echo '<li><a href="' . htmlspecialchars($l['href']) . '">' . htmlspecialchars($this->t($l['text'])) . '</a></li>';
+	}
+	echo '</ul>';
+}
+
+
+
+
+$this->includeAtTemplateBase('includes/footer.php');
+?>
diff --git a/modules/ldap/lib/ConfigHelper.php b/modules/ldap/lib/ConfigHelper.php
index fea883ea5..f9aef64db 100644
--- a/modules/ldap/lib/ConfigHelper.php
+++ b/modules/ldap/lib/ConfigHelper.php
@@ -212,7 +212,52 @@ class sspmod_ldap_ConfigHelper {
 		return $ldap->getAttributes($dn, $this->attributes);
 	}
 
-}
 
+	/**
+	 * Search for a DN.
+	 *
+	 * @param string|array $attribute
+	 * The attribute name(s) searched for. If set to NULL, values from
+	 * configuration is used.
+	 * @param string $value
+	 * The attribute value searched for.
+	 * @param bool $allowZeroHits
+	 * Determines if the method will throw an exception if no
+	 * hits are found. Defaults to FALSE.
+	 * @return string
+	 * The DN of the matching element, if found. If no element was
+	 * found and $allowZeroHits is set to FALSE, an exception will
+	 * be thrown; otherwise NULL will be returned.
+	 * @throws SimpleSAML_Error_AuthSource if:
+	 * - LDAP search encounter some problems when searching cataloge
+	 * - Not able to connect to LDAP server
+	 * @throws SimpleSAML_Error_UserNotFound if:
+	 * - $allowZeroHits er TRUE and no result is found
+	 *
+	 */
+	public function searchfordn($attribute, $value, $allowZeroHits) {
+		$ldap = new SimpleSAML_Auth_LDAP($this->hostname,
+			$this->enableTLS,
+			$this->debug,
+			$this->timeout);
+
+		if ($attribute == NULL)
+			$attribute = $this->searchAttributes;
 
-?>
\ No newline at end of file
+		return $ldap->searchfordn($this->searchBase, $attribute,
+			$value, $allowZeroHits);
+	}
+
+	public function getAttributes($dn, $attributes = NULL) {
+		if ($attributes == NULL)
+			$attributes = $this->attributes;
+
+		$ldap = new SimpleSAML_Auth_LDAP($this->hostname,
+			$this->enableTLS,
+			$this->debug,
+			$this->timeout);
+
+		return $ldap->getAttributes($dn, $attributes);
+	}
+
+}
-- 
GitLab