From 27fc2659dd538e64bd16b0cff86f4c4d027a1802 Mon Sep 17 00:00:00 2001 From: Tim van Dijen <tvdijen@gmail.com> Date: Sat, 6 Jan 2018 23:03:52 +0100 Subject: [PATCH] PSR-2 --- lib/SimpleSAML/Configuration.php | 4 + lib/SimpleSAML/MetadataConfiguration.php | 331 ++++++++ modules/cdc/config-templates/module_cdc.php | 34 +- modules/cdc/lib/Auth/Process/CDC.php | 126 +-- modules/cdc/lib/Client.php | 97 +-- modules/cdc/lib/Server.php | 816 ++++++++++---------- 6 files changed, 874 insertions(+), 534 deletions(-) create mode 100644 lib/SimpleSAML/MetadataConfiguration.php diff --git a/lib/SimpleSAML/Configuration.php b/lib/SimpleSAML/Configuration.php index ea553ee13..8110ff5d5 100644 --- a/lib/SimpleSAML/Configuration.php +++ b/lib/SimpleSAML/Configuration.php @@ -93,6 +93,10 @@ class SimpleSAML_Configuration implements \SimpleSAML\Utils\ClearableState $this->location = $location; } + public function getLocation() + { + return $this->location; + } /** * Load the given configuration file. diff --git a/lib/SimpleSAML/MetadataConfiguration.php b/lib/SimpleSAML/MetadataConfiguration.php new file mode 100644 index 000000000..880d81d01 --- /dev/null +++ b/lib/SimpleSAML/MetadataConfiguration.php @@ -0,0 +1,331 @@ +<?php + + +/** + * Metadata configuration of SimpleSAMLphp + * + * @author Andreas Aakre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + * @package SimpleSAMLphp + */ +class SimpleSAML_MetadataConfiguration extends \SimpleSAML_Configuration +{ + /** + * Initializes a configuration from the given array. + * + * @param array $config The configuration array. + * @param string $location The location which will be given when an error occurs. + */ + public function __construct($config, $location) + { + assert(is_array($config)); + assert(is_string($location)); + + parent::__construct($config, $location); + } + + + /** + * Retrieve the default binding for the given endpoint type. + * + * This function combines the current metadata type (SAML 2 / SAML 1.1) + * with the endpoint type to determine which binding is the default. + * + * @param string $endpointType The endpoint type. + * + * @return string The default binding. + * + * @throws Exception If the default binding is missing for this endpoint type. + */ + private function getDefaultBinding($endpointType) + { + assert(is_string($endpointType)); + + $set = $this->getString('metadata-set'); + switch ($set.':'.$endpointType) { + case 'saml20-idp-remote:SingleSignOnService': + case 'saml20-idp-remote:SingleLogoutService': + case 'saml20-sp-remote:SingleLogoutService': + return \SAML2\Constants::BINDING_HTTP_REDIRECT; + case 'saml20-sp-remote:AssertionConsumerService': + return \SAML2\Constants::BINDING_HTTP_POST; + case 'saml20-idp-remote:ArtifactResolutionService': + return \SAML2\Constants::BINDING_SOAP; + case 'shib13-idp-remote:SingleSignOnService': + return 'urn:mace:shibboleth:1.0:profiles:AuthnRequest'; + case 'shib13-sp-remote:AssertionConsumerService': + return 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post'; + default: + throw new Exception('Missing default binding for '.$endpointType.' in '.$set); + } + } + + + /** + * Helper function for dealing with metadata endpoints. + * + * @param string $endpointType The endpoint type. + * + * @return array Array of endpoints of the given type. + * + * @throws Exception If any element of the configuration options for this endpoint type is incorrect. + */ + public function getEndpoints($endpointType) + { + assert(is_string($endpointType)); + + $loc = $this->getLocation().'['.var_export($endpointType, true).']:'; + + $configuration = $this->toArray(); + if (!array_key_exists($endpointType, $configuration)) { + // no endpoints of the given type + return array(); + } + + + $eps = $configuration[$endpointType]; + if (is_string($eps)) { + // for backwards-compatibility + $eps = array($eps); + } elseif (!is_array($eps)) { + throw new Exception($loc.': Expected array or string.'); + } + + + foreach ($eps as $i => &$ep) { + $iloc = $loc.'['.var_export($i, true).']'; + + if (is_string($ep)) { + // for backwards-compatibility + $ep = array( + 'Location' => $ep, + 'Binding' => $this->getDefaultBinding($endpointType), + ); + $responseLocation = $this->getString($endpointType.'Response', null); + if ($responseLocation !== null) { + $ep['ResponseLocation'] = $responseLocation; + } + } elseif (!is_array($ep)) { + throw new Exception($iloc.': Expected a string or an array.'); + } + + if (!array_key_exists('Location', $ep)) { + throw new Exception($iloc.': Missing Location.'); + } + if (!is_string($ep['Location'])) { + throw new Exception($iloc.': Location must be a string.'); + } + + if (!array_key_exists('Binding', $ep)) { + throw new Exception($iloc.': Missing Binding.'); + } + if (!is_string($ep['Binding'])) { + throw new Exception($iloc.': Binding must be a string.'); + } + + if (array_key_exists('ResponseLocation', $ep)) { + if (!is_string($ep['ResponseLocation'])) { + throw new Exception($iloc.': ResponseLocation must be a string.'); + } + } + + if (array_key_exists('index', $ep)) { + if (!is_int($ep['index'])) { + throw new Exception($iloc.': index must be an integer.'); + } + } + } + + return $eps; + } + + + /** + * Find an endpoint of the given type, using a list of supported bindings as a way to prioritize. + * + * @param string $endpointType The endpoint type. + * @param array $bindings Sorted array of acceptable bindings. + * @param mixed $default The default value to return if no matching endpoint is found. If no default is provided, + * an exception will be thrown. + * + * @return array|null The default endpoint, or null if no acceptable endpoints are used. + * + * @throws Exception If no supported endpoint is found. + */ + public function getEndpointPrioritizedByBinding($endpointType, array $bindings, $default = self::REQUIRED_OPTION) + { + assert(is_string($endpointType)); + + $endpoints = $this->getEndpoints($endpointType); + + foreach ($bindings as $binding) { + foreach ($endpoints as $ep) { + if ($ep['Binding'] === $binding) { + return $ep; + } + } + } + + if ($default === self::REQUIRED_OPTION) { + $loc = $this->getLocation().'['.var_export($endpointType, true).']:'; + throw new Exception($loc.'Could not find a supported '.$endpointType.' endpoint.'); + } + + return $default; + } + + + /** + * Find the default endpoint of the given type. + * + * @param string $endpointType The endpoint type. + * @param array $bindings Array with acceptable bindings. Can be null if any binding is allowed. + * @param mixed $default The default value to return if no matching endpoint is found. If no default is provided, + * an exception will be thrown. + * + * @return array|null The default endpoint, or null if no acceptable endpoints are used. + * + * @throws Exception If no supported endpoint is found. + */ + public function getDefaultEndpoint($endpointType, array $bindings = null, $default = self::REQUIRED_OPTION) + { + assert(is_string($endpointType)); + + $endpoints = $this->getEndpoints($endpointType); + + $defaultEndpoint = \SimpleSAML\Utils\Config\Metadata::getDefaultEndpoint($endpoints, $bindings); + if ($defaultEndpoint !== null) { + return $defaultEndpoint; + } + + if ($default === self::REQUIRED_OPTION) { + $loc = $this->getLocation().'['.var_export($endpointType, true).']:'; + throw new Exception($loc.'Could not find a supported '.$endpointType.' endpoint.'); + } + + return $default; + } + + + /** + * Retrieve a string which may be localized into many languages. + * + * The default language returned is always 'en'. + * + * @param string $name The name of the option. + * @param mixed $default The default value. If no default is given, and the option isn't found, an exception will + * be thrown. + * + * @return array Associative array with language => string pairs. + * + * @throws Exception If the translation is not an array or a string, or its index or value are not strings. + */ + public function getLocalizedString($name, $default = self::REQUIRED_OPTION) + { + assert(is_string($name)); + + $ret = $this->getValue($name, $default); + if ($ret === $default) { + // the option wasn't found, or it matches the default value. In any case, return this value + return $ret; + } + + $loc = $this->getLocation().'['.var_export($name, true).']'; + + if (is_string($ret)) { + $ret = array('en' => $ret,); + } + + if (!is_array($ret)) { + throw new Exception($loc.': Must be an array or a string.'); + } + + foreach ($ret as $k => $v) { + if (!is_string($k)) { + throw new Exception($loc.': Invalid language code: '.var_export($k, true)); + } + if (!is_string($v)) { + throw new Exception($loc.'['.var_export($v, true).']: Must be a string.'); + } + } + + return $ret; + } + + + /** + * Get public key from metadata. + * + * @param string|null $use The purpose this key can be used for. (encryption or signing). + * @param bool $required Whether the public key is required. If this is true, a + * missing key will cause an exception. Default is false. + * @param string $prefix The prefix which should be used when reading from the metadata + * array. Defaults to ''. + * + * @return array Public key data, or empty array if no public key or was found. + * + * @throws Exception If the certificate or public key cannot be loaded from a file. + * @throws SimpleSAML_Error_Exception If the file does not contain a valid PEM-encoded certificate, or there is no + * certificate in the metadata. + */ + public function getPublicKeys($use = null, $required = false, $prefix = '') + { + assert(is_bool($required)); + assert(is_string($prefix)); + + if ($this->hasValue($prefix.'keys')) { + $ret = array(); + foreach ($this->getArray($prefix.'keys') as $key) { + if ($use !== null && isset($key[$use]) && !$key[$use]) { + continue; + } + if (isset($key['X509Certificate'])) { + // Strip whitespace from key + $key['X509Certificate'] = preg_replace('/\s+/', '', $key['X509Certificate']); + } + $ret[] = $key; + } + return $ret; + } elseif ($this->hasValue($prefix.'certData')) { + $certData = $this->getString($prefix.'certData'); + $certData = preg_replace('/\s+/', '', $certData); + return array( + array( + 'encryption' => true, + 'signing' => true, + 'type' => 'X509Certificate', + 'X509Certificate' => $certData, + ), + ); + } elseif ($this->hasValue($prefix.'certificate')) { + $file = $this->getString($prefix.'certificate'); + $file = \SimpleSAML\Utils\Config::getCertPath($file); + $data = @file_get_contents($file); + + if ($data === false) { + throw new Exception($this->getLocation().': Unable to load certificate/public key from file "'.$file.'".'); + } + + // extract certificate data (if this is a certificate) + $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m'; + if (!preg_match($pattern, $data, $matches)) { + throw new SimpleSAML_Error_Exception( + $this->getLocation().': Could not find PEM encoded certificate in "'.$file.'".' + ); + } + $certData = preg_replace('/\s+/', '', $matches[1]); + + return array( + array( + 'encryption' => true, + 'signing' => true, + 'type' => 'X509Certificate', + 'X509Certificate' => $certData, + ), + ); + } elseif ($required === true) { + throw new SimpleSAML_Error_Exception($this->getLocation().': Missing certificate in metadata.'); + } else { + return array(); + } + } +} diff --git a/modules/cdc/config-templates/module_cdc.php b/modules/cdc/config-templates/module_cdc.php index 77b83bdce..0e5013c87 100644 --- a/modules/cdc/config-templates/module_cdc.php +++ b/modules/cdc/config-templates/module_cdc.php @@ -1,24 +1,22 @@ <?php $config = array( - 'example.org' => array( + 'example.org' => array( + /* + * The shared key for this CDC server. + */ + 'key' => 'ExampleSharedKey', - /* - * The shared key for this CDC server. - */ - 'key' => 'ExampleSharedKey', + /* + * The URL to the server script. + */ + 'server' => 'https://my-cdc.example.org/simplesaml/module.php/cdc/server.php', - /* - * The URL to the server script. - */ - 'server' => 'https://my-cdc.example.org/simplesaml/module.php/cdc/server.php', - - /* - * The lifetime of our cookie, in seconds. - * - * If this is 0, the cookie will expire when the browser is closed. - */ - 'cookie.lifetime' => 0, - - ), + /* + * The lifetime of our cookie, in seconds. + * + * If this is 0, the cookie will expire when the browser is closed. + */ + 'cookie.lifetime' => 0, + ), ); diff --git a/modules/cdc/lib/Auth/Process/CDC.php b/modules/cdc/lib/Auth/Process/CDC.php index bdcf4b706..9641da2ab 100644 --- a/modules/cdc/lib/Auth/Process/CDC.php +++ b/modules/cdc/lib/Auth/Process/CDC.php @@ -5,67 +5,67 @@ * * @package SimpleSAMLphp */ -class sspmod_cdc_Auth_Process_CDC extends SimpleSAML_Auth_ProcessingFilter { - - - /** - * Our CDC domain. - * - * @var string - */ - private $domain; - - - /** - * Our CDC client. - * - * @var sspmod_cdc_Client - */ - private $client; - - - /** - * Initialize this filter. - * - * @param array $config Configuration information about this filter. - * @param mixed $reserved For future use. - */ - public function __construct($config, $reserved) { - parent::__construct($config, $reserved); - assert(is_array($config)); - - if (!isset($config['domain'])) { - throw new SimpleSAML_Error_Exception('Missing domain option in cdc:CDC filter.'); - } - $this->domain = (string)$config['domain']; - - $this->client = new sspmod_cdc_Client($this->domain); - } - - - /** - * Redirect to page setting CDC. - * - * @param array &$state The request state. - */ - public function process(&$state) { - assert(is_array($state)); - - if (!isset($state['Source']['entityid'])) { - SimpleSAML\Logger::warning('saml:CDC: Could not find IdP entityID.'); - return; - } - - // Save state and build request - $id = SimpleSAML_Auth_State::saveState($state, 'cdc:resume'); - - $returnTo = SimpleSAML\Module::getModuleURL('cdc/resume.php', array('domain' => $this->domain)); - - $params = array( - 'id' => $id, - 'entityID' => $state['Source']['entityid'], - ); - $this->client->sendRequest($returnTo, 'append', $params); - } - +class sspmod_cdc_Auth_Process_CDC extends SimpleSAML_Auth_ProcessingFilter +{ + /** + * Our CDC domain. + * + * @var string + */ + private $domain; + + + /** + * Our CDC client. + * + * @var sspmod_cdc_Client + */ + private $client; + + + /** + * Initialize this filter. + * + * @param array $config Configuration information about this filter. + * @param mixed $reserved For future use. + */ + public function __construct($config, $reserved) + { + parent::__construct($config, $reserved); + assert(is_array($config)); + + if (!isset($config['domain'])) { + throw new SimpleSAML_Error_Exception('Missing domain option in cdc:CDC filter.'); + } + $this->domain = (string)$config['domain']; + + $this->client = new sspmod_cdc_Client($this->domain); + } + + + /** + * Redirect to page setting CDC. + * + * @param array &$state The request state. + */ + public function process(&$state) + { + assert(is_array($state)); + + if (!isset($state['Source']['entityid'])) { + SimpleSAML\Logger::warning('saml:CDC: Could not find IdP entityID.'); + return; + } + + // Save state and build request + $id = SimpleSAML_Auth_State::saveState($state, 'cdc:resume'); + + $returnTo = SimpleSAML\Module::getModuleURL('cdc/resume.php', array('domain' => $this->domain)); + + $params = array( + 'id' => $id, + 'entityID' => $state['Source']['entityid'], + ); + $this->client->sendRequest($returnTo, 'append', $params); + } } diff --git a/modules/cdc/lib/Client.php b/modules/cdc/lib/Client.php index 41c119a56..ab59fa628 100644 --- a/modules/cdc/lib/Client.php +++ b/modules/cdc/lib/Client.php @@ -5,62 +5,63 @@ * * @package SimpleSAMLphp */ -class sspmod_cdc_Client { +class sspmod_cdc_Client +{ + /** + * Our CDC domain. + * + * @var string + */ + private $domain; - /** - * Our CDC domain. - * - * @var string - */ - private $domain; + /** + * The CDC server we send requests to. + * + * @var sspmod_cdc_Server|NULL + */ + private $server; - /** - * The CDC server we send requests to. - * - * @var sspmod_cdc_Server|NULL - */ - private $server; + /** + * Initialize a CDC client. + * + * @param string $domain The domain we should query the server for. + */ + public function __construct($domain) + { + assert(is_string($domain)); - /** - * Initialize a CDC client. - * - * @param string $domain The domain we should query the server for. - */ - public function __construct($domain) { - assert(is_string($domain)); + $this->domain = $domain; + $this->server = new sspmod_cdc_Server($domain); + } - $this->domain = $domain; - $this->server = new sspmod_cdc_Server($domain); - } + /** + * Receive a CDC response. + * + * @return array|NULL The response, or NULL if no response is received. + */ + public function getResponse() + { + return $this->server->getResponse(); + } - /** - * Receive a CDC response. - * - * @return array|NULL The response, or NULL if no response is received. - */ - public function getResponse() { - return $this->server->getResponse(); - } - - - /** - * Send a request. - * - * @param string $returnTo The URL we should return to afterwards. - * @param string $op The operation we are performing. - * @param array $params Additional parameters. - */ - public function sendRequest($returnTo, $op, array $params = array()) { - assert(is_string($returnTo)); - assert(is_string($op)); - - $params['op'] = $op; - $params['return'] = $returnTo; - $this->server->sendRequest($params); - } + /** + * Send a request. + * + * @param string $returnTo The URL we should return to afterwards. + * @param string $op The operation we are performing. + * @param array $params Additional parameters. + */ + public function sendRequest($returnTo, $op, array $params = array()) + { + assert(is_string($returnTo)); + assert(is_string($op)); + $params['op'] = $op; + $params['return'] = $returnTo; + $this->server->sendRequest($params); + } } diff --git a/modules/cdc/lib/Server.php b/modules/cdc/lib/Server.php index 569a33d98..22e15a1a4 100644 --- a/modules/cdc/lib/Server.php +++ b/modules/cdc/lib/Server.php @@ -5,409 +5,415 @@ * * @package SimpleSAMLphp */ -class sspmod_cdc_Server { - - /** - * The domain. - * - * @var string - */ - private $domain; - - - /** - * The URL to the server. - * - * @var string - */ - private $server; - - - /** - * Our shared key. - * - * @var string - */ - private $key; - - - /** - * The lifetime of our cookie, in seconds. - * - * If this is 0, the cookie will expire when the browser is closed. - * - * @param int - */ - private $cookieLifetime; - - - /** - * Initialize a CDC server. - * - * @param string $domain The domain we are a server for. - */ - public function __construct($domain) { - assert(is_string($domain)); - - $cdcConfig = SimpleSAML_Configuration::getConfig('module_cdc.php'); - $config = $cdcConfig->getConfigItem($domain, NULL); - - if ($config === NULL) { - throw new SimpleSAML_Error_Exception('Unknown CDC domain: ' . var_export($domain, TRUE)); - } - - $this->domain = $domain; - $this->server = $config->getString('server'); - $this->key = $config->getString('key'); - $this->cookieLifetime = $config->getInteger('cookie.lifetime', 0); - - if ($this->key === 'ExampleSharedKey') { - throw new SimpleSAML_Error_Exception('Key for CDC domain ' . var_export($domain, TRUE) . ' not changed from default.'); - } - } - - - /** - * Send a request to this CDC server. - * - * @param array $request The CDC request. - */ - public function sendRequest(array $request) { - assert(isset($request['return'])); - assert(isset($request['op'])); - - $request['domain'] = $this->domain; - $this->send($this->server, 'CDCRequest', $request); - } - - - /** - * Parse and validate response received from a CDC server. - * - * @return array|NULL The response, or NULL if no response is received. - */ - public function getResponse() { - - $response = self::get('CDCResponse'); - if ($response === NULL) { - return NULL; - } - - if ($response['domain'] !== $this->domain) { - throw new SimpleSAML_Error_Exception('Response received from wrong domain.'); - } - - $this->validate('CDCResponse'); - - return $response; - } - - - /** - * Parse and process a CDC request. - */ - public static function processRequest() { - $request = self::get('CDCRequest'); - if ($request === NULL) { - throw new SimpleSAML_Error_BadRequest('Missing "CDCRequest" parameter.'); - } - - $domain = $request['domain']; - $server = new sspmod_cdc_Server($domain); - - $server->validate('CDCRequest'); - - $server->handleRequest($request); - } - - - /** - * Handle a parsed CDC requst. - * - * @param array $request - */ - private function handleRequest(array $request) { - - if (!isset($request['op'])) { - throw new SimpleSAML_Error_BadRequest('Missing "op" in CDC request.'); - } - $op = (string)$request['op']; - - SimpleSAML\Logger::info('Received CDC request with "op": ' . var_export($op, TRUE)); - - if (!isset($request['return'])) { - throw new SimpleSAML_Error_BadRequest('Missing "return" in CDC request.'); - } - $return = (string)$request['return']; - - switch ($op) { - case 'append': - $response = $this->handleAppend($request); - break; - case 'delete': - $response = $this->handleDelete($request); - break; - case 'read': - $response = $this->handleRead($request); - break; - default: - $response = 'unknown-op'; - } - - if (is_string($response)) { - $response = array( - 'status' => $response, - ); - } - - $response['op'] = $op; - if (isset($request['id'])) { - $response['id'] = (string)$request['id']; - } - $response['domain'] = $this->domain; - - $this->send($return, 'CDCResponse', $response); - } - - - /** - * Handle an append request. - * - * @param array $request The request. - * @return array The response. - */ - private function handleAppend(array $request) { - - if (!isset($request['entityID'])) { - throw new SimpleSAML_Error_BadRequest('Missing entityID in append request.'); - } - $entityID = (string)$request['entityID']; - - $list = $this->getCDC(); - - $prevIndex = array_search($entityID, $list, TRUE); - if ($prevIndex !== FALSE) { - unset($list[$prevIndex]); - } - $list[] = $entityID; - - $this->setCDC($list); - - return 'ok'; - } - - - /** - * Handle a delete request. - * - * @param array $request The request. - * @return array The response. - */ - private function handleDelete(array $request) { - $params = array( - 'path' => '/', - 'domain' => '.' . $this->domain, - 'secure' => TRUE, - 'httponly' => FALSE, - ); - - \SimpleSAML\Utils\HTTP::setCookie('_saml_idp', NULL, $params, FALSE); - return 'ok'; - } - - - /** - * Handle a read request. - * - * @param array $request The request. - * @return array The response. - */ - private function handleRead(array $request) { - - $list = $this->getCDC(); - - return array( - 'status' => 'ok', - 'cdc' => $list, - ); - } - - - /** - * Helper function for parsing and validating a CDC message. - * - * @param string $parameter The name of the query parameter. - * @return array|NULL The response, or NULL if no response is received. - */ - private static function get($parameter) { - assert(is_string($parameter)); - - if (!isset($_REQUEST[$parameter])) { - return NULL; - } - $message = (string)$_REQUEST[$parameter]; - - $message = @base64_decode($message); - if ($message === FALSE) { - throw new SimpleSAML_Error_BadRequest('Error base64-decoding CDC message.'); - } - - $message = @json_decode($message, TRUE); - if ($message === FALSE) { - throw new SimpleSAML_Error_BadRequest('Error json-decoding CDC message.'); - } - - if (!isset($message['timestamp'])) { - throw new SimpleSAML_Error_BadRequest('Missing timestamp in CDC message.'); - } - $timestamp = (int)$message['timestamp']; - - if ($timestamp + 60 < time()) { - throw new SimpleSAML_Error_BadRequest('CDC signature has expired.'); - } - if ($timestamp - 60 > time()) { - throw new SimpleSAML_Error_BadRequest('CDC signature from the future.'); - } - - if (!isset($message['domain'])) { - throw new SimpleSAML_Error_BadRequest('Missing domain in CDC message.'); - } - - return $message; - } - - - /** - * Helper function for validating the signature on a CDC message. - * - * Will throw an exception if the message is invalid. - * - * @param string $parameter The name of the query parameter. - */ - private function validate($parameter) { - assert(is_string($parameter)); - assert(isset($_REQUEST[$parameter])); - - $message = (string)$_REQUEST[$parameter]; - - if (!isset($_REQUEST['Signature'])) { - throw new SimpleSAML_Error_BadRequest('Missing Signature on CDC message.'); - } - $signature = (string)$_REQUEST['Signature']; - - $cSignature = $this->calcSignature($message); - if ($signature !== $cSignature) { - throw new SimpleSAML_Error_BadRequest('Invalid signature on CDC message.'); - } - } - - - /** - * Helper function for sending CDC messages. - * - * @param string $to The URL the message should be delivered to. - * @param string $parameter The query parameter the message should be sent in. - * @param array $message The CDC message. - */ - private function send($to, $parameter, array $message) { - assert(is_string($to)); - assert(is_string($parameter)); - - $message['timestamp'] = time(); - $message = json_encode($message); - $message = base64_encode($message); - - $signature = $this->calcSignature($message); - - $params = array( - $parameter => $message, - 'Signature' => $signature, - ); - - $url = \SimpleSAML\Utils\HTTP::addURLParameters($to, $params); - if (strlen($url) < 2048) { - \SimpleSAML\Utils\HTTP::redirectTrustedURL($url); - } else { - \SimpleSAML\Utils\HTTP::submitPOSTData($to, $params); - } - } - - - /** - * Calculate the signature on the given message. - * - * @param string $rawMessage The base64-encoded message. - * @return string The signature. - */ - private function calcSignature($rawMessage) { - assert(is_string($rawMessage)); - - return sha1($this->key . $rawMessage . $this->key); - } - - - /** - * Get the IdP entities saved in the common domain cookie. - * - * @return array List of IdP entities. - */ - private function getCDC() { - - if (!isset($_COOKIE['_saml_idp'])) { - return array(); - } - - $ret = (string)$_COOKIE['_saml_idp']; - $ret = explode(' ', $ret); - foreach ($ret as &$idp) { - $idp = base64_decode($idp); - if ($idp === FALSE) { - // Not properly base64 encoded - SimpleSAML\Logger::warning('CDC - Invalid base64-encoding of CDC entry.'); - return array(); - } - } - - return $ret; - } - - - /** - * Build a CDC cookie string. - * - * @param array $list The list of IdPs. - * @return string The CDC cookie value. - */ - function setCDC(array $list) { - - foreach ($list as &$value) { - $value = base64_encode($value); - } - - $cookie = implode(' ', $list); - - while (strlen($cookie) > 4000) { - // The cookie is too long. Remove the oldest elements until it is short enough - $tmp = explode(' ', $cookie, 2); - if (count($tmp) === 1) { - /* - * We are left with a single entityID whose base64 - * representation is too long to fit in a cookie. - */ - break; - } - $cookie = $tmp[1]; - } - - $params = array( - 'lifetime' => $this->cookieLifetime, - 'path' => '/', - 'domain' => '.' . $this->domain, - 'secure' => TRUE, - 'httponly' => FALSE, - ); - - \SimpleSAML\Utils\HTTP::setCookie('_saml_idp', $cookie, $params, FALSE); - } - +class sspmod_cdc_Server +{ + /** + * The domain. + * + * @var string + */ + private $domain; + + + /** + * The URL to the server. + * + * @var string + */ + private $server; + + + /** + * Our shared key. + * + * @var string + */ + private $key; + + + /** + * The lifetime of our cookie, in seconds. + * + * If this is 0, the cookie will expire when the browser is closed. + * + * @param int + */ + private $cookieLifetime; + + + /** + * Initialize a CDC server. + * + * @param string $domain The domain we are a server for. + */ + public function __construct($domain) + { + assert(is_string($domain)); + + $cdcConfig = SimpleSAML_Configuration::getConfig('module_cdc.php'); + $config = $cdcConfig->getConfigItem($domain, null); + + if ($config === null) { + throw new SimpleSAML_Error_Exception('Unknown CDC domain: ' . var_export($domain, true)); + } + + $this->domain = $domain; + $this->server = $config->getString('server'); + $this->key = $config->getString('key'); + $this->cookieLifetime = $config->getInteger('cookie.lifetime', 0); + + if ($this->key === 'ExampleSharedKey') { + throw new SimpleSAML_Error_Exception('Key for CDC domain ' . var_export($domain, true) . ' not changed from default.'); + } + } + + + /** + * Send a request to this CDC server. + * + * @param array $request The CDC request. + */ + public function sendRequest(array $request) + { + assert(isset($request['return'])); + assert(isset($request['op'])); + + $request['domain'] = $this->domain; + $this->send($this->server, 'CDCRequest', $request); + } + + + /** + * Parse and validate response received from a CDC server. + * + * @return array|NULL The response, or NULL if no response is received. + */ + public function getResponse() + { + $response = self::get('CDCResponse'); + if ($response === null) { + return null; + } + + if ($response['domain'] !== $this->domain) { + throw new SimpleSAML_Error_Exception('Response received from wrong domain.'); + } + + $this->validate('CDCResponse'); + + return $response; + } + + + /** + * Parse and process a CDC request. + */ + public static function processRequest() + { + $request = self::get('CDCRequest'); + if ($request === null) { + throw new SimpleSAML_Error_BadRequest('Missing "CDCRequest" parameter.'); + } + + $domain = $request['domain']; + $server = new sspmod_cdc_Server($domain); + + $server->validate('CDCRequest'); + $server->handleRequest($request); + } + + + /** + * Handle a parsed CDC requst. + * + * @param array $request + */ + private function handleRequest(array $request) + { + if (!isset($request['op'])) { + throw new SimpleSAML_Error_BadRequest('Missing "op" in CDC request.'); + } + $op = (string)$request['op']; + + SimpleSAML\Logger::info('Received CDC request with "op": ' . var_export($op, true)); + + if (!isset($request['return'])) { + throw new SimpleSAML_Error_BadRequest('Missing "return" in CDC request.'); + } + $return = (string)$request['return']; + + switch ($op) { + case 'append': + $response = $this->handleAppend($request); + break; + case 'delete': + $response = $this->handleDelete($request); + break; + case 'read': + $response = $this->handleRead($request); + break; + default: + $response = 'unknown-op'; + } + + if (is_string($response)) { + $response = array( + 'status' => $response, + ); + } + + $response['op'] = $op; + if (isset($request['id'])) { + $response['id'] = (string)$request['id']; + } + $response['domain'] = $this->domain; + + $this->send($return, 'CDCResponse', $response); + } + + + /** + * Handle an append request. + * + * @param array $request The request. + * @return array The response. + */ + private function handleAppend(array $request) + { + if (!isset($request['entityID'])) { + throw new SimpleSAML_Error_BadRequest('Missing entityID in append request.'); + } + $entityID = (string)$request['entityID']; + + $list = $this->getCDC(); + + $prevIndex = array_search($entityID, $list, true); + if ($prevIndex !== false) { + unset($list[$prevIndex]); + } + $list[] = $entityID; + + $this->setCDC($list); + + return 'ok'; + } + + + /** + * Handle a delete request. + * + * @param array $request The request. + * @return array The response. + */ + private function handleDelete(array $request) + { + $params = array( + 'path' => '/', + 'domain' => '.' . $this->domain, + 'secure' => true, + 'httponly' => false, + ); + + \SimpleSAML\Utils\HTTP::setCookie('_saml_idp', null, $params, false); + return 'ok'; + } + + + /** + * Handle a read request. + * + * @param array $request The request. + * @return array The response. + */ + private function handleRead(array $request) + { + $list = $this->getCDC(); + + return array( + 'status' => 'ok', + 'cdc' => $list, + ); + } + + + /** + * Helper function for parsing and validating a CDC message. + * + * @param string $parameter The name of the query parameter. + * @return array|NULL The response, or NULL if no response is received. + */ + private static function get($parameter) + { + assert(is_string($parameter)); + + if (!isset($_REQUEST[$parameter])) { + return null; + } + $message = (string)$_REQUEST[$parameter]; + + $message = @base64_decode($message); + if ($message === false) { + throw new SimpleSAML_Error_BadRequest('Error base64-decoding CDC message.'); + } + + $message = @json_decode($message, true); + if ($message === false) { + throw new SimpleSAML_Error_BadRequest('Error json-decoding CDC message.'); + } + + if (!isset($message['timestamp'])) { + throw new SimpleSAML_Error_BadRequest('Missing timestamp in CDC message.'); + } + $timestamp = (int)$message['timestamp']; + + if ($timestamp + 60 < time()) { + throw new SimpleSAML_Error_BadRequest('CDC signature has expired.'); + } + if ($timestamp - 60 > time()) { + throw new SimpleSAML_Error_BadRequest('CDC signature from the future.'); + } + + if (!isset($message['domain'])) { + throw new SimpleSAML_Error_BadRequest('Missing domain in CDC message.'); + } + + return $message; + } + + + /** + * Helper function for validating the signature on a CDC message. + * + * Will throw an exception if the message is invalid. + * + * @param string $parameter The name of the query parameter. + */ + private function validate($parameter) + { + assert(is_string($parameter)); + assert(isset($_REQUEST[$parameter])); + + $message = (string)$_REQUEST[$parameter]; + + if (!isset($_REQUEST['Signature'])) { + throw new SimpleSAML_Error_BadRequest('Missing Signature on CDC message.'); + } + $signature = (string)$_REQUEST['Signature']; + + $cSignature = $this->calcSignature($message); + if ($signature !== $cSignature) { + throw new SimpleSAML_Error_BadRequest('Invalid signature on CDC message.'); + } + } + + + /** + * Helper function for sending CDC messages. + * + * @param string $to The URL the message should be delivered to. + * @param string $parameter The query parameter the message should be sent in. + * @param array $message The CDC message. + */ + private function send($to, $parameter, array $message) + { + assert(is_string($to)); + assert(is_string($parameter)); + + $message['timestamp'] = time(); + $message = json_encode($message); + $message = base64_encode($message); + + $signature = $this->calcSignature($message); + + $params = array( + $parameter => $message, + 'Signature' => $signature, + ); + + $url = \SimpleSAML\Utils\HTTP::addURLParameters($to, $params); + if (strlen($url) < 2048) { + \SimpleSAML\Utils\HTTP::redirectTrustedURL($url); + } else { + \SimpleSAML\Utils\HTTP::submitPOSTData($to, $params); + } + } + + + /** + * Calculate the signature on the given message. + * + * @param string $rawMessage The base64-encoded message. + * @return string The signature. + */ + private function calcSignature($rawMessage) + { + assert(is_string($rawMessage)); + + return sha1($this->key . $rawMessage . $this->key); + } + + + /** + * Get the IdP entities saved in the common domain cookie. + * + * @return array List of IdP entities. + */ + private function getCDC() + { + if (!isset($_COOKIE['_saml_idp'])) { + return array(); + } + + $ret = (string)$_COOKIE['_saml_idp']; + $ret = explode(' ', $ret); + foreach ($ret as &$idp) { + $idp = base64_decode($idp); + if ($idp === false) { + // Not properly base64 encoded + SimpleSAML\Logger::warning('CDC - Invalid base64-encoding of CDC entry.'); + return array(); + } + } + + return $ret; + } + + + /** + * Build a CDC cookie string. + * + * @param array $list The list of IdPs. + * @return string The CDC cookie value. + */ + function setCDC(array $list) + { + foreach ($list as &$value) { + $value = base64_encode($value); + } + + $cookie = implode(' ', $list); + + while (strlen($cookie) > 4000) { + // The cookie is too long. Remove the oldest elements until it is short enough + $tmp = explode(' ', $cookie, 2); + if (count($tmp) === 1) { + /* + * We are left with a single entityID whose base64 + * representation is too long to fit in a cookie. + */ + break; + } + $cookie = $tmp[1]; + } + + $params = array( + 'lifetime' => $this->cookieLifetime, + 'path' => '/', + 'domain' => '.' . $this->domain, + 'secure' => true, + 'httponly' => false, + ); + + \SimpleSAML\Utils\HTTP::setCookie('_saml_idp', $cookie, $params, false); + } } -- GitLab