. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * * Neither the name of Robert Richards nor the names of his * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * @author Robert Richards * @copyright 2007-2020 Robert Richards * @license http://www.opensource.org/licenses/bsd-license.php BSD License */ class XMLSecurityDSig { const XMLDSIGNS = 'http://www.w3.org/2000/09/xmldsig#'; const SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'; const SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'; const SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384'; const SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'; const RIPEMD160 = 'http://www.w3.org/2001/04/xmlenc#ripemd160'; const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; const C14N_COMMENTS = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments'; const EXC_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#'; const EXC_C14N_COMMENTS = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments'; const template = ' '; const BASE_TEMPLATE = ' '; /** @var DOMElement|null */ public $sigNode = null; /** @var array */ public $idKeys = array(); /** @var array */ public $idNS = array(); /** @var string|null */ private $signedInfo = null; /** @var DomXPath|null */ private $xPathCtx = null; /** @var string|null */ private $canonicalMethod = null; /** @var string */ private $prefix = ''; /** @var string */ private $searchpfx = 'secdsig'; /** * This variable contains an associative array of validated nodes. * @var array|null */ private $validatedNodes = null; /** * @param string $prefix */ public function __construct($prefix='ds') { $template = self::BASE_TEMPLATE; if (! empty($prefix)) { $this->prefix = $prefix.':'; $search = array("ownerDocument; } if ($doc) { $xpath = new DOMXPath($doc); $xpath->registerNamespace('secdsig', self::XMLDSIGNS); $query = ".//secdsig:Signature"; $nodeset = $xpath->query($query, $objDoc); $this->sigNode = $nodeset->item($pos); $query = "./secdsig:SignedInfo"; $nodeset = $xpath->query($query, $this->sigNode); if ($nodeset->length > 1) { throw new Exception("Invalid structure - Too many SignedInfo elements found"); } return $this->sigNode; } return null; } /** * @param string $name * @param null|string $value * @return DOMElement */ public function createNewSignNode($name, $value=null) { $doc = $this->sigNode->ownerDocument; if (! is_null($value)) { $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix.$name, $value); } else { $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix.$name); } return $node; } /** * @param string $method * @throws Exception */ public function setCanonicalMethod($method) { switch ($method) { case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': case 'http://www.w3.org/2001/10/xml-exc-c14n#': case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': $this->canonicalMethod = $method; break; default: throw new Exception('Invalid Canonical Method'); } if ($xpath = $this->getXPathObj()) { $query = './'.$this->searchpfx.':SignedInfo'; $nodeset = $xpath->query($query, $this->sigNode); if ($sinfo = $nodeset->item(0)) { $query = './'.$this->searchpfx.'CanonicalizationMethod'; $nodeset = $xpath->query($query, $sinfo); if (! ($canonNode = $nodeset->item(0))) { $canonNode = $this->createNewSignNode('CanonicalizationMethod'); $sinfo->insertBefore($canonNode, $sinfo->firstChild); } $canonNode->setAttribute('Algorithm', $this->canonicalMethod); } } } /** * @param DOMNode $node * @param string $canonicalmethod * @param null|array $arXPath * @param null|array $prefixList * @return string */ private function canonicalizeData($node, $canonicalmethod, $arXPath=null, $prefixList=null) { $exclusive = false; $withComments = false; switch ($canonicalmethod) { case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': $exclusive = false; $withComments = false; break; case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': $withComments = true; break; case 'http://www.w3.org/2001/10/xml-exc-c14n#': $exclusive = true; break; case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': $exclusive = true; $withComments = true; break; } if (is_null($arXPath) && ($node instanceof DOMNode) && ($node->ownerDocument !== null) && $node->isSameNode($node->ownerDocument->documentElement)) { /* Check for any PI or comments as they would have been excluded */ $element = $node; while ($refnode = $element->previousSibling) { if ($refnode->nodeType == XML_PI_NODE || (($refnode->nodeType == XML_COMMENT_NODE) && $withComments)) { break; } $element = $refnode; } if ($refnode == null) { $node = $node->ownerDocument; } } return $node->C14N($exclusive, $withComments, $arXPath, $prefixList); } /** * @return null|string */ public function canonicalizeSignedInfo() { $doc = $this->sigNode->ownerDocument; $canonicalmethod = null; if ($doc) { $xpath = $this->getXPathObj(); $query = "./secdsig:SignedInfo"; $nodeset = $xpath->query($query, $this->sigNode); if ($nodeset->length > 1) { throw new Exception("Invalid structure - Too many SignedInfo elements found"); } if ($signInfoNode = $nodeset->item(0)) { $query = "./secdsig:CanonicalizationMethod"; $nodeset = $xpath->query($query, $signInfoNode); $prefixList = null; if ($canonNode = $nodeset->item(0)) { $canonicalmethod = $canonNode->getAttribute('Algorithm'); foreach ($canonNode->childNodes as $node) { if ($node->localName == 'InclusiveNamespaces') { if ($pfx = $node->getAttribute('PrefixList')) { $arpfx = array_filter(explode(' ', $pfx)); if (count($arpfx) > 0) { $prefixList = array_merge($prefixList ? $prefixList : array(), $arpfx); } } } } } $this->signedInfo = $this->canonicalizeData($signInfoNode, $canonicalmethod, null, $prefixList); return $this->signedInfo; } } return null; } /** * @param string $digestAlgorithm * @param string $data * @param bool $encode * @return string * @throws Exception */ public function calculateDigest($digestAlgorithm, $data, $encode = true) { switch ($digestAlgorithm) { case self::SHA1: $alg = 'sha1'; break; case self::SHA256: $alg = 'sha256'; break; case self::SHA384: $alg = 'sha384'; break; case self::SHA512: $alg = 'sha512'; break; case self::RIPEMD160: $alg = 'ripemd160'; break; default: throw new Exception("Cannot validate digest: Unsupported Algorithm <$digestAlgorithm>"); } $digest = hash($alg, $data, true); if ($encode) { $digest = base64_encode($digest); } return $digest; } /** * @param $refNode * @param string $data * @return bool */ public function validateDigest($refNode, $data) { $xpath = new DOMXPath($refNode->ownerDocument); $xpath->registerNamespace('secdsig', self::XMLDSIGNS); $query = 'string(./secdsig:DigestMethod/@Algorithm)'; $digestAlgorithm = $xpath->evaluate($query, $refNode); $digValue = $this->calculateDigest($digestAlgorithm, $data, false); $query = 'string(./secdsig:DigestValue)'; $digestValue = $xpath->evaluate($query, $refNode); return ($digValue === base64_decode($digestValue)); } /** * @param $refNode * @param DOMNode $objData * @param bool $includeCommentNodes * @return string */ public function processTransforms($refNode, $objData, $includeCommentNodes = true) { $data = $objData; $xpath = new DOMXPath($refNode->ownerDocument); $xpath->registerNamespace('secdsig', self::XMLDSIGNS); $query = './secdsig:Transforms/secdsig:Transform'; $nodelist = $xpath->query($query, $refNode); $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; $arXPath = null; $prefixList = null; foreach ($nodelist AS $transform) { $algorithm = $transform->getAttribute("Algorithm"); switch ($algorithm) { case 'http://www.w3.org/2001/10/xml-exc-c14n#': case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': if (!$includeCommentNodes) { /* We remove comment nodes by forcing it to use a canonicalization * without comments. */ $canonicalMethod = 'http://www.w3.org/2001/10/xml-exc-c14n#'; } else { $canonicalMethod = $algorithm; } $node = $transform->firstChild; while ($node) { if ($node->localName == 'InclusiveNamespaces') { if ($pfx = $node->getAttribute('PrefixList')) { $arpfx = array(); $pfxlist = explode(" ", $pfx); foreach ($pfxlist AS $pfx) { $val = trim($pfx); if (! empty($val)) { $arpfx[] = $val; } } if (count($arpfx) > 0) { $prefixList = $arpfx; } } break; } $node = $node->nextSibling; } break; case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': if (!$includeCommentNodes) { /* We remove comment nodes by forcing it to use a canonicalization * without comments. */ $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; } else { $canonicalMethod = $algorithm; } break; case 'http://www.w3.org/TR/1999/REC-xpath-19991116': $node = $transform->firstChild; while ($node) { if ($node->localName == 'XPath') { $arXPath = array(); $arXPath['query'] = '(.//. | .//@* | .//namespace::*)['.$node->nodeValue.']'; $arXPath['namespaces'] = array(); $nslist = $xpath->query('./namespace::*', $node); foreach ($nslist AS $nsnode) { if ($nsnode->localName != "xml") { $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue; } } break; } $node = $node->nextSibling; } break; } } if ($data instanceof DOMNode) { $data = $this->canonicalizeData($objData, $canonicalMethod, $arXPath, $prefixList); } return $data; } /** * @param DOMNode $refNode * @return bool */ public function processRefNode($refNode) { $dataObject = null; /* * Depending on the URI, we may not want to include comments in the result * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel */ $includeCommentNodes = true; if ($uri = $refNode->getAttribute("URI")) { $arUrl = parse_url($uri); if (empty($arUrl['path'])) { if ($identifier = $arUrl['fragment']) { /* This reference identifies a node with the given id by using * a URI on the form "#identifier". This should not include comments. */ $includeCommentNodes = false; $xPath = new DOMXPath($refNode->ownerDocument); if ($this->idNS && is_array($this->idNS)) { foreach ($this->idNS as $nspf => $ns) { $xPath->registerNamespace($nspf, $ns); } } $iDlist = '@Id="'.XPath::filterAttrValue($identifier, XPath::DOUBLE_QUOTE).'"'; if (is_array($this->idKeys)) { foreach ($this->idKeys as $idKey) { $iDlist .= " or @".XPath::filterAttrName($idKey).'="'. XPath::filterAttrValue($identifier, XPath::DOUBLE_QUOTE).'"'; } } $query = '//*['.$iDlist.']'; $dataObject = $xPath->query($query)->item(0); } else { $dataObject = $refNode->ownerDocument; } } } else { /* This reference identifies the root node with an empty URI. This should * not include comments. */ $includeCommentNodes = false; $dataObject = $refNode->ownerDocument; } $data = $this->processTransforms($refNode, $dataObject, $includeCommentNodes); if (!$this->validateDigest($refNode, $data)) { return false; } if ($dataObject instanceof DOMNode) { /* Add this node to the list of validated nodes. */ if (! empty($identifier)) { $this->validatedNodes[$identifier] = $dataObject; } else { $this->validatedNodes[] = $dataObject; } } return true; } /** * @param DOMNode $refNode * @return null */ public function getRefNodeID($refNode) { if ($uri = $refNode->getAttribute("URI")) { $arUrl = parse_url($uri); if (empty($arUrl['path'])) { if ($identifier = $arUrl['fragment']) { return $identifier; } } } return null; } /** * @return array * @throws Exception */ public function getRefIDs() { $refids = array(); $xpath = $this->getXPathObj(); $query = "./secdsig:SignedInfo[1]/secdsig:Reference"; $nodeset = $xpath->query($query, $this->sigNode); if ($nodeset->length == 0) { throw new Exception("Reference nodes not found"); } foreach ($nodeset AS $refNode) { $refids[] = $this->getRefNodeID($refNode); } return $refids; } /** * @return bool * @throws Exception */ public function validateReference() { $docElem = $this->sigNode->ownerDocument->documentElement; if (! $docElem->isSameNode($this->sigNode)) { if ($this->sigNode->parentNode != null) { $this->sigNode->parentNode->removeChild($this->sigNode); } } $xpath = $this->getXPathObj(); $query = "./secdsig:SignedInfo[1]/secdsig:Reference"; $nodeset = $xpath->query($query, $this->sigNode); if ($nodeset->length == 0) { throw new Exception("Reference nodes not found"); } /* Initialize/reset the list of validated nodes. */ $this->validatedNodes = array(); foreach ($nodeset AS $refNode) { if (! $this->processRefNode($refNode)) { /* Clear the list of validated nodes. */ $this->validatedNodes = null; throw new Exception("Reference validation failed"); } } return true; } /** * @param DOMNode $sinfoNode * @param DOMDocument $node * @param string $algorithm * @param null|array $arTransforms * @param null|array $options */ private function addRefInternal($sinfoNode, $node, $algorithm, $arTransforms=null, $options=null) { $prefix = null; $prefix_ns = null; $id_name = 'Id'; $overwrite_id = true; $force_uri = false; if (is_array($options)) { $prefix = empty($options['prefix']) ? null : $options['prefix']; $prefix_ns = empty($options['prefix_ns']) ? null : $options['prefix_ns']; $id_name = empty($options['id_name']) ? 'Id' : $options['id_name']; $overwrite_id = !isset($options['overwrite']) ? true : (bool) $options['overwrite']; $force_uri = !isset($options['force_uri']) ? false : (bool) $options['force_uri']; } $attname = $id_name; if (! empty($prefix)) { $attname = $prefix.':'.$attname; } $refNode = $this->createNewSignNode('Reference'); $sinfoNode->appendChild($refNode); if (! $node instanceof DOMDocument) { $uri = null; if (! $overwrite_id) { $uri = $prefix_ns ? $node->getAttributeNS($prefix_ns, $id_name) : $node->getAttribute($id_name); } if (empty($uri)) { $uri = self::generateGUID(); $node->setAttributeNS($prefix_ns, $attname, $uri); } $refNode->setAttribute("URI", '#'.$uri); } elseif ($force_uri) { $refNode->setAttribute("URI", ''); } $transNodes = $this->createNewSignNode('Transforms'); $refNode->appendChild($transNodes); if (is_array($arTransforms)) { foreach ($arTransforms AS $transform) { $transNode = $this->createNewSignNode('Transform'); $transNodes->appendChild($transNode); if (is_array($transform) && (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116'])) && (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']))) { $transNode->setAttribute('Algorithm', 'http://www.w3.org/TR/1999/REC-xpath-19991116'); $XPathNode = $this->createNewSignNode('XPath', $transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']); $transNode->appendChild($XPathNode); if (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'])) { foreach ($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'] AS $prefix => $namespace) { $XPathNode->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:$prefix", $namespace); } } } else { $transNode->setAttribute('Algorithm', $transform); } } } elseif (! empty($this->canonicalMethod)) { $transNode = $this->createNewSignNode('Transform'); $transNodes->appendChild($transNode); $transNode->setAttribute('Algorithm', $this->canonicalMethod); } $canonicalData = $this->processTransforms($refNode, $node); $digValue = $this->calculateDigest($algorithm, $canonicalData); $digestMethod = $this->createNewSignNode('DigestMethod'); $refNode->appendChild($digestMethod); $digestMethod->setAttribute('Algorithm', $algorithm); $digestValue = $this->createNewSignNode('DigestValue', $digValue); $refNode->appendChild($digestValue); } /** * @param DOMDocument $node * @param string $algorithm * @param null|array $arTransforms * @param null|array $options */ public function addReference($node, $algorithm, $arTransforms=null, $options=null) { if ($xpath = $this->getXPathObj()) { $query = "./secdsig:SignedInfo"; $nodeset = $xpath->query($query, $this->sigNode); if ($sInfo = $nodeset->item(0)) { $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options); } } } /** * @param array $arNodes * @param string $algorithm * @param null|array $arTransforms * @param null|array $options */ public function addReferenceList($arNodes, $algorithm, $arTransforms=null, $options=null) { if ($xpath = $this->getXPathObj()) { $query = "./secdsig:SignedInfo"; $nodeset = $xpath->query($query, $this->sigNode); if ($sInfo = $nodeset->item(0)) { foreach ($arNodes AS $node) { $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options); } } } } /** * @param DOMElement|string $data * @param null|string $mimetype * @param null|string $encoding * @return DOMElement */ public function addObject($data, $mimetype=null, $encoding=null) { $objNode = $this->createNewSignNode('Object'); $this->sigNode->appendChild($objNode); if (! empty($mimetype)) { $objNode->setAttribute('MimeType', $mimetype); } if (! empty($encoding)) { $objNode->setAttribute('Encoding', $encoding); } if ($data instanceof DOMElement) { $newData = $this->sigNode->ownerDocument->importNode($data, true); } else { $newData = $this->sigNode->ownerDocument->createTextNode($data); } $objNode->appendChild($newData); return $objNode; } /** * @param null|DOMNode $node * @return null|XMLSecurityKey */ public function locateKey($node=null) { if (empty($node)) { $node = $this->sigNode; } if (! $node instanceof DOMNode) { return null; } if ($doc = $node->ownerDocument) { $xpath = new DOMXPath($doc); $xpath->registerNamespace('secdsig', self::XMLDSIGNS); $query = "string(./secdsig:SignedInfo/secdsig:SignatureMethod/@Algorithm)"; $algorithm = $xpath->evaluate($query, $node); if ($algorithm) { try { $objKey = new XMLSecurityKey($algorithm, array('type' => 'public')); } catch (Exception $e) { return null; } return $objKey; } } return null; } /** * Returns: * Bool when verifying HMAC_SHA1; * Int otherwise, with following meanings: * 1 on succesful signature verification, * 0 when signature verification failed, * -1 if an error occurred during processing. * * NOTE: be very careful when checking the int return value, because in * PHP, -1 will be cast to True when in boolean context. Always check the * return value in a strictly typed way, e.g. "$obj->verify(...) === 1". * * @param XMLSecurityKey $objKey * @return bool|int * @throws Exception */ public function verify($objKey) { $doc = $this->sigNode->ownerDocument; $xpath = new DOMXPath($doc); $xpath->registerNamespace('secdsig', self::XMLDSIGNS); $query = "string(./secdsig:SignatureValue)"; $sigValue = $xpath->evaluate($query, $this->sigNode); if (empty($sigValue)) { throw new Exception("Unable to locate SignatureValue"); } return $objKey->verifySignature($this->signedInfo, base64_decode($sigValue)); } /** * @param XMLSecurityKey $objKey * @param string $data * @return mixed|string */ public function signData($objKey, $data) { return $objKey->signData($data); } /** * @param XMLSecurityKey $objKey * @param null|DOMNode $appendToNode */ public function sign($objKey, $appendToNode = null) { // If we have a parent node append it now so C14N properly works if ($appendToNode != null) { $this->resetXPathObj(); $this->appendSignature($appendToNode); $this->sigNode = $appendToNode->lastChild; } if ($xpath = $this->getXPathObj()) { $query = "./secdsig:SignedInfo"; $nodeset = $xpath->query($query, $this->sigNode); if ($sInfo = $nodeset->item(0)) { $query = "./secdsig:SignatureMethod"; $nodeset = $xpath->query($query, $sInfo); $sMethod = $nodeset->item(0); $sMethod->setAttribute('Algorithm', $objKey->type); $data = $this->canonicalizeData($sInfo, $this->canonicalMethod); $sigValue = base64_encode($this->signData($objKey, $data)); $sigValueNode = $this->createNewSignNode('SignatureValue', $sigValue); if ($infoSibling = $sInfo->nextSibling) { $infoSibling->parentNode->insertBefore($sigValueNode, $infoSibling); } else { $this->sigNode->appendChild($sigValueNode); } } } } public function appendCert() { } /** * @param XMLSecurityKey $objKey * @param null|DOMNode $parent */ public function appendKey($objKey, $parent=null) { $objKey->serializeKey($parent); } /** * This function inserts the signature element. * * The signature element will be appended to the element, unless $beforeNode is specified. If $beforeNode * is specified, the signature element will be inserted as the last element before $beforeNode. * * @param DOMNode $node The node the signature element should be inserted into. * @param DOMNode $beforeNode The node the signature element should be located before. * * @return DOMNode The signature element node */ public function insertSignature($node, $beforeNode = null) { $document = $node->ownerDocument; $signatureElement = $document->importNode($this->sigNode, true); if ($beforeNode == null) { return $node->insertBefore($signatureElement); } else { return $node->insertBefore($signatureElement, $beforeNode); } } /** * @param DOMNode $parentNode * @param bool $insertBefore * @return DOMNode */ public function appendSignature($parentNode, $insertBefore = false) { $beforeNode = $insertBefore ? $parentNode->firstChild : null; return $this->insertSignature($parentNode, $beforeNode); } /** * @param string $cert * @param bool $isPEMFormat * @return string */ public static function get509XCert($cert, $isPEMFormat=true) { $certs = self::staticGet509XCerts($cert, $isPEMFormat); if (! empty($certs)) { return $certs[0]; } return ''; } /** * @param string $certs * @param bool $isPEMFormat * @return array */ public static function staticGet509XCerts($certs, $isPEMFormat=true) { if ($isPEMFormat) { $data = ''; $certlist = array(); $arCert = explode("\n", $certs); $inData = false; foreach ($arCert AS $curData) { if (! $inData) { if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) { $inData = true; } } else { if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) { $inData = false; $certlist[] = $data; $data = ''; continue; } $data .= trim($curData); } } return $certlist; } else { return array($certs); } } /** * @param DOMElement $parentRef * @param string $cert * @param bool $isPEMFormat * @param bool $isURL * @param null|DOMXPath $xpath * @param null|array $options * @throws Exception */ public static function staticAdd509Cert($parentRef, $cert, $isPEMFormat=true, $isURL=false, $xpath=null, $options=null) { if ($isURL) { $cert = file_get_contents($cert); } if (! $parentRef instanceof DOMElement) { throw new Exception('Invalid parent Node parameter'); } $baseDoc = $parentRef->ownerDocument; if (empty($xpath)) { $xpath = new DOMXPath($parentRef->ownerDocument); $xpath->registerNamespace('secdsig', self::XMLDSIGNS); } $query = "./secdsig:KeyInfo"; $nodeset = $xpath->query($query, $parentRef); $keyInfo = $nodeset->item(0); $dsig_pfx = ''; if (! $keyInfo) { $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS); if (! empty($pfx)) { $dsig_pfx = $pfx.":"; } $inserted = false; $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'KeyInfo'); $query = "./secdsig:Object"; $nodeset = $xpath->query($query, $parentRef); if ($sObject = $nodeset->item(0)) { $sObject->parentNode->insertBefore($keyInfo, $sObject); $inserted = true; } if (! $inserted) { $parentRef->appendChild($keyInfo); } } else { $pfx = $keyInfo->lookupPrefix(self::XMLDSIGNS); if (! empty($pfx)) { $dsig_pfx = $pfx.":"; } } // Add all certs if there are more than one $certs = self::staticGet509XCerts($cert, $isPEMFormat); // Attach X509 data node $x509DataNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509Data'); $keyInfo->appendChild($x509DataNode); $issuerSerial = false; $subjectName = false; if (is_array($options)) { if (! empty($options['issuerSerial'])) { $issuerSerial = true; } if (! empty($options['subjectName'])) { $subjectName = true; } } // Attach all certificate nodes and any additional data foreach ($certs as $X509Cert) { if ($issuerSerial || $subjectName) { if ($certData = openssl_x509_parse("-----BEGIN CERTIFICATE-----\n".chunk_split($X509Cert, 64, "\n")."-----END CERTIFICATE-----\n")) { if ($subjectName && ! empty($certData['subject'])) { if (is_array($certData['subject'])) { $parts = array(); foreach ($certData['subject'] AS $key => $value) { if (is_array($value)) { foreach ($value as $valueElement) { array_unshift($parts, "$key=$valueElement"); } } else { array_unshift($parts, "$key=$value"); } } $subjectNameValue = implode(',', $parts); } else { $subjectNameValue = $certData['issuer']; } $x509SubjectNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509SubjectName', $subjectNameValue); $x509DataNode->appendChild($x509SubjectNode); } if ($issuerSerial && ! empty($certData['issuer']) && ! empty($certData['serialNumber'])) { if (is_array($certData['issuer'])) { $parts = array(); foreach ($certData['issuer'] AS $key => $value) { array_unshift($parts, "$key=$value"); } $issuerName = implode(',', $parts); } else { $issuerName = $certData['issuer']; } $x509IssuerNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509IssuerSerial'); $x509DataNode->appendChild($x509IssuerNode); $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509IssuerName', $issuerName); $x509IssuerNode->appendChild($x509Node); $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509SerialNumber', $certData['serialNumber']); $x509IssuerNode->appendChild($x509Node); } } } $x509CertNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509Certificate', $X509Cert); $x509DataNode->appendChild($x509CertNode); } } /** * @param string $cert * @param bool $isPEMFormat * @param bool $isURL * @param null|array $options */ public function add509Cert($cert, $isPEMFormat=true, $isURL=false, $options=null) { if ($xpath = $this->getXPathObj()) { self::staticAdd509Cert($this->sigNode, $cert, $isPEMFormat, $isURL, $xpath, $options); } } /** * This function appends a node to the KeyInfo. * * The KeyInfo element will be created if one does not exist in the document. * * @param DOMNode $node The node to append to the KeyInfo. * * @return DOMNode The KeyInfo element node */ public function appendToKeyInfo($node) { $parentRef = $this->sigNode; $baseDoc = $parentRef->ownerDocument; $xpath = $this->getXPathObj(); if (empty($xpath)) { $xpath = new DOMXPath($parentRef->ownerDocument); $xpath->registerNamespace('secdsig', self::XMLDSIGNS); } $query = "./secdsig:KeyInfo"; $nodeset = $xpath->query($query, $parentRef); $keyInfo = $nodeset->item(0); if (! $keyInfo) { $dsig_pfx = ''; $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS); if (! empty($pfx)) { $dsig_pfx = $pfx.":"; } $inserted = false; $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'KeyInfo'); $query = "./secdsig:Object"; $nodeset = $xpath->query($query, $parentRef); if ($sObject = $nodeset->item(0)) { $sObject->parentNode->insertBefore($keyInfo, $sObject); $inserted = true; } if (! $inserted) { $parentRef->appendChild($keyInfo); } } $keyInfo->appendChild($node); return $keyInfo; } /** * This function retrieves an associative array of the validated nodes. * * The array will contain the id of the referenced node as the key and the node itself * as the value. * * Returns: * An associative array of validated nodes or null if no nodes have been validated. * * @return array Associative array of validated nodes */ public function getValidatedNodes() { return $this->validatedNodes; } }