This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
spip-saml/inc/simplesamlphp/lib/SAML2/Message.php

484 lines
11 KiB
PHP

<?php
/**
* Base class for all SAML 2 messages.
*
* Implements what is common between the samlp:RequestAbstractType and
* samlp:StatusResponseType element types.
*
* @package simpleSAMLphp
* @version $Id$
*/
abstract class SAML2_Message implements SAML2_SignedElement {
/**
* The name of the root element of the DOM tree for the message.
*
* Used when creating a DOM tree from the message.
*
* @var string
*/
private $tagName;
/**
* The identifier of this message.
*
* @var string
*/
private $id;
/**
* The issue timestamp of this message, as an UNIX timestamp.
*
* @var int
*/
private $issueInstant;
/**
* The destination URL of this message if it is known.
*
* @var string|NULL
*/
private $destination;
/**
* The entity id of the issuer of this message, or NULL if unknown.
*
* @var string|NULL
*/
private $issuer;
/**
* The RelayState associated with this message.
*
* @var string|NULL
*/
private $relayState;
/**
* The DOMDocument we are currently building.
*
* This variable is used while generating XML from this message. It holds the
* DOMDocument of the XML we are generating.
*
* @var DOMDocument
*/
protected $document;
/**
* The private key we should use to sign the message.
*
* The private key can be NULL, in which case the message is sent unsigned.
*
* @var XMLSecurityKey|NULL
*/
private $signatureKey;
/**
* List of certificates that should be included in the message.
*
* @var array
*/
private $certificates;
/**
* Available methods for validating this message.
*
* @var array
*/
private $validators;
/**
* Initialize a message.
*
* This constructor takes an optional parameter with a DOMElement. If this
* parameter is given, the message will be initialized with data from that
* XML element.
*
* If no XML element is given, the message is initialized with suitable
* default values.
*
* @param string $tagName The tag name of the root element.
* @param DOMElement|NULL $xml The input message.
*/
protected function __construct($tagName, DOMElement $xml = NULL) {
assert('is_string($tagName)');
$this->tagName = $tagName;
$this->id = SimpleSAML_Utilities::generateID();
$this->issueInstant = time();
$this->certificates = array();
$this->validators = array();
if ($xml === NULL) {
return;
}
if (!$xml->hasAttribute('ID')) {
throw new Exception('Missing ID attribute on SAML message.');
}
$this->id = $xml->getAttribute('ID');
if ($xml->getAttribute('Version') !== '2.0') {
/* Currently a very strict check. */
throw new Exception('Unsupported version: ' . $xml->getAttribute('Version'));
}
$this->issueInstant = SimpleSAML_Utilities::parseSAML2Time($xml->getAttribute('IssueInstant'));
if ($xml->hasAttribute('Destination')) {
$this->destination = $xml->getAttribute('Destination');
}
$issuer = SAML2_Utils::xpQuery($xml, './saml_assertion:Issuer');
if (!empty($issuer)) {
$this->issuer = trim($issuer[0]->textContent);
}
/* Validate the signature element of the message. */
try {
$sig = SAML2_Utils::validateElement($xml);
if ($sig !== FALSE) {
$this->certificates = $sig['Certificates'];
$this->validators[] = array(
'Function' => array('SAML2_Utils', 'validateSignature'),
'Data' => $sig,
);
}
} catch (Exception $e) {
/* Ignore signature validation errors. */
}
}
/**
* Add a method for validating this message.
*
* This function is used by the HTTP-Redirect binding, to make it possible to
* check the signature against the one included in the query string.
*
* @param callback $function The function which should be called.
* @param mixed $data The data that should be included as the first parameter to the function.
*/
public function addValidator($function, $data) {
assert('is_callable($function)');
$this->validators[] = array(
'Function' => $function,
'Data' => $data,
);
}
/**
* Validate this message against a public key.
*
* TRUE is returned on success, FALSE is returned if we don't have any
* signature we can validate. An exception is thrown if the signature
* validation fails.
*
* @param XMLSecurityKey $key The key we should check against.
* @return boolean TRUE on success, FALSE when we don't have a signature.
*/
public function validate(XMLSecurityKey $key) {
if (count($this->validators) === 0) {
return FALSE;
}
$exceptions = array();
foreach ($this->validators as $validator) {
$function = $validator['Function'];
$data = $validator['Data'];
try {
call_user_func($function, $data, $key);
/* We were able to validate the message with this validator. */
return TRUE;
} catch (Exception $e) {
$exceptions[] = $e;
}
}
/* No validators were able to validate the message. */
throw $exceptions[0];
}
/**
* Retrieve the identifier of this message.
*
* @return string The identifier of this message.
*/
public function getId() {
return $this->id;
}
/**
* Set the identifier of this message.
*
* @param string $id The new identifier of this message.
*/
public function setId($id) {
assert('is_string($id)');
$this->id = $id;
}
/**
* Retrieve the issue timestamp of this message.
*
* @return int The issue timestamp of this message, as an UNIX timestamp.
*/
public function getIssueInstant() {
return $this->issueInstant;
}
/**
* Set the issue timestamp of this message.
*
* @param int $issueInstant The new issue timestamp of this message, as an UNIX timestamp.
*/
public function setIssueInstant($issueInstant) {
assert('is_int($issueInstant)');
$this->issueInstant = $issueInstant;
}
/**
* Retrieve the destination of this message.
*
* @return string|NULL The destination of this message, or NULL if no destination is given.
*/
public function getDestination() {
return $this->destination;
}
/**
* Set the destination of this message.
*
* @param string|NULL $destination The new destination of this message.
*/
public function setDestination($destination) {
assert('is_string($destination) || is_null($destination)');
$this->destination = $destination;
}
/**
* Retrieve the issuer if this message.
*
* @return string|NULL The issuer of this message, or NULL if no issuer is given.
*/
public function getIssuer() {
return $this->issuer;
}
/**
* Set the issuer of this message.
*
* @param string|NULL $issuer The new issuer of this message.
*/
public function setIssuer($issuer) {
assert('is_string($issuer) || is_null($issuer)');
$this->issuer = $issuer;
}
/**
* Retrieve the RelayState associated with this message.
*
* @return string|NULL The RelayState, or NULL if no RelayState is given.
*/
public function getRelayState() {
return $this->relayState;
}
/**
* Set the RelayState associated with this message.
*
* @param string|NULL $relayState The new RelayState.
*/
public function setRelayState($relayState) {
assert('is_string($relayState) || is_null($relayState)');
$this->relayState = $relayState;
}
/**
* Convert this message to an unsigned XML document.
*
* This method does not sign the resulting XML document.
*
* @return DOMElement The root element of the DOM tree.
*/
public function toUnsignedXML() {
$this->document = new DOMDocument();
$root = $this->document->createElementNS(SAML2_Const::NS_SAMLP, 'samlp:' . $this->tagName);
$this->document->appendChild($root);
/* Ugly hack to add another namespace declaration to the root element. */
$root->setAttributeNS(SAML2_Const::NS_SAML, 'saml:tmp', 'tmp');
$root->removeAttributeNS(SAML2_Const::NS_SAML, 'tmp');
$root->setAttribute('ID', $this->id);
$root->setAttribute('Version', '2.0');
$root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant));
if ($this->destination !== NULL) {
$root->setAttribute('Destination', $this->destination);
}
if ($this->issuer !== NULL) {
SAML2_Utils::addString($root, SAML2_Const::NS_SAML, 'saml:Issuer', $this->issuer);
}
return $root;
}
/**
* Convert this message to a signed XML document.
*
* This method sign the resulting XML document if the private key for
* the signature is set.
*
* @return DOMElement The root element of the DOM tree.
*/
public function toSignedXML() {
$root = $this->toUnsignedXML();
if ($this->signatureKey === NULL) {
/* We don't have a key to sign it with. */
return $root;
}
/* Find the position we should insert the signature node at. */
if ($this->issuer !== NULL) {
/*
* We have an issuer node. The signature node should come
* after the issuer node.
*/
$issuerNode = $root->firstChild;
$insertBefore = $issuerNode->nextSibling;
} else {
/* No issuer node - the signature element should be the first element. */
$insertBefore = $root->firstChild;
}
SAML2_Utils::insertSignature($this->signatureKey, $this->certificates, $root, $insertBefore);
return $root;
}
/**
* Retrieve the private key we should use to sign the message.
*
* @return XMLSecurityKey|NULL The key, or NULL if no key is specified.
*/
public function getSignatureKey() {
return $this->signatureKey;
}
/**
* Set the private key we should use to sign the message.
*
* If the key is NULL, the message will be sent unsigned.
*
* @param XMLSecurityKey|NULL $key
*/
public function setSignatureKey(XMLsecurityKey $signatureKey = NULL) {
$this->signatureKey = $signatureKey;
}
/**
* Set the certificates that should be included in the message.
*
* The certificates should be strings with the PEM encoded data.
*
* @param array $certificates An array of certificates.
*/
public function setCertificates(array $certificates) {
$this->certificates = $certificates;
}
/**
* Retrieve the certificates that are included in the message.
*
* @return array An array of certificates.
*/
public function getCertificates() {
return $this->certificates;
}
/**
* Convert an XML element into a message.
*
* @param DOMElement $xml The root XML element.
* @return SAML2_Message The message.
*/
public static function fromXML(DOMElement $xml) {
if ($xml->namespaceURI !== SAML2_Const::NS_SAMLP) {
throw new Exception('Unknown namespace of SAML message: ' . var_export($xml->namespaceURI, TRUE));
}
switch ($xml->localName) {
case 'AttributeQuery':
return new SAML2_AttributeQuery($xml);
case 'AuthnRequest':
return new SAML2_AuthnRequest($xml);
case 'LogoutResponse':
return new SAML2_LogoutResponse($xml);
case 'LogoutRequest':
return new SAML2_LogoutRequest($xml);
case 'Response':
return new SAML2_Response($xml);
case 'ArtifactResponse':
return new SAML2_ArtifactResponse($xml);
case 'ArtifactResolve':
return new SAML2_ArtifactResolve($xml);
default:
throw new Exception('Unknown SAML message: ' . var_export($xml->localName, TRUE));
}
}
}
?>