runff 1.0 commit
This commit is contained in:
140
lib/SimpleSAML/XML/Errors.php
Executable file
140
lib/SimpleSAML/XML/Errors.php
Executable 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
77
lib/SimpleSAML/XML/Parser.php
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
lib/SimpleSAML/XML/Shib13/AuthnRequest.php
Executable file
53
lib/SimpleSAML/XML/Shib13/AuthnRequest.php
Executable 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;
|
||||
}
|
||||
}
|
||||
480
lib/SimpleSAML/XML/Shib13/AuthnResponse.php
Executable file
480
lib/SimpleSAML/XML/Shib13/AuthnResponse.php
Executable 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
313
lib/SimpleSAML/XML/Signer.php
Executable 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
445
lib/SimpleSAML/XML/Validator.php
Executable 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.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user