runff 1.0 commit

This commit is contained in:
rock64
2019-04-29 16:09:00 +02:00
commit f1567f989b
1268 changed files with 98652 additions and 0 deletions

60
lib/SimpleSAML/Utils/Arrays.php Executable file
View File

@@ -0,0 +1,60 @@
<?php
namespace SimpleSAML\Utils;
/**
* Array-related utility methods.
*
* @package SimpleSAMLphp
*/
class Arrays
{
/**
* Put a non-array variable into an array.
*
* @param mixed $data The data to place into an array.
* @param mixed $index The index or key of the array where to place the data. Defaults to 0.
*
* @return array An array with one element containing $data, with key $index, or $data itself if it's already an
* array.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function arrayize($data, $index = 0)
{
return (is_array($data)) ? $data : array($index => $data);
}
/**
* This function transposes a two-dimensional array, so that $a['k1']['k2'] becomes $a['k2']['k1'].
*
* @param array $array The two-dimensional array to transpose.
*
* @return mixed The transposed array, or false if $array is not a valid two-dimensional array.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
*/
public static function transpose($array)
{
if (!is_array($array)) {
return false;
}
$ret = array();
foreach ($array as $k1 => $a2) {
if (!is_array($a2)) {
return false;
}
foreach ($a2 as $k2 => $v) {
if (!array_key_exists($k2, $ret)) {
$ret[$k2] = array();
}
$ret[$k2][$k1] = $v;
}
}
return $ret;
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace SimpleSAML\Utils;
/**
* Attribute-related utility methods.
*
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAML
*/
class Attributes
{
/**
* Look for an attribute in a normalized attributes array, failing if it's not there.
*
* @param array $attributes The normalized array containing attributes.
* @param string $expected The name of the attribute we are looking for.
* @param bool $allow_multiple Whether to allow multiple values in the attribute or not.
*
* @return mixed The value of the attribute we are expecting. If the attribute has multiple values and
* $allow_multiple is set to true, the first value will be returned.
*
* @throws \InvalidArgumentException If $attributes is not an array or $expected is not a string.
* @throws \SimpleSAML_Error_Exception If the expected attribute was not found in the attributes array.
*/
public static function getExpectedAttribute($attributes, $expected, $allow_multiple = false)
{
if (!is_array($attributes)) {
throw new \InvalidArgumentException(
'The attributes array is not an array, it is: '.print_r($attributes, true).'.'
);
}
if (!is_string($expected)) {
throw new \InvalidArgumentException(
'The expected attribute is not a string, it is: '.print_r($expected, true).'.'
);
}
if (!array_key_exists($expected, $attributes)) {
throw new \SimpleSAML_Error_Exception("No such attribute '".$expected."' found.");
}
$attribute = $attributes[$expected];
if (!is_array($attribute)) {
throw new \InvalidArgumentException('The attributes array is not normalized, values should be arrays.');
}
if (count($attribute) === 0) {
throw new \SimpleSAML_Error_Exception("Empty attribute '".$expected."'.'");
} elseif (count($attribute) > 1) {
if ($allow_multiple === false) {
throw new \SimpleSAML_Error_Exception(
'More than one value found for the attribute, multiple values not allowed.'
);
}
}
return reset($attribute);
}
/**
* Validate and normalize an array with attributes.
*
* This function takes in an associative array with attributes, and parses and validates
* this array. On success, it will return a normalized array, where each attribute name
* is an index to an array of one or more strings. On failure an exception will be thrown.
* This exception will contain an message describing what is wrong.
*
* @param array $attributes The array containing attributes that we should validate and normalize.
*
* @return array The normalized attributes array.
* @throws \InvalidArgumentException If input is not an array, array keys are not strings or attribute values are
* not strings.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function normalizeAttributesArray($attributes)
{
if (!is_array($attributes)) {
throw new \InvalidArgumentException(
'The attributes array is not an array, it is: '.print_r($attributes, true).'".'
);
}
$newAttrs = array();
foreach ($attributes as $name => $values) {
if (!is_string($name)) {
throw new \InvalidArgumentException('Invalid attribute name: "'.print_r($name, true).'".');
}
$values = Arrays::arrayize($values);
foreach ($values as $value) {
if (!is_string($value)) {
throw new \InvalidArgumentException(
'Invalid attribute value for attribute '.$name.': "'.print_r($value, true).'".'
);
}
}
$newAttrs[$name] = $values;
}
return $newAttrs;
}
/**
* Extract an attribute's namespace, or revert to default.
*
* This function takes in a namespaced attribute name and splits it in a namespace/attribute name tuple.
* When no namespace is found in the attribute name, it will be namespaced with the default namespace.
* This default namespace can be overriden by supplying a second parameter to this function.
*
* @param string $name The namespaced attribute name.
* @param string $defaultns The default namespace that should be used when no namespace is found.
*
* @return array The attribute name, split to the namespace and the actual attribute name.
*/
public static function getAttributeNamespace($name, $defaultns)
{
$slash = strrpos($name, '/');
if ($slash !== false) {
$defaultns = substr($name, 0, $slash);
$name = substr($name, $slash + 1);
}
return array(htmlspecialchars($defaultns), htmlspecialchars($name));
}
}

76
lib/SimpleSAML/Utils/Auth.php Executable file
View File

@@ -0,0 +1,76 @@
<?php
namespace SimpleSAML\Utils;
use SimpleSAML\Module;
/**
* Auth-related utility methods.
*
* @package SimpleSAMLphp
*/
class Auth
{
/**
* Retrieve a admin login URL.
*
* @param string|NULL $returnTo The URL the user should arrive on after admin authentication. Defaults to null.
*
* @return string A URL which can be used for admin authentication.
* @throws \InvalidArgumentException If $returnTo is neither a string nor null.
*/
public static function getAdminLoginURL($returnTo = null)
{
if (!(is_string($returnTo) || is_null($returnTo))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
if ($returnTo === null) {
$returnTo = HTTP::getSelfURL();
}
return Module::getModuleURL('core/login-admin.php', array('ReturnTo' => $returnTo));
}
/**
* Check whether the current user is admin.
*
* @return boolean True if the current user is an admin user, false otherwise.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isAdmin()
{
$session = \SimpleSAML_Session::getSessionFromRequest();
return $session->isValid('admin') || $session->isValid('login-admin');
}
/**
* Require admin access to the current page.
*
* This is a helper function for limiting a page to those with administrative access. It will redirect the user to
* a login page if the current user doesn't have admin access.
*
* @return void This function will only return if the user is admin.
* @throws \SimpleSAML_Error_Exception If no "admin" authentication source was configured.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function requireAdmin()
{
if (self::isAdmin()) {
return;
}
// not authenticated as admin user, start authentication
if (\SimpleSAML_Auth_Source::getById('admin') !== null) {
$as = new \SimpleSAML\Auth\Simple('admin');
$as->login();
} else {
throw new \SimpleSAML_Error_Exception(
'Cannot find "admin" auth source, and admin privileges are required.'
);
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace SimpleSAML\Utils;
/**
* Indicates an implementation caches state internally and may be cleared.
*
* Primarily designed to allow SSP state to be cleared between unit tests.
* @package SimpleSAML\Utils
*/
interface ClearableState
{
/**
* Clear any cached internal state.
*/
public static function clearInternalState();
}

92
lib/SimpleSAML/Utils/Config.php Executable file
View File

@@ -0,0 +1,92 @@
<?php
namespace SimpleSAML\Utils;
/**
* Utility class for SimpleSAMLphp configuration management and manipulation.
*
* @package SimpleSAMLphp
*/
class Config
{
/**
* Resolves a path that may be relative to the cert-directory.
*
* @param string $path The (possibly relative) path to the file.
*
* @return string The file path.
* @throws \InvalidArgumentException If $path is not a string.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getCertPath($path)
{
if (!is_string($path)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$globalConfig = \SimpleSAML_Configuration::getInstance();
$base = $globalConfig->getPathValue('certdir', 'cert/');
return System::resolvePath($path, $base);
}
/**
* Retrieve the secret salt.
*
* This function retrieves the value which is configured as the secret salt. It will check that the value exists
* and is set to a non-default value. If it isn't, an exception will be thrown.
*
* The secret salt can be used as a component in hash functions, to make it difficult to test all possible values
* in order to retrieve the original value. It can also be used as a simple method for signing data, by hashing the
* data together with the salt.
*
* @return string The secret salt.
* @throws \InvalidArgumentException If the secret salt hasn't been configured.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getSecretSalt()
{
$secretSalt = \SimpleSAML_Configuration::getInstance()->getString('secretsalt');
if ($secretSalt === 'defaultsecretsalt') {
throw new \InvalidArgumentException('The "secretsalt" configuration option must be set to a secret value.');
}
return $secretSalt;
}
/**
* Returns the path to the config dir
*
* If the SIMPLESAMLPHP_CONFIG_DIR environment variable has been set, it takes precedence over the default
* $simplesamldir/config directory.
*
* @return string The path to the configuration directory.
*/
public static function getConfigDir()
{
$configDir = dirname(dirname(dirname(__DIR__))) . '/config';
/** @var string|false $configDirEnv */
$configDirEnv = getenv('SIMPLESAMLPHP_CONFIG_DIR');
if($configDirEnv === false) {
$configDirEnv = getenv('REDIRECT_SIMPLESAMLPHP_CONFIG_DIR');
}
if ($configDirEnv !== false) {
if (!is_dir($configDirEnv)) {
throw new \InvalidArgumentException(
sprintf(
'Config directory specified by environment variable SIMPLESAMLPHP_CONFIG_DIR is not a ' .
'directory. Given: "%s"',
$configDirEnv
)
);
}
$configDir = $configDirEnv;
}
return $configDir;
}
}

View File

@@ -0,0 +1,282 @@
<?php
namespace SimpleSAML\Utils\Config;
/**
* Class with utilities to fetch different configuration objects from metadata configuration arrays.
*
* @package SimpleSAMLphp
* @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no>
*/
class Metadata
{
/**
* The string that identities Entity Categories.
*
* @var string
*/
public static $ENTITY_CATEGORY = 'http://macedir.org/entity-category';
/**
* The string the identifies the REFEDS "Hide From Discovery" Entity Category.
*
* @var string
*/
public static $HIDE_FROM_DISCOVERY = 'http://refeds.org/category/hide-from-discovery';
/**
* Valid options for the ContactPerson element
*
* The 'attributes' option isn't defined in section 2.3.2.2 of the OASIS document, but
* it is required to allow additons to the main contact person element for trust
* frameworks.
*
* @var array The valid configuration options for a contact configuration array.
* @see "Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0", section 2.3.2.2.
*/
public static $VALID_CONTACT_OPTIONS = array(
'contactType',
'emailAddress',
'givenName',
'surName',
'telephoneNumber',
'company',
'attributes',
);
/**
* @var array The valid types of contact for a contact configuration array.
* @see "Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0", section 2.3.2.2.
*/
public static $VALID_CONTACT_TYPES = array(
'technical',
'support',
'administrative',
'billing',
'other',
);
/**
* Parse and sanitize a contact from an array.
*
* Accepts an array with the following elements:
* - contactType The type of the contact (as string). Mandatory.
* - emailAddress Email address (as string), or array of email addresses. Optional.
* - telephoneNumber Telephone number of contact (as string), or array of telephone numbers. Optional.
* - name Full name of contact, either as <GivenName> <SurName>, or as <SurName>, <GivenName>. Optional.
* - surName Surname of contact (as string). Optional.
* - givenName Given name of contact (as string). Optional.
* - company Company name of contact (as string). Optional.
*
* The following values are allowed as "contactType":
* - technical
* - support
* - administrative
* - billing
* - other
*
* If given a "name" it will try to decompose it into its given name and surname, only if neither givenName nor
* surName are present. It works as follows:
* - "surname1 surname2, given_name1 given_name2"
* givenName: "given_name1 given_name2"
* surname: "surname1 surname2"
* - "given_name surname"
* givenName: "given_name"
* surname: "surname"
*
* otherwise it will just return the name as "givenName" in the resulting array.
*
* @param array $contact The contact to parse and sanitize.
*
* @return array An array holding valid contact configuration options. If a key 'name' was part of the input array,
* it will try to decompose the name into its parts, and place the parts into givenName and surName, if those are
* missing.
* @throws \InvalidArgumentException If $contact is neither an array nor null, or the contact does not conform to
* valid configuration rules for contacts.
*/
public static function getContact($contact)
{
if (!(is_array($contact) || is_null($contact))) {
throw new \InvalidArgumentException('Invalid input parameters');
}
// check the type
if (!isset($contact['contactType']) || !in_array($contact['contactType'], self::$VALID_CONTACT_TYPES, true)) {
$types = join(', ', array_map(
function ($t) {
return '"'.$t.'"';
},
self::$VALID_CONTACT_TYPES
));
throw new \InvalidArgumentException('"contactType" is mandatory and must be one of '.$types.".");
}
// check attributes is an associative array
if (isset($contact['attributes'])) {
if (empty($contact['attributes'])
|| !is_array($contact['attributes'])
|| count(array_filter(array_keys($contact['attributes']), 'is_string')) === 0
) {
throw new \InvalidArgumentException('"attributes" must be an array and cannot be empty.');
}
}
// try to fill in givenName and surName from name
if (isset($contact['name']) && !isset($contact['givenName']) && !isset($contact['surName'])) {
// first check if it's comma separated
$names = explode(',', $contact['name'], 2);
if (count($names) === 2) {
$contact['surName'] = preg_replace('/\s+/', ' ', trim($names[0]));
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($names[1]));
} else {
// check if it's in "given name surname" format
$names = explode(' ', preg_replace('/\s+/', ' ', trim($contact['name'])));
if (count($names) === 2) {
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($names[0]));
$contact['surName'] = preg_replace('/\s+/', ' ', trim($names[1]));
} else {
// nothing works, return it as given name
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($contact['name']));
}
}
}
// check givenName
if (isset($contact['givenName']) && (
empty($contact['givenName']) || !is_string($contact['givenName'])
)
) {
throw new \InvalidArgumentException('"givenName" must be a string and cannot be empty.');
}
// check surName
if (isset($contact['surName']) && (
empty($contact['surName']) || !is_string($contact['surName'])
)
) {
throw new \InvalidArgumentException('"surName" must be a string and cannot be empty.');
}
// check company
if (isset($contact['company']) && (
empty($contact['company']) || !is_string($contact['company'])
)
) {
throw new \InvalidArgumentException('"company" must be a string and cannot be empty.');
}
// check emailAddress
if (isset($contact['emailAddress'])) {
if (empty($contact['emailAddress']) ||
!(is_string($contact['emailAddress']) || is_array($contact['emailAddress']))
) {
throw new \InvalidArgumentException('"emailAddress" must be a string or an array and cannot be empty.');
}
if (is_array($contact['emailAddress'])) {
foreach ($contact['emailAddress'] as $address) {
if (!is_string($address) || empty($address)) {
throw new \InvalidArgumentException('Email addresses must be a string and cannot be empty.');
}
}
}
}
// check telephoneNumber
if (isset($contact['telephoneNumber'])) {
if (empty($contact['telephoneNumber']) ||
!(is_string($contact['telephoneNumber']) || is_array($contact['telephoneNumber']))
) {
throw new \InvalidArgumentException(
'"telephoneNumber" must be a string or an array and cannot be empty.'
);
}
if (is_array($contact['telephoneNumber'])) {
foreach ($contact['telephoneNumber'] as $address) {
if (!is_string($address) || empty($address)) {
throw new \InvalidArgumentException('Telephone numbers must be a string and cannot be empty.');
}
}
}
}
// make sure only valid options are outputted
return array_intersect_key($contact, array_flip(self::$VALID_CONTACT_OPTIONS));
}
/**
* Find the default endpoint in an endpoint array.
*
* @param array $endpoints An array with endpoints.
* @param array $bindings An array with acceptable bindings. Can be null if any binding is allowed.
*
* @return array|NULL The default endpoint, or null if no acceptable endpoints are used.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getDefaultEndpoint(array $endpoints, array $bindings = null)
{
$firstNotFalse = null;
$firstAllowed = null;
// look through the endpoint list for acceptable endpoints
foreach ($endpoints as $ep) {
if ($bindings !== null && !in_array($ep['Binding'], $bindings, true)) {
// unsupported binding, skip it
continue;
}
if (isset($ep['isDefault'])) {
if ($ep['isDefault'] === true) {
// this is the first endpoint with isDefault set to true
return $ep;
}
// isDefault is set to false, but the endpoint is still usable as a last resort
if ($firstAllowed === null) {
// this is the first endpoint that we can use
$firstAllowed = $ep;
}
} else {
if ($firstNotFalse === null) {
// this is the first endpoint without isDefault set
$firstNotFalse = $ep;
}
}
}
if ($firstNotFalse !== null) {
// we have an endpoint without isDefault set to false
return $firstNotFalse;
}
/* $firstAllowed either contains the first endpoint we can use, or it contains null if we cannot use any of the
* endpoints. Either way we return its value.
*/
return $firstAllowed;
}
/**
* Determine if an entity should be hidden in the discovery service.
*
* This method searches for the "Hide From Discovery" REFEDS Entity Category, and tells if the entity should be
* hidden or not depending on it.
*
* @see https://refeds.org/category/hide-from-discovery
*
* @param array $metadata An associative array with the metadata representing an entity.
*
* @return boolean True if the entity should be hidden, false otherwise.
*/
public static function isHiddenFromDiscovery(array $metadata)
{
\SimpleSAML\Logger::maskErrors(E_ALL);
$hidden = in_array(self::$HIDE_FROM_DISCOVERY, $metadata['EntityAttributes'][self::$ENTITY_CATEGORY], true);
\SimpleSAML\Logger::popErrorMask();
return $hidden === true;
}
}

471
lib/SimpleSAML/Utils/Crypto.php Executable file
View File

@@ -0,0 +1,471 @@
<?php
namespace SimpleSAML\Utils;
/**
* A class for cryptography-related functions.
*
* @package SimpleSAMLphp
*/
class Crypto
{
/**
* Decrypt data using AES-256-CBC and the key provided as a parameter.
*
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
* @param string $secret The secret to use to decrypt the data.
*
* @return string The decrypted data.
* @throws \InvalidArgumentException If $ciphertext is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @see \SimpleSAML\Utils\Crypto::aesDecrypt()
*/
private static function _aesDecrypt($ciphertext, $secret)
{
if (!is_string($ciphertext)) {
throw new \InvalidArgumentException(
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
);
}
/** @var int $len */
$len = mb_strlen($ciphertext, '8bit');
if ($len < 48) {
throw new \InvalidArgumentException(
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
);
}
if (!function_exists("openssl_decrypt")) {
throw new \SimpleSAML_Error_Exception("The openssl PHP module is not loaded.");
}
// derive encryption and authentication keys from the secret
$key = openssl_digest($secret, 'sha512');
$hmac = mb_substr($ciphertext, 0, 32, '8bit');
$iv = mb_substr($ciphertext, 32, 16, '8bit');
$msg = mb_substr($ciphertext, 48, $len - 48, '8bit');
// authenticate the ciphertext
if (self::secureCompare(hash_hmac('sha256', $iv.$msg, substr($key, 64, 64), true), $hmac)) {
$plaintext = openssl_decrypt(
$msg,
'AES-256-CBC',
substr($key, 0, 64),
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
$iv
);
if ($plaintext !== false) {
return $plaintext;
}
}
throw new \SimpleSAML_Error_Exception("Failed to decrypt ciphertext.");
}
/**
* Decrypt data using AES-256-CBC and the system-wide secret salt as key.
*
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
*
* @return string The decrypted data.
* @throws \InvalidArgumentException If $ciphertext is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function aesDecrypt($ciphertext)
{
return self::_aesDecrypt($ciphertext, Config::getSecretSalt());
}
/**
* Encrypt data using AES-256-CBC and the key provided as a parameter.
*
* @param string $data The data to encrypt.
* @param string $secret The secret to use to encrypt the data.
*
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
* @throws \InvalidArgumentException If $data is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @see \SimpleSAML\Utils\Crypto::aesEncrypt()
*/
private static function _aesEncrypt($data, $secret)
{
if (!is_string($data)) {
throw new \InvalidArgumentException('Input parameter "$data" must be a string.');
}
if (!function_exists("openssl_encrypt")) {
throw new \SimpleSAML_Error_Exception('The openssl PHP module is not loaded.');
}
// derive encryption and authentication keys from the secret
$key = openssl_digest($secret, 'sha512');
// generate a random IV
$iv = openssl_random_pseudo_bytes(16);
// encrypt the message
/** @var string|false $ciphertext */
$ciphertext = openssl_encrypt(
$data,
'AES-256-CBC',
substr($key, 0, 64),
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
$iv
);
if ($ciphertext === false) {
throw new \SimpleSAML_Error_Exception("Failed to encrypt plaintext.");
}
// return the ciphertext with proper authentication
return hash_hmac('sha256', $iv.$ciphertext, substr($key, 64, 64), true).$iv.$ciphertext;
}
/**
* Encrypt data using AES-256-CBC and the system-wide secret salt as key.
*
* @param string $data The data to encrypt.
*
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
* @throws \InvalidArgumentException If $data is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function aesEncrypt($data)
{
return self::_aesEncrypt($data, Config::getSecretSalt());
}
/**
* Convert data from DER to PEM encoding.
*
* @param string $der Data encoded in DER format.
* @param string $type The type of data we are encoding, as expressed by the PEM header. Defaults to "CERTIFICATE".
* @return string The same data encoded in PEM format.
* @see RFC7648 for known types and PEM format specifics.
*/
public static function der2pem($der, $type = 'CERTIFICATE')
{
return "-----BEGIN ".$type."-----\n".
chunk_split(base64_encode($der), 64, "\n").
"-----END ".$type."-----\n";
}
/**
* Load a private key from metadata.
*
* This function loads a private key from a metadata array. It looks for the following elements:
* - 'privatekey': Name of a private key file in the cert-directory.
* - 'privatekey_pass': Password for the private key.
*
* It returns and array with the following elements:
* - 'PEM': Data for the private key, in PEM-format.
* - 'password': Password for the private key.
*
* @param \SimpleSAML_Configuration $metadata The metadata array the private key should be loaded from.
* @param bool $required Whether the private key is required. If this is true, a
* missing key will cause an exception. Defaults to false.
* @param string $prefix The prefix which should be used when reading from the metadata
* array. Defaults to ''.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
*
* @return array|NULL Extracted private key, or NULL if no private key is present.
* @throws \InvalidArgumentException If $required is not boolean or $prefix is not a string.
* @throws \SimpleSAML_Error_Exception If no private key is found in the metadata, or it was not possible to load
* it.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function loadPrivateKey(\SimpleSAML_Configuration $metadata, $required = false, $prefix = '', $full_path = false)
{
if (!is_bool($required) || !is_string($prefix) || !is_bool($full_path)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$file = $metadata->getString($prefix.'privatekey', null);
if ($file === null) {
// no private key found
if ($required) {
throw new \SimpleSAML_Error_Exception('No private key found in metadata.');
} else {
return null;
}
}
if (!$full_path) {
$file = Config::getCertPath($file);
}
$data = @file_get_contents($file);
if ($data === false) {
throw new \SimpleSAML_Error_Exception('Unable to load private key from file "'.$file.'"');
}
$ret = array(
'PEM' => $data,
);
if ($metadata->hasValue($prefix.'privatekey_pass')) {
$ret['password'] = $metadata->getString($prefix.'privatekey_pass');
}
return $ret;
}
/**
* Get public key or certificate from metadata.
*
* This function implements a function to retrieve the public key or certificate from a metadata array.
*
* It will search for the following elements in the metadata:
* - 'certData': The certificate as a base64-encoded string.
* - 'certificate': A file with a certificate or public key in PEM-format.
* - 'certFingerprint': The fingerprint of the certificate. Can be a single fingerprint, or an array of multiple
* valid fingerprints. (deprecated)
*
* This function will return an array with these elements:
* - 'PEM': The public key/certificate in PEM-encoding.
* - 'certData': The certificate data, base64 encoded, on a single line. (Only present if this is a certificate.)
* - 'certFingerprint': Array of valid certificate fingerprints. (Deprecated. Only present if this is a
* certificate.)
*
* @param \SimpleSAML_Configuration $metadata The metadata.
* @param bool $required Whether the private 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|NULL Public key or certificate data, or NULL if no public key or certificate was found.
* @throws \InvalidArgumentException If $metadata is not an instance of \SimpleSAML_Configuration, $required is not
* boolean or $prefix is not a string.
* @throws \SimpleSAML_Error_Exception If no private key is found in the metadata, or it was not possible to load
* it.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Lasse Birnbaum Jensen
*/
public static function loadPublicKey(\SimpleSAML_Configuration $metadata, $required = false, $prefix = '')
{
if (!is_bool($required) || !is_string($prefix)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$keys = $metadata->getPublicKeys(null, false, $prefix);
if (!empty($keys)) {
foreach ($keys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
if ($key['signing'] !== true) {
continue;
}
$certData = $key['X509Certificate'];
$pem = "-----BEGIN CERTIFICATE-----\n".
chunk_split($certData, 64).
"-----END CERTIFICATE-----\n";
$certFingerprint = strtolower(sha1(base64_decode($certData)));
return array(
'certData' => $certData,
'PEM' => $pem,
'certFingerprint' => array($certFingerprint),
);
}
// no valid key found
} elseif ($metadata->hasValue($prefix.'certFingerprint')) {
// we only have a fingerprint available
$fps = $metadata->getArrayizeString($prefix.'certFingerprint');
// normalize fingerprint(s) - lowercase and no colons
foreach ($fps as &$fp) {
assert(is_string($fp));
$fp = strtolower(str_replace(':', '', $fp));
}
/*
* We can't build a full certificate from a fingerprint, and may as well return an array with only the
* fingerprint(s) immediately.
*/
return array('certFingerprint' => $fps);
}
// no public key/certificate available
if ($required) {
throw new \SimpleSAML_Error_Exception('No public key / certificate found in metadata.');
} else {
return null;
}
}
/**
* Convert from PEM to DER encoding.
*
* @param string $pem Data encoded in PEM format.
* @return string The same data encoded in DER format.
* @throws \InvalidArgumentException If $pem is not encoded in PEM format.
* @see RFC7648 for PEM format specifics.
*/
public static function pem2der($pem)
{
$pem = trim($pem);
$begin = "-----BEGIN ";
$end = "-----END ";
$lines = explode("\n", $pem);
$last = count($lines) - 1;
if (strpos($lines[0], $begin) !== 0) {
throw new \InvalidArgumentException("pem2der: input is not encoded in PEM format.");
}
unset($lines[0]);
if (strpos($lines[$last], $end) !== 0) {
throw new \InvalidArgumentException("pem2der: input is not encoded in PEM format.");
}
unset($lines[$last]);
return base64_decode(implode($lines));
}
/**
* This function hashes a password with a given algorithm.
*
* @param string $password The password to hash.
* @param string $algorithm The hashing algorithm, uppercase, optionally prepended with 'S' (salted). See
* hash_algos() for a complete list of hashing algorithms.
* @param string $salt An optional salt to use.
*
* @return string The hashed password.
* @throws \InvalidArgumentException If the input parameters are not strings.
* @throws \SimpleSAML_Error_Exception If the algorithm specified is not supported.
*
* @see hash_algos()
*
* @author Dyonisius Visser, TERENA <visser@terena.org>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function pwHash($password, $algorithm, $salt = null)
{
if (!is_string($algorithm) || !is_string($password)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// hash w/o salt
if (in_array(strtolower($algorithm), hash_algos(), true)) {
$alg_str = '{'.str_replace('SHA1', 'SHA', $algorithm).'}'; // LDAP compatibility
$hash = hash(strtolower($algorithm), $password, true);
return $alg_str.base64_encode($hash);
}
// hash w/ salt
if ($salt === null) { // no salt provided, generate one
// default 8 byte salt, but 4 byte for LDAP SHA1 hashes
$bytes = ($algorithm == 'SSHA1') ? 4 : 8;
$salt = openssl_random_pseudo_bytes($bytes);
}
if ($algorithm[0] == 'S' && in_array(substr(strtolower($algorithm), 1), hash_algos(), true)) {
$alg = substr(strtolower($algorithm), 1); // 'sha256' etc
$alg_str = '{'.str_replace('SSHA1', 'SSHA', $algorithm).'}'; // LDAP compatibility
$hash = hash($alg, $password.$salt, true);
return $alg_str.base64_encode($hash.$salt);
}
throw new \SimpleSAML_Error_Exception('Hashing algorithm \''.strtolower($algorithm).'\' is not supported');
}
/**
* Compare two strings securely.
*
* This method checks if two strings are equal in constant time, avoiding timing attacks. Use it every time we need
* to compare a string with a secret that shouldn't be leaked, i.e. when verifying passwords, one-time codes, etc.
*
* @param string $known A known string.
* @param string $user A user-provided string to compare with the known string.
*
* @return bool True if both strings are equal, false otherwise.
*/
public static function secureCompare($known, $user)
{
if (function_exists('hash_equals')) {
// use hash_equals() if available (PHP >= 5.6)
return hash_equals($known, $user);
}
// compare manually in constant time
$len = mb_strlen($known, '8bit'); // see mbstring.func_overload
if ($len !== mb_strlen($user, '8bit')) {
return false; // length differs
}
$diff = 0;
for ($i = 0; $i < $len; $i++) {
$diff |= ord($known[$i]) ^ ord($user[$i]);
}
// if all the bytes in $a and $b are identical, $diff should be equal to 0
return $diff === 0;
}
/**
* This function checks if a password is valid
*
* @param string $hash The password as it appears in password file, optionally prepended with algorithm.
* @param string $password The password to check in clear.
*
* @return boolean True if the hash corresponds with the given password, false otherwise.
* @throws \InvalidArgumentException If the input parameters are not strings.
* @throws \SimpleSAML_Error_Exception If the algorithm specified is not supported.
*
* @author Dyonisius Visser, TERENA <visser@terena.org>
*/
public static function pwValid($hash, $password)
{
if (!is_string($hash) || !is_string($password)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// match algorithm string (e.g. '{SSHA256}', '{MD5}')
if (preg_match('/^{(.*?)}(.*)$/', $hash, $matches)) {
// LDAP compatibility
$alg = preg_replace('/^(S?SHA)$/', '${1}1', $matches[1]);
// hash w/o salt
if (in_array(strtolower($alg), hash_algos(), true)) {
return self::secureCompare($hash, self::pwHash($password, $alg));
}
// hash w/ salt
if ($alg[0] === 'S' && in_array(substr(strtolower($alg), 1), hash_algos(), true)) {
$php_alg = substr(strtolower($alg), 1);
// get hash length of this algorithm to learn how long the salt is
$hash_length = strlen(hash($php_alg, '', true));
$salt = substr(base64_decode($matches[2]), $hash_length);
return self::secureCompare($hash, self::pwHash($password, $alg, $salt));
}
} else {
return $hash === $password;
}
throw new \SimpleSAML_Error_Exception('Hashing algorithm \''.strtolower($alg).'\' is not supported');
}
}

1222
lib/SimpleSAML/Utils/HTTP.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
<?php
namespace SimpleSAML\Utils;
/**
* Provides a non-static wrapper for the HTTP utility class.
*
* @package SimpleSAML\Utils
*/
class HttpAdapter
{
/**
* @see HTTP::getServerHTTPS()
*/
public function getServerHTTPS()
{
return HTTP::getServerHTTPS();
}
/**
* @see HTTP::getServerPort()
*/
public function getServerPort()
{
return HTTP::getServerPort();
}
/**
* @see HTTP::addURLParameters()
*/
public function addURLParameters($url, $parameters)
{
return HTTP::addURLParameters($url, $parameters);
}
/**
* @see HTTP::checkSessionCookie()
*/
public function checkSessionCookie($retryURL = null)
{
HTTP::checkSessionCookie($retryURL);
}
/**
* @see HTTP::checkURLAllowed()
*/
public function checkURLAllowed($url, array $trustedSites = null)
{
return HTTP::checkURLAllowed($url, $trustedSites);
}
/**
* @see HTTP::fetch()
*/
public function fetch($url, $context = array(), $getHeaders = false)
{
return HTTP::fetch($url, $context, $getHeaders);
}
/**
* @see HTTP::getAcceptLanguage()
*/
public function getAcceptLanguage()
{
return HTTP::getAcceptLanguage();
}
/**
* @see HTTP::guessBasePath()
*/
public function guessBasePath()
{
return HTTP::guessBasePath();
}
/**
* @see HTTP::getBaseURL()
*/
public function getBaseURL()
{
return HTTP::getBaseURL();
}
/**
* @see HTTP::getFirstPathElement()
*/
public function getFirstPathElement($trailingslash = true)
{
return HTTP::getFirstPathElement($trailingslash);
}
/**
* @see HTTP::getPOSTRedirectURL()
*/
public function getPOSTRedirectURL($destination, $data)
{
return HTTP::getPOSTRedirectURL($destination, $data);
}
/**
* @see HTTP::getSelfHost()
*/
public function getSelfHost()
{
return HTTP::getSelfHost();
}
/**
* @see HTTP::getSelfHostWithNonStandardPort()
*/
public function getSelfHostWithNonStandardPort()
{
return HTTP::getSelfHostWithNonStandardPort();
}
/**
* @see HTTP::getSelfHostWithPath()
*/
public function getSelfHostWithPath()
{
return HTTP::getSelfHostWithPath();
}
/**
* @see HTTP::getSelfURL()
*/
public function getSelfURL()
{
return HTTP::getSelfURL();
}
/**
* @see HTTP::getSelfURLHost()
*/
public function getSelfURLHost()
{
return HTTP::getSelfURLHost();
}
/**
* @see HTTP::getSelfURLNoQuery()
*/
public function getSelfURLNoQuery()
{
return HTTP::getSelfURLNoQuery();
}
/**
* @see HTTP::isHTTPS()
*/
public function isHTTPS()
{
return HTTP::isHTTPS();
}
/**
* @see HTTP::normalizeURL()
*/
public function normalizeURL($url)
{
return HTTP::normalizeURL($url);
}
/**
* @see HTTP::parseQueryString()
*/
public function parseQueryString($query_string)
{
return HTTP::parseQueryString($query_string);
}
/**
* @see HTTP::redirectTrustedURL()
*/
public function redirectTrustedURL($url, $parameters = array())
{
HTTP::redirectTrustedURL($url, $parameters);
}
/**
* @see HTTP::redirectUntrustedURL()
*/
public function redirectUntrustedURL($url, $parameters = array())
{
HTTP::redirectUntrustedURL($url, $parameters);
}
/**
* @see HTTP::resolveURL()
*/
public function resolveURL($url, $base = null)
{
return HTTP::resolveURL($url, $base);
}
/**
* @see HTTP::setCookie()
*/
public function setCookie($name, $value, $params = null, $throw = true)
{
HTTP::setCookie($name, $value, $params, $throw);
}
/**
* @see HTTP::submitPOSTData()
*/
public function submitPOSTData($destination, $data)
{
HTTP::submitPOSTData($destination, $data);
}
}

85
lib/SimpleSAML/Utils/Net.php Executable file
View File

@@ -0,0 +1,85 @@
<?php
namespace SimpleSAML\Utils;
/**
* Net-related utility methods.
*
* @package SimpleSAMLphp
*/
class Net
{
/**
* Check whether an IP address is part of a CIDR.
*
* @param string $cidr The network CIDR address.
* @param string $ip The IP address to check. Optional. Current remote address will be used if none specified. Do
* not rely on default parameter if running behind load balancers.
*
* @return boolean True if the IP address belongs to the specified CIDR, false otherwise.
*
* @author Andreas Åkre Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Brook Schofield, GÉANT
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function ipCIDRcheck($cidr, $ip = null)
{
if ($ip === null) {
$ip = $_SERVER['REMOTE_ADDR'];
}
if (strpos($cidr, '/') === false) {
return false;
}
list ($net, $mask) = explode('/', $cidr);
$mask = intval($mask);
$ip_ip = array();
$ip_net = array();
if (strstr($ip, ':') || strstr($net, ':')) {
// Validate IPv6 with inet_pton, convert to hex with bin2hex
// then store as a long with hexdec
$ip_pack = @inet_pton($ip);
$net_pack = @inet_pton($net);
if ($ip_pack === false || $net_pack === false) {
// not valid IPv6 address (warning silenced)
return false;
}
$ip_ip = str_split(bin2hex($ip_pack), 8);
foreach ($ip_ip as &$value) {
$value = hexdec($value);
}
$ip_net = str_split(bin2hex($net_pack), 8);
foreach ($ip_net as &$value) {
$value = hexdec($value);
}
} else {
$ip_ip[0] = ip2long($ip);
$ip_net[0] = ip2long($net);
}
for ($i = 0; $mask > 0 && $i < sizeof($ip_ip); $i++) {
if ($mask > 32) {
$iteration_mask = 32;
} else {
$iteration_mask = $mask;
}
$mask -= 32;
$ip_mask = ~((1 << (32 - $iteration_mask)) - 1);
$ip_net_mask = $ip_net[$i] & $ip_mask;
$ip_ip_mask = $ip_ip[$i] & $ip_mask;
if ($ip_ip_mask != $ip_net_mask) {
return false;
}
}
return true;
}
}

30
lib/SimpleSAML/Utils/Random.php Executable file
View File

@@ -0,0 +1,30 @@
<?php
namespace SimpleSAML\Utils;
/**
* Utility class for random data generation and manipulation.
*
* @package SimpleSAMLphp
*/
class Random
{
/**
* The fixed length of random identifiers.
*/
const ID_LENGTH = 43;
/**
* Generate a random identifier, ID_LENGTH bytes long.
*
* @return string A ID_LENGTH-bytes long string with a random, hex-encoded string.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function generateID()
{
return '_'.bin2hex(openssl_random_pseudo_bytes((int)((self::ID_LENGTH - 1)/2)));
}
}

239
lib/SimpleSAML/Utils/System.php Executable file
View File

@@ -0,0 +1,239 @@
<?php
namespace SimpleSAML\Utils;
/**
* System-related utility methods.
*
* @package SimpleSAMLphp
*/
class System
{
const WINDOWS = 1;
const LINUX = 2;
const OSX = 3;
const HPUX = 4;
const UNIX = 5;
const BSD = 6;
const IRIX = 7;
const SUNOS = 8;
/**
* This function returns the Operating System we are running on.
*
* @return mixed A predefined constant identifying the OS we are running on. False if we are unable to determine it.
*
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function getOS()
{
if (stristr(PHP_OS, 'LINUX')) {
return self::LINUX;
}
if (stristr(PHP_OS, 'DARWIN')) {
return self::OSX;
}
if (stristr(PHP_OS, 'WIN')) {
return self::WINDOWS;
}
if (stristr(PHP_OS, 'BSD')) {
return self::BSD;
}
if (stristr(PHP_OS, 'UNIX')) {
return self::UNIX;
}
if (stristr(PHP_OS, 'HP-UX')) {
return self::HPUX;
}
if (stristr(PHP_OS, 'IRIX')) {
return self::IRIX;
}
if (stristr(PHP_OS, 'SUNOS')) {
return self::SUNOS;
}
return false;
}
/**
* This function retrieves the path to a directory where temporary files can be saved.
*
* @return string Path to a temporary directory, without a trailing directory separator.
* @throws \SimpleSAML_Error_Exception If the temporary directory cannot be created or it exists and does not belong
* to the current user.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function getTempDir()
{
$globalConfig = \SimpleSAML_Configuration::getInstance();
$tempDir = rtrim(
$globalConfig->getString(
'tempdir',
sys_get_temp_dir().DIRECTORY_SEPARATOR.'simplesaml'
),
DIRECTORY_SEPARATOR
);
if (!is_dir($tempDir)) {
if (!mkdir($tempDir, 0700, true)) {
$error = error_get_last();
throw new \SimpleSAML_Error_Exception(
'Error creating temporary directory "'.$tempDir.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
} elseif (function_exists('posix_getuid')) {
// check that the owner of the temp directory is the current user
$stat = lstat($tempDir);
if ($stat['uid'] !== posix_getuid()) {
throw new \SimpleSAML_Error_Exception(
'Temporary directory "'.$tempDir.'" does not belong to the current user.'
);
}
}
return $tempDir;
}
/**
* Resolve a (possibly) relative path from the given base path.
*
* A path which starts with a '/' is assumed to be absolute, all others are assumed to be
* relative. The default base path is the root of the SimpleSAMLphp installation.
*
* @param string $path The path we should resolve.
* @param string|null $base The base path, where we should search for $path from. Default value is the root of the
* SimpleSAMLphp installation.
*
* @return string An absolute path referring to $path.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function resolvePath($path, $base = null)
{
if ($base === null) {
$config = \SimpleSAML_Configuration::getInstance();
$base = $config->getBaseDir();
}
// normalise directory separator
$base = str_replace('\\', '/', $base);
$path = str_replace('\\', '/', $path);
// remove trailing slashes
$base = rtrim($base, '/');
$path = rtrim($path, '/');
// check for absolute path
if (substr($path, 0, 1) === '/') {
// absolute path. */
$ret = '/';
} elseif (static::pathContainsDriveLetter($path)) {
$ret = '';
} else {
// path relative to base
$ret = $base;
}
$path = explode('/', $path);
foreach ($path as $d) {
if ($d === '.') {
continue;
} elseif ($d === '..') {
$ret = dirname($ret);
} else {
if ($ret && substr($ret, -1) !== '/') {
$ret .= '/';
}
$ret .= $d;
}
}
return $ret;
}
/**
* Atomically write a file.
*
* This is a helper function for writing data atomically to a file. It does this by writing the file data to a
* temporary file, then renaming it to the required file name.
*
* @param string $filename The path to the file we want to write to.
* @param string $data The data we should write to the file.
* @param int $mode The permissions to apply to the file. Defaults to 0600.
*
* @throws \InvalidArgumentException If any of the input parameters doesn't have the proper types.
* @throws \SimpleSAML_Error_Exception If the file cannot be saved, permissions cannot be changed or it is not
* possible to write to the target file.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Andjelko Horvat
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*
* @return void
*/
public static function writeFile($filename, $data, $mode = 0600)
{
if (!is_string($filename) || !is_string($data) || !is_numeric($mode)) {
throw new \InvalidArgumentException('Invalid input parameters');
}
$tmpFile = self::getTempDir().DIRECTORY_SEPARATOR.rand();
$res = @file_put_contents($tmpFile, $data);
if ($res === false) {
$error = error_get_last();
throw new \SimpleSAML_Error_Exception(
'Error saving file "'.$tmpFile.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
if (self::getOS() !== self::WINDOWS) {
if (!chmod($tmpFile, $mode)) {
unlink($tmpFile);
$error = error_get_last();
//$error = (is_array($error) ? $error['message'] : 'no error available');
throw new \SimpleSAML_Error_Exception(
'Error changing file mode of "'.$tmpFile.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
}
if (!rename($tmpFile, $filename)) {
unlink($tmpFile);
$error = error_get_last();
throw new \SimpleSAML_Error_Exception(
'Error moving "'.$tmpFile.'" to "'.$filename.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
if (function_exists('opcache_invalidate')) {
opcache_invalidate($filename);
}
}
/**
* Check if the supplied path contains a Windows-style drive letter.
*
* @param string $path
*
* @return bool
*/
private static function pathContainsDriveLetter($path)
{
$letterAsciiValue = ord(strtoupper(substr($path, 0, 1)));
return substr($path, 1, 1) === ':'
&& $letterAsciiValue >= 65 && $letterAsciiValue <= 90;
}
}

167
lib/SimpleSAML/Utils/Time.php Executable file
View File

@@ -0,0 +1,167 @@
<?php
/**
* Time-related utility methods.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Utils;
use SimpleSAML\Logger;
class Time
{
/**
* Whether the timezone has been initialized or not.
*
* @var bool
*/
private static $tz_initialized = false;
/**
* This function generates a timestamp on the form used by the SAML protocols.
*
* @param int $instant The time the timestamp should represent. Defaults to current time.
*
* @return string The timestamp.
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function generateTimestamp($instant = null)
{
if ($instant === null) {
$instant = time();
}
return gmdate('Y-m-d\TH:i:s\Z', $instant);
}
/**
* Initialize the timezone.
*
* This function should be called before any calls to date().
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*
* @throws \SimpleSAML_Error_Exception If the timezone set in the configuration is invalid.
*
* @return void
*/
public static function initTimezone()
{
if (self::$tz_initialized) {
return;
}
$globalConfig = \SimpleSAML_Configuration::getInstance();
$timezone = $globalConfig->getString('timezone', null);
if ($timezone !== null) {
if (!date_default_timezone_set($timezone)) {
throw new \SimpleSAML_Error_Exception('Invalid timezone set in the "timezone" option in config.php.');
}
self::$tz_initialized = true;
return;
}
// we don't have a timezone configured
Logger::maskErrors(E_ALL);
$serverTimezone = date_default_timezone_get();
Logger::popErrorMask();
// set the timezone to the default
date_default_timezone_set($serverTimezone);
self::$tz_initialized = true;
}
/**
* Interpret a ISO8601 duration value relative to a given timestamp. Please note no fractions are allowed, neither
* durations specified in the formats PYYYYMMDDThhmmss nor P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss].
*
* @param string $duration The duration, as a string.
* @param int $timestamp The unix timestamp we should apply the duration to. Optional, default to the current
* time.
*
* @return int The new timestamp, after the duration is applied.
* @throws \InvalidArgumentException If $duration is not a valid ISO 8601 duration or if the input parameters do
* not have the right data types.
*/
public static function parseDuration($duration, $timestamp = null)
{
if (!(is_string($duration) && (is_int($timestamp) || is_null($timestamp)))) {
throw new \InvalidArgumentException('Invalid input parameters');
}
// parse the duration. We use a very strict pattern
$durationRegEx = '#^(-?)P(?:(?:(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)'.
'(?:[.,]\d+)?S)?)?)|(?:(\\d+)W))$#D';
if (!preg_match($durationRegEx, $duration, $matches)) {
throw new \InvalidArgumentException('Invalid ISO 8601 duration: '.$duration);
}
$durYears = (empty($matches[2]) ? 0 : (int) $matches[2]);
$durMonths = (empty($matches[3]) ? 0 : (int) $matches[3]);
$durDays = (empty($matches[4]) ? 0 : (int) $matches[4]);
$durHours = (empty($matches[5]) ? 0 : (int) $matches[5]);
$durMinutes = (empty($matches[6]) ? 0 : (int) $matches[6]);
$durSeconds = (empty($matches[7]) ? 0 : (int) $matches[7]);
$durWeeks = (empty($matches[8]) ? 0 : (int) $matches[8]);
if (!empty($matches[1])) {
// negative
$durYears = -$durYears;
$durMonths = -$durMonths;
$durDays = -$durDays;
$durHours = -$durHours;
$durMinutes = -$durMinutes;
$durSeconds = -$durSeconds;
$durWeeks = -$durWeeks;
}
if ($timestamp === null) {
$timestamp = time();
}
if ($durYears !== 0 || $durMonths !== 0) {
/* Special handling of months and years, since they aren't a specific interval, but
* instead depend on the current time.
*/
/* We need the year and month from the timestamp. Unfortunately, PHP doesn't have the
* gmtime function. Instead we use the gmdate function, and split the result.
*/
$yearmonth = explode(':', gmdate('Y:n', $timestamp));
$year = (int) ($yearmonth[0]);
$month = (int) ($yearmonth[1]);
// remove the year and month from the timestamp
$timestamp -= gmmktime(0, 0, 0, $month, 1, $year);
// add years and months, and normalize the numbers afterwards
$year += $durYears;
$month += $durMonths;
while ($month > 12) {
$year += 1;
$month -= 12;
}
while ($month < 1) {
$year -= 1;
$month += 12;
}
// add year and month back into timestamp
$timestamp += gmmktime(0, 0, 0, $month, 1, $year);
}
// add the other elements
$timestamp += $durWeeks * 7 * 24 * 60 * 60;
$timestamp += $durDays * 24 * 60 * 60;
$timestamp += $durHours * 60 * 60;
$timestamp += $durMinutes * 60;
$timestamp += $durSeconds;
return $timestamp;
}
}

453
lib/SimpleSAML/Utils/XML.php Executable file
View File

@@ -0,0 +1,453 @@
<?php
/**
* Utility class for XML and DOM manipulation.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Utils;
use SimpleSAML\Logger;
use SimpleSAML\XML\Errors;
class XML
{
/**
* This function performs some sanity checks on XML documents, and optionally validates them against their schema
* if the 'validatexml' debugging option is enabled. A warning will be printed to the log if validation fails.
*
* @param string $message The SAML document we want to check.
* @param string $type The type of document. Can be one of:
* - 'saml20'
* - 'saml11'
* - 'saml-meta'
*
* @throws \InvalidArgumentException If $message is not a string or $type is not a string containing one of the
* values allowed.
* @throws \SimpleSAML_Error_Exception If $message contains a doctype declaration.
*
* @return void
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function checkSAMLMessage($message, $type)
{
$allowed_types = array('saml20', 'saml11', 'saml-meta');
if (!(is_string($message) && in_array($type, $allowed_types, true))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// a SAML message should not contain a doctype-declaration
if (strpos($message, '<!DOCTYPE') !== false) {
throw new \SimpleSAML_Error_Exception('XML contained a doctype declaration.');
}
// see if debugging is enabled for XML validation
$debug = \SimpleSAML_Configuration::getInstance()->getArrayize('debug', array('validatexml' => false));
$enabled = \SimpleSAML_Configuration::getInstance()->getBoolean('debug.validatexml', false);
if (!(in_array('validatexml', $debug, true) // implicitly enabled
|| (array_key_exists('validatexml', $debug) && $debug['validatexml'] === true) // explicitly enabled
// TODO: deprecate this option and remove it in 2.0
|| $enabled // old 'debug.validatexml' configuration option
)) {
// XML validation is disabled
return;
}
$result = true;
switch ($type) {
case 'saml11':
$result = self::isValid($message, 'oasis-sstc-saml-schema-protocol-1.1.xsd');
break;
case 'saml20':
$result = self::isValid($message, 'saml-schema-protocol-2.0.xsd');
break;
case 'saml-meta':
$result = self::isValid($message, 'saml-schema-metadata-2.0.xsd');
}
if ($result !== true) {
Logger::warning($result);
}
}
/**
* Helper function to log SAML messages that we send or receive.
*
* @param string|\DOMElement $message The message, as an string containing the XML or an XML element.
* @param string $type Whether this message is sent or received, encrypted or decrypted. The following
* values are supported:
* - 'in': for messages received.
* - 'out': for outgoing messages.
* - 'decrypt': for decrypted messages.
* - 'encrypt': for encrypted messages.
*
* @throws \InvalidArgumentException If $type is not a string or $message is neither a string nor a \DOMElement.
*
* @return void
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function debugSAMLMessage($message, $type)
{
if (!(is_string($type) && (is_string($message) || $message instanceof \DOMElement))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// see if debugging is enabled for SAML messages
$debug = \SimpleSAML_Configuration::getInstance()->getArrayize('debug', array('saml' => false));
if (!(in_array('saml', $debug, true) // implicitly enabled
|| (array_key_exists('saml', $debug) && $debug['saml'] === true) // explicitly enabled
// TODO: deprecate the old style and remove it in 2.0
|| (array_key_exists(0, $debug) && $debug[0] === true) // old style 'debug'
)) {
// debugging messages is disabled
return;
}
if ($message instanceof \DOMElement) {
$message = $message->ownerDocument->saveXML($message);
}
switch ($type) {
case 'in':
Logger::debug('Received message:');
break;
case 'out':
Logger::debug('Sending message:');
break;
case 'decrypt':
Logger::debug('Decrypted message:');
break;
case 'encrypt':
Logger::debug('Encrypted message:');
break;
default:
assert(false);
}
$str = self::formatXMLString($message);
foreach (explode("\n", $str) as $line) {
Logger::debug($line);
}
}
/**
* Format a DOM element.
*
* This function takes in a DOM element, and inserts whitespace to make it more readable. Note that whitespace
* added previously will be removed.
*
* @param \DOMNode $root The root element which should be formatted.
* @param string $indentBase The indentation this element should be assumed to have. Defaults to an empty
* string.
*
* @throws \InvalidArgumentException If $root is not a DOMElement or $indentBase is not a string.
*
* @return void
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function formatDOMElement(\DOMNode $root, $indentBase = '')
{
if (!is_string($indentBase)) {
throw new \InvalidArgumentException('Invalid input parameters');
}
// check what this element contains
$fullText = ''; // all text in this element
$textNodes = array(); // text nodes which should be deleted
$childNodes = array(); // other child nodes
for ($i = 0; $i < $root->childNodes->length; $i++) {
/** @var \DOMElement $child */
$child = $root->childNodes->item($i);
if ($child instanceof \DOMText) {
$textNodes[] = $child;
$fullText .= $child->wholeText;
} elseif ($child instanceof \DOMComment || $child instanceof \DOMElement) {
$childNodes[] = $child;
} else {
// unknown node type. We don't know how to format this
return;
}
}
$fullText = trim($fullText);
if (strlen($fullText) > 0) {
// we contain textelf
$hasText = true;
} else {
$hasText = false;
}
$hasChildNode = (count($childNodes) > 0);
if ($hasText && $hasChildNode) {
// element contains both text and child nodes - we don't know how to format this one
return;
}
// remove text nodes
foreach ($textNodes as $node) {
$root->removeChild($node);
}
if ($hasText) {
// only text - add a single text node to the element with the full text
$root->appendChild(new \DOMText($fullText));
return;
}
if (!$hasChildNode) {
// empty node. Nothing to do
return;
}
/* Element contains only child nodes - add indentation before each one, and
* format child elements.
*/
$childIndentation = $indentBase.' ';
foreach ($childNodes as $node) {
// add indentation before node
$root->insertBefore(new \DOMText("\n".$childIndentation), $node);
// format child elements
if ($node instanceof \DOMElement) {
self::formatDOMElement($node, $childIndentation);
}
}
// add indentation before closing tag
$root->appendChild(new \DOMText("\n".$indentBase));
}
/**
* Format an XML string.
*
* This function formats an XML string using the formatDOMElement() function.
*
* @param string $xml An XML string which should be formatted.
* @param string $indentBase Optional indentation which should be applied to all the output. Optional, defaults
* to ''.
*
* @return string The formatted string.
* @throws \InvalidArgumentException If the parameters are not strings.
* @throws \DOMException If the input does not parse correctly as an XML string.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function formatXMLString($xml, $indentBase = '')
{
if (!is_string($xml) || !is_string($indentBase)) {
throw new \InvalidArgumentException('Invalid input parameters');
}
try {
$doc = \SAML2\DOMDocumentFactory::fromString($xml);
} catch (\Exception $e) {
throw new \DOMException('Error parsing XML string.');
}
$root = $doc->firstChild;
self::formatDOMElement($root, $indentBase);
return $doc->saveXML($root);
}
/**
* This function finds direct descendants of a DOM element with the specified
* localName and namespace. They are returned in an array.
*
* This function accepts the same shortcuts for namespaces as the isDOMNodeOfType function.
*
* @param \DOMNode $element The element we should look in.
* @param string $localName The name the element should have.
* @param string $namespaceURI The namespace the element should have.
*
* @return array Array with the matching elements in the order they are found. An empty array is
* returned if no elements match.
* @throws \InvalidArgumentException If $element is not an instance of DOMElement, $localName is not a string or
* $namespaceURI is not a string.
*/
public static function getDOMChildren(\DOMNode $element, $localName, $namespaceURI)
{
if (!is_string($localName) || !is_string($namespaceURI)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$ret = array();
for ($i = 0; $i < $element->childNodes->length; $i++) {
/** @var \DOMElement $child */
$child = $element->childNodes->item($i);
// skip text nodes and comment elements
if ($child instanceof \DOMText || $child instanceof \DOMComment) {
continue;
}
if (self::isDOMNodeOfType($child, $localName, $namespaceURI) === true) {
$ret[] = $child;
}
}
return $ret;
}
/**
* This function extracts the text from DOMElements which should contain only text content.
*
* @param \DOMElement $element The element we should extract text from.
*
* @return string The text content of the element.
* @throws \SimpleSAML_Error_Exception If the element contains a non-text child node.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getDOMText(\DOMElement $element)
{
$txt = '';
for ($i = 0; $i < $element->childNodes->length; $i++) {
/** @var \DOMElement $child */
$child = $element->childNodes->item($i);
if (!($child instanceof \DOMText)) {
throw new \SimpleSAML_Error_Exception($element->localName.' contained a non-text child node.');
}
$txt .= $child->wholeText;
}
$txt = trim($txt);
return $txt;
}
/**
* This function checks if the DOMElement has the correct localName and namespaceURI.
*
* We also define the following shortcuts for namespaces:
* - '@ds': 'http://www.w3.org/2000/09/xmldsig#'
* - '@md': 'urn:oasis:names:tc:SAML:2.0:metadata'
* - '@saml1': 'urn:oasis:names:tc:SAML:1.0:assertion'
* - '@saml1md': 'urn:oasis:names:tc:SAML:profiles:v1metadata'
* - '@saml1p': 'urn:oasis:names:tc:SAML:1.0:protocol'
* - '@saml2': 'urn:oasis:names:tc:SAML:2.0:assertion'
* - '@saml2p': 'urn:oasis:names:tc:SAML:2.0:protocol'
*
* @param \DOMNode $element The element we should check.
* @param string $name The local name the element should have.
* @param string $nsURI The namespaceURI the element should have.
*
* @return boolean True if both namespace and local name matches, false otherwise.
* @throws \InvalidArgumentException If the namespace shortcut is unknown.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isDOMNodeOfType(\DOMNode $element, $name, $nsURI)
{
if (!is_string($name) || !is_string($nsURI) || strlen($nsURI) === 0) {
// most likely a comment-node
return false;
}
// check if the namespace is a shortcut, and expand it if it is
if ($nsURI[0] === '@') {
// the defined shortcuts
$shortcuts = array(
'@ds' => 'http://www.w3.org/2000/09/xmldsig#',
'@md' => 'urn:oasis:names:tc:SAML:2.0:metadata',
'@saml1' => 'urn:oasis:names:tc:SAML:1.0:assertion',
'@saml1md' => 'urn:oasis:names:tc:SAML:profiles:v1metadata',
'@saml1p' => 'urn:oasis:names:tc:SAML:1.0:protocol',
'@saml2' => 'urn:oasis:names:tc:SAML:2.0:assertion',
'@saml2p' => 'urn:oasis:names:tc:SAML:2.0:protocol',
'@shibmd' => 'urn:mace:shibboleth:metadata:1.0',
);
// check if it is a valid shortcut
if (!array_key_exists($nsURI, $shortcuts)) {
throw new \InvalidArgumentException('Unknown namespace shortcut: '.$nsURI);
}
// expand the shortcut
$nsURI = $shortcuts[$nsURI];
}
if ($element->localName !== $name) {
return false;
}
if ($element->namespaceURI !== $nsURI) {
return false;
}
return true;
}
/**
* This function attempts to validate an XML string against the specified schema. It will parse the string into a
* DOM document and validate this document against the schema.
*
* Note that this function returns values that are evaluated as a logical true, both when validation works and when
* it doesn't. Please use strict comparisons to check the values returned.
*
* @param string|\DOMDocument $xml The XML string or document which should be validated.
* @param string $schema The filename of the schema that should be used to validate the document.
*
* @return boolean|string Returns a string with errors found if validation fails. True if validation passes ok.
* @throws \InvalidArgumentException If $schema is not a string, or $xml is neither a string nor a \DOMDocument.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isValid($xml, $schema)
{
if (!(is_string($schema) && (is_string($xml) || $xml instanceof \DOMDocument))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
Errors::begin();
if ($xml instanceof \DOMDocument) {
$dom = $xml;
$res = true;
} else {
try {
$dom = \SAML2\DOMDocumentFactory::fromString($xml);
$res = true;
} catch (\Exception $e) {
$res = false;
}
}
if ($res) {
$config = \SimpleSAML_Configuration::getInstance();
/** @var string $schemaPath */
$schemaPath = $config->resolvePath('schemas');
$schemaFile = $schemaPath.'/'.$schema;
$res = $dom->schemaValidate($schemaFile);
if ($res) {
Errors::end();
return true;
}
$errorText = "Schema validation failed on XML string:\n";
} else {
$errorText = "Failed to parse XML string for schema validation:\n";
}
$errors = Errors::end();
$errorText .= Errors::formatErrors($errors);
return $errorText;
}
}