<?php
/**
 * Determine if the SAML response is valid using a provided x509 certificate
 *
 * LICENSE: This Software is the property of Lifestyle Webconsulting GmbH (Aschaffenburg, Germany)
 * and is protected by copyright law - it is NOT Freeware.
 *
 * Any unauthorized use of this software without a valid license
 * is a violation of the license agreement and will be prosecuted by
 * civil and criminal law.
 *
 * @copyright  2015 Lifestyle Webconsulting GmbH
 * @link       http://www.life-style.de
 */

namespace LifeStyle\Tools\SamlBundle\Saml;

use LifeStyle\Tools\SamlBundle\Exception\InvalidSamlResponseException;

require_once __DIR__ . '/../Library/xmlseclibs/xmlseclibs.php';

/**
 * Determine if the SAML response is valid using a provided x509 certificate.
 */
class XmlSec
{
    /**
     * Saml config
     *
     * @var Configuration
     */
    protected $samlConfig;

    /**
     * The document to be tested.
     *
     * @var \DomDocument
     */
    protected $document;

    /**
     * Construct the SamlXmlSec object.
     *
     * @param Configuration $samlConfig Settings containing the necessary X.509 certificate to decode the XML.
     * @param Response $response The document to test.
     */
    public function __construct(Configuration $samlConfig, Response $response)
    {
        $this->samlConfig = $samlConfig;
        $this->document = clone $response->document;
    }

    /**
     * Verify that the document only contains a single Assertion
     *
     * @return bool TRUE if the document passes.
     */
    public function validateNumAssertions()
    {
        $rootNode = $this->document;
        $assertionNodes = $rootNode->getElementsByTagName('Assertion');
        return ($assertionNodes->length == 1);
    }

    /**
     * Verify that the document is still valid according
     *
     * @return bool
     */
    public function validateTimestamps()
    {
        $rootNode = $this->document;
        $timestampNodes = $rootNode->getElementsByTagName('Conditions');
        for ($i = 0; $i < $timestampNodes->length; $i++) {
            $nbAttribute = $timestampNodes->item($i)->attributes->getNamedItem("NotBefore");
            $naAttribute = $timestampNodes->item($i)->attributes->getNamedItem("NotOnOrAfter");
            if ($nbAttribute && strtotime($nbAttribute->textContent) > time()) {
                return false;
            }
            if ($naAttribute && strtotime($naAttribute->textContent) <= time()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Validate saml response
     *
     * @return bool
     * @throws InvalidSamlResponseException
     */
    public function isValid()
    {
        $objXMLSecDSig = new \XMLSecurityDSig();

        $objDSig = $objXMLSecDSig->locateSignature($this->document);
        if (!$objDSig) {
            throw new InvalidSamlResponseException('Cannot locate Signature Node');
        }
        $objXMLSecDSig->canonicalizeSignedInfo();
        $objXMLSecDSig->idKeys = array('ID');

        $retVal = $objXMLSecDSig->validateReference();
        if (!$retVal) {
            throw new InvalidSamlResponseException('Reference Validation Failed');
        }

        $singleAssertion = $this->validateNumAssertions();
        if (!$singleAssertion) {
            throw new InvalidSamlResponseException('Multiple assertions are not supported');
        }

        $validTimestamps = $this->validateTimestamps();
        if (!$validTimestamps) {
            throw new InvalidSamlResponseException('Timing issues (please check your clock settings)');
        }

        $objKey = $objXMLSecDSig->locateKey();
        if (!$objKey) {
            throw new InvalidSamlResponseException('We have no idea about the key');
        }
        \XMLSecEnc::staticLocateKeyInfo($objKey, $objDSig);

        $objKey->loadKey($this->samlConfig->getIdpPublicCertificate(), FALSE, TRUE);

        return ($objXMLSecDSig->verify($objKey) === 1);
    }
}
