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

140
lib/SimpleSAML/XML/Errors.php Executable file
View File

@@ -0,0 +1,140 @@
<?php
/**
* This class defines an interface for accessing errors from the XML library.
*
* In PHP versions which doesn't support accessing error information, this class
* will hide that, and pretend that no errors were logged.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML;
use LibXMLError;
class Errors
{
/**
* @var array This is an stack of error logs. The topmost element is the one we are currently working on.
*/
private static $errorStack = array();
/**
* @var bool This is the xml error state we had before we began logging.
*/
private static $xmlErrorState;
/**
* Append current XML errors to the current stack level.
*/
private static function addErrors()
{
$currentErrors = libxml_get_errors();
libxml_clear_errors();
$level = count(self::$errorStack) - 1;
self::$errorStack[$level] = array_merge(self::$errorStack[$level], $currentErrors);
}
/**
* Start error logging.
*
* A call to this function will begin a new error logging context. Every call must have
* a corresponding call to end().
*/
public static function begin()
{
// Check whether the error access functions are present
if (!function_exists('libxml_use_internal_errors')) {
return;
}
if (count(self::$errorStack) === 0) {
// No error logging is currently in progress. Initialize it.
self::$xmlErrorState = libxml_use_internal_errors(true);
libxml_clear_errors();
} else {
/* We have already started error logging. Append the current errors to the
* list of errors in this level.
*/
self::addErrors();
}
// Add a new level to the error stack
self::$errorStack[] = array();
}
/**
* End error logging.
*
* @return array An array with the LibXMLErrors which has occurred since begin() was called.
*/
public static function end()
{
// Check whether the error access functions are present
if (!function_exists('libxml_use_internal_errors')) {
// Pretend that no errors occurred
return array();
}
// Add any errors which may have occurred
self::addErrors();
$ret = array_pop(self::$errorStack);
if (count(self::$errorStack) === 0) {
// Disable our error logging and restore the previous state
libxml_use_internal_errors(self::$xmlErrorState);
}
return $ret;
}
/**
* Format an error as a string.
*
* This function formats the given LibXMLError object as a string.
*
* @param \LibXMLError $error The LibXMLError which should be formatted.
* @return string A string representing the given LibXMLError.
*/
public static function formatError($error)
{
assert($error instanceof LibXMLError);
return 'level=' . $error->level . ',code=' . $error->code . ',line=' . $error->line . ',col=' . $error->column .
',msg=' . trim($error->message);
}
/**
* Format a list of errors as a string.
*
* This fucntion takes an array of LibXMLError objects and creates a string with all the errors.
* Each error will be separated by a newline, and the string will end with a newline-character.
*
* @param array $errors An array of errors.
* @return string A string representing the errors. An empty string will be returned if there were no
* errors in the array.
*/
public static function formatErrors($errors)
{
assert(is_array($errors));
$ret = '';
foreach ($errors as $error) {
$ret .= self::formatError($error) . "\n";
}
return $ret;
}
}

77
lib/SimpleSAML/XML/Parser.php Executable file
View File

@@ -0,0 +1,77 @@
<?php
/**
* This file will help doing XPath queries in SAML 2 XML documents.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML;
class Parser
{
public $simplexml = null;
public function __construct($xml)
{
;
$this->simplexml = new \SimpleXMLElement($xml);
$this->simplexml->registerXPathNamespace('saml2', 'urn:oasis:names:tc:SAML:2.0:assertion');
$this->simplexml->registerXPathNamespace('saml2meta', 'urn:oasis:names:tc:SAML:2.0:metadata');
$this->simplexml->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#');
}
public static function fromSimpleXMLElement(\SimpleXMLElement $element)
{
// Traverse all existing namespaces in element
$namespaces = $element->getNamespaces();
foreach ($namespaces as $prefix => $ns) {
$element[(($prefix === '') ? 'xmlns' : 'xmlns:' . $prefix)] = $ns;
}
/* Create a new parser with the xml document where the namespace definitions
* are added.
*/
$parser = new Parser($element->asXML());
return $parser;
}
public function getValueDefault($xpath, $defvalue)
{
try {
return $this->getValue($xpath, true);
} catch (\Exception $e) {
return $defvalue;
}
}
public function getValue($xpath, $required = false)
{
$result = $this->simplexml->xpath($xpath);
if (!is_array($result) || empty($result)) {
if ($required) {
throw new \Exception('Could not get value from XML document using the following XPath expression: ' . $xpath);
} else {
return null;
}
}
return (string) $result[0];
}
public function getValueAlternatives(array $xpath, $required = false)
{
foreach ($xpath as $x) {
$seek = $this->getValue($x);
if ($seek) {
return $seek;
}
}
if ($required) {
throw new \Exception('Could not get value from XML document using multiple alternative XPath expressions.');
} else {
return null;
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* The Shibboleth 1.3 Authentication Request. Not part of SAML 1.1,
* but an extension using query paramters no XML.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML\Shib13;
class AuthnRequest
{
private $issuer = null;
private $relayState = null;
public function setRelayState($relayState)
{
$this->relayState = $relayState;
}
public function getRelayState()
{
return $this->relayState;
}
public function setIssuer($issuer)
{
$this->issuer = $issuer;
}
public function getIssuer()
{
return $this->issuer;
}
public function createRedirect($destination, $shire)
{
$metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$idpmetadata = $metadata->getMetaDataConfig($destination, 'shib13-idp-remote');
$desturl = $idpmetadata->getDefaultEndpoint('SingleSignOnService', array('urn:mace:shibboleth:1.0:profiles:AuthnRequest'));
$desturl = $desturl['Location'];
$target = $this->getRelayState();
$url = $desturl . '?' .
'providerId=' . urlencode($this->getIssuer()) .
'&shire=' . urlencode($shire) .
(isset($target) ? '&target=' . urlencode($target) : '');
return $url;
}
}

View File

@@ -0,0 +1,480 @@
<?php
/**
* A Shibboleth 1.3 authentication response.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML\Shib13;
use DOMDocument;
use DOMNode;
use SAML2\DOMDocumentFactory;
use SAML2\Utils;
use SimpleSAML\Utils\Config;
use SimpleSAML\Utils\Random;
use SimpleSAML\Utils\Time;
use SimpleSAML\XML\Validator;
class AuthnResponse
{
/**
* @var \SimpleSAML\XML\Validator This variable contains an XML validator for this message.
*/
private $validator = null;
/**
* @var bool Whether this response was validated by some external means (e.g. SSL).
*/
private $messageValidated = false;
const SHIB_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:1.0:protocol';
const SHIB_ASSERT_NS = 'urn:oasis:names:tc:SAML:1.0:assertion';
/**
* @var \DOMDocument The DOMDocument which represents this message.
*/
private $dom;
/**
* @var string|null The relaystate which is associated with this response.
*/
private $relayState = null;
/**
* Set whether this message was validated externally.
*
* @param bool $messageValidated TRUE if the message is already validated, FALSE if not.
*/
public function setMessageValidated($messageValidated)
{
assert(is_bool($messageValidated));
$this->messageValidated = $messageValidated;
}
public function setXML($xml)
{
assert(is_string($xml));
try {
$this->dom = DOMDocumentFactory::fromString(str_replace("\r", "", $xml));
} catch (\Exception $e) {
throw new \Exception('Unable to parse AuthnResponse XML.');
}
}
public function setRelayState($relayState)
{
$this->relayState = $relayState;
}
public function getRelayState()
{
return $this->relayState;
}
public function validate()
{
assert($this->dom instanceof DOMDocument);
if ($this->messageValidated) {
// This message was validated externally
return true;
}
// Validate the signature
$this->validator = new Validator($this->dom, array('ResponseID', 'AssertionID'));
// Get the issuer of the response
$issuer = $this->getIssuer();
// Get the metadata of the issuer
$metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$md = $metadata->getMetaDataConfig($issuer, 'shib13-idp-remote');
$publicKeys = $md->getPublicKeys('signing');
if (!empty($publicKeys)) {
$certFingerprints = array();
foreach ($publicKeys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
$certFingerprints[] = sha1(base64_decode($key['X509Certificate']));
}
$this->validator->validateFingerprint($certFingerprints);
} elseif ($md->hasValue('certFingerprint')) {
$certFingerprints = $md->getArrayizeString('certFingerprint');
// Validate the fingerprint
$this->validator->validateFingerprint($certFingerprints);
} elseif ($md->hasValue('caFile')) {
// Validate against CA
$this->validator->validateCA(Config::getCertPath($md->getString('caFile')));
} else {
throw new \SimpleSAML_Error_Exception('Missing certificate in Shibboleth 1.3 IdP Remote metadata for identity provider [' . $issuer . '].');
}
return true;
}
/**
* Checks if the given node is validated by the signature on this response.
*
* @param \DOMElement $node Node to be validated.
* @return bool TRUE if the node is validated or FALSE if not.
*/
private function isNodeValidated($node)
{
if ($this->messageValidated) {
// This message was validated externally
return true;
}
if ($this->validator === null) {
return false;
}
// Convert the node to a DOM node if it is an element from SimpleXML
if ($node instanceof \SimpleXMLElement) {
$node = dom_import_simplexml($node);
}
assert($node instanceof DOMNode);
return $this->validator->isNodeValidated($node);
}
/**
* This function runs an xPath query on this authentication response.
*
* @param string $query The query which should be run.
* @param \DOMNode $node The node which this query is relative to. If this node is NULL (the default)
* then the query will be relative to the root of the response.
* @return \DOMNodeList
*/
private function doXPathQuery($query, $node = null)
{
assert(is_string($query));
assert($this->dom instanceof DOMDocument);
if ($node === null) {
$node = $this->dom->documentElement;
}
assert($node instanceof DOMNode);
$xPath = new \DOMXpath($this->dom);
$xPath->registerNamespace('shibp', self::SHIB_PROTOCOL_NS);
$xPath->registerNamespace('shib', self::SHIB_ASSERT_NS);
return $xPath->query($query, $node);
}
/**
* Retrieve the session index of this response.
*
* @return string|null The session index of this response.
*/
public function getSessionIndex()
{
assert($this->dom instanceof DOMDocument);
$query = '/shibp:Response/shib:Assertion/shib:AuthnStatement';
$nodelist = $this->doXPathQuery($query);
if ($node = $nodelist->item(0)) {
return $node->getAttribute('SessionIndex');
}
return null;
}
public function getAttributes()
{
$metadata = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$md = $metadata->getMetadata($this->getIssuer(), 'shib13-idp-remote');
$base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false;
if (! ($this->dom instanceof \DOMDocument)) {
return array();
}
$attributes = array();
$assertions = $this->doXPathQuery('/shibp:Response/shib:Assertion');
foreach ($assertions as $assertion) {
if (!$this->isNodeValidated($assertion)) {
throw new \Exception('Shib13 AuthnResponse contained an unsigned assertion.');
}
$conditions = $this->doXPathQuery('shib:Conditions', $assertion);
if ($conditions && $conditions->length > 0) {
$condition = $conditions->item(0);
$start = $condition->getAttribute('NotBefore');
$end = $condition->getAttribute('NotOnOrAfter');
if ($start && $end) {
if (!self::checkDateConditions($start, $end)) {
error_log('Date check failed ... (from ' . $start . ' to ' . $end . ')');
continue;
}
}
}
$attribute_nodes = $this->doXPathQuery('shib:AttributeStatement/shib:Attribute/shib:AttributeValue', $assertion);
/** @var \DOMElement $attribute */
foreach ($attribute_nodes as $attribute) {
$value = $attribute->textContent;
$name = $attribute->parentNode->getAttribute('AttributeName');
if ($attribute->hasAttribute('Scope')) {
$scopePart = '@' . $attribute->getAttribute('Scope');
} else {
$scopePart = '';
}
if (!is_string($name)) {
throw new \Exception('Shib13 Attribute node without an AttributeName.');
}
if (!array_key_exists($name, $attributes)) {
$attributes[$name] = array();
}
if ($base64) {
$encodedvalues = explode('_', $value);
foreach ($encodedvalues as $v) {
$attributes[$name][] = base64_decode($v) . $scopePart;
}
} else {
$attributes[$name][] = $value . $scopePart;
}
}
}
return $attributes;
}
public function getIssuer()
{
$query = '/shibp:Response/shib:Assertion/@Issuer';
$nodelist = $this->doXPathQuery($query);
if ($attr = $nodelist->item(0)) {
return $attr->value;
} else {
throw new \Exception('Could not find Issuer field in Authentication response');
}
}
public function getNameID()
{
$nameID = array();
$query = '/shibp:Response/shib:Assertion/shib:AuthenticationStatement/shib:Subject/shib:NameIdentifier';
$nodelist = $this->doXPathQuery($query);
if ($node = $nodelist->item(0)) {
$nameID["Value"] = $node->nodeValue;
$nameID["Format"] = $node->getAttribute('Format');
}
return $nameID;
}
/**
* Build a authentication response.
*
* @param \SimpleSAML_Configuration $idp Metadata for the IdP the response is sent from.
* @param \SimpleSAML_Configuration $sp Metadata for the SP the response is sent to.
* @param string $shire The endpoint on the SP the response is sent to.
* @param array|null $attributes The attributes which should be included in the response.
* @return string The response.
*/
public function generate(\SimpleSAML_Configuration $idp, \SimpleSAML_Configuration $sp, $shire, $attributes)
{
assert(is_string($shire));
assert($attributes === null || is_array($attributes));
if ($sp->hasValue('scopedattributes')) {
$scopedAttributes = $sp->getArray('scopedattributes');
} elseif ($idp->hasValue('scopedattributes')) {
$scopedAttributes = $idp->getArray('scopedattributes');
} else {
$scopedAttributes = array();
}
$id = Random::generateID();
$issueInstant = Time::generateTimestamp();
// 30 seconds timeskew back in time to allow differing clocks
$notBefore = Time::generateTimestamp(time() - 30);
$assertionExpire = Time::generateTimestamp(time() + 60 * 5);# 5 minutes
$assertionid = Random::generateID();
$spEntityId = $sp->getString('entityid');
$audience = $sp->getString('audience', $spEntityId);
$base64 = $sp->getBoolean('base64attributes', false);
$namequalifier = $sp->getString('NameQualifier', $spEntityId);
$nameid = Random::generateID();
$subjectNode =
'<Subject>' .
'<NameIdentifier' .
' Format="urn:mace:shibboleth:1.0:nameIdentifier"' .
' NameQualifier="' . htmlspecialchars($namequalifier) . '"' .
'>' .
htmlspecialchars($nameid) .
'</NameIdentifier>' .
'<SubjectConfirmation>' .
'<ConfirmationMethod>' .
'urn:oasis:names:tc:SAML:1.0:cm:bearer' .
'</ConfirmationMethod>' .
'</SubjectConfirmation>' .
'</Subject>';
$encodedattributes = '';
if (is_array($attributes)) {
$encodedattributes .= '<AttributeStatement>';
$encodedattributes .= $subjectNode;
foreach ($attributes as $name => $value) {
$encodedattributes .= $this->enc_attribute($name, $value, $base64, $scopedAttributes);
}
$encodedattributes .= '</AttributeStatement>';
}
/*
* The SAML 1.1 response message
*/
$response = '<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" IssueInstant="' . $issueInstant. '"
MajorVersion="1" MinorVersion="1"
Recipient="' . htmlspecialchars($shire) . '" ResponseID="' . $id . '">
<Status>
<StatusCode Value="samlp:Success" />
</Status>
<Assertion xmlns="urn:oasis:names:tc:SAML:1.0:assertion"
AssertionID="' . $assertionid . '" IssueInstant="' . $issueInstant. '"
Issuer="' . htmlspecialchars($idp->getString('entityid')) . '" MajorVersion="1" MinorVersion="1">
<Conditions NotBefore="' . $notBefore. '" NotOnOrAfter="'. $assertionExpire . '">
<AudienceRestrictionCondition>
<Audience>' . htmlspecialchars($audience) . '</Audience>
</AudienceRestrictionCondition>
</Conditions>
<AuthenticationStatement AuthenticationInstant="' . $issueInstant. '"
AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:unspecified">' .
$subjectNode . '
</AuthenticationStatement>
' . $encodedattributes . '
</Assertion>
</Response>';
return $response;
}
/**
* Format a shib13 attribute.
*
* @param string $name Name of the attribute.
* @param array $values Values of the attribute (as an array of strings).
* @param bool $base64 Whether the attriubte values should be base64-encoded.
* @param array $scopedAttributes Array of attributes names which are scoped.
* @return string The attribute encoded as an XML-string.
*/
private function enc_attribute($name, $values, $base64, $scopedAttributes)
{
assert(is_string($name));
assert(is_array($values));
assert(is_bool($base64));
assert(is_array($scopedAttributes));
if (in_array($name, $scopedAttributes, true)) {
$scoped = true;
} else {
$scoped = false;
}
$attr = '<Attribute AttributeName="' . htmlspecialchars($name) . '" AttributeNamespace="urn:mace:shibboleth:1.0:attributeNamespace:uri">';
foreach ($values as $value) {
$scopePart = '';
if ($scoped) {
$tmp = explode('@', $value, 2);
if (count($tmp) === 2) {
$value = $tmp[0];
$scopePart = ' Scope="' . htmlspecialchars($tmp[1]) . '"';
}
}
if ($base64) {
$value = base64_encode($value);
}
$attr .= '<AttributeValue' . $scopePart . '>' . htmlspecialchars($value) . '</AttributeValue>';
}
$attr .= '</Attribute>';
return $attr;
}
/**
* Check if we are currently between the given date & time conditions.
*
* Note that this function allows a 10-minute leap from the initial time as marked by $start.
*
* @param string|null $start A SAML2 timestamp marking the start of the period to check. Defaults to null, in which
* case there's no limitations in the past.
* @param string|null $end A SAML2 timestamp marking the end of the period to check. Defaults to null, in which
* case there's no limitations in the future.
*
* @return bool True if the current time belongs to the period specified by $start and $end. False otherwise.
*
* @see \SAML2\Utils::xsDateTimeToTimestamp.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
protected static function checkDateConditions($start = null, $end = null)
{
$currentTime = time();
if (!empty($start)) {
$startTime = Utils::xsDateTimeToTimestamp($start);
// allow for a 10 minute difference in time
if (($startTime < 0) || (($startTime - 600) > $currentTime)) {
return false;
}
}
if (!empty($end)) {
$endTime = Utils::xsDateTimeToTimestamp($end);
if (($endTime < 0) || ($endTime <= $currentTime)) {
return false;
}
}
return true;
}
}

313
lib/SimpleSAML/XML/Signer.php Executable file
View File

@@ -0,0 +1,313 @@
<?php
/**
* A helper class for signing XML.
*
* This is a helper class for signing XML documents.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML;
use DOMComment;
use DOMElement;
use DOMText;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use RobRichards\XMLSecLibs\XMLSecurityKey;
use SimpleSAML\Utils\Config;
class Signer
{
/**
* @var string The name of the ID attribute.
*/
private $idAttrName = '';
/**
* @var XMLSecurityKey|bool The private key (as an XMLSecurityKey).
*/
private $privateKey = false;
/**
* @var string The certificate (as text).
*/
private $certificate = '';
/**
* @var array Extra certificates which should be included in the response.
*/
private $extraCertificates = array();
/**
* Constructor for the metadata signer.
*
* You can pass an list of options as key-value pairs in the array. This allows you to initialize
* a metadata signer in one call.
*
* The following keys are recognized:
* - privatekey The file with the private key, relative to the cert-directory.
* - privatekey_pass The passphrase for the private key.
* - certificate The file with the certificate, relative to the cert-directory.
* - privatekey_array The private key, as an array returned from SimpleSAML_Utilities::loadPrivateKey.
* - publickey_array The public key, as an array returned from SimpleSAML_Utilities::loadPublicKey.
* - id The name of the ID attribute.
*
* @param array $options Associative array with options for the constructor. Defaults to an empty array.
*/
public function __construct($options = array())
{
assert(is_array($options));
if (array_key_exists('privatekey', $options)) {
$pass = null;
if (array_key_exists('privatekey_pass', $options)) {
$pass = $options['privatekey_pass'];
}
$this->loadPrivateKey($options['privatekey'], $pass);
}
if (array_key_exists('certificate', $options)) {
$this->loadCertificate($options['certificate']);
}
if (array_key_exists('privatekey_array', $options)) {
$this->loadPrivateKeyArray($options['privatekey_array']);
}
if (array_key_exists('publickey_array', $options)) {
$this->loadPublicKeyArray($options['publickey_array']);
}
if (array_key_exists('id', $options)) {
$this->setIdAttribute($options['id']);
}
}
/**
* Set the private key from an array.
*
* This function loads the private key from an array matching what is returned
* by SimpleSAML_Utilities::loadPrivateKey(...).
*
* @param array $privatekey The private key.
*/
public function loadPrivateKeyArray($privatekey)
{
assert(is_array($privatekey));
assert(array_key_exists('PEM', $privatekey));
$this->privateKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'private'));
if (array_key_exists('password', $privatekey)) {
$this->privateKey->passphrase = $privatekey['password'];
}
$this->privateKey->loadKey($privatekey['PEM'], false);
}
/**
* Set the private key.
*
* Will throw an exception if unable to load the private key.
*
* @param string $file The file which contains the private key. The path is assumed to be relative
* to the cert-directory.
* @param string|null $pass The passphrase on the private key. Pass no value or NULL if the private
* key is unencrypted.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
* @throws \Exception
*/
public function loadPrivateKey($file, $pass = null, $full_path = false)
{
assert(is_string($file));
assert(is_string($pass) || $pass === null);
assert(is_bool($full_path));
if (!$full_path) {
$keyFile = Config::getCertPath($file);
} else {
$keyFile = $file;
}
if (!file_exists($keyFile)) {
throw new \Exception('Could not find private key file "' . $keyFile . '".');
}
$keyData = file_get_contents($keyFile);
if ($keyData === false) {
throw new \Exception('Unable to read private key file "' . $keyFile . '".');
}
$privatekey = array('PEM' => $keyData);
if ($pass !== null) {
$privatekey['password'] = $pass;
}
$this->loadPrivateKeyArray($privatekey);
}
/**
* Set the public key / certificate we should include in the signature.
*
* This function loads the public key from an array matching what is returned
* by SimpleSAML_Utilities::loadPublicKey(...).
*
* @param array $publickey The public key.
* @throws \Exception
*/
public function loadPublicKeyArray($publickey)
{
assert(is_array($publickey));
if (!array_key_exists('PEM', $publickey)) {
// We have a public key with only a fingerprint
throw new \Exception('Tried to add a certificate fingerprint in a signature.');
}
// For now, we only assume that the public key is an X509 certificate
$this->certificate = $publickey['PEM'];
}
/**
* Set the certificate we should include in the signature.
*
* If this function isn't called, no certificate will be included.
* Will throw an exception if unable to load the certificate.
*
* @param string $file The file which contains the certificate. The path is assumed to be relative to
* the cert-directory.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
* @throws \Exception
*/
public function loadCertificate($file, $full_path = false)
{
assert(is_string($file));
assert(is_bool($full_path));
if (!$full_path) {
$certFile = Config::getCertPath($file);
} else {
$certFile = $file;
}
if (!file_exists($certFile)) {
throw new \Exception('Could not find certificate file "' . $certFile . '".');
}
$cert = file_get_contents($certFile);
if ($cert === false) {
throw new \Exception('Unable to read certificate file "' . $certFile . '".');
}
$this->certificate = $cert;
}
/**
* Set the attribute name for the ID value.
*
* @param string $idAttrName The name of the attribute which contains the id.
*/
public function setIDAttribute($idAttrName)
{
assert(is_string($idAttrName));
$this->idAttrName = $idAttrName;
}
/**
* Add an extra certificate to the certificate chain in the signature.
*
* Extra certificates will be added to the certificate chain in the order they
* are added.
*
* @param string $file The file which contains the certificate, relative to the cert-directory.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
* @throws \Exception
*/
public function addCertificate($file, $full_path = false)
{
assert(is_string($file));
assert(is_bool($full_path));
if (!$full_path) {
$certFile = Config::getCertPath($file);
} else {
$certFile = $file;
}
if (!file_exists($certFile)) {
throw new \Exception('Could not find extra certificate file "' . $certFile . '".');
}
$certificate = file_get_contents($certFile);
if ($certificate === false) {
throw new \Exception('Unable to read extra certificate file "' . $certFile . '".');
}
$this->extraCertificates[] = $certificate;
}
/**
* Signs the given DOMElement and inserts the signature at the given position.
*
* The private key must be set before calling this function.
*
* @param \DOMElement $node The DOMElement we should generate a signature for.
* @param \DOMElement $insertInto The DOMElement we should insert the signature element into.
* @param \DOMElement $insertBefore The element we should insert the signature element before. Defaults to NULL,
* in which case the signature will be appended to the element spesified in
* $insertInto.
* @throws \Exception
*/
public function sign($node, $insertInto, $insertBefore = null)
{
assert($node instanceof DOMElement);
assert($insertInto instanceof DOMElement);
assert($insertBefore === null || $insertBefore instanceof DOMElement ||
$insertBefore instanceof DOMComment || $insertBefore instanceof DOMText);
if ($this->privateKey === false) {
throw new \Exception('Private key not set.');
}
$objXMLSecDSig = new XMLSecurityDSig();
$objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$options = array();
if (!empty($this->idAttrName)) {
$options['id_name'] = $this->idAttrName;
}
$objXMLSecDSig->addReferenceList(
array($node),
XMLSecurityDSig::SHA256,
array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N),
$options
);
/** @var \RobRichards\XMLSecLibs\XMLSecurityKey $this->privateKey */
$objXMLSecDSig->sign($this->privateKey);
// Add the certificate to the signature
$objXMLSecDSig->add509Cert($this->certificate, true);
// Add extra certificates
foreach ($this->extraCertificates as $certificate) {
$objXMLSecDSig->add509Cert($certificate, true);
}
$objXMLSecDSig->insertSignature($insertInto, $insertBefore);
}
}

445
lib/SimpleSAML/XML/Validator.php Executable file
View File

@@ -0,0 +1,445 @@
<?php
/**
* This class implements helper functions for XML validation.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
namespace SimpleSAML\XML;
use RobRichards\XMLSecLibs\XMLSecEnc;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use SimpleSAML\Logger;
class Validator
{
/**
* @var string This variable contains the X509 certificate the XML document
* was signed with, or NULL if it wasn't signed with an X509 certificate.
*/
private $x509Certificate;
/**
* @var array|null This variable contains the nodes which are signed.
*/
private $validNodes = null;
/**
* This function initializes the validator.
*
* This function accepts an optional parameter $publickey, which is the public key
* or certificate which should be used to validate the signature. This parameter can
* take the following values:
* - NULL/FALSE: No validation will be performed. This is the default.
* - A string: Assumed to be a PEM-encoded certificate / public key.
* - An array: Assumed to be an array returned by SimpleSAML_Utilities::loadPublicKey.
*
* @param \DOMNode $xmlNode The XML node which contains the Signature element.
* @param string|array $idAttribute The ID attribute which is used in node references. If
* this attribute is NULL (the default), then we will use whatever is the default
* ID. Can be eigther a string with one value, or an array with multiple ID
* attrbute names.
* @param array|bool $publickey The public key / certificate which should be used to validate the XML node.
* @throws \Exception
*/
public function __construct($xmlNode, $idAttribute = null, $publickey = false)
{
assert($xmlNode instanceof \DOMNode);
if ($publickey === null) {
$publickey = false;
} elseif (is_string($publickey)) {
$publickey = array(
'PEM' => $publickey,
);
} else {
assert($publickey === false || is_array($publickey));
}
// Create an XML security object
$objXMLSecDSig = new XMLSecurityDSig();
// Add the id attribute if the user passed in an id attribute
if ($idAttribute !== null) {
if (is_string($idAttribute)) {
$objXMLSecDSig->idKeys[] = $idAttribute;
} elseif (is_array($idAttribute)) {
foreach ($idAttribute as $ida) {
$objXMLSecDSig->idKeys[] = $ida;
}
}
}
// Locate the XMLDSig Signature element to be used
$signatureElement = $objXMLSecDSig->locateSignature($xmlNode);
if (!$signatureElement) {
throw new \Exception('Could not locate XML Signature element.');
}
// Canonicalize the XMLDSig SignedInfo element in the message
$objXMLSecDSig->canonicalizeSignedInfo();
// Validate referenced xml nodes
if (!$objXMLSecDSig->validateReference()) {
throw new \Exception('XMLsec: digest validation failed');
}
// Find the key used to sign the document
$objKey = $objXMLSecDSig->locateKey();
if (empty($objKey)) {
throw new \Exception('Error loading key to handle XML signature');
}
// Load the key data
if ($publickey !== false && array_key_exists('PEM', $publickey)) {
// We have PEM data for the public key / certificate
$objKey->loadKey($publickey['PEM']);
} else {
// No PEM data. Search for key in signature
if (!XMLSecEnc::staticLocateKeyInfo($objKey, $signatureElement)) {
throw new \Exception('Error finding key data for XML signature validation.');
}
if ($publickey !== false) {
/* $publickey is set, and should therefore contain one or more fingerprints.
* Check that the response contains a certificate with a matching
* fingerprint.
*/
assert(is_array($publickey['certFingerprint']));
$certificate = $objKey->getX509Certificate();
if ($certificate === null) {
// Wasn't signed with an X509 certificate
throw new \Exception('Message wasn\'t signed with an X509 certificate,' .
' and no public key was provided in the metadata.');
}
self::validateCertificateFingerprint($certificate, $publickey['certFingerprint']);
// Key OK
}
}
// Check the signature
if ($objXMLSecDSig->verify($objKey) !== 1) {
throw new \Exception("Unable to validate Signature");
}
// Extract the certificate
$this->x509Certificate = $objKey->getX509Certificate();
// Find the list of validated nodes
$this->validNodes = $objXMLSecDSig->getValidatedNodes();
}
/**
* Retrieve the X509 certificate which was used to sign the XML.
*
* This function will return the certificate as a PEM-encoded string. If the XML
* wasn't signed by an X509 certificate, NULL will be returned.
*
* @return string The certificate as a PEM-encoded string, or NULL if not signed with an X509 certificate.
*/
public function getX509Certificate()
{
return $this->x509Certificate;
}
/**
* Calculates the fingerprint of an X509 certificate.
*
* @param string $x509cert The certificate as a base64-encoded string. The string may optionally
* be framed with '-----BEGIN CERTIFICATE-----' and '-----END CERTIFICATE-----'.
* @return string The fingerprint as a 40-character lowercase hexadecimal number. NULL is returned if the
* argument isn't an X509 certificate.
*/
private static function calculateX509Fingerprint($x509cert)
{
assert(is_string($x509cert));
$lines = explode("\n", $x509cert);
$data = '';
foreach ($lines as $line) {
// Remove '\r' from end of line if present
$line = rtrim($line);
if ($line === '-----BEGIN CERTIFICATE-----') {
// Delete junk from before the certificate
$data = '';
} elseif ($line === '-----END CERTIFICATE-----') {
// Ignore data after the certificate
break;
} elseif ($line === '-----BEGIN PUBLIC KEY-----') {
// This isn't an X509 certificate
return null;
} else {
// Append the current line to the certificate data
$data .= $line;
}
}
/* $data now contains the certificate as a base64-encoded string. The fingerprint
* of the certificate is the sha1-hash of the certificate.
*/
return strtolower(sha1(base64_decode($data)));
}
/**
* Helper function for validating the fingerprint.
*
* Checks the fingerprint of a certificate against an array of valid fingerprints.
* Will throw an exception if none of the fingerprints matches.
*
* @param string $certificate The X509 certificate we should validate.
* @param array $fingerprints The valid fingerprints.
* @throws \Exception
*/
private static function validateCertificateFingerprint($certificate, $fingerprints)
{
assert(is_string($certificate));
assert(is_array($fingerprints));
$certFingerprint = self::calculateX509Fingerprint($certificate);
if ($certFingerprint === null) {
// Couldn't calculate fingerprint from X509 certificate. Should not happen.
throw new \Exception('Unable to calculate fingerprint from X509' .
' certificate. Maybe it isn\'t an X509 certificate?');
}
foreach ($fingerprints as $fp) {
assert(is_string($fp));
if ($fp === $certFingerprint) {
// The fingerprints matched
return;
}
}
// None of the fingerprints matched. Throw an exception describing the error.
throw new \Exception('Invalid fingerprint of certificate. Expected one of [' .
implode('], [', $fingerprints) . '], but got [' . $certFingerprint . ']');
}
/**
* Validate the fingerprint of the certificate which was used to sign this document.
*
* This function accepts either a string, or an array of strings as a parameter. If this
* is an array, then any string (certificate) in the array can match. If this is a string,
* then that string must match,
*
* @param string|array $fingerprints The fingerprints which should match. This can be a single string,
* or an array of fingerprints.
* @throws \Exception
*/
public function validateFingerprint($fingerprints)
{
assert(is_string($fingerprints) || is_array($fingerprints));
if ($this->x509Certificate === null) {
throw new \Exception('Key used to sign the message was not an X509 certificate.');
}
if (!is_array($fingerprints)) {
$fingerprints = array($fingerprints);
}
// Normalize the fingerprints
foreach ($fingerprints as &$fp) {
assert(is_string($fp));
// Make sure that the fingerprint is in the correct format
$fp = strtolower(str_replace(":", "", $fp));
}
self::validateCertificateFingerprint($this->x509Certificate, $fingerprints);
}
/**
* This function checks if the given XML node was signed.
*
* @param \DOMNode $node The XML node which we should verify that was signed.
*
* @return bool TRUE if this node (or a parent node) was signed. FALSE if not.
*/
public function isNodeValidated($node)
{
assert($node instanceof \DOMNode);
while ($node !== null) {
if (in_array($node, $this->validNodes, true)) {
return true;
}
$node = $node->parentNode;
}
/* Neither this node nor any of the parent nodes could be found in the list of
* signed nodes.
*/
return false;
}
/**
* Validate the certificate used to sign the XML against a CA file.
*
* This function throws an exception if unable to validate against the given CA file.
*
* @param string $caFile File with trusted certificates, in PEM-format.
* @throws \Exception
*/
public function validateCA($caFile)
{
assert(is_string($caFile));
if ($this->x509Certificate === null) {
throw new \Exception('Key used to sign the message was not an X509 certificate.');
}
self::validateCertificate($this->x509Certificate, $caFile);
}
/**
* Validate a certificate against a CA file, by using the builtin
* openssl_x509_checkpurpose function
*
* @param string $certificate The certificate, in PEM format.
* @param string $caFile File with trusted certificates, in PEM-format.
* @return boolean|string TRUE on success, or a string with error messages if it failed.
* @deprecated
*/
private static function validateCABuiltIn($certificate, $caFile)
{
assert(is_string($certificate));
assert(is_string($caFile));
// Clear openssl errors
while (openssl_error_string() !== false) {
}
$res = openssl_x509_checkpurpose($certificate, X509_PURPOSE_ANY, array($caFile));
$errors = '';
// Log errors
while (($error = openssl_error_string()) !== false) {
$errors .= ' [' . $error . ']';
}
if ($res !== true) {
return $errors;
}
return true;
}
/**
* Validate the certificate used to sign the XML against a CA file, by using the "openssl verify" command.
*
* This function uses the openssl verify command to verify a certificate, to work around limitations
* on the openssl_x509_checkpurpose function. That function will not work on certificates without a purpose
* set.
*
* @param string $certificate The certificate, in PEM format.
* @param string $caFile File with trusted certificates, in PEM-format.
* @return bool|string TRUE on success, a string with error messages on failure.
* @throws \Exception
* @deprecated
*/
private static function validateCAExec($certificate, $caFile)
{
assert(is_string($certificate));
assert(is_string($caFile));
$command = array(
'openssl', 'verify',
'-CAfile', $caFile,
'-purpose', 'any',
);
$cmdline = '';
foreach ($command as $c) {
$cmdline .= escapeshellarg($c) . ' ';
}
$cmdline .= '2>&1';
$descSpec = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
);
$process = proc_open($cmdline, $descSpec, $pipes);
if (!is_resource($process)) {
throw new \Exception('Failed to execute verification command: ' . $cmdline);
}
if (fwrite($pipes[0], $certificate) === false) {
throw new \Exception('Failed to write certificate for verification.');
}
fclose($pipes[0]);
$out = '';
while (!feof($pipes[1])) {
$line = trim(fgets($pipes[1]));
if (strlen($line) > 0) {
$out .= ' [' . $line . ']';
}
}
fclose($pipes[1]);
$status = proc_close($process);
if ($status !== 0 || $out !== ' [stdin: OK]') {
return $out;
}
return true;
}
/**
* Validate the certificate used to sign the XML against a CA file.
*
* This function throws an exception if unable to validate against the given CA file.
*
* @param string $certificate The certificate, in PEM format.
* @param string $caFile File with trusted certificates, in PEM-format.
* @throws \Exception
* @deprecated
*/
public static function validateCertificate($certificate, $caFile)
{
assert(is_string($certificate));
assert(is_string($caFile));
if (!file_exists($caFile)) {
throw new \Exception('Could not load CA file: ' . $caFile);
}
Logger::debug('Validating certificate against CA file: ' . var_export($caFile, true));
$resBuiltin = self::validateCABuiltIn($certificate, $caFile);
if ($resBuiltin !== true) {
Logger::debug('Failed to validate with internal function: ' . var_export($resBuiltin, true));
$resExternal = self::validateCAExec($certificate, $caFile);
if ($resExternal !== true) {
Logger::debug('Failed to validate with external function: ' . var_export($resExternal, true));
throw new \Exception('Could not verify certificate against CA file "'
. $caFile . '". Internal result:' . $resBuiltin .
' External result:' . $resExternal);
}
}
Logger::debug('Successfully validated certificate.');
}
}