From 4a5d7d1dd4c69787d1c5c3f9f09f2efa3c29948d Mon Sep 17 00:00:00 2001 From: Enrique de la Hoz <enrique.delahoz@uah.es> Date: Tue, 2 Dec 2008 11:26:24 +0000 Subject: [PATCH] Infocard Module Added:RP Support git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@1041 44740490-163a-0410-bde0-09ae8108e29a --- .../config-template/config-login-infocard.php | 97 ++++++ modules/InfoCard/default-disable | 3 + .../InfoCard/dictionaries/logininfocard.php | 186 +++++++++++ modules/InfoCard/lib/Auth/Source/ICAuth.php | 80 +++++ modules/InfoCard/lib/RP/InfoCard.php | 311 ++++++++++++++++++ modules/InfoCard/lib/RP/LICENSE.txt | 27 ++ .../InfoCard/lib/RP/Zend_InfoCard_Claims.php | 304 +++++++++++++++++ .../RP/Zend_InfoCard_Xml_Assertion_Saml.php | 272 +++++++++++++++ .../lib/RP/Zend_InfoCard_Xml_Security.php | 302 +++++++++++++++++ .../Zend_InfoCard_Xml_Security_Transform.php | 113 +++++++ ..._Security_Transform_EnvelopedSignature.php | 55 ++++ ...Card_Xml_Security_Transform_XmlExcC14N.php | 52 +++ .../templates/default/login-infocard.php | 58 ++++ modules/InfoCard/www/login-infocard.php | 54 +++ .../www/resources/infocard_114x80.png | Bin 0 -> 3821 bytes 15 files changed, 1914 insertions(+) create mode 100644 modules/InfoCard/config-template/config-login-infocard.php create mode 100644 modules/InfoCard/default-disable create mode 100644 modules/InfoCard/dictionaries/logininfocard.php create mode 100644 modules/InfoCard/lib/Auth/Source/ICAuth.php create mode 100644 modules/InfoCard/lib/RP/InfoCard.php create mode 100644 modules/InfoCard/lib/RP/LICENSE.txt create mode 100644 modules/InfoCard/lib/RP/Zend_InfoCard_Claims.php create mode 100644 modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Assertion_Saml.php create mode 100644 modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security.php create mode 100644 modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform.php create mode 100644 modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform_EnvelopedSignature.php create mode 100644 modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform_XmlExcC14N.php create mode 100644 modules/InfoCard/templates/default/login-infocard.php create mode 100644 modules/InfoCard/www/login-infocard.php create mode 100644 modules/InfoCard/www/resources/infocard_114x80.png diff --git a/modules/InfoCard/config-template/config-login-infocard.php b/modules/InfoCard/config-template/config-login-infocard.php new file mode 100644 index 000000000..055fee0a7 --- /dev/null +++ b/modules/InfoCard/config-template/config-login-infocard.php @@ -0,0 +1,97 @@ +<?php + +/* +* AUTHOR: Samuel Muñoz Hidalgo +* EMAIL: samuel.mh@gmail.com +* LAST REVISION: 1-DEC-08 +* DESCRIPTION: 'login-infocard' module configuration. + + +-server_key: +-server_crt: +-IClogo: InfoCard logo (template's button) + + +Definitions taken from: +A Guide to Using the Identity Selector +Interoperability Profile V1.5 within Web +Applications and Browsers. +Copyright Microsoft +" +-issuer (optional) + This parameter specifies the URL of the STS from which to obtain a token. If omitted, no + specific STS is requested. The special value + “http://schemas.xmlsoap.org/ws/2005/05/identity/issuer/self” specifies that the + token should come from a Self-issued Identity Provider. + +-issuerPolicy (optional) + This parameter specifies the URL of an endpoint from which the STS’s WS-SecurityPolicy + can be retrieved using WS-MetadataExchange. This endpoint must use HTTPS. + +-tokenType (optional) + This parameter specifies the type of the token to be requested from the STS as a URI. Th + parameter can be omitted if the STS and the Web site front-end have a mutual + understanding about what token type will be provided or if the Web site is willing to accep + any token type. + +-requiredClaims (optional) + This parameter specifies the types of claims that must be supplied by the identity. If + omitted, there are no required claims. The value of requiredClaims is a space-separate + list of URIs, each specifying a required claim type. + +-optionalClaims (optional) + This parameter specifies the types of optional claims that may be supplied by the identity + If omitted, there are no optional claims. The value of optionalClaims is a space-separat + list of URIs, each specifying a claim type that can be optionally submitted. + +-privacyUrl (optional) + This parameter specifies the URL of the human-readable Privacy Policy of the site, if + provided. +" + + +-Claims supported by the current schema + givenname + surname + emailaddress + streetaddress + locality + stateorprovince + postalcode + country + primaryphone + dateofbirth + privatepersonalid + gender + webpage + +*/ + + +$config = array ( + + 'server_key' => '/etc/apache2/ssl/idp.key', + 'server_crt' => '/etc/apache2/ssl/idp.crt', + 'IClogo' => 'resources/infocard_114x80.png', + + + 'InfoCard' => array( + 'schema' => 'http://schemas.xmlsoap.org/ws/2005/05/identity', + 'issuer' => 'https://sts/tokenservice.php', + 'issuerPolicy' => '', + 'privacyURL' => '', + 'tokenType' => 'urn:oasis:names:tc:SAML:1.0:assertion', + 'requiredClaims' => array( + 'privatepersonalidentifier' => array('displayTag'=>"Id", 'description'=>"id"), + 'givenname' => array('displayTag'=>"Given Name", 'description'=>"etc"), + 'surname' => array('displayTag'=>"Surname", 'description'=>"apellidos"), + 'emailaddress' => array('displayTag'=>"e-mail", 'description'=>"E-mail address") + ), + 'optionalClaims' => array( + 'country' => array('displayTag'=>"country", 'description'=>"PaĂs"), + 'webpage' => array('displayTag'=>"webpage", 'description'=>"Página web") + ), + ), +); + +?> \ No newline at end of file diff --git a/modules/InfoCard/default-disable b/modules/InfoCard/default-disable new file mode 100644 index 000000000..25615cb47 --- /dev/null +++ b/modules/InfoCard/default-disable @@ -0,0 +1,3 @@ +This file indicates that the default state of this module +is enabled. To disable, create a file named disable in the +same directory as this file. diff --git a/modules/InfoCard/dictionaries/logininfocard.php b/modules/InfoCard/dictionaries/logininfocard.php new file mode 100644 index 000000000..8d8dfa65c --- /dev/null +++ b/modules/InfoCard/dictionaries/logininfocard.php @@ -0,0 +1,186 @@ +<?php + +/* +* AUTHOR: Samuel Muñoz Hidalgo +* EMAIL: samuel.mh@gmail.com +* LAST REVISION: 1-DEC-08 +* DESCRIPTION: 'login-infocard' module dictionary. +*/ + +$lang = array( + 'wrong_IC' => array ( + 'no' => '', + 'nn' => '', + 'da' => '', + 'en' => 'Invalid InfoCard', + 'de' => '', + 'sv' => '', + 'fi' => '', + 'es' => 'InfoCard errĂłnea', + 'fr' => '', + 'nl' => '', + 'lb' => '', + 'sl' => '', + 'hr' => '', + 'hu' => '', + 'pt' => '', + 'pt-BR' => '', + ), + 'error_header' => array ( + 'no' => 'Feil', + 'nn' => 'Feil', + 'da' => 'Fejl', + 'en' => 'Error', + 'de' => 'Fehler', + 'sv' => 'Fel', + 'fi' => 'Virhe', + 'es' => 'Error', + 'fr' => 'Erreur', + 'nl' => 'Fout', + 'lb' => 'Fehler', + 'sl' => 'Napaka', + 'hr' => 'Greška', + 'hu' => 'Hiba', + 'pt' => 'Erro', + 'pt-BR' => 'Erro', + ), + 'user_IC_header' => array ( + 'no' => '', + 'nn' => '', + 'da' => '', + 'en' => 'Select an InfoCard', + 'de' => '', + 'sv' => '', + 'fi' => '', + 'es' => 'Seleccione una InfoCard', + 'fr' => '', + 'nl' => '', + 'lb' => '', + 'sl' => '', + 'hr' => '', + 'hu' => '', + 'pt' => '', + 'pt-BR' => '', + ), + 'user_IC_text' => array ( + 'no' => '', + 'nn' => '', + 'da' => '', + 'en' => 'A service has requested you to authenticate yourself. Please click on the image below to start a session with an InfoCard.', + 'de' => '', + 'sv' => '', + 'fi' => '', + 'es' => 'Un servicio solicita que se autentique. Pinche en la imagen inferior para iniciar una sesiĂłn con una InfoCard.', + 'fr' => '', + 'nl' => '', + 'lb' => '', + 'sl' => '', + 'hr' => '', + 'hu' => '', + 'pt' => '', + 'pt-BR' => '', + ), + 'login_button' => array ( + 'no' => '', + 'nn' => '', + 'da' => '', + 'en' => ' ', + 'de' => '', + 'sv' => '', + 'fi' => '', + 'es' => ' ', + 'fr' => '', + 'nl' => '', + 'lb' => '', + 'sl' => '', + 'hr' => '', + 'hu' => '', + 'pt' => '', + 'pt-BR' => '', + ), + 'help_header' => array ( + 'no' => '', + 'nn' => '', + 'da' => '', + 'en' => 'HELP! What is an InfoCard?', + 'de' => '', + 'sv' => '', + 'fi' => '', + 'es' => '¡Ayuda! ÂżQuĂ© es una InfoCard?', + 'fr' => '', + 'nl' => '', + 'lb' => '', + 'sl' => '', + 'hr' => '', + 'hu' => '', + 'pt' => '', + 'pt-BR' => '', + ), + 'help_text' => array ( + 'no' => '', + 'nn' => '', + 'da' => '', + 'en' => 'Information Cards (aka InfoCard) is a web authentication technology. Contact with your services provider in order to configure your computer and gives you and Information Card (identification virtual card).', + 'de' => '', + 'sv' => '', + 'fi' => '', + 'es' => 'Information Cards (alias Infocard) es una tecnologĂa de autenticaciĂłn web. Consulte con su proveedor de servicios para que le ayude a configurar su máquina y le expida una Information Card (tarjeta virtual de identificaciĂłn).', + 'fr' => '', + 'nl' => '', + 'lb' => '', + 'sl' => '', + 'hr' => '', + 'hu' => '', + 'pt' => '', + 'pt-BR' => '', + ), + 'help_desk_link' => array ( + 'no' => 'Hjemmesiden til brukerstøtte', + 'nn' => 'Heimeside for brukarstøtte', + 'da' => 'Servicedesk', + 'en' => 'Help desk homepage', + 'de' => 'Seite des Helpdesk', + 'sv' => 'Hemsida för helpdesk', + 'es' => 'Página de soporte tĂ©cnico', + 'nl' => 'Helpdesk homepage', + 'sl' => 'Spletna stran tehniÄŤne podpore uporabnikom.', + 'hr' => 'Helpdesk stranice', + 'hu' => 'ĂśgyfĂ©lszolgálat weboldala', + 'pt' => 'Página do serviço de apoio ao utilizador', + 'pt-BR' => 'Central de Ajuda', + ), + 'help_desk_email' => array ( + 'no' => 'Send e-post til brukerstøtte', + 'nn' => 'Send epost til brukarstøtte', + 'da' => 'Send en e-mail servicedesk', + 'en' => 'Send e-mail to help desk', + 'de' => 'Email an den Helpdesk senden', + 'sv' => 'Skicka e-post till helpdesk', + 'es' => 'Enviar correo electrĂłnico al soporte tĂ©cnico', + 'nl' => 'Stuur een e-mail naar de helpdesk', + 'sl' => 'Pošlji e-poštno sporoÄŤilo tehniÄŤni podpori.', + 'hr' => 'Pošaljite e-mail helpdesk sluĹľbi', + 'hu' => 'KĂĽldjön e-mailt az ĂĽgyfĂ©lszolgálatnak', + 'pt' => 'Enviar um e-mail para o serviço de apoio ao utilizador', + 'pt-BR' => 'Envie um e-mail para a Central de Ajuda.', + ), + 'contact_info' => array ( + 'no' => 'Kontaktinformasjon:', + 'nn' => 'Kontaktinformasjon:', + 'da' => 'Kontaktoplysninger', + 'en' => 'Contact information:', + 'de' => 'Kontakt', + 'sv' => 'Kontaktinformation:', + 'es' => 'InformaciĂłn de contacto:', + 'nl' => 'Contact informatie', + 'sl' => 'Kontakt', + 'hr' => 'Kontakt podaci', + 'hu' => 'ElĂ©rĂ©si informáciĂłk', + 'pt' => 'Contactos:', + 'pt-BR' => 'Informações de Contato', + ), + +); + + +?> \ No newline at end of file diff --git a/modules/InfoCard/lib/Auth/Source/ICAuth.php b/modules/InfoCard/lib/Auth/Source/ICAuth.php new file mode 100644 index 000000000..b16167bd6 --- /dev/null +++ b/modules/InfoCard/lib/Auth/Source/ICAuth.php @@ -0,0 +1,80 @@ +<?php +/* +* AUTHOR: Samuel Muñoz Hidalgo +* EMAIL: samuel.mh@gmail.com +* LAST REVISION: 1-DEC-08 +* DESCRIPTION: +* 'login-infocard' module. +* Auth class +*/ + +class sspmod_InfoCard_Auth_Source_ICAuth extends SimpleSAML_Auth_Source { + + //The string used to identify our states. + const STAGEID = 'sspmod_core_Auth_UserPassBase.state'; + + + //The key of the AuthId field in the state. + const AUTHID = 'sspmod_core_Auth_UserPassBase.AuthId'; + + + public function __construct($info, $config) { + assert('is_array($info)'); + assert('is_array($config)'); + + /* Call the parent constructor first, as required by the interface. */ + parent::__construct($info, $config); + } + + + public function authenticate(&$state) { + assert('is_array($state)'); + + /* We are going to need the authId in order to retrieve this authentication source later. */ + $state[self::AUTHID] = $this->authId; + $id = SimpleSAML_Auth_State::saveState($state, self::STAGEID); + $url = SimpleSAML_Module::getModuleURL('InfoCard/login-infocard.php'); + SimpleSAML_Utilities::redirect($url, array('AuthState' => $id)); + } + + + public static function handleLogin($authStateId, $xmlToken) { + assert('is_string($authStateId)'); + + /* Retrieve the authentication state. */ + $state = SimpleSAML_Auth_State::loadState($authStateId, self::STAGEID); + + /* Find authentication source. */ + assert('array_key_exists(self::AUTHID, $state)'); + $source = SimpleSAML_Auth_Source::getById($state[self::AUTHID]); + if ($source === NULL) { + throw new Exception('Could not find authentication source with id ' . $state[self::AUTHID]); + } + + $config = SimpleSAML_Configuration::getInstance(); + $autoconfig = $config->copyFromBase('logininfocard', 'config-login-infocard.php'); + $server_key = $autoconfig->getValue('server_key'); + $server_crt = $autoconfig->getValue('server_crt'); + $Infocard = $autoconfig->getValue('InfoCard'); + + $infocard = new sspmod_InfoCard_RP_InfoCard(); + $infocard->addCertificatePair($server_key,$server_crt); + $claims = $infocard->process($xmlToken); + if($claims->isValid()) { + $attributes = array(); + foreach ($Infocard['requiredClaims'] as $claim => $data){ + $attributes[$claim] = array($claims->$claim); + } + foreach ($Infocard['optionalClaims'] as $claim => $data){ + $attributes[$claim] = array($claims->$claim); + } + $state['Attributes'] = $attributes; + SimpleSAML_Auth_Source::completeAuth($state); + } else { + return 'wrong_IC'; + } + } + +} + +?> \ No newline at end of file diff --git a/modules/InfoCard/lib/RP/InfoCard.php b/modules/InfoCard/lib/RP/InfoCard.php new file mode 100644 index 000000000..442629195 --- /dev/null +++ b/modules/InfoCard/lib/RP/InfoCard.php @@ -0,0 +1,311 @@ +<?php + +require_once 'Zend_InfoCard_Claims.php'; + +class sspmod_InfoCard_RP_InfoCard +{ + + const XENC_NS = "http://www.w3.org/2001/04/xmlenc#"; + const XENC_ELEMENT_TYPE = "http://www.w3.org/2001/04/xmlenc#Element"; + const XENC_ENC_ALGO = "http://www.w3.org/2001/04/xmlenc#aes256-cbc"; + const XENC_KEYINFO_ENC_ALGO = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"; + + const DSIG_NS = "http://www.w3.org/2000/09/xmldsig#"; + const DSIG_RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + const DSIG_ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"; + const DSIG_SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"; + + const CANON_EXCLUSIVE = "http://www.w3.org/2001/10/xml-exc-c14n#"; + + const WSSE_NS = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"; + const WSSE_KEYID_VALUE_TYPE = "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#ThumbprintSHA1"; + + const XMLSOAP_SELF_ISSUED = "http://schemas.xmlsoap.org/ws/2005/05/identity/issuer/self"; + + const XMLSOAP_CLAIMS_NS = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims'; + + const SAML_ASSERTION_1_0_NS = "urn:oasis:names:tc:SAML:1.0:assertion"; + const SAML_ASSERTION_1_1_NS = "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1"; + + protected $_private_key_file; + protected $_public_key_file; + protected $_password; + protected $_sxml; + + public function __construct() { + if(!extension_loaded('mcrypt')) { + SimpleSAML_Logger::debug("Use of the InfoCard component requires the mcrypt extension to be enabled in PHP"); + } + + if(!extension_loaded('openssl')) { + SimpleSAML_Logger::debug("Use of the InfoCard component requires the openssl extension to be enabled in PHP"); + } + } + + public function addCertificatePair($private_key_file, $public_key_file, $password = null) { + $this->_private_key_file = $private_key_file; + $this->_public_key_file = $public_key_file; + $this->_password = $password; + + if(!file_exists($this->_private_key_file)) { + SimpleSAML_Logger::debug("Private key file does not exists"); + } + + if(!file_exists($this->_public_key_file)) { + SimpleSAML_Logger::debug("Public key file does not exists"); + } + + if(!is_readable($this->_private_key_file)) { + SimpleSAML_Logger::debug("Private key file is not readable"); + } + + if(!is_readable($this->_public_key_file)) { + SimpleSAML_Logger::debug("Public key file is not readable"); + } + } + + public function process($xmlToken) { + if(strpos($xmlToken, "EncryptedData") === false ) { + return self::processUnSecureToken($xmlToken); + } + else { + return self::processSecureToken($xmlToken); + } + } + + private function processSecureToken($xmlToken) { + $retval = new Zend_InfoCard_Claims(); + + try { + $this->_sxml = simplexml_load_string($xmlToken); + $decryptedToken = self::decryptToken($xmlToken); + } + catch(Exception $e) { + $retval->setError('Failed to extract assertion document'); + SimpleSAML_Logger::debug('Failed to extract assertion document'); + $retval->setCode(Zend_InfoCard_Claims::RESULT_PROCESSING_FAILURE); + return $retval; + } + + try { + $assertions = self::getAssertions($decryptedToken); + } + catch(Exception $e) { + $retval->setError('Failure processing assertion document'); + SimpleSAML_Logger::debug('Failure processing assertion document'); + $retval->setCode(Zend_InfoCard_Claims::RESULT_PROCESSING_FAILURE); + return $retval; + } + + try { + $reference_id = self::ValidateSignature($assertions); + self::checkConditions($reference_id, $assertions); + } + catch(Exception $e) { + $retval->setError($e->getMessage()); + $retval->setCode(Zend_InfoCard_Claims::RESULT_VALIDATION_FAILURE); + return $retval; + } + + return self::getClaims($retval, $assertions); + } + + private function processUnsecureToken($xmlToken) { + $retval = new Zend_InfoCard_Claims(); + + try { + $assertions = self::getAssertions($xmlToken); + } + catch(Exception $e) { + $retval->setError('Failure processing assertion document'); + SimpleSAML_Logger::debug('Failure processing assertion document'); + $retval->setCode(Zend_InfoCard_Claims::RESULT_PROCESSING_FAILURE); + return $retval; + } + + return self::getClaims($retval, $assertions); + } + + private function ValidateSignature($assertions) { + include_once 'Zend_InfoCard_Xml_Security.php'; + $reference_id = Zend_InfoCard_Xml_Security::validateXMLSignature($assertions->asXML()); + if(!$reference_id) { + SimpleSAML_Logger::debug("Failure Validating the Signature of the assertion document"); + } + + return $reference_id; + } + + private function checkConditions($reference_id, $assertions) { + if($reference_id[0] == '#') { + $reference_id = substr($reference_id, 1); + } else { + SimpleSAML_Logger::debug("Reference of document signature does not reference the local document"); + } + + if($reference_id != $assertions->getAssertionID()) { + SimpleSAML_Logger::debug("Reference of document signature does not reference the local document"); + } + + $conditions = $assertions->getConditions(); + if(is_array($condition_error = $assertions->validateConditions($conditions))) { + SimpleSAML_Logger::debug("Conditions of assertion document are not met: {$condition_error[1]} ({$condition_error[0]})"); + } + } + + private function getClaims($retval, $assertions) { + $attributes = $assertions->getAttributes(); + $retval->setClaims($attributes); + if($retval->getCode() == 0) { + $retval->setCode(Zend_InfoCard_Claims::RESULT_SUCCESS); + } + + return $retval; + } + + private function getAssertions($strXmlData) { + $sxe = simplexml_load_string($strXmlData); + $namespaces = $sxe->getDocNameSpaces(); + foreach($namespaces as $namespace) { + switch($namespace) { + case self::SAML_ASSERTION_1_0_NS: + include_once 'Zend_InfoCard_Xml_Assertion_Saml.php'; + return simplexml_load_string($strXmlData, 'Zend_InfoCard_Xml_Assertion_Saml', null); + } + } + + SimpleSAML_Logger::debug("Unable to determine Assertion type by Namespace"); + } + + private function decryptToken($xmlToken) { + if($this->_sxml['Type'] != self::XENC_ELEMENT_TYPE) { + SimpleSAML_Logger::debug("Unknown EncryptedData type found"); + } + + $this->_sxml->registerXPathNamespace('enc', self::XENC_NS); + list($encryptionMethod) = $this->_sxml->xpath("//enc:EncryptionMethod"); + if(!$encryptionMethod instanceof SimpleXMLElement) { + SimpleSAML_Logger::debug("EncryptionMethod node not found"); + } + + $encMethodDom = dom_import_simplexml($encryptionMethod); + if(!$encMethodDom instanceof DOMElement) { + SimpleSAML_Logger::debug("Failed to create DOM from EncryptionMethod node"); + } + + if(!$encMethodDom->hasAttribute("Algorithm")) { + SimpleSAML_Logger::debug("Unable to determine the encryption algorithm in the Symmetric enc:EncryptionMethod XML block"); + } + + $algo = $encMethodDom->getAttribute("Algorithm"); + if($algo != self::XENC_ENC_ALGO) { + SimpleSAML_Logger::debug("Unsupported encryption algorithm"); + } + + $this->_sxml->registerXPathNamespace('ds', self::DSIG_NS); + list($keyInfo) = $this->_sxml->xpath("ds:KeyInfo"); + if(!$keyInfo instanceof SimpleXMLElement) { + SimpleSAML_Logger::debug("KeyInfo node not found"); + } + + $keyInfo->registerXPathNamespace('enc', self::XENC_NS); + list($encryptedKey) = $keyInfo->xpath("enc:EncryptedKey"); + if(!$encryptedKey instanceof SimpleXMLElement) { + SimpleSAML_Logger::debug("EncryptedKey element not found in KeyInfo"); + } + + $encryptedKey->registerXPathNamespace('enc', self::XENC_NS); + list($keyInfoEncryptionMethod) = $encryptedKey->xpath("enc:EncryptionMethod"); + if(!$keyInfoEncryptionMethod instanceof SimpleXMLElement) { + SimpleSAML_Logger::debug("EncryptionMethod element not found in EncryptedKey"); + } + + $keyInfoEncMethodDom = dom_import_simplexml($keyInfoEncryptionMethod); + if(!$keyInfoEncMethodDom instanceof DOMElement) { + SimpleSAML_Logger::debug("Failed to create DOM from EncryptionMethod node"); + } + + if(!$keyInfoEncMethodDom->hasAttribute("Algorithm")) { + SimpleSAML_Logger::debug("Unable to determine the encryption algorithm in the Symmetric enc:EncryptionMethod XML block"); + } + + $keyInfoEncMethodAlgo = $keyInfoEncMethodDom->getAttribute("Algorithm"); + if($keyInfoEncMethodAlgo != self::XENC_KEYINFO_ENC_ALGO) { + SimpleSAML_Logger::debug("Unsupported encryption algorithm"); + } + + $encryptedKey->registerXPathNamespace('ds', self::DSIG_NS); + $encryptedKey->registerXPathNamespace('wsse', self::WSSE_NS); + list($keyIdentifier) = $encryptedKey->xpath("ds:KeyInfo/wsse:SecurityTokenReference/wsse:KeyIdentifier"); + if(!$keyIdentifier instanceof SimpleXMLElement) { + SimpleSAML_Logger::debug("KeyInfo/SecurityTokenReference/KeyIdentifier node not found in KeyInfo"); + } + + $keyIdDom = dom_import_simplexml($keyIdentifier); + if(!$keyIdDom instanceof DOMElement) { + SimpleSAML_Logger::debug("Failed to create DOM from KeyIdentifier node"); + } + + if(!$keyIdDom->hasAttribute("ValueType")) { + SimpleSAML_Logger::debug("Unable to determine ValueType of KeyIdentifier"); + } + + $valueType = $keyIdDom->getAttribute("ValueType"); + if($valueType != self::WSSE_KEYID_VALUE_TYPE) { + SimpleSAML_Logger::debug("Unsupported KeyIdentifier ValueType"); + } + + list($cipherValue) = $encryptedKey->xpath("enc:CipherData/enc:CipherValue"); + if(!$cipherValue instanceof SimpleXMLElement) { + SimpleSAML_Logger::debug("CipherValue node found in EncryptedKey"); + } + + $base64DecodeSupportsStrictParam = version_compare(PHP_VERSION, '5.2.0', '>='); + + if ($base64DecodeSupportsStrictParam) { + $keyCipherValueBase64Decoded = base64_decode($cipherValue, true); + } else { + $keyCipherValueBase64Decoded = base64_decode($cipherValue); + } + + $private_key = openssl_pkey_get_private(array(file_get_contents($this->_private_key_file), $this->_password)); + if(!$private_key) { + SimpleSAML_Logger::debug("Unable to load private key"); + } + + $result = openssl_private_decrypt($keyCipherValueBase64Decoded, $symmetricKey, $private_key, OPENSSL_PKCS1_OAEP_PADDING); + openssl_free_key($private_key); + + if(!$result) { + SimpleSAML_Logger::debug("Unable to decrypt symmetric key"); + } + + list($cipherValue) = $this->_sxml->xpath("enc:CipherData/enc:CipherValue"); + if(!$cipherValue instanceof SimpleXMLElement) { + SimpleSAML_Logger::debug("CipherValue node found in EncryptedData"); + } + + if ($base64DecodeSupportsStrictParam) { + $keyCipherValueBase64Decoded = base64_decode($cipherValue, true); + } else { + $keyCipherValueBase64Decoded = base64_decode($cipherValue); + } + + $mcrypt_iv = substr($keyCipherValueBase64Decoded, 0, 16); + $keyCipherValueBase64Decoded = substr($keyCipherValueBase64Decoded, 16); + $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $symmetricKey, $keyCipherValueBase64Decoded, MCRYPT_MODE_CBC, $mcrypt_iv); + + if(!$decrypted) { + SimpleSAML_Logger::debug("Unable to decrypt token"); + } + + $decryptedLength = strlen($decrypted); + $paddingLength = substr($decrypted, $decryptedLength -1, 1); + $decrypted = substr($decrypted, 0, $decryptedLength - ord($paddingLength)); + $decrypted = rtrim($decrypted, "\0"); + + return $decrypted; + } +} + +?> diff --git a/modules/InfoCard/lib/RP/LICENSE.txt b/modules/InfoCard/lib/RP/LICENSE.txt new file mode 100644 index 000000000..77a910f6a --- /dev/null +++ b/modules/InfoCard/lib/RP/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2008, Zend Technologies USA, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Zend Technologies USA, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/modules/InfoCard/lib/RP/Zend_InfoCard_Claims.php b/modules/InfoCard/lib/RP/Zend_InfoCard_Claims.php new file mode 100644 index 000000000..321d1b149 --- /dev/null +++ b/modules/InfoCard/lib/RP/Zend_InfoCard_Claims.php @@ -0,0 +1,304 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_InfoCard + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id: Claims.php 9094 2008-03-30 18:36:55Z thomas $ + */ + +/** + * Result value of the InfoCard component, contains any error messages and claims + * from the processing of an information card. + * + * @category Zend + * @package Zend_InfoCard + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_InfoCard_Claims +{ + /** + * Successful validation and extraion of claims + */ + const RESULT_SUCCESS = 1; + + /** + * Indicates there was an error processing the XML document + */ + const RESULT_PROCESSING_FAILURE = 2; + + /** + * Indicates that the signature values within the XML document failed verification + */ + const RESULT_VALIDATION_FAILURE = 3; + + /** + * The default namespace to assume in these claims + * + * @var string + */ + protected $_defaultNamespace = null; + + /** + * A boolean indicating if the claims should be consider "valid" or not based on processing + * + * @var bool + */ + protected $_isValid = true; + + /** + * The error message if any + * + * @var string + */ + protected $_error = ""; + + /** + * An array of claims taken from the information card + * + * @var array + */ + protected $_claims; + + /** + * The result code of processing the information card as defined by the constants of this class + * + * @var integer + */ + protected $_code; + + /** + * Override for the safeguard which ensures that you don't use claims which failed validation. + * Used in situations when there was a validation error you'd like to ignore + * + * @return Zend_InfoCard_Claims + */ + public function forceValid() + { + trigger_error("Forcing Claims to be valid although it is a security risk", E_USER_WARNING); + $this->_isValid = true; + return $this; + } + + /** + * Retrieve the PPI (Private Personal Identifier) associated with the information card + * + * @return string the private personal identifier + */ + public function getCardID() + { + return $this->getClaim('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier'); + } + + /** + * Retrieves the default namespace used in this information card. If a default namespace was not + * set, it figures out which one to consider 'default' by taking the first namespace sorted by use-count + * in claims + * + * @throws Exception + * @return string The default namespace + */ + public function getDefaultNamespace() + { + + if(is_null($this->_defaultNamespace)) { + + $namespaces = array(); + $leader = ''; + foreach($this->_claims as $claim) { + + if(!isset($namespaces[$claim['namespace']])) { + $namespaces[$claim['namespace']] = 1; + } else { + $namespaces[$claim['namespace']]++; + } + + if(empty($leader) || ($namespaces[$claim['namespace']] > $leader)) { + $leader = $claim['namespace']; + } + } + + if(empty($leader)) { + throw new Exception("Failed to determine default namespace"); + } + + $this->setDefaultNamespace($leader); + } + + return $this->_defaultNamespace; + } + + /** + * Set the default namespace, overriding any existing default + * + * @throws Exception + * @param string $namespace The default namespace to use + * @return Zend_InfoCard_Claims + */ + public function setDefaultNamespace($namespace) + { + + foreach($this->_claims as $claim) { + if($namespace == $claim['namespace']) { + $this->_defaultNamespace = $namespace; + return $this; + } + } + + throw new Exception("At least one claim must exist in specified namespace to make it the default namespace"); + } + + /** + * Indicates if this claim object contains validated claims or not + * + * @return bool + */ + public function isValid() + { + return $this->_isValid; + } + + /** + * Set the error message contained within the claims object + * + * @param string $error The error message + * @return Zend_InfoCard_Claims + */ + public function setError($error) + { + $this->_error = $error; + $this->_isValid = false; + return $this; + } + + /** + * Retrieve the error message contained within the claims object + * + * @return string The error message + */ + public function getErrorMsg() + { + return $this->_error; + } + + /** + * Set the claims for the claims object. Can only be set once and is done + * by the component itself. Internal use only. + * + * @throws Exception + * @param array $claims + * @return Zend_InfoCard_Claims + */ + public function setClaims(Array $claims) + { + if(!is_null($this->_claims)) { + throw new Exception("Claim objects are read-only"); + } + + $this->_claims = $claims; + return $this; + } + + /** + * Set the result code of the claims object. + * + * @throws Exception + * @param int $code The result code + * @return Zend_InfoCard_Claims + */ + public function setCode($code) + { + switch($code) { + case self::RESULT_PROCESSING_FAILURE: + case self::RESULT_SUCCESS: + case self::RESULT_VALIDATION_FAILURE: + $this->_code = $code; + return $this; + } + + throw new Exception("Attempted to set unknown error code"); + } + + /** + * Gets the result code of the claims object + * + * @return integer The result code + */ + public function getCode() + { + return $this->_code; + } + + /** + * Get a claim by providing its complete claim URI + * + * @param string $claimURI The complete claim URI to retrieve + * @return mixed The claim matching that specific URI or null if not found + */ + public function getClaim($claimURI) + { + if($this->claimExists($claimURI)) { + return $this->_claims[$claimURI]['value']; + } + + return null; + } + + /** + * Indicates if a specific claim URI exists or not within the object + * + * @param string $claimURI The complete claim URI to check + * @return bool true if the claim exists, false if not found + */ + public function claimExists($claimURI) + { + return isset($this->_claims[$claimURI]); + } + + /** + * Magic helper function + * @throws Exception + */ + public function __unset($k) + { + throw new Exception("Claim objects are read-only"); + } + + /** + * Magic helper function + */ + public function __isset($k) + { + return $this->claimExists("{$this->getDefaultNamespace()}/$k"); + } + + /** + * Magic helper function + */ + public function __get($k) + { + return $this->getClaim("{$this->getDefaultNamespace()}/$k"); + } + + /** + * Magic helper function + * @throws Exception + */ + public function __set($k, $v) + { + throw new Exception("Claim objects are read-only"); + } +} diff --git a/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Assertion_Saml.php b/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Assertion_Saml.php new file mode 100644 index 000000000..b82d63603 --- /dev/null +++ b/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Assertion_Saml.php @@ -0,0 +1,272 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_InfoCard + * @subpackage Zend_InfoCard_Xml + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id: Saml.php 9094 2008-03-30 18:36:55Z thomas $ + */ + +/** + * A Xml Assertion Document in SAML Token format + * + * @category Zend + * @package Zend_InfoCard + * @subpackage Zend_InfoCard_Xml + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_InfoCard_Xml_Assertion_Saml extends SimpleXMLElement +{ + + /** + * Audience Restriction Condition + */ + const CONDITION_AUDIENCE = 'AudienceRestrictionCondition'; + + /** + * The URI for a 'bearer' confirmation + */ + const CONFIRMATION_BEARER = 'urn:oasis:names:tc:SAML:1.0:cm:bearer'; + + /** + * The amount of time in seconds to buffer when checking conditions to ensure + * that differences between client/server clocks don't interfer too much + */ + const CONDITION_TIME_ADJ = 3600; // +- 5 minutes + + protected function _getServerName() { + return $_SERVER['SERVER_NAME']; + } + + protected function _getServerPort() { + return $_SERVER['SERVER_PORT']; + } + + /** + * Validate the conditions array returned from the getConditions() call + * + * @param array $conditions An array of condtions for the assertion taken from getConditions() + * @return mixed Boolean true on success, an array of condition, error message on failure + */ + public function validateConditions(Array $conditions) + { + + $currentTime = time(); + + if(!empty($conditions)) { + + foreach($conditions as $condition => $conditionValue) { + switch(strtolower($condition)) { + case 'audiencerestrictioncondition': + + $serverName = $this->_getServerName(); + $serverPort = $this->_getServerPort(); + + $self_aliases[] = $serverName; + $self_aliases[] = "{{$serverName}:{$serverPort}"; + + $found = false; + if(is_array($conditionValue)) { + foreach($conditionValue as $audience) { + + list(,,$audience) = explode('/', $audience); + if(in_array($audience, $self_aliases)) { + $found = true; + break; + } + } + } + + if(!$found) { + return array($condition, 'Could not find self in allowed audience list'); + } + + break; + case 'notbefore': + $notbeforetime = strtotime($conditionValue); + + if($currentTime < $notbeforetime) { + if($currentTime + self::CONDITION_TIME_ADJ < $notbeforetime) { + return array($condition, 'Current time is before specified window'); + } + } + + break; + case 'notonorafter': + $notonoraftertime = strtotime($conditionValue); + + if($currentTime >= $notonoraftertime) { + if($currentTime - self::CONDITION_TIME_ADJ >= $notonoraftertime) { + return array($condition, 'Current time is after specified window'); + } + } + + break; + + } + } + } + return true; + } + + /** + * Get the Assertion URI for this type of Assertion + * + * @return string the Assertion URI + */ + public function getAssertionURI() + { + return Zend_InfoCard_Xml_Assertion::TYPE_SAML; + } + + /** + * Get the Major Version of the SAML Assertion + * + * @return integer The major version number + */ + public function getMajorVersion() + { + return (int)(string)$this['MajorVersion']; + } + + /** + * The Minor Version of the SAML Assertion + * + * @return integer The minor version number + */ + public function getMinorVersion() + { + return (int)(string)$this['MinorVersion']; + } + + /** + * Get the Assertion ID of the assertion + * + * @return string The Assertion ID + */ + public function getAssertionID() + { + return (string)$this['AssertionID']; + } + + /** + * Get the Issuer URI of the assertion + * + * @return string the URI of the assertion Issuer + */ + public function getIssuer() + { + return (string)$this['Issuer']; + } + + /** + * Get the Timestamp of when the assertion was issued + * + * @return integer a UNIX timestamp representing when the assertion was issued + */ + public function getIssuedTimestamp() + { + return strtotime((string)$this['IssueInstant']); + } + + /** + * Return an array of conditions which the assertions are predicated on + * + * @throws Exception + * @return array an array of conditions + */ + public function getConditions() + { + + list($conditions) = $this->xpath("//saml:Conditions"); + + if(!($conditions instanceof SimpleXMLElement)) { + throw new Exception("Unable to find the saml:Conditions block"); + } + + $retval = array(); + + foreach($conditions->children('urn:oasis:names:tc:SAML:1.0:assertion') as $key => $value) { + switch($key) { + case self::CONDITION_AUDIENCE: + foreach($value->children('urn:oasis:names:tc:SAML:1.0:assertion') as $audience_key => $audience_value) { + if($audience_key == 'Audience') { + $retval[$key][] = (string)$audience_value; + } + } + break; + } + } + + $retval['NotBefore'] = (string)$conditions['NotBefore']; + $retval['NotOnOrAfter'] = (string)$conditions['NotOnOrAfter']; + + return $retval; + } + + /** + * Get they KeyInfo element for the Subject KeyInfo block + * + * @todo Not Yet Implemented + * @ignore + */ + public function getSubjectKeyInfo() + { + /** + * @todo Not sure if this is part of the scope for now.. + */ + + if($this->getConfirmationMethod() == self::CONFIRMATION_BEARER) { + throw new Exception("Cannot get Subject Key Info when Confirmation Method was Bearer"); + } + } + + /** + * Return the Confirmation Method URI used in the Assertion + * + * @return string The confirmation method URI + */ + public function getConfirmationMethod() + { + list($confirmation) = $this->xPath("//saml:ConfirmationMethod"); + return (string)$confirmation; + } + + /** + * Return an array of attributes (claims) contained within the assertion + * + * @return array An array of attributes / claims within the assertion + */ + public function getAttributes() + { + $attributes = $this->xPath('//saml:Attribute'); + + $retval = array(); + foreach($attributes as $key => $value) { + + $retkey = (string)$value['AttributeNamespace'].'/'.(string)$value['AttributeName']; + + $retval[$retkey]['name'] = (string)$value['AttributeName']; + $retval[$retkey]['namespace'] = (string)$value['AttributeNamespace']; + + list($aValue) = $value->children('urn:oasis:names:tc:SAML:1.0:assertion'); + $retval[$retkey]['value'] = (string)$aValue; + } + + return $retval; + } +} diff --git a/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security.php b/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security.php new file mode 100644 index 000000000..da4ef9f79 --- /dev/null +++ b/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security.php @@ -0,0 +1,302 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_InfoCard + * @subpackage Zend_InfoCard_Xml_Security + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id: Security.php 9094 2008-03-30 18:36:55Z thomas $ + */ + +/** + * Zend_InfoCard_Xml_Security_Transform + */ +require_once 'Zend_InfoCard_Xml_Security_Transform.php'; + +/** + * + * @category Zend + * @package Zend_InfoCard + * @subpackage Zend_InfoCard_Xml_Security + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_InfoCard_Xml_Security +{ + /** + * ASN.1 type INTEGER class + */ + const ASN_TYPE_INTEGER = 0x02; + + /** + * ASN.1 type BIT STRING class + */ + const ASN_TYPE_BITSTRING = 0x03; + + /** + * ASN.1 type SEQUENCE class + */ + const ASN_TYPE_SEQUENCE = 0x30; + + /** + * The URI for Canonical Method C14N Exclusive + */ + const CANONICAL_METHOD_C14N_EXC = 'http://www.w3.org/2001/10/xml-exc-c14n#'; + + /** + * The URI for Signature Method SHA1 + */ + const SIGNATURE_METHOD_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; + + /** + * The URI for Digest Method SHA1 + */ + const DIGEST_METHOD_SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'; + + /** + * The Identifier for RSA Keys + */ + const RSA_KEY_IDENTIFIER = '300D06092A864886F70D0101010500'; + + /** + * Constructor (disabled) + * + * @return void + */ + private function __construct() + { + } + + /** + * Validates the signature of a provided XML block + * + * @param string $strXMLInput An XML block containing a Signature + * @return bool True if the signature validated, false otherwise + * @throws Exception + */ + static public function validateXMLSignature($strXMLInput) + { + if(!extension_loaded('openssl')) { + SimpleSAML_Logger::debug("You must have the openssl extension installed to use this class"); + } + + $sxe = simplexml_load_string($strXMLInput); + + $sxe->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); + + list($canonMethod) = $sxe->xpath("//ds:Signature/ds:SignedInfo/ds:CanonicalizationMethod"); + switch((string)$canonMethod['Algorithm']) { + case self::CANONICAL_METHOD_C14N_EXC: + $cMethod = (string)$canonMethod['Algorithm']; + break; + default: + SimpleSAML_Logger::debug("Unknown or unsupported CanonicalizationMethod Requested"); + } + + list($signatureMethod) = $sxe->xpath("//ds:Signature/ds:SignedInfo/ds:SignatureMethod"); + switch((string)$signatureMethod['Algorithm']) { + case self::SIGNATURE_METHOD_SHA1: + $sMethod = (string)$signatureMethod['Algorithm']; + break; + default: + SimpleSAML_Logger::debug("Unknown or unsupported SignatureMethod Requested"); + } + + list($digestMethod) = $sxe->xpath("//ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestMethod"); + switch((string)$digestMethod['Algorithm']) { + case self::DIGEST_METHOD_SHA1: + $dMethod = (string)$digestMethod['Algorithm']; + break; + default: + SimpleSAML_Logger::debug("Unknown or unsupported DigestMethod Requested"); + } + + $base64DecodeSupportsStrictParam = version_compare(PHP_VERSION, '5.2.0', '>='); + + list($digestValue) = $sxe->xpath("//ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestValue"); + if ($base64DecodeSupportsStrictParam) { + $dValue = base64_decode((string)$digestValue, true); + } else { + $dValue = base64_decode((string)$digestValue); + } + + list($signatureValueElem) = $sxe->xpath("//ds:Signature/ds:SignatureValue"); + if ($base64DecodeSupportsStrictParam) { + $signatureValue = base64_decode((string)$signatureValueElem, true); + } else { + $signatureValue = base64_decode((string)$signatureValueElem); + } + + $transformer = new Zend_InfoCard_Xml_Security_Transform(); + + //need to fix this later + $transforms = $sxe->xpath("//ds:Signature/ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform"); + while(list( , $transform) = each($transforms)) { + $transformer->addTransform((string)$transform['Algorithm']); + } + + $transformed_xml = $transformer->applyTransforms($strXMLInput); + + //$transformed_xml_binhash = pack("H*", sha1($transformed_xml)); + $transformed_xml_binhash = pack("H*", sha1($transformed_xml)); + + if($transformed_xml_binhash != $dValue) { + SimpleSAML_Logger::debug("Locally Transformed XML (".$transformed_xml_binhash.") does not match XML Document (".$dValue."). Cannot Verify Signature"); + } + + $public_key = null; + + + $sxe->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); + list($keyValue) = $sxe->xpath("//ds:Signature/ds:KeyInfo"); + + $keyValue->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); + + list($x509cert) = $keyValue->xpath("ds:X509Data/ds:X509Certificate"); + list($rsaKeyValue) = $keyValue->xpath("ds:KeyValue/ds:RSAKeyValue"); + + switch(true) { + case isset($x509cert): + + $certificate = (string)$x509cert; + + + $pem = "-----BEGIN CERTIFICATE-----\n" . + wordwrap($certificate, 64, "\n", true) . + "\n-----END CERTIFICATE-----"; + + $public_key = openssl_pkey_get_public($pem); + + if(!$public_key) { + SimpleSAML_Logger::debug("Unable to extract and prcoess X509 Certificate from KeyValue"); + } + + break; + case isset($rsaKeyValue): + + $rsaKeyValue->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); + list($modulus) = $rsaKeyValue->xpath("ds:Modulus"); + list($exponent) = $rsaKeyValue->xpath("ds:Exponent"); + if(!isset($modulus) || + !isset($exponent)) { + SimpleSAML_Logger::debug("RSA Key Value not in Modulus/Exponent form"); + } + + $modulus = base64_decode((string)$modulus); + $exponent = base64_decode((string)$exponent); + + $pem_public_key = self::_getPublicKeyFromModExp($modulus, $exponent); + + $public_key = openssl_pkey_get_public ($pem_public_key); + + break; + default: + SimpleSAML_Logger::debug("Unable to determine or unsupported representation of the KeyValue block"); + } + + $transformer = new Zend_InfoCard_Xml_Security_Transform(); + $transformer->addTransform((string)$canonMethod['Algorithm']); + + list($signedInfo) = $sxe->xpath("//ds:Signature/ds:SignedInfo"); + $signedInfoXML = self::addNamespace($signedInfo, "http://www.w3.org/2000/09/xmldsig#"); + + $canonical_signedinfo = $transformer->applyTransforms($signedInfoXML); + + if(openssl_verify($canonical_signedinfo, $signatureValue, $public_key)) { + list($reference) = $sxe->xpath("//ds:Signature/ds:SignedInfo/ds:Reference"); + return (string)$reference['URI']; + } + + return false; + } + + private function addNamespace($xmlElem, $ns) { + $xmlElem->addAttribute('DS_NS', $ns); + $xml = $xmlElem->asXML(); + if(preg_match("/<(\w+)\:\w+/", $xml, $matches)) { + $prefix = $matches[1]; + $xml = str_replace("DS_NS", "xmlns:" . $prefix, $xml); + } + else { + $xml = str_replace("DS_NS", "xmlns", $xml); + } + + return $xml; + } + + /** + * Transform an RSA Key in Modulus/Exponent format into a PEM encoding and + * return an openssl resource for it + * + * @param string $modulus The RSA Modulus in binary format + * @param string $exponent The RSA exponent in binary format + * @return string The PEM encoded version of the key + */ + static protected function _getPublicKeyFromModExp($modulus, $exponent) + { + $modulusInteger = self::_encodeValue($modulus, self::ASN_TYPE_INTEGER); + $exponentInteger = self::_encodeValue($exponent, self::ASN_TYPE_INTEGER); + $modExpSequence = self::_encodeValue($modulusInteger . $exponentInteger, self::ASN_TYPE_SEQUENCE); + $modExpBitString = self::_encodeValue($modExpSequence, self::ASN_TYPE_BITSTRING); + + $binRsaKeyIdentifier = pack( "H*", self::RSA_KEY_IDENTIFIER ); + + $publicKeySequence = self::_encodeValue($binRsaKeyIdentifier . $modExpBitString, self::ASN_TYPE_SEQUENCE); + + $publicKeyInfoBase64 = base64_encode( $publicKeySequence ); + + $publicKeyString = "-----BEGIN PUBLIC KEY-----\n"; + $publicKeyString .= wordwrap($publicKeyInfoBase64, 64, "\n", true); + $publicKeyString .= "\n-----END PUBLIC KEY-----\n"; + + return $publicKeyString; + } + + /** + * Encode a limited set of data types into ASN.1 encoding format + * which is used in X.509 certificates + * + * @param string $data The data to encode + * @param const $type The encoding format constant + * @return string The encoded value + * @throws Exception + */ + static protected function _encodeValue($data, $type) + { + // Null pad some data when we get it (integer values > 128 and bitstrings) + if( (($type == self::ASN_TYPE_INTEGER) && (ord($data) > 0x7f)) || + ($type == self::ASN_TYPE_BITSTRING)) { + $data = "\0$data"; + } + + $len = strlen($data); + + // encode the value based on length of the string + // I'm fairly confident that this is by no means a complete implementation + // but it is enough for our purposes + switch(true) { + case ($len < 128): + return sprintf("%c%c%s", $type, $len, $data); + case ($len < 0x0100): + return sprintf("%c%c%c%s", $type, 0x81, $len, $data); + case ($len < 0x010000): + return sprintf("%c%c%c%c%s", $type, 0x82, $len / 0x0100, $len % 0x0100, $data); + default: + SimpleSAML_Logger::debug("Could not encode value"); + } + + SimpleSAML_Logger::debug("Invalid code path"); + } +} diff --git a/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform.php b/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform.php new file mode 100644 index 000000000..d96bd216d --- /dev/null +++ b/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform.php @@ -0,0 +1,113 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_InfoCard + * @subpackage Zend_InfoCard_Xml_Security + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id: Transform.php 9094 2008-03-30 18:36:55Z thomas $ + */ + +require_once 'Zend_InfoCard_Xml_Security_Transform_EnvelopedSignature.php'; +require_once 'Zend_InfoCard_Xml_Security_Transform_XmlExcC14N.php'; + +/** + * A class to create a transform rule set based on XML URIs and then apply those rules + * in the correct order to a given XML input + * + * @category Zend + * @package Zend_InfoCard + * @subpackage Zend_InfoCard_Xml_Security + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_InfoCard_Xml_Security_Transform +{ + /** + * A list of transforms to apply + * + * @var array + */ + protected $_transformList = array(); + + /** + * Returns the name of the transform class based on a given URI + * + * @throws Exception + * @param string $uri The transform URI + * @return string The transform implementation class name + */ + protected function _findClassbyURI($uri) + { + switch($uri) { + case 'http://www.w3.org/2000/09/xmldsig#enveloped-signature': + return 'Zend_InfoCard_Xml_Security_Transform_EnvelopedSignature'; + case 'http://www.w3.org/2001/10/xml-exc-c14n#': + return 'Zend_InfoCard_Xml_Security_Transform_XmlExcC14N'; + default: + SimpleSAML_Logger::debug("Unknown or Unsupported Transformation Requested"); + } + } + + /** + * Add a Transform URI to the list of transforms to perform + * + * @param string $uri The Transform URI + * @return Zend_InfoCard_Xml_Security_Transform + */ + public function addTransform($uri) + { + $class = $this->_findClassbyURI($uri); + + $this->_transformList[] = array('uri' => $uri, + 'class' => $class); + return $this; + } + + /** + * Return the list of transforms to perform + * + * @return array The list of transforms + */ + public function getTransformList() + { + return $this->_transformList; + } + + /** + * Apply the transforms in the transform list to the input XML document + * + * @param string $strXmlDocument The input XML + * @return string The XML after the transformations have been applied + */ + public function applyTransforms($strXmlDocument) + { + $transformer = null; + foreach($this->_transformList as $transform) { + switch($transform['class']) { + case 'Zend_InfoCard_Xml_Security_Transform_EnvelopedSignature': + $transformer = new Zend_InfoCard_Xml_Security_Transform_EnvelopedSignature(); + break; + case 'Zend_InfoCard_Xml_Security_Transform_XmlExcC14N': + $transformer = new Zend_InfoCard_Xml_Security_Transform_XmlExcC14N(); + break; + } + + $strXmlDocument = $transformer->transform($strXmlDocument); + } + + return $strXmlDocument; + } +} diff --git a/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform_EnvelopedSignature.php b/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform_EnvelopedSignature.php new file mode 100644 index 000000000..42d2f126b --- /dev/null +++ b/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform_EnvelopedSignature.php @@ -0,0 +1,55 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_InfoCard + * @subpackage Zend_InfoCard_Xml_Security + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id: EnvelopedSignature.php 9094 2008-03-30 18:36:55Z thomas $ + */ + +/** + * A object implementing the EnvelopedSignature XML Transform + * + * @category Zend + * @package Zend_InfoCard + * @subpackage Zend_InfoCard_Xml_Security + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_InfoCard_Xml_Security_Transform_EnvelopedSignature +{ + /** + * Transforms the XML Document according to the EnvelopedSignature Transform + * + * @throws Exception + * @param string $strXMLData The input XML data + * @return string the transformed XML data + */ + public function transform($strXMLData) + { + $sxe = simplexml_load_string($strXMLData); + $sxe->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); + + list($signature) = $sxe->xpath("//ds:Signature"); + if(!isset($signature)) { + SimpleSAML_Logger::debug("Unable to locate Signature Block for EnvelopedSignature Transform"); + } + + $transformed_xml = str_replace($signature->asXML(), "", $sxe->asXML()); + + return $transformed_xml; + } +} diff --git a/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform_XmlExcC14N.php b/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform_XmlExcC14N.php new file mode 100644 index 000000000..c680dd8b7 --- /dev/null +++ b/modules/InfoCard/lib/RP/Zend_InfoCard_Xml_Security_Transform_XmlExcC14N.php @@ -0,0 +1,52 @@ +<?php +/** + * Zend Framework + * + * LICENSE + * + * This source file is subject to the new BSD license that is bundled + * with this package in the file LICENSE.txt. + * It is also available through the world-wide-web at this URL: + * http://framework.zend.com/license/new-bsd + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@zend.com so we can send you a copy immediately. + * + * @category Zend + * @package Zend_InfoCard + * @subpackage Zend_InfoCard_Xml_Security + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * @version $Id: XmlExcC14N.php 9094 2008-03-30 18:36:55Z thomas $ + */ + +/** + * A Transform to perform C14n XML Exclusive Canonicalization + * + * @category Zend + * @package Zend_InfoCard + * @subpackage Zend_InfoCard_Xml_Security + * @copyright Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + */ +class Zend_InfoCard_Xml_Security_Transform_XmlExcC14N +{ + /** + * Transform the input XML based on C14n XML Exclusive Canonicalization rules + * + * @throws Exception + * @param string $strXMLData The input XML + * @return string The output XML + */ + public function transform($strXMLData) + { + $dom = new DOMDocument(); + $dom->loadXML($strXMLData); + + if(method_exists($dom, 'C14N')) { + return $dom->C14N(true, false); + } + + SimpleSAML_Logger::debug("This transform requires the C14N() method to exist in the DOM extension"); + } +} diff --git a/modules/InfoCard/templates/default/login-infocard.php b/modules/InfoCard/templates/default/login-infocard.php new file mode 100644 index 000000000..1b439071a --- /dev/null +++ b/modules/InfoCard/templates/default/login-infocard.php @@ -0,0 +1,58 @@ +<?php +/* +* AUTHOR: Samuel Muñoz Hidalgo +* EMAIL: samuel.mh@gmail.com +* LAST REVISION: 1-DEC-08 +* DESCRIPTION: 'login-infocard' module template. +*/ + $this->includeAtTemplateBase('includes/header.php'); + if (!array_key_exists('icon', $this->data)) $this->data['icon'] = 'lock.png'; + if (isset($this->data['error'])) { ?> + <div style="border-left: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8; background: #f5f5f5"> + <img src="/<?php echo $this->data['baseurlpath']; ?>resources/icons/bomb.png" style="float: left; margin: 15px " /> + <h2><?php echo $this->t('error_header'); ?></h2> + + <p><?php echo $this->t($this->data['error']); ?> </p> + </div> + <?php } ?> + + <h2 style="break: both"><?php echo $this->t('user_IC_header'); ?></h2> + + <p><?php echo $this->t('user_IC_text'); ?></p> + + <form name="ctl00" id="ctl00" method="post" action="?"> + <?php foreach ($this->data['stateparams'] as $name => $value) { + echo('<input type="hidden" name="' . htmlspecialchars($name) . '" value="' . htmlspecialchars($value) . '" />'); + }?> + <ic:informationCard xmlns:ic="<?php echo $this->data['InfoCard']['schema'] ?>" name='xmlToken' + issuer="<?php echo $this->data['InfoCard']['issuer']; ?>" + issuerPolicy="<?php echo $this->data['InfoCard']['issuerPolicy']; ?>" + tokenType="<?php echo $this->data['InfoCard']['tokenType']; ?>" + privacyUrl="<?php echo $this->data['InfoCard']['privacyURL']; ?>" + privacyVersion="<?php echo $this->data['InfoCard']['privacyVersion']; ?>"> + <?php + $schema = $this->data['InfoCard']['schema']."/claims/"; + foreach ($this->data['InfoCard']['requiredClaims'] as $claim=>$data) { + echo "<ic:add claimType = \"$schema".$claim."\" optional=\"false\" />\n"; + } + foreach ($this->data['InfoCard']['optionalClaims'] as $claim=>$data) { + echo "<ic:add claimType = \"$schema".$claim."\" optional=\"true\" />\n"; + } + unset($value);?> + </ic:informationCard> + <input type='image' src="<?php echo $this->data['IClogo']; ?>" align='center' style='cursor:pointer' /> + </form> + + <?php if (strcmp($this->data['CardGenerator'],'')>0) { + echo '<h2>Or get one</h2>'; + echo '<table border="0">'; + echo "<form action=\"". $this->data['CardGenerator'] ."\" method='post'>"; + echo "<tr><td>Username: </td><td><input type='text' name='username' value='usuario' /></tr></td>"; + echo "<tr><td>Password: </td><td><input type='password' name='password' value='clave' /></tr></td>"; + echo "<tr><td></td><td><input type='submit' name='Get_card' value='Get InfoCard' /></tr></td>"; + echo '</form>'; + echo '</table>'; + } ?> + <h2><?php echo $this->t('help_header'); ?></h2> + <p><?php echo $this->t('help_text'); ?></p> +<?php $this->includeAtTemplateBase('includes/footer.php'); ?> \ No newline at end of file diff --git a/modules/InfoCard/www/login-infocard.php b/modules/InfoCard/www/login-infocard.php new file mode 100644 index 000000000..577c533cf --- /dev/null +++ b/modules/InfoCard/www/login-infocard.php @@ -0,0 +1,54 @@ +<?php + +/* +* AUTHOR: Samuel Muñoz Hidalgo +* EMAIL: samuel.mh@gmail.com +* LAST REVISION: 1-DEC-08 +* DESCRIPTION: +* 'login-infocard' module. +* Allows an user to authenticate to the system with an Information Card. +* Infocard's claims are extracted passed as attributes. +*/ + + +/* Load the configuration. */ +$config = SimpleSAML_Configuration::getInstance(); +$autoconfig = $config->copyFromBase('logininfocard', 'config-login-infocard.php'); + +$server_key = $autoconfig->getValue('server_key'); +$server_crt = $autoconfig->getValue('server_crt'); +$IClogo = $autoconfig->getValue('IClogo'); +$Infocard = $autoconfig->getValue('InfoCard'); +$cardGenerator = $autoconfig->getValue('CardGenerator'); + + +/* Load the session of the current user. */ +$session = SimpleSAML_Session::getInstance(); +if($session == NULL) { + SimpleSAML_Utilities::fatalError($session->getTrackID(), 'NOSESSION'); +} + + +if (!array_key_exists('AuthState', $_REQUEST)) { + throw new SimpleSAML_Error_BadRequest('Missing AuthState parameter.'); +} +$authStateId = $_REQUEST['AuthState']; + + +if(array_key_exists('xmlToken', $_POST) && ($_POST['xmlToken']!=NULL) ) { + $error = sspmod_InfoCard_Auth_Source_ICAuth::handleLogin($authStateId, $_POST['xmlToken']); +}else { + $error = NULL; +} + +//Login Page +$t = new SimpleSAML_XHTML_Template($config, 'InfoCard:login-infocard.php', 'logininfocard'); //(configuracion, template, diccionario) +$t->data['header'] = 'simpleSAMLphp: Infocard login'; +$t->data['stateparams'] = array('AuthState' => $authStateId); +$t->data['IClogo'] = $IClogo; +$t->data['InfoCard'] = $Infocard; +$t->data['CardGenerator'] = $cardGenerator; +$t->data['error'] = $error; +$t->show(); +exit(); +?> diff --git a/modules/InfoCard/www/resources/infocard_114x80.png b/modules/InfoCard/www/resources/infocard_114x80.png new file mode 100644 index 0000000000000000000000000000000000000000..6dba25fbd5b54d68bdf1be245e5c8807bfd06e69 GIT binary patch literal 3821 zcmV<J4ifQ+P)<h;3K|Lk000e1NJLTq0043T002-31^@s6#knCs0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU=k4Z#9RCwC#UCVP6#~J@-W+hk&gph26 zfr=4j6dPjW*oMR>yK*C!;3VfnzPOUARE6Agpi=n*IJhcRImF@=A4w{yR3)ht<s5Lp zj=`8mu#LbaKnSq3dab10oz8U6WBc)SPtWeo!feqn+MeE?S!sXs>+jKD_vb)Lc?aHo zHh4J}9WMvRFgz*UC=0=HCOW2m55K6GIy)%<o(PV~J<sof(On}jx_cA`h6bK|#a$?? zi>vVP_Ct7h=WCd`Hba}HC>{Zzx2%B-K>TU&a&r8oy~7hnCZKOqA8W5|Y{25eJpM#D z4*db_p94vq{ZDyM=*Rj*{i1sKc61NcACh|V*_yw~!Tw4|e$Q#`+PFg4|F96QueP1w z?crNWdu_c2*Uw#t=}&J`mL3nlJe3M$0OEV)t>wwrj=l=pp5Dg5oBsTNFf)Avs;es~ zh_35_>$%{0UR3wOI(>Cr_8Qi`W<BUH^mU^)tZ#IGzHjp$ZUicDudIr;sprsqSS<VS z=Ls-=1Of$5I4A6tp2zP&be_B))#dkyPXs_K_bc$x-#&)wQk4$1Z)<_1APzk>H2M0m z?=lunU%v*|FJFRst)?HFgD=wKflNS&>s-44kek%S>s;g>&2}D0dT+;dj9-2h5OGkk zJ`nxDfnorm;6(MLe{^lqpD6Y4q43!U)4!Lp^X%ylVe!G@$pFk>C1m1<Q_92`#Ea+7 z!j%jEHG}AcH+Lg;QBd9JNa`MK_mcMJ5djkSb>i!q?O1=|GvZ;Edm@{!j#iGyeRTJT z{zwCe&;)`9kw_pabqPe}TEelFfFB(7dX0^a6dQ_BfYtFx1U7+;6q}QDIE_J^xjAi? z#xaOIXc!0%|DKD3ojeTuIR~02^^HIT3swnVespf?qcB!^5FBnqjfOrV*CS!a0VURn z)Gl2M{1ELY2PM_|+8V=%Vns0vu-nEn@&r|Es7$28X)1{HLdQi|g8=0rpb6l4dNymA zNrzKCKwt%sdQJMU8-Er7gkfOIHZZW3zcyr|AB@4i?<e(*^>wJ#HkkerSQFF~Jjw8g z5S^sjDWxuziK!qaEb(xrd4}Q9^KejOg+_TWJOSWD1J(kRT`q#aZWCiU)zZi?uw*KJ zA3B^inJBW71YoW%FEel_4)3SG%_PN;GIDZke2md`=H?BfOpN8C${rkqglPs=ItI`% zkehPR$u1WS0EtZPfLsI{xd^yiBs>%SuudXY2Iq}NgAK9Nrl;6ZXAK4N&7I*11JYJ5 zs&Xys@aSpepQqeER)!puT^Zyes@yo*bES>6As4|U7n}7MEXG9QD{PGGYmq4!VRx)V zfecKJ$k<;<A5sySW�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 literal 0 HcmV?d00001 -- GitLab