Subversion Repositories oidplus

Rev

Rev 1050 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2. namespace RobRichards\XMLSecLibs;
  3.  
  4. use DOMDocument;
  5. use DOMElement;
  6. use DOMNode;
  7. use DOMXPath;
  8. use Exception;
  9. use RobRichards\XMLSecLibs\Utils\XPath as XPath;
  10.  
  11. /**
  12.  * xmlseclibs.php
  13.  *
  14.  * Copyright (c) 2007-2020, Robert Richards <rrichards@cdatazone.org>.
  15.  * All rights reserved.
  16.  *
  17.  * Redistribution and use in source and binary forms, with or without
  18.  * modification, are permitted provided that the following conditions
  19.  * are met:
  20.  *
  21.  *   * Redistributions of source code must retain the above copyright
  22.  *     notice, this list of conditions and the following disclaimer.
  23.  *
  24.  *   * Redistributions in binary form must reproduce the above copyright
  25.  *     notice, this list of conditions and the following disclaimer in
  26.  *     the documentation and/or other materials provided with the
  27.  *     distribution.
  28.  *
  29.  *   * Neither the name of Robert Richards nor the names of his
  30.  *     contributors may be used to endorse or promote products derived
  31.  *     from this software without specific prior written permission.
  32.  *
  33.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  34.  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  35.  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  36.  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  37.  * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  38.  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  39.  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  40.  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  41.  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  42.  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  43.  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  44.  * POSSIBILITY OF SUCH DAMAGE.
  45.  *
  46.  * @author    Robert Richards <rrichards@cdatazone.org>
  47.  * @copyright 2007-2020 Robert Richards <rrichards@cdatazone.org>
  48.  * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License
  49.  */
  50.  
  51. class XMLSecurityDSig
  52. {
  53.     const XMLDSIGNS = 'http://www.w3.org/2000/09/xmldsig#';
  54.     const SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1';
  55.     const SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256';
  56.     const SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384';
  57.     const SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512';
  58.     const RIPEMD160 = 'http://www.w3.org/2001/04/xmlenc#ripemd160';
  59.  
  60.     const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
  61.     const C14N_COMMENTS = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments';
  62.     const EXC_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#';
  63.     const EXC_C14N_COMMENTS = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments';
  64.  
  65.     const template = '<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
  66.  <ds:SignedInfo>
  67.    <ds:SignatureMethod />
  68.  </ds:SignedInfo>
  69. </ds:Signature>';
  70.  
  71.     const BASE_TEMPLATE = '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
  72.  <SignedInfo>
  73.    <SignatureMethod />
  74.  </SignedInfo>
  75. </Signature>';
  76.  
  77.     /** @var DOMElement|null */
  78.     public $sigNode = null;
  79.  
  80.     /** @var array */
  81.     public $idKeys = array();
  82.  
  83.     /** @var array */
  84.     public $idNS = array();
  85.  
  86.     /** @var string|null */
  87.     private $signedInfo = null;
  88.  
  89.     /** @var DomXPath|null */
  90.     private $xPathCtx = null;
  91.  
  92.     /** @var string|null */
  93.     private $canonicalMethod = null;
  94.  
  95.     /** @var string */
  96.     private $prefix = '';
  97.  
  98.     /** @var string */
  99.     private $searchpfx = 'secdsig';
  100.  
  101.     /**
  102.      * This variable contains an associative array of validated nodes.
  103.      * @var array|null
  104.      */
  105.     private $validatedNodes = null;
  106.  
  107.     /**
  108.      * @param string $prefix
  109.      */
  110.     public function __construct($prefix='ds')
  111.     {
  112.         $template = self::BASE_TEMPLATE;
  113.         if (! empty($prefix)) {
  114.             $this->prefix = $prefix.':';
  115.             $search = array("<S", "</S", "xmlns=");
  116.             $replace = array("<$prefix:S", "</$prefix:S", "xmlns:$prefix=");
  117.             $template = str_replace($search, $replace, $template);
  118.         }
  119.         $sigdoc = new DOMDocument();
  120.         $sigdoc->loadXML($template);
  121.         $this->sigNode = $sigdoc->documentElement;
  122.     }
  123.  
  124.     /**
  125.      * Reset the XPathObj to null
  126.      */
  127.     private function resetXPathObj()
  128.     {
  129.         $this->xPathCtx = null;
  130.     }
  131.  
  132.     /**
  133.      * Returns the XPathObj or null if xPathCtx is set and sigNode is empty.
  134.      *
  135.      * @return DOMXPath|null
  136.      */
  137.     private function getXPathObj()
  138.     {
  139.         if (empty($this->xPathCtx) && ! empty($this->sigNode)) {
  140.             $xpath = new DOMXPath($this->sigNode->ownerDocument);
  141.             $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
  142.             $this->xPathCtx = $xpath;
  143.         }
  144.         return $this->xPathCtx;
  145.     }
  146.  
  147.     /**
  148.      * Generate guid
  149.      *
  150.      * @param string $prefix Prefix to use for guid. defaults to pfx
  151.      *
  152.      * @return string The generated guid
  153.      */
  154.     public static function generateGUID($prefix='pfx')
  155.     {
  156.         $uuid = md5(uniqid(mt_rand(), true));
  157.         $guid = $prefix.substr($uuid, 0, 8)."-".
  158.                 substr($uuid, 8, 4)."-".
  159.                 substr($uuid, 12, 4)."-".
  160.                 substr($uuid, 16, 4)."-".
  161.                 substr($uuid, 20, 12);
  162.         return $guid;
  163.     }
  164.  
  165.     /**
  166.      * Generate guid
  167.      *
  168.      * @param string $prefix Prefix to use for guid. defaults to pfx
  169.      *
  170.      * @return string The generated guid
  171.      *
  172.      * @deprecated Method deprecated in Release 1.4.1
  173.      */
  174.     public static function generate_GUID($prefix='pfx')
  175.     {
  176.         return self::generateGUID($prefix);
  177.     }
  178.  
  179.     /**
  180.      * @param DOMDocument $objDoc
  181.      * @param int $pos
  182.      * @return DOMNode|null
  183.      */
  184.     public function locateSignature($objDoc, $pos=0)
  185.     {
  186.         if ($objDoc instanceof DOMDocument) {
  187.             $doc = $objDoc;
  188.         } else {
  189.             $doc = $objDoc->ownerDocument;
  190.         }
  191.         if ($doc) {
  192.             $xpath = new DOMXPath($doc);
  193.             $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
  194.             $query = ".//secdsig:Signature";
  195.             $nodeset = $xpath->query($query, $objDoc);
  196.             $this->sigNode = $nodeset->item($pos);
  197.             $query = "./secdsig:SignedInfo";
  198.             $nodeset = $xpath->query($query, $this->sigNode);
  199.             if ($nodeset->length > 1) {
  200.                 throw new Exception("Invalid structure - Too many SignedInfo elements found");
  201.             }
  202.             return $this->sigNode;
  203.         }
  204.         return null;
  205.     }
  206.  
  207.     /**
  208.      * @param string $name
  209.      * @param null|string $value
  210.      * @return DOMElement
  211.      */
  212.     public function createNewSignNode($name, $value=null)
  213.     {
  214.         $doc = $this->sigNode->ownerDocument;
  215.         if (! is_null($value)) {
  216.             $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix.$name, $value);
  217.         } else {
  218.             $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix.$name);
  219.         }
  220.         return $node;
  221.     }
  222.  
  223.     /**
  224.      * @param string $method
  225.      * @throws Exception
  226.      */
  227.     public function setCanonicalMethod($method)
  228.     {
  229.         switch ($method) {
  230.             case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':
  231.             case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':
  232.             case 'http://www.w3.org/2001/10/xml-exc-c14n#':
  233.             case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':
  234.                 $this->canonicalMethod = $method;
  235.                 break;
  236.             default:
  237.                 throw new Exception('Invalid Canonical Method');
  238.         }
  239.         if ($xpath = $this->getXPathObj()) {
  240.             $query = './'.$this->searchpfx.':SignedInfo';
  241.             $nodeset = $xpath->query($query, $this->sigNode);
  242.             if ($sinfo = $nodeset->item(0)) {
  243.                 $query = './'.$this->searchpfx.'CanonicalizationMethod';
  244.                 $nodeset = $xpath->query($query, $sinfo);
  245.                 if (! ($canonNode = $nodeset->item(0))) {
  246.                     $canonNode = $this->createNewSignNode('CanonicalizationMethod');
  247.                     $sinfo->insertBefore($canonNode, $sinfo->firstChild);
  248.                 }
  249.                 $canonNode->setAttribute('Algorithm', $this->canonicalMethod);
  250.             }
  251.         }
  252.     }
  253.  
  254.     /**
  255.      * @param DOMNode $node
  256.      * @param string $canonicalmethod
  257.      * @param null|array $arXPath
  258.      * @param null|array $prefixList
  259.      * @return string
  260.      */
  261.     private function canonicalizeData($node, $canonicalmethod, $arXPath=null, $prefixList=null)
  262.     {
  263.         $exclusive = false;
  264.         $withComments = false;
  265.         switch ($canonicalmethod) {
  266.             case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':
  267.                 $exclusive = false;
  268.                 $withComments = false;
  269.                 break;
  270.             case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':
  271.                 $withComments = true;
  272.                 break;
  273.             case 'http://www.w3.org/2001/10/xml-exc-c14n#':
  274.                 $exclusive = true;
  275.                 break;
  276.             case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':
  277.                 $exclusive = true;
  278.                 $withComments = true;
  279.                 break;
  280.         }
  281.  
  282.         if (is_null($arXPath) && ($node instanceof DOMNode) && ($node->ownerDocument !== null) && $node->isSameNode($node->ownerDocument->documentElement)) {
  283.             /* Check for any PI or comments as they would have been excluded */
  284.             $element = $node;
  285.             while ($refnode = $element->previousSibling) {
  286.                 if ($refnode->nodeType == XML_PI_NODE || (($refnode->nodeType == XML_COMMENT_NODE) && $withComments)) {
  287.                     break;
  288.                 }
  289.                 $element = $refnode;
  290.             }
  291.             if ($refnode == null) {
  292.                 $node = $node->ownerDocument;
  293.             }
  294.         }
  295.  
  296.         return $node->C14N($exclusive, $withComments, $arXPath, $prefixList);
  297.     }
  298.  
  299.     /**
  300.      * @return null|string
  301.      */
  302.     public function canonicalizeSignedInfo()
  303.     {
  304.  
  305.         $doc = $this->sigNode->ownerDocument;
  306.         $canonicalmethod = null;
  307.         if ($doc) {
  308.             $xpath = $this->getXPathObj();
  309.             $query = "./secdsig:SignedInfo";
  310.             $nodeset = $xpath->query($query, $this->sigNode);
  311.             if ($nodeset->length > 1) {
  312.                 throw new Exception("Invalid structure - Too many SignedInfo elements found");
  313.             }
  314.             if ($signInfoNode = $nodeset->item(0)) {
  315.                 $query = "./secdsig:CanonicalizationMethod";
  316.                 $nodeset = $xpath->query($query, $signInfoNode);
  317.                 $prefixList = null;
  318.                 if ($canonNode = $nodeset->item(0)) {
  319.                     $canonicalmethod = $canonNode->getAttribute('Algorithm');
  320.                     foreach ($canonNode->childNodes as $node)
  321.                     {
  322.                         if ($node->localName == 'InclusiveNamespaces') {
  323.                             if ($pfx = $node->getAttribute('PrefixList')) {
  324.                                 $arpfx = array_filter(explode(' ', $pfx));
  325.                                 if (count($arpfx) > 0) {
  326.                                     $prefixList = array_merge($prefixList ? $prefixList : array(), $arpfx);
  327.                                 }
  328.                             }
  329.                         }
  330.                     }
  331.                 }
  332.                 $this->signedInfo = $this->canonicalizeData($signInfoNode, $canonicalmethod, null, $prefixList);
  333.                 return $this->signedInfo;
  334.             }
  335.         }
  336.         return null;
  337.     }
  338.  
  339.     /**
  340.      * @param string $digestAlgorithm
  341.      * @param string $data
  342.      * @param bool $encode
  343.      * @return string
  344.      * @throws Exception
  345.      */
  346.     public function calculateDigest($digestAlgorithm, $data, $encode = true)
  347.     {
  348.         switch ($digestAlgorithm) {
  349.             case self::SHA1:
  350.                 $alg = 'sha1';
  351.                 break;
  352.             case self::SHA256:
  353.                 $alg = 'sha256';
  354.                 break;
  355.             case self::SHA384:
  356.                 $alg = 'sha384';
  357.                 break;
  358.             case self::SHA512:
  359.                 $alg = 'sha512';
  360.                 break;
  361.             case self::RIPEMD160:
  362.                 $alg = 'ripemd160';
  363.                 break;
  364.             default:
  365.                 throw new Exception("Cannot validate digest: Unsupported Algorithm <$digestAlgorithm>");
  366.         }
  367.  
  368.         $digest = hash($alg, $data, true);
  369.         if ($encode) {
  370.             $digest = base64_encode($digest);
  371.         }
  372.         return $digest;
  373.  
  374.     }
  375.  
  376.     /**
  377.      * @param $refNode
  378.      * @param string $data
  379.      * @return bool
  380.      */
  381.     public function validateDigest($refNode, $data)
  382.     {
  383.         $xpath = new DOMXPath($refNode->ownerDocument);
  384.         $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
  385.         $query = 'string(./secdsig:DigestMethod/@Algorithm)';
  386.         $digestAlgorithm = $xpath->evaluate($query, $refNode);
  387.         $digValue = $this->calculateDigest($digestAlgorithm, $data, false);
  388.         $query = 'string(./secdsig:DigestValue)';
  389.         $digestValue = $xpath->evaluate($query, $refNode);
  390.         return ($digValue === base64_decode($digestValue));
  391.     }
  392.  
  393.     /**
  394.      * @param $refNode
  395.      * @param DOMNode $objData
  396.      * @param bool $includeCommentNodes
  397.      * @return string
  398.      */
  399.     public function processTransforms($refNode, $objData, $includeCommentNodes = true)
  400.     {
  401.         $data = $objData;
  402.         $xpath = new DOMXPath($refNode->ownerDocument);
  403.         $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
  404.         $query = './secdsig:Transforms/secdsig:Transform';
  405.         $nodelist = $xpath->query($query, $refNode);
  406.         $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
  407.         $arXPath = null;
  408.         $prefixList = null;
  409.         foreach ($nodelist AS $transform) {
  410.             $algorithm = $transform->getAttribute("Algorithm");
  411.             switch ($algorithm) {
  412.                 case 'http://www.w3.org/2001/10/xml-exc-c14n#':
  413.                 case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':
  414.  
  415.                     if (!$includeCommentNodes) {
  416.                         /* We remove comment nodes by forcing it to use a canonicalization
  417.                          * without comments.
  418.                          */
  419.                         $canonicalMethod = 'http://www.w3.org/2001/10/xml-exc-c14n#';
  420.                     } else {
  421.                         $canonicalMethod = $algorithm;
  422.                     }
  423.  
  424.                     $node = $transform->firstChild;
  425.                     while ($node) {
  426.                         if ($node->localName == 'InclusiveNamespaces') {
  427.                             if ($pfx = $node->getAttribute('PrefixList')) {
  428.                                 $arpfx = array();
  429.                                 $pfxlist = explode(" ", $pfx);
  430.                                 foreach ($pfxlist AS $pfx) {
  431.                                     $val = trim($pfx);
  432.                                     if (! empty($val)) {
  433.                                         $arpfx[] = $val;
  434.                                     }
  435.                                 }
  436.                                 if (count($arpfx) > 0) {
  437.                                     $prefixList = $arpfx;
  438.                                 }
  439.                             }
  440.                             break;
  441.                         }
  442.                         $node = $node->nextSibling;
  443.                     }
  444.             break;
  445.                 case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':
  446.                 case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':
  447.                     if (!$includeCommentNodes) {
  448.                         /* We remove comment nodes by forcing it to use a canonicalization
  449.                          * without comments.
  450.                          */
  451.                         $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
  452.                     } else {
  453.                         $canonicalMethod = $algorithm;
  454.                     }
  455.  
  456.                     break;
  457.                 case 'http://www.w3.org/TR/1999/REC-xpath-19991116':
  458.                     $node = $transform->firstChild;
  459.                     while ($node) {
  460.                         if ($node->localName == 'XPath') {
  461.                             $arXPath = array();
  462.                             $arXPath['query'] = '(.//. | .//@* | .//namespace::*)['.$node->nodeValue.']';
  463.                             $arXPath['namespaces'] = array();
  464.                             $nslist = $xpath->query('./namespace::*', $node);
  465.                             foreach ($nslist AS $nsnode) {
  466.                                 if ($nsnode->localName != "xml") {
  467.                                     $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue;
  468.                                 }
  469.                             }
  470.                             break;
  471.                         }
  472.                         $node = $node->nextSibling;
  473.                     }
  474.                     break;
  475.             }
  476.         }
  477.         if ($data instanceof DOMNode) {
  478.             $data = $this->canonicalizeData($objData, $canonicalMethod, $arXPath, $prefixList);
  479.         }
  480.         return $data;
  481.     }
  482.  
  483.     /**
  484.      * @param DOMNode $refNode
  485.      * @return bool
  486.      */
  487.     public function processRefNode($refNode)
  488.     {
  489.         $dataObject = null;
  490.  
  491.         /*
  492.          * Depending on the URI, we may not want to include comments in the result
  493.          * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
  494.          */
  495.         $includeCommentNodes = true;
  496.  
  497.         if ($uri = $refNode->getAttribute("URI")) {
  498.             $arUrl = parse_url($uri);
  499.             if (empty($arUrl['path'])) {
  500.                 if ($identifier = $arUrl['fragment']) {
  501.  
  502.                     /* This reference identifies a node with the given id by using
  503.                      * a URI on the form "#identifier". This should not include comments.
  504.                      */
  505.                     $includeCommentNodes = false;
  506.  
  507.                     $xPath = new DOMXPath($refNode->ownerDocument);
  508.                     if ($this->idNS && is_array($this->idNS)) {
  509.                         foreach ($this->idNS as $nspf => $ns) {
  510.                             $xPath->registerNamespace($nspf, $ns);
  511.                         }
  512.                     }
  513.                     $iDlist = '@Id="'.XPath::filterAttrValue($identifier, XPath::DOUBLE_QUOTE).'"';
  514.                     if (is_array($this->idKeys)) {
  515.                         foreach ($this->idKeys as $idKey) {
  516.                             $iDlist .= " or @".XPath::filterAttrName($idKey).'="'.
  517.                                 XPath::filterAttrValue($identifier, XPath::DOUBLE_QUOTE).'"';
  518.                         }
  519.                     }
  520.                     $query = '//*['.$iDlist.']';
  521.                     $dataObject = $xPath->query($query)->item(0);
  522.                 } else {
  523.                     $dataObject = $refNode->ownerDocument;
  524.                 }
  525.             }
  526.         } else {
  527.             /* This reference identifies the root node with an empty URI. This should
  528.              * not include comments.
  529.              */
  530.             $includeCommentNodes = false;
  531.  
  532.             $dataObject = $refNode->ownerDocument;
  533.         }
  534.         $data = $this->processTransforms($refNode, $dataObject, $includeCommentNodes);
  535.         if (!$this->validateDigest($refNode, $data)) {
  536.             return false;
  537.         }
  538.  
  539.         if ($dataObject instanceof DOMNode) {
  540.             /* Add this node to the list of validated nodes. */
  541.             if (! empty($identifier)) {
  542.                 $this->validatedNodes[$identifier] = $dataObject;
  543.             } else {
  544.                 $this->validatedNodes[] = $dataObject;
  545.             }
  546.         }
  547.  
  548.         return true;
  549.     }
  550.  
  551.     /**
  552.      * @param DOMNode $refNode
  553.      * @return null
  554.      */
  555.     public function getRefNodeID($refNode)
  556.     {
  557.         if ($uri = $refNode->getAttribute("URI")) {
  558.             $arUrl = parse_url($uri);
  559.             if (empty($arUrl['path'])) {
  560.                 if ($identifier = $arUrl['fragment']) {
  561.                     return $identifier;
  562.                 }
  563.             }
  564.         }
  565.         return null;
  566.     }
  567.  
  568.     /**
  569.      * @return array
  570.      * @throws Exception
  571.      */
  572.     public function getRefIDs()
  573.     {
  574.         $refids = array();
  575.  
  576.         $xpath = $this->getXPathObj();
  577.         $query = "./secdsig:SignedInfo[1]/secdsig:Reference";
  578.         $nodeset = $xpath->query($query, $this->sigNode);
  579.         if ($nodeset->length == 0) {
  580.             throw new Exception("Reference nodes not found");
  581.         }
  582.         foreach ($nodeset AS $refNode) {
  583.             $refids[] = $this->getRefNodeID($refNode);
  584.         }
  585.         return $refids;
  586.     }
  587.  
  588.     /**
  589.      * @return bool
  590.      * @throws Exception
  591.      */
  592.     public function validateReference()
  593.     {
  594.         $docElem = $this->sigNode->ownerDocument->documentElement;
  595.         if (! $docElem->isSameNode($this->sigNode)) {
  596.             if ($this->sigNode->parentNode != null) {
  597.                 $this->sigNode->parentNode->removeChild($this->sigNode);
  598.             }
  599.         }
  600.         $xpath = $this->getXPathObj();
  601.         $query = "./secdsig:SignedInfo[1]/secdsig:Reference";
  602.         $nodeset = $xpath->query($query, $this->sigNode);
  603.         if ($nodeset->length == 0) {
  604.             throw new Exception("Reference nodes not found");
  605.         }
  606.  
  607.         /* Initialize/reset the list of validated nodes. */
  608.         $this->validatedNodes = array();
  609.  
  610.         foreach ($nodeset AS $refNode) {
  611.             if (! $this->processRefNode($refNode)) {
  612.                 /* Clear the list of validated nodes. */
  613.                 $this->validatedNodes = null;
  614.                 throw new Exception("Reference validation failed");
  615.             }
  616.         }
  617.         return true;
  618.     }
  619.  
  620.     /**
  621.      * @param DOMNode $sinfoNode
  622.      * @param DOMDocument $node
  623.      * @param string $algorithm
  624.      * @param null|array $arTransforms
  625.      * @param null|array $options
  626.      */
  627.     private function addRefInternal($sinfoNode, $node, $algorithm, $arTransforms=null, $options=null)
  628.     {
  629.         $prefix = null;
  630.         $prefix_ns = null;
  631.         $id_name = 'Id';
  632.         $overwrite_id  = true;
  633.         $force_uri = false;
  634.  
  635.         if (is_array($options)) {
  636.             $prefix = empty($options['prefix']) ? null : $options['prefix'];
  637.             $prefix_ns = empty($options['prefix_ns']) ? null : $options['prefix_ns'];
  638.             $id_name = empty($options['id_name']) ? 'Id' : $options['id_name'];
  639.             $overwrite_id = !isset($options['overwrite']) ? true : (bool) $options['overwrite'];
  640.             $force_uri = !isset($options['force_uri']) ? false : (bool) $options['force_uri'];
  641.         }
  642.  
  643.         $attname = $id_name;
  644.         if (! empty($prefix)) {
  645.             $attname = $prefix.':'.$attname;
  646.         }
  647.  
  648.         $refNode = $this->createNewSignNode('Reference');
  649.         $sinfoNode->appendChild($refNode);
  650.  
  651.         if (! $node instanceof DOMDocument) {
  652.             $uri = null;
  653.             if (! $overwrite_id) {
  654.                 $uri = $prefix_ns ? $node->getAttributeNS($prefix_ns, $id_name) : $node->getAttribute($id_name);
  655.             }
  656.             if (empty($uri)) {
  657.                 $uri = self::generateGUID();
  658.                 $node->setAttributeNS($prefix_ns, $attname, $uri);
  659.             }
  660.             $refNode->setAttribute("URI", '#'.$uri);
  661.         } elseif ($force_uri) {
  662.             $refNode->setAttribute("URI", '');
  663.         }
  664.  
  665.         $transNodes = $this->createNewSignNode('Transforms');
  666.         $refNode->appendChild($transNodes);
  667.  
  668.         if (is_array($arTransforms)) {
  669.             foreach ($arTransforms AS $transform) {
  670.                 $transNode = $this->createNewSignNode('Transform');
  671.                 $transNodes->appendChild($transNode);
  672.                 if (is_array($transform) &&
  673.                     (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116'])) &&
  674.                     (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']))) {
  675.                     $transNode->setAttribute('Algorithm', 'http://www.w3.org/TR/1999/REC-xpath-19991116');
  676.                     $XPathNode = $this->createNewSignNode('XPath', $transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']);
  677.                     $transNode->appendChild($XPathNode);
  678.                     if (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'])) {
  679.                         foreach ($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'] AS $prefix => $namespace) {
  680.                             $XPathNode->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:$prefix", $namespace);
  681.                         }
  682.                     }
  683.                 } else {
  684.                     $transNode->setAttribute('Algorithm', $transform);
  685.                 }
  686.             }
  687.         } elseif (! empty($this->canonicalMethod)) {
  688.             $transNode = $this->createNewSignNode('Transform');
  689.             $transNodes->appendChild($transNode);
  690.             $transNode->setAttribute('Algorithm', $this->canonicalMethod);
  691.         }
  692.  
  693.         $canonicalData = $this->processTransforms($refNode, $node);
  694.         $digValue = $this->calculateDigest($algorithm, $canonicalData);
  695.  
  696.         $digestMethod = $this->createNewSignNode('DigestMethod');
  697.         $refNode->appendChild($digestMethod);
  698.         $digestMethod->setAttribute('Algorithm', $algorithm);
  699.  
  700.         $digestValue = $this->createNewSignNode('DigestValue', $digValue);
  701.         $refNode->appendChild($digestValue);
  702.     }
  703.  
  704.     /**
  705.      * @param DOMDocument $node
  706.      * @param string $algorithm
  707.      * @param null|array $arTransforms
  708.      * @param null|array $options
  709.      */
  710.     public function addReference($node, $algorithm, $arTransforms=null, $options=null)
  711.     {
  712.         if ($xpath = $this->getXPathObj()) {
  713.             $query = "./secdsig:SignedInfo";
  714.             $nodeset = $xpath->query($query, $this->sigNode);
  715.             if ($sInfo = $nodeset->item(0)) {
  716.                 $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options);
  717.             }
  718.         }
  719.     }
  720.  
  721.     /**
  722.      * @param array $arNodes
  723.      * @param string $algorithm
  724.      * @param null|array $arTransforms
  725.      * @param null|array $options
  726.      */
  727.     public function addReferenceList($arNodes, $algorithm, $arTransforms=null, $options=null)
  728.     {
  729.         if ($xpath = $this->getXPathObj()) {
  730.             $query = "./secdsig:SignedInfo";
  731.             $nodeset = $xpath->query($query, $this->sigNode);
  732.             if ($sInfo = $nodeset->item(0)) {
  733.                 foreach ($arNodes AS $node) {
  734.                     $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options);
  735.                 }
  736.             }
  737.         }
  738.     }
  739.  
  740.     /**
  741.      * @param DOMElement|string $data
  742.      * @param null|string $mimetype
  743.      * @param null|string $encoding
  744.      * @return DOMElement
  745.      */
  746.     public function addObject($data, $mimetype=null, $encoding=null)
  747.     {
  748.         $objNode = $this->createNewSignNode('Object');
  749.         $this->sigNode->appendChild($objNode);
  750.         if (! empty($mimetype)) {
  751.             $objNode->setAttribute('MimeType', $mimetype);
  752.         }
  753.         if (! empty($encoding)) {
  754.             $objNode->setAttribute('Encoding', $encoding);
  755.         }
  756.  
  757.         if ($data instanceof DOMElement) {
  758.             $newData = $this->sigNode->ownerDocument->importNode($data, true);
  759.         } else {
  760.             $newData = $this->sigNode->ownerDocument->createTextNode($data);
  761.         }
  762.         $objNode->appendChild($newData);
  763.  
  764.         return $objNode;
  765.     }
  766.  
  767.     /**
  768.      * @param null|DOMNode $node
  769.      * @return null|XMLSecurityKey
  770.      */
  771.     public function locateKey($node=null)
  772.     {
  773.         if (empty($node)) {
  774.             $node = $this->sigNode;
  775.         }
  776.         if (! $node instanceof DOMNode) {
  777.             return null;
  778.         }
  779.         if ($doc = $node->ownerDocument) {
  780.             $xpath = new DOMXPath($doc);
  781.             $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
  782.             $query = "string(./secdsig:SignedInfo/secdsig:SignatureMethod/@Algorithm)";
  783.             $algorithm = $xpath->evaluate($query, $node);
  784.             if ($algorithm) {
  785.                 try {
  786.                     $objKey = new XMLSecurityKey($algorithm, array('type' => 'public'));
  787.                 } catch (Exception $e) {
  788.                     return null;
  789.                 }
  790.                 return $objKey;
  791.             }
  792.         }
  793.         return null;
  794.     }
  795.  
  796.     /**
  797.      * Returns:
  798.      *  Bool when verifying HMAC_SHA1;
  799.      *  Int otherwise, with following meanings:
  800.      *    1 on succesful signature verification,
  801.      *    0 when signature verification failed,
  802.      *   -1 if an error occurred during processing.
  803.      *
  804.      * NOTE: be very careful when checking the int return value, because in
  805.      * PHP, -1 will be cast to True when in boolean context. Always check the
  806.      * return value in a strictly typed way, e.g. "$obj->verify(...) === 1".
  807.      *
  808.      * @param XMLSecurityKey $objKey
  809.      * @return bool|int
  810.      * @throws Exception
  811.      */
  812.     public function verify($objKey)
  813.     {
  814.         $doc = $this->sigNode->ownerDocument;
  815.         $xpath = new DOMXPath($doc);
  816.         $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
  817.         $query = "string(./secdsig:SignatureValue)";
  818.         $sigValue = $xpath->evaluate($query, $this->sigNode);
  819.         if (empty($sigValue)) {
  820.             throw new Exception("Unable to locate SignatureValue");
  821.         }
  822.         return $objKey->verifySignature($this->signedInfo, base64_decode($sigValue));
  823.     }
  824.  
  825.     /**
  826.      * @param XMLSecurityKey $objKey
  827.      * @param string $data
  828.      * @return mixed|string
  829.      */
  830.     public function signData($objKey, $data)
  831.     {
  832.         return $objKey->signData($data);
  833.     }
  834.  
  835.     /**
  836.      * @param XMLSecurityKey $objKey
  837.      * @param null|DOMNode $appendToNode
  838.      */
  839.     public function sign($objKey, $appendToNode = null)
  840.     {
  841.         // If we have a parent node append it now so C14N properly works
  842.         if ($appendToNode != null) {
  843.             $this->resetXPathObj();
  844.             $this->appendSignature($appendToNode);
  845.             $this->sigNode = $appendToNode->lastChild;
  846.         }
  847.         if ($xpath = $this->getXPathObj()) {
  848.             $query = "./secdsig:SignedInfo";
  849.             $nodeset = $xpath->query($query, $this->sigNode);
  850.             if ($sInfo = $nodeset->item(0)) {
  851.                 $query = "./secdsig:SignatureMethod";
  852.                 $nodeset = $xpath->query($query, $sInfo);
  853.                 $sMethod = $nodeset->item(0);
  854.                 $sMethod->setAttribute('Algorithm', $objKey->type);
  855.                 $data = $this->canonicalizeData($sInfo, $this->canonicalMethod);
  856.                 $sigValue = base64_encode($this->signData($objKey, $data));
  857.                 $sigValueNode = $this->createNewSignNode('SignatureValue', $sigValue);
  858.                 if ($infoSibling = $sInfo->nextSibling) {
  859.                     $infoSibling->parentNode->insertBefore($sigValueNode, $infoSibling);
  860.                 } else {
  861.                     $this->sigNode->appendChild($sigValueNode);
  862.                 }
  863.             }
  864.         }
  865.     }
  866.  
  867.     public function appendCert()
  868.     {
  869.  
  870.     }
  871.  
  872.     /**
  873.      * @param XMLSecurityKey $objKey
  874.      * @param null|DOMNode $parent
  875.      */
  876.     public function appendKey($objKey, $parent=null)
  877.     {
  878.         $objKey->serializeKey($parent);
  879.     }
  880.  
  881.  
  882.     /**
  883.      * This function inserts the signature element.
  884.      *
  885.      * The signature element will be appended to the element, unless $beforeNode is specified. If $beforeNode
  886.      * is specified, the signature element will be inserted as the last element before $beforeNode.
  887.      *
  888.      * @param DOMNode $node       The node the signature element should be inserted into.
  889.      * @param DOMNode $beforeNode The node the signature element should be located before.
  890.      *
  891.      * @return DOMNode The signature element node
  892.      */
  893.     public function insertSignature($node, $beforeNode = null)
  894.     {
  895.  
  896.         $document = $node->ownerDocument;
  897.         $signatureElement = $document->importNode($this->sigNode, true);
  898.  
  899.         if ($beforeNode == null) {
  900.             return $node->insertBefore($signatureElement);
  901.         } else {
  902.             return $node->insertBefore($signatureElement, $beforeNode);
  903.         }
  904.     }
  905.  
  906.     /**
  907.      * @param DOMNode $parentNode
  908.      * @param bool $insertBefore
  909.      * @return DOMNode
  910.      */
  911.     public function appendSignature($parentNode, $insertBefore = false)
  912.     {
  913.         $beforeNode = $insertBefore ? $parentNode->firstChild : null;
  914.         return $this->insertSignature($parentNode, $beforeNode);
  915.     }
  916.  
  917.     /**
  918.      * @param string $cert
  919.      * @param bool $isPEMFormat
  920.      * @return string
  921.      */
  922.     public static function get509XCert($cert, $isPEMFormat=true)
  923.     {
  924.         $certs = self::staticGet509XCerts($cert, $isPEMFormat);
  925.         if (! empty($certs)) {
  926.             return $certs[0];
  927.         }
  928.         return '';
  929.     }
  930.  
  931.     /**
  932.      * @param string $certs
  933.      * @param bool $isPEMFormat
  934.      * @return array
  935.      */
  936.     public static function staticGet509XCerts($certs, $isPEMFormat=true)
  937.     {
  938.         if ($isPEMFormat) {
  939.             $data = '';
  940.             $certlist = array();
  941.             $arCert = explode("\n", $certs);
  942.             $inData = false;
  943.             foreach ($arCert AS $curData) {
  944.                 if (! $inData) {
  945.                     if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) {
  946.                         $inData = true;
  947.                     }
  948.                 } else {
  949.                     if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) {
  950.                         $inData = false;
  951.                         $certlist[] = $data;
  952.                         $data = '';
  953.                         continue;
  954.                     }
  955.                     $data .= trim($curData);
  956.                 }
  957.             }
  958.             return $certlist;
  959.         } else {
  960.             return array($certs);
  961.         }
  962.     }
  963.  
  964.     /**
  965.      * @param DOMElement $parentRef
  966.      * @param string $cert
  967.      * @param bool $isPEMFormat
  968.      * @param bool $isURL
  969.      * @param null|DOMXPath $xpath
  970.      * @param null|array $options
  971.      * @throws Exception
  972.      */
  973.     public static function staticAdd509Cert($parentRef, $cert, $isPEMFormat=true, $isURL=false, $xpath=null, $options=null)
  974.     {
  975.         if ($isURL) {
  976.             $cert = file_get_contents($cert);
  977.         }
  978.         if (! $parentRef instanceof DOMElement) {
  979.             throw new Exception('Invalid parent Node parameter');
  980.         }
  981.         $baseDoc = $parentRef->ownerDocument;
  982.  
  983.         if (empty($xpath)) {
  984.             $xpath = new DOMXPath($parentRef->ownerDocument);
  985.             $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
  986.         }
  987.  
  988.         $query = "./secdsig:KeyInfo";
  989.         $nodeset = $xpath->query($query, $parentRef);
  990.         $keyInfo = $nodeset->item(0);
  991.         $dsig_pfx = '';
  992.         if (! $keyInfo) {
  993.             $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS);
  994.             if (! empty($pfx)) {
  995.                 $dsig_pfx = $pfx.":";
  996.             }
  997.             $inserted = false;
  998.             $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'KeyInfo');
  999.  
  1000.             $query = "./secdsig:Object";
  1001.             $nodeset = $xpath->query($query, $parentRef);
  1002.             if ($sObject = $nodeset->item(0)) {
  1003.                 $sObject->parentNode->insertBefore($keyInfo, $sObject);
  1004.                 $inserted = true;
  1005.             }
  1006.  
  1007.             if (! $inserted) {
  1008.                 $parentRef->appendChild($keyInfo);
  1009.             }
  1010.         } else {
  1011.             $pfx = $keyInfo->lookupPrefix(self::XMLDSIGNS);
  1012.             if (! empty($pfx)) {
  1013.                 $dsig_pfx = $pfx.":";
  1014.             }
  1015.         }
  1016.  
  1017.         // Add all certs if there are more than one
  1018.         $certs = self::staticGet509XCerts($cert, $isPEMFormat);
  1019.  
  1020.         // Attach X509 data node
  1021.         $x509DataNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509Data');
  1022.         $keyInfo->appendChild($x509DataNode);
  1023.  
  1024.         $issuerSerial = false;
  1025.         $subjectName = false;
  1026.         if (is_array($options)) {
  1027.             if (! empty($options['issuerSerial'])) {
  1028.                 $issuerSerial = true;
  1029.             }
  1030.             if (! empty($options['subjectName'])) {
  1031.                 $subjectName = true;
  1032.             }
  1033.         }
  1034.  
  1035.         // Attach all certificate nodes and any additional data
  1036.         foreach ($certs as $X509Cert) {
  1037.             if ($issuerSerial || $subjectName) {
  1038.                 if ($certData = openssl_x509_parse("-----BEGIN CERTIFICATE-----\n".chunk_split($X509Cert, 64, "\n")."-----END CERTIFICATE-----\n")) {
  1039.                     if ($subjectName && ! empty($certData['subject'])) {
  1040.                         if (is_array($certData['subject'])) {
  1041.                             $parts = array();
  1042.                             foreach ($certData['subject'] AS $key => $value) {
  1043.                                 if (is_array($value)) {
  1044.                                     foreach ($value as $valueElement) {
  1045.                                         array_unshift($parts, "$key=$valueElement");
  1046.                                     }
  1047.                                 } else {
  1048.                                     array_unshift($parts, "$key=$value");
  1049.                                 }
  1050.                             }
  1051.                             $subjectNameValue = implode(',', $parts);
  1052.                         } else {
  1053.                             $subjectNameValue = $certData['issuer'];
  1054.                         }
  1055.                         $x509SubjectNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509SubjectName', $subjectNameValue);
  1056.                         $x509DataNode->appendChild($x509SubjectNode);
  1057.                     }
  1058.                     if ($issuerSerial && ! empty($certData['issuer']) && ! empty($certData['serialNumber'])) {
  1059.                         if (is_array($certData['issuer'])) {
  1060.                             $parts = array();
  1061.                             foreach ($certData['issuer'] AS $key => $value) {
  1062.                                 array_unshift($parts, "$key=$value");
  1063.                             }
  1064.                             $issuerName = implode(',', $parts);
  1065.                         } else {
  1066.                             $issuerName = $certData['issuer'];
  1067.                         }
  1068.  
  1069.                         $x509IssuerNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509IssuerSerial');
  1070.                         $x509DataNode->appendChild($x509IssuerNode);
  1071.  
  1072.                         $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509IssuerName', $issuerName);
  1073.                         $x509IssuerNode->appendChild($x509Node);
  1074.                         $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509SerialNumber', $certData['serialNumber']);
  1075.                         $x509IssuerNode->appendChild($x509Node);
  1076.                     }
  1077.                 }
  1078.  
  1079.             }
  1080.             $x509CertNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509Certificate', $X509Cert);
  1081.             $x509DataNode->appendChild($x509CertNode);
  1082.         }
  1083.     }
  1084.  
  1085.     /**
  1086.      * @param string $cert
  1087.      * @param bool $isPEMFormat
  1088.      * @param bool $isURL
  1089.      * @param null|array $options
  1090.      */
  1091.     public function add509Cert($cert, $isPEMFormat=true, $isURL=false, $options=null)
  1092.     {
  1093.         if ($xpath = $this->getXPathObj()) {
  1094.             self::staticAdd509Cert($this->sigNode, $cert, $isPEMFormat, $isURL, $xpath, $options);
  1095.         }
  1096.     }
  1097.  
  1098.     /**
  1099.      * This function appends a node to the KeyInfo.
  1100.      *
  1101.      * The KeyInfo element will be created if one does not exist in the document.
  1102.      *
  1103.      * @param DOMNode $node The node to append to the KeyInfo.
  1104.      *
  1105.      * @return DOMNode The KeyInfo element node
  1106.      */
  1107.     public function appendToKeyInfo($node)
  1108.     {
  1109.         $parentRef = $this->sigNode;
  1110.         $baseDoc = $parentRef->ownerDocument;
  1111.  
  1112.         $xpath = $this->getXPathObj();
  1113.         if (empty($xpath)) {
  1114.             $xpath = new DOMXPath($parentRef->ownerDocument);
  1115.             $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
  1116.         }
  1117.  
  1118.         $query = "./secdsig:KeyInfo";
  1119.         $nodeset = $xpath->query($query, $parentRef);
  1120.         $keyInfo = $nodeset->item(0);
  1121.         if (! $keyInfo) {
  1122.             $dsig_pfx = '';
  1123.             $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS);
  1124.             if (! empty($pfx)) {
  1125.                 $dsig_pfx = $pfx.":";
  1126.             }
  1127.             $inserted = false;
  1128.             $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'KeyInfo');
  1129.  
  1130.             $query = "./secdsig:Object";
  1131.             $nodeset = $xpath->query($query, $parentRef);
  1132.             if ($sObject = $nodeset->item(0)) {
  1133.                 $sObject->parentNode->insertBefore($keyInfo, $sObject);
  1134.                 $inserted = true;
  1135.             }
  1136.  
  1137.             if (! $inserted) {
  1138.                 $parentRef->appendChild($keyInfo);
  1139.             }
  1140.         }
  1141.  
  1142.         $keyInfo->appendChild($node);
  1143.  
  1144.         return $keyInfo;
  1145.     }
  1146.  
  1147.     /**
  1148.      * This function retrieves an associative array of the validated nodes.
  1149.      *
  1150.      * The array will contain the id of the referenced node as the key and the node itself
  1151.      * as the value.
  1152.      *
  1153.      * Returns:
  1154.      *  An associative array of validated nodes or null if no nodes have been validated.
  1155.      *
  1156.      *  @return array Associative array of validated nodes
  1157.      */
  1158.     public function getValidatedNodes()
  1159.     {
  1160.         return $this->validatedNodes;
  1161.     }
  1162. }
  1163.