1<?php 2/** 3 * xmlseclibs.php 4 * 5 * Copyright (c) 2007-2019, Robert Richards <rrichards@cdatazone.org>. 6 * All rights reserved. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 12 * * Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 15 * * Redistributions in binary form must reproduce the above copyright 16 * notice, this list of conditions and the following disclaimer in 17 * the documentation and/or other materials provided with the 18 * distribution. 19 * 20 * * Neither the name of Robert Richards nor the names of his 21 * contributors may be used to endorse or promote products derived 22 * from this software without specific prior written permission. 23 * 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 25 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 26 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 27 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 28 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 29 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 30 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 33 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 34 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 * POSSIBILITY OF SUCH DAMAGE. 36 * 37 * @author Robert Richards <rrichards@cdatazone.org> 38 * @copyright 2007-2019 Robert Richards <rrichards@cdatazone.org> 39 * @license http://www.opensource.org/licenses/bsd-license.php BSD License 40 * @version 3.0.4 modified 41 */ 42 43class XMLSecurityKey { 44 const TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'; 45 const AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'; 46 const AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc'; 47 const AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'; 48 const RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'; 49 const RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'; 50 const DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'; 51 const RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; 52 const RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; 53 const RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'; 54 const RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'; 55 const HMAC_SHA1 = 'http://www.w3.org/2000/09/xmldsig#hmac-sha1'; 56 57 private $cryptParams = array(); 58 public $type = 0; 59 public $key = null; 60 public $passphrase = ""; 61 public $iv = null; 62 public $name = null; 63 public $keyChain = null; 64 public $isEncrypted = false; 65 public $encryptedCtx = null; 66 public $guid = null; 67 68 /** 69 * This variable contains the certificate as a string if this key represents an X509-certificate. 70 * If this key doesn't represent a certificate, this will be null. 71 */ 72 private $x509Certificate = null; 73 74 /* This variable contains the certificate thunbprint if we have loaded an X509-certificate. */ 75 private $X509Thumbprint = null; 76 77 public function __construct($type, $params=null) { 78 switch ($type) { 79 case (XMLSecurityKey::TRIPLEDES_CBC): 80 $this->cryptParams['library'] = 'mcrypt'; 81 $this->cryptParams['cipher'] = MCRYPT_TRIPLEDES; 82 $this->cryptParams['mode'] = MCRYPT_MODE_CBC; 83 $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'; 84 $this->cryptParams['keysize'] = 24; 85 break; 86 case (XMLSecurityKey::AES128_CBC): 87 $this->cryptParams['library'] = 'mcrypt'; 88 $this->cryptParams['cipher'] = MCRYPT_RIJNDAEL_128; 89 $this->cryptParams['mode'] = MCRYPT_MODE_CBC; 90 $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'; 91 $this->cryptParams['keysize'] = 16; 92 break; 93 case (XMLSecurityKey::AES192_CBC): 94 $this->cryptParams['library'] = 'mcrypt'; 95 $this->cryptParams['cipher'] = MCRYPT_RIJNDAEL_128; 96 $this->cryptParams['mode'] = MCRYPT_MODE_CBC; 97 $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc'; 98 $this->cryptParams['keysize'] = 24; 99 break; 100 case (XMLSecurityKey::AES256_CBC): 101 $this->cryptParams['library'] = 'mcrypt'; 102 $this->cryptParams['cipher'] = MCRYPT_RIJNDAEL_128; 103 $this->cryptParams['mode'] = MCRYPT_MODE_CBC; 104 $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'; 105 $this->cryptParams['keysize'] = 32; 106 break; 107 case (XMLSecurityKey::RSA_1_5): 108 $this->cryptParams['library'] = 'openssl'; 109 $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; 110 $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'; 111 if (is_array($params) && ! empty($params['type'])) { 112 if ($params['type'] == 'public' || $params['type'] == 'private') { 113 $this->cryptParams['type'] = $params['type']; 114 break; 115 } 116 } 117 throw new Exception('Certificate "type" (private/public) must be passed via parameters'); 118 case (XMLSecurityKey::RSA_OAEP_MGF1P): 119 $this->cryptParams['library'] = 'openssl'; 120 $this->cryptParams['padding'] = OPENSSL_PKCS1_OAEP_PADDING; 121 $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'; 122 $this->cryptParams['hash'] = null; 123 if (is_array($params) && ! empty($params['type'])) { 124 if ($params['type'] == 'public' || $params['type'] == 'private') { 125 $this->cryptParams['type'] = $params['type']; 126 break; 127 } 128 } 129 throw new Exception('Certificate "type" (private/public) must be passed via parameters'); 130 case (XMLSecurityKey::RSA_SHA1): 131 $this->cryptParams['library'] = 'openssl'; 132 $this->cryptParams['method'] = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; 133 $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; 134 if (is_array($params) && ! empty($params['type'])) { 135 if ($params['type'] == 'public' || $params['type'] == 'private') { 136 $this->cryptParams['type'] = $params['type']; 137 break; 138 } 139 } 140 throw new Exception('Certificate "type" (private/public) must be passed via parameters'); 141 case (XMLSecurityKey::RSA_SHA256): 142 $this->cryptParams['library'] = 'openssl'; 143 $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; 144 $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; 145 $this->cryptParams['digest'] = 'SHA256'; 146 if (is_array($params) && ! empty($params['type'])) { 147 if ($params['type'] == 'public' || $params['type'] == 'private') { 148 $this->cryptParams['type'] = $params['type']; 149 break; 150 } 151 } 152 throw new Exception('Certificate "type" (private/public) must be passed via parameters'); 153 case (XMLSecurityKey::RSA_SHA384): 154 $this->cryptParams['library'] = 'openssl'; 155 $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'; 156 $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; 157 $this->cryptParams['digest'] = 'SHA384'; 158 if (is_array($params) && ! empty($params['type'])) { 159 if ($params['type'] == 'public' || $params['type'] == 'private') { 160 $this->cryptParams['type'] = $params['type']; 161 break; 162 } 163 } 164 throw new Exception('Certificate "type" (private/public) must be passed via parameters'); 165 case (XMLSecurityKey::RSA_SHA512): 166 $this->cryptParams['library'] = 'openssl'; 167 $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'; 168 $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; 169 $this->cryptParams['digest'] = 'SHA512'; 170 if (is_array($params) && ! empty($params['type'])) { 171 if ($params['type'] == 'public' || $params['type'] == 'private') { 172 $this->cryptParams['type'] = $params['type']; 173 break; 174 } 175 } 176 throw new Exception('Certificate "type" (private/public) must be passed via parameters'); 177 case (XMLSecurityKey::HMAC_SHA1): 178 $this->cryptParams['library'] = $type; 179 $this->cryptParams['method'] = 'http://www.w3.org/2000/09/xmldsig#hmac-sha1'; 180 break; 181 default: 182 throw new Exception('Invalid Key Type'); 183 } 184 $this->type = $type; 185 } 186 187 /** 188 * Retrieve the key size for the symmetric encryption algorithm.. 189 * 190 * If the key size is unknown, or this isn't a symmetric encryption algorithm, 191 * null is returned. 192 * 193 * @return int|null The number of bytes in the key. 194 */ 195 public function getSymmetricKeySize() { 196 if (! isset($this->cryptParams['keysize'])) { 197 return null; 198 } 199 return $this->cryptParams['keysize']; 200 } 201 202 public function generateSessionKey() { 203 if (!isset($this->cryptParams['keysize'])) { 204 throw new Exception('Unknown key size for type "' . $this->type . '".'); 205 } 206 $keysize = $this->cryptParams['keysize']; 207 208 if (function_exists('openssl_random_pseudo_bytes')) { 209 /* We have PHP >= 5.3 - use openssl to generate session key. */ 210 $key = openssl_random_pseudo_bytes($keysize); 211 } else { 212 /* Generating random key using iv generation routines */ 213 $key = mcrypt_create_iv($keysize, MCRYPT_RAND); 214 } 215 216 if ($this->type === XMLSecurityKey::TRIPLEDES_CBC) { 217 /* Make sure that the generated key has the proper parity bits set. 218 * Mcrypt doesn't care about the parity bits, but others may care. 219 */ 220 for ($i = 0; $i < strlen($key); $i++) { 221 $byte = ord($key[$i]) & 0xfe; 222 $parity = 1; 223 for ($j = 1; $j < 8; $j++) { 224 $parity ^= ($byte >> $j) & 1; 225 } 226 $byte |= $parity; 227 $key[$i] = chr($byte); 228 } 229 } 230 231 $this->key = $key; 232 return $key; 233 } 234 235 public static function getRawThumbprint($cert) { 236 237 $arCert = explode("\n", $cert); 238 $data = ''; 239 $inData = false; 240 241 foreach ($arCert AS $curData) { 242 if (! $inData) { 243 if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) { 244 $inData = true; 245 } 246 } else { 247 if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) { 248 break; 249 } 250 $data .= trim($curData); 251 } 252 } 253 254 if (! empty($data)) { 255 return strtolower(sha1(base64_decode($data))); 256 } 257 258 return null; 259 } 260 261 public function loadKey($key, $isFile=false, $isCert = false) { 262 if ($isFile) { 263 $this->key = file_get_contents($key); 264 } else { 265 $this->key = $key; 266 } 267 if ($isCert) { 268 $this->key = openssl_x509_read($this->key); 269 openssl_x509_export($this->key, $str_cert); 270 $this->x509Certificate = $str_cert; 271 $this->key = $str_cert; 272 } else { 273 $this->x509Certificate = null; 274 } 275 if ($this->cryptParams['library'] == 'openssl') { 276 if ($this->cryptParams['type'] == 'public') { 277 if ($isCert) { 278 /* Load the thumbprint if this is an X509 certificate. */ 279 $this->X509Thumbprint = self::getRawThumbprint($this->key); 280 } 281 $this->key = openssl_get_publickey($this->key); 282 } else { 283 $this->key = openssl_get_privatekey($this->key, $this->passphrase); 284 } 285 } else if (isset($this->cryptParams['cipher']) && $this->cryptParams['cipher'] == MCRYPT_RIJNDAEL_128) { 286 /* Check key length */ 287 switch ($this->type) { 288 case (XMLSecurityKey::AES256_CBC): 289 if (strlen($this->key) < 25) { 290 throw new Exception('Key must contain at least 25 characters for this cipher'); 291 } 292 break; 293 case (XMLSecurityKey::AES192_CBC): 294 if (strlen($this->key) < 17) { 295 throw new Exception('Key must contain at least 17 characters for this cipher'); 296 } 297 break; 298 } 299 } 300 } 301 302 private function encryptMcrypt($data) { 303 $td = mcrypt_module_open($this->cryptParams['cipher'], '', $this->cryptParams['mode'], ''); 304 $this->iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($td), MCRYPT_RAND); 305 mcrypt_generic_init($td, $this->key, $this->iv); 306 if ($this->cryptParams['mode'] == MCRYPT_MODE_CBC) { 307 $bs = mcrypt_enc_get_block_size($td); 308 for ($datalen0=$datalen=strlen($data); (($datalen%$bs)!=($bs-1)); $datalen++) 309 $data.=chr(mt_rand(1, 127)); 310 $data.=chr($datalen-$datalen0+1); 311 } 312 $encrypted_data = $this->iv.mcrypt_generic($td, $data); 313 mcrypt_generic_deinit($td); 314 mcrypt_module_close($td); 315 return $encrypted_data; 316 } 317 318 private function decryptMcrypt($data) { 319 $td = mcrypt_module_open($this->cryptParams['cipher'], '', $this->cryptParams['mode'], ''); 320 $iv_length = mcrypt_enc_get_iv_size($td); 321 322 $this->iv = substr($data, 0, $iv_length); 323 $data = substr($data, $iv_length); 324 325 mcrypt_generic_init($td, $this->key, $this->iv); 326 $decrypted_data = mdecrypt_generic($td, $data); 327 mcrypt_generic_deinit($td); 328 mcrypt_module_close($td); 329 if ($this->cryptParams['mode'] == MCRYPT_MODE_CBC) { 330 $dataLen = strlen($decrypted_data); 331 $paddingLength = substr($decrypted_data, $dataLen - 1, 1); 332 $decrypted_data = substr($decrypted_data, 0, $dataLen - ord($paddingLength)); 333 } 334 return $decrypted_data; 335 } 336 337 private function encryptOpenSSL($data) { 338 if ($this->cryptParams['type'] == 'public') { 339 if (! openssl_public_encrypt($data, $encrypted_data, $this->key, $this->cryptParams['padding'])) { 340 throw new Exception('Failure encrypting Data'); 341 } 342 } else { 343 if (! openssl_private_encrypt($data, $encrypted_data, $this->key, $this->cryptParams['padding'])) { 344 throw new Exception('Failure encrypting Data'); 345 } 346 } 347 return $encrypted_data; 348 } 349 350 private function decryptOpenSSL($data) { 351 if ($this->cryptParams['type'] == 'public') { 352 if (! openssl_public_decrypt($data, $decrypted, $this->key, $this->cryptParams['padding'])) { 353 throw new Exception('Failure decrypting Data'); 354 } 355 } else { 356 if (! openssl_private_decrypt($data, $decrypted, $this->key, $this->cryptParams['padding'])) { 357 throw new Exception('Failure decrypting Data'); 358 } 359 } 360 return $decrypted; 361 } 362 363 private function signOpenSSL($data) { 364 $algo = OPENSSL_ALGO_SHA1; 365 if (! empty($this->cryptParams['digest'])) { 366 $algo = $this->cryptParams['digest']; 367 } 368 if (! openssl_sign ($data, $signature, $this->key, $algo)) { 369 throw new Exception('Failure Signing Data: ' . openssl_error_string() . ' - ' . $algo); 370 } 371 return $signature; 372 } 373 374 private function verifyOpenSSL($data, $signature) { 375 $algo = OPENSSL_ALGO_SHA1; 376 if (! empty($this->cryptParams['digest'])) { 377 $algo = $this->cryptParams['digest']; 378 } 379 return openssl_verify ($data, $signature, $this->key, $algo); 380 } 381 382 public function encryptData($data) { 383 switch ($this->cryptParams['library']) { 384 case 'mcrypt': 385 return $this->encryptMcrypt($data); 386 case 'openssl': 387 return $this->encryptOpenSSL($data); 388 } 389 } 390 391 public function decryptData($data) { 392 switch ($this->cryptParams['library']) { 393 case 'mcrypt': 394 return $this->decryptMcrypt($data); 395 case 'openssl': 396 return $this->decryptOpenSSL($data); 397 } 398 } 399 400 public function signData($data) { 401 switch ($this->cryptParams['library']) { 402 case 'openssl': 403 return $this->signOpenSSL($data); 404 case (XMLSecurityKey::HMAC_SHA1): 405 return hash_hmac("sha1", $data, $this->key, true); 406 } 407 } 408 409 public function verifySignature($data, $signature) { 410 switch ($this->cryptParams['library']) { 411 case 'openssl': 412 return $this->verifyOpenSSL($data, $signature); 413 case (XMLSecurityKey::HMAC_SHA1): 414 $expectedSignature = hash_hmac("sha1", $data, $this->key, true); 415 return strcmp($signature, $expectedSignature) == 0; 416 } 417 } 418 419 public function getAlgorithm() { 420 return $this->cryptParams['method']; 421 } 422 423 static function makeAsnSegment($type, $string) { 424 switch ($type){ 425 case 0x02: 426 if (ord($string) > 0x7f) 427 $string = chr(0).$string; 428 break; 429 case 0x03: 430 $string = chr(0).$string; 431 break; 432 } 433 434 $length = strlen($string); 435 436 if ($length < 128){ 437 $output = sprintf("%c%c%s", $type, $length, $string); 438 } else if ($length < 0x0100){ 439 $output = sprintf("%c%c%c%s", $type, 0x81, $length, $string); 440 } else if ($length < 0x010000) { 441 $output = sprintf("%c%c%c%c%s", $type, 0x82, $length/0x0100, $length%0x0100, $string); 442 } else { 443 $output = null; 444 } 445 return($output); 446 } 447 448 /* Modulus and Exponent must already be base64 decoded */ 449 static function convertRSA($modulus, $exponent) { 450 /* make an ASN publicKeyInfo */ 451 $exponentEncoding = XMLSecurityKey::makeAsnSegment(0x02, $exponent); 452 $modulusEncoding = XMLSecurityKey::makeAsnSegment(0x02, $modulus); 453 $sequenceEncoding = XMLSecurityKey:: makeAsnSegment(0x30, $modulusEncoding.$exponentEncoding); 454 $bitstringEncoding = XMLSecurityKey::makeAsnSegment(0x03, $sequenceEncoding); 455 $rsaAlgorithmIdentifier = pack("H*", "300D06092A864886F70D0101010500"); 456 $publicKeyInfo = XMLSecurityKey::makeAsnSegment (0x30, $rsaAlgorithmIdentifier.$bitstringEncoding); 457 458 /* encode the publicKeyInfo in base64 and add PEM brackets */ 459 $publicKeyInfoBase64 = base64_encode($publicKeyInfo); 460 $encoding = "-----BEGIN PUBLIC KEY-----\n"; 461 $offset = 0; 462 while ($segment=substr($publicKeyInfoBase64, $offset, 64)){ 463 $encoding = $encoding.$segment."\n"; 464 $offset += 64; 465 } 466 return $encoding."-----END PUBLIC KEY-----\n"; 467 } 468 469 public function serializeKey($parent) { 470 471 } 472 473 474 475 /** 476 * Retrieve the X509 certificate this key represents. 477 * 478 * Will return the X509 certificate in PEM-format if this key represents 479 * an X509 certificate. 480 * 481 * @return The X509 certificate or null if this key doesn't represent an X509-certificate. 482 */ 483 public function getX509Certificate() { 484 return $this->x509Certificate; 485 } 486 487 /* Get the thumbprint of this X509 certificate. 488 * 489 * Returns: 490 * The thumbprint as a lowercase 40-character hexadecimal number, or null 491 * if this isn't a X509 certificate. 492 */ 493 public function getX509Thumbprint() { 494 return $this->X509Thumbprint; 495 } 496 497 498 /** 499 * Create key from an EncryptedKey-element. 500 * 501 * @param DOMElement $element The EncryptedKey-element. 502 * @return XMLSecurityKey The new key. 503 */ 504 public static function fromEncryptedKeyElement(DOMElement $element) { 505 506 $objenc = new XMLSecEnc(); 507 $objenc->setNode($element); 508 if (! $objKey = $objenc->locateKey()) { 509 throw new Exception("Unable to locate algorithm for this Encrypted Key"); 510 } 511 $objKey->isEncrypted = true; 512 $objKey->encryptedCtx = $objenc; 513 XMLSecEnc::staticLocateKeyInfo($objKey, $element); 514 return $objKey; 515 } 516 517} 518 519 520class XMLSecurityDSig { 521 const XMLDSIGNS = 'http://www.w3.org/2000/09/xmldsig#'; 522 const SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'; 523 const SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'; 524 const SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384'; 525 const SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'; 526 const RIPEMD160 = 'http://www.w3.org/2001/04/xmlenc#ripemd160'; 527 528 const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; 529 const C14N_COMMENTS = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments'; 530 const EXC_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#'; 531 const EXC_C14N_COMMENTS = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments'; 532 533 const template = '<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> 534 <ds:SignedInfo> 535 <ds:SignatureMethod /> 536 </ds:SignedInfo> 537</ds:Signature>'; 538 539 public $sigNode = null; 540 public $idKeys = array(); 541 public $idNS = array(); 542 private $signedInfo = null; 543 private $xPathCtx = null; 544 private $canonicalMethod = null; 545 private $prefix = 'ds'; 546 private $searchpfx = 'secdsig'; 547 548 /* This variable contains an associative array of validated nodes. */ 549 private $validatedNodes = null; 550 551 public function __construct() { 552 $sigdoc = new DOMDocument(); 553 $sigdoc->loadXML(XMLSecurityDSig::template); 554 $this->sigNode = $sigdoc->documentElement; 555 } 556 557 private function resetXPathObj() { 558 $this->xPathCtx = null; 559 } 560 561 private function getXPathObj() { 562 if (empty($this->xPathCtx) && ! empty($this->sigNode)) { 563 $xpath = new DOMXPath($this->sigNode->ownerDocument); 564 $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); 565 $this->xPathCtx = $xpath; 566 } 567 return $this->xPathCtx; 568 } 569 570 static function generateGUID($prefix='pfx') { 571 $uuid = md5(uniqid(mt_rand(), true)); 572 $guid = $prefix.substr($uuid,0,8)."-". 573 substr($uuid,8,4)."-". 574 substr($uuid,12,4)."-". 575 substr($uuid,16,4)."-". 576 substr($uuid,20,12); 577 return $guid; 578 } 579 580 public function locateSignature($objDoc, $pos=0) { 581 if ($objDoc instanceof DOMDocument) { 582 $doc = $objDoc; 583 } else { 584 $doc = $objDoc->ownerDocument; 585 } 586 if ($doc) { 587 $xpath = new DOMXPath($doc); 588 $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); 589 $query = ".//secdsig:Signature"; 590 $nodeset = $xpath->query($query, $objDoc); 591 $this->sigNode = $nodeset->item($pos); 592 $query = "./secdsig:SignedInfo"; 593 $nodeset = $xpath->query($query, $this->sigNode); 594 if ($nodeset->length > 1) { 595 throw new Exception("Invalid structure - Too many SignedInfo elements found"); 596 } 597 return $this->sigNode; 598 } 599 return null; 600 } 601 602 public function createNewSignNode($name, $value=null) { 603 $doc = $this->sigNode->ownerDocument; 604 if (! is_null($value)) { 605 $node = $doc->createElementNS(XMLSecurityDSig::XMLDSIGNS, $this->prefix.':'.$name, $value); 606 } else { 607 $node = $doc->createElementNS(XMLSecurityDSig::XMLDSIGNS, $this->prefix.':'.$name); 608 } 609 return $node; 610 } 611 612 public function setCanonicalMethod($method) { 613 switch ($method) { 614 case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': 615 case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': 616 case 'http://www.w3.org/2001/10/xml-exc-c14n#': 617 case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': 618 $this->canonicalMethod = $method; 619 break; 620 default: 621 throw new Exception('Invalid Canonical Method'); 622 } 623 if ($xpath = $this->getXPathObj()) { 624 $query = './'.$this->searchpfx.':SignedInfo'; 625 $nodeset = $xpath->query($query, $this->sigNode); 626 if ($sinfo = $nodeset->item(0)) { 627 $query = './'.$this->searchpfx.'CanonicalizationMethod'; 628 $nodeset = $xpath->query($query, $sinfo); 629 if (! ($canonNode = $nodeset->item(0))) { 630 $canonNode = $this->createNewSignNode('CanonicalizationMethod'); 631 $sinfo->insertBefore($canonNode, $sinfo->firstChild); 632 } 633 $canonNode->setAttribute('Algorithm', $this->canonicalMethod); 634 } 635 } 636 } 637 638 private function canonicalizeData($node, $canonicalmethod, $arXPath=null, $prefixList=null) { 639 $exclusive = false; 640 $withComments = false; 641 switch ($canonicalmethod) { 642 case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': 643 $exclusive = false; 644 $withComments = false; 645 break; 646 case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': 647 $withComments = true; 648 break; 649 case 'http://www.w3.org/2001/10/xml-exc-c14n#': 650 $exclusive = true; 651 break; 652 case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': 653 $exclusive = true; 654 $withComments = true; 655 break; 656 } 657 658 if (is_null($arXPath) && ($node instanceof DOMNode) && ($node->ownerDocument !== null) && $node->isSameNode($node->ownerDocument->documentElement)) { 659 /* Check for any PI or comments as they would have been excluded */ 660 $element = $node; 661 while ($refnode = $element->previousSibling) { 662 if ($refnode->nodeType == XML_PI_NODE || (($refnode->nodeType == XML_COMMENT_NODE) && $withComments)) { 663 break; 664 } 665 $element = $refnode; 666 } 667 if ($refnode == null) { 668 $node = $node->ownerDocument; 669 } 670 } 671 672 return $node->C14N($exclusive, $withComments, $arXPath, $prefixList); 673 } 674 675 public function canonicalizeSignedInfo() { 676 677 $doc = $this->sigNode->ownerDocument; 678 $canonicalmethod = null; 679 if ($doc) { 680 $xpath = $this->getXPathObj(); 681 $query = "./secdsig:SignedInfo"; 682 $nodeset = $xpath->query($query, $this->sigNode); 683 if ($nodeset->length > 1) { 684 throw new Exception("Invalid structure - Too many SignedInfo elements found"); 685 } 686 if ($signInfoNode = $nodeset->item(0)) { 687 $query = "./secdsig:CanonicalizationMethod"; 688 $nodeset = $xpath->query($query, $signInfoNode); 689 if ($canonNode = $nodeset->item(0)) { 690 $canonicalmethod = $canonNode->getAttribute('Algorithm'); 691 } 692 $this->signedInfo = $this->canonicalizeData($signInfoNode, $canonicalmethod); 693 return $this->signedInfo; 694 } 695 } 696 return null; 697 } 698 699 public function calculateDigest ($digestAlgorithm, $data, $encode = true) { 700 switch ($digestAlgorithm) { 701 case XMLSecurityDSig::SHA1: 702 $alg = 'sha1'; 703 break; 704 case XMLSecurityDSig::SHA256: 705 $alg = 'sha256'; 706 break; 707 case XMLSecurityDSig::SHA384: 708 $alg = 'sha384'; 709 break; 710 case XMLSecurityDSig::SHA512: 711 $alg = 'sha512'; 712 break; 713 case XMLSecurityDSig::RIPEMD160: 714 $alg = 'ripemd160'; 715 break; 716 default: 717 throw new Exception("Cannot validate digest: Unsupported Algorithm <$digestAlgorithm>"); 718 } 719 720 $digest = hash($alg, $data, true); 721 if ($encode) { 722 $digest = base64_encode($digest); 723 } 724 return $digest; 725 } 726 727 public function validateDigest($refNode, $data) { 728 $xpath = new DOMXPath($refNode->ownerDocument); 729 $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); 730 $query = 'string(./secdsig:DigestMethod/@Algorithm)'; 731 $digestAlgorithm = $xpath->evaluate($query, $refNode); 732 $digValue = $this->calculateDigest($digestAlgorithm, $data, false); 733 $query = 'string(./secdsig:DigestValue)'; 734 $digestValue = $xpath->evaluate($query, $refNode); 735 return ($digValue === base64_decode($digestValue)); 736 } 737 738 public function processTransforms($refNode, $objData, $includeCommentNodes = true) { 739 $data = $objData; 740 $xpath = new DOMXPath($refNode->ownerDocument); 741 $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); 742 $query = './secdsig:Transforms/secdsig:Transform'; 743 $nodelist = $xpath->query($query, $refNode); 744 $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; 745 $arXPath = null; 746 $prefixList = null; 747 foreach ($nodelist AS $transform) { 748 $algorithm = $transform->getAttribute("Algorithm"); 749 switch ($algorithm) { 750 case 'http://www.w3.org/2001/10/xml-exc-c14n#': 751 case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': 752 753 if(!$includeCommentNodes) { 754 /* We remove comment nodes by forcing it to use a canonicalization 755 * without comments. 756 */ 757 $canonicalMethod = 'http://www.w3.org/2001/10/xml-exc-c14n#'; 758 } else { 759 $canonicalMethod = $algorithm; 760 } 761 762 $node = $transform->firstChild; 763 while ($node) { 764 if ($node->localName == 'InclusiveNamespaces') { 765 if ($pfx = $node->getAttribute('PrefixList')) { 766 $arpfx = array(); 767 $pfxlist = explode(" ", $pfx); 768 foreach ($pfxlist AS $pfx) { 769 $val = trim($pfx); 770 if (! empty($val)) { 771 $arpfx[] = $val; 772 } 773 } 774 if (count($arpfx) > 0) { 775 $prefixList = $arpfx; 776 } 777 } 778 break; 779 } 780 $node = $node->nextSibling; 781 } 782 break; 783 case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': 784 case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': 785 if(!$includeCommentNodes) { 786 /* We remove comment nodes by forcing it to use a canonicalization 787 * without comments. 788 */ 789 $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; 790 } else { 791 $canonicalMethod = $algorithm; 792 } 793 794 break; 795 case 'http://www.w3.org/TR/1999/REC-xpath-19991116': 796 $node = $transform->firstChild; 797 while ($node) { 798 if ($node->localName == 'XPath') { 799 $arXPath = array(); 800 $arXPath['query'] = '(.//. | .//@* | .//namespace::*)['.$node->nodeValue.']'; 801 $arXPath['namespaces'] = array(); 802 $nslist = $xpath->query('./namespace::*', $node); 803 foreach ($nslist AS $nsnode) { 804 if ($nsnode->localName != "xml") { 805 $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue; 806 } 807 } 808 break; 809 } 810 $node = $node->nextSibling; 811 } 812 break; 813 } 814 } 815 if ($data instanceof DOMNode) { 816 $data = $this->canonicalizeData($objData, $canonicalMethod, $arXPath, $prefixList); 817 } 818 return $data; 819 } 820 821 public function processRefNode($refNode) { 822 $dataObject = null; 823 824 /* 825 * Depending on the URI, we may not want to include comments in the result 826 * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel 827 */ 828 $includeCommentNodes = true; 829 830 if ($uri = $refNode->getAttribute("URI")) { 831 $arUrl = parse_url($uri); 832 if (empty($arUrl['path'])) { 833 if ($identifier = $arUrl['fragment']) { 834 835 /* This reference identifies a node with the given id by using 836 * a URI on the form "#identifier". This should not include comments. 837 */ 838 $includeCommentNodes = false; 839 840 $xPath = new DOMXPath($refNode->ownerDocument); 841 if ($this->idNS && is_array($this->idNS)) { 842 foreach ($this->idNS AS $nspf=>$ns) { 843 $xPath->registerNamespace($nspf, $ns); 844 } 845 } 846 $iDlist = '@Id="'.$identifier.'"'; 847 if (is_array($this->idKeys)) { 848 foreach ($this->idKeys AS $idKey) { 849 $iDlist .= " or @$idKey='$identifier'"; 850 } 851 } 852 $query = '//*['.$iDlist.']'; 853 $dataObject = $xPath->query($query)->item(0); 854 } else { 855 $dataObject = $refNode->ownerDocument; 856 } 857 } 858 } else { 859 /* This reference identifies the root node with an empty URI. This should 860 * not include comments. 861 */ 862 $includeCommentNodes = false; 863 864 $dataObject = $refNode->ownerDocument; 865 } 866 $data = $this->processTransforms($refNode, $dataObject, $includeCommentNodes); 867 if (!$this->validateDigest($refNode, $data)) { 868 return false; 869 } 870 871 if ($dataObject instanceof DOMNode) { 872 /* Add this node to the list of validated nodes. */ 873 if(! empty($identifier)) { 874 $this->validatedNodes[$identifier] = $dataObject; 875 } else { 876 $this->validatedNodes[] = $dataObject; 877 } 878 } 879 880 return true; 881 } 882 883 public function getRefNodeID($refNode) { 884 if ($uri = $refNode->getAttribute("URI")) { 885 $arUrl = parse_url($uri); 886 if (empty($arUrl['path'])) { 887 if ($identifier = $arUrl['fragment']) { 888 return $identifier; 889 } 890 } 891 } 892 return null; 893 } 894 895 public function getRefIDs() { 896 $refids = array(); 897 898 $xpath = $this->getXPathObj(); 899 $query = "./secdsig:SignedInfo[1]/secdsig:Reference"; 900 $nodeset = $xpath->query($query, $this->sigNode); 901 if ($nodeset->length == 0) { 902 throw new Exception("Reference nodes not found"); 903 } 904 foreach ($nodeset AS $refNode) { 905 $refids[] = $this->getRefNodeID($refNode); 906 } 907 return $refids; 908 } 909 910 public function validateReference() { 911 $docElem = $this->sigNode->ownerDocument->documentElement; 912 if (! $docElem->isSameNode($this->sigNode)) { 913 $this->sigNode->parentNode->removeChild($this->sigNode); 914 } 915 $xpath = $this->getXPathObj(); 916 $query = "./secdsig:SignedInfo[1]/secdsig:Reference"; 917 $nodeset = $xpath->query($query, $this->sigNode); 918 if ($nodeset->length == 0) { 919 throw new Exception("Reference nodes not found"); 920 } 921 922 /* Initialize/reset the list of validated nodes. */ 923 $this->validatedNodes = array(); 924 925 foreach ($nodeset AS $refNode) { 926 if (! $this->processRefNode($refNode)) { 927 /* Clear the list of validated nodes. */ 928 $this->validatedNodes = null; 929 throw new Exception("Reference validation failed"); 930 } 931 } 932 return true; 933 } 934 935 private function addRefInternal($sinfoNode, $node, $algorithm, $arTransforms=null, $options=null) { 936 $prefix = null; 937 $prefix_ns = null; 938 $id_name = 'Id'; 939 $overwrite_id = true; 940 $force_uri = false; 941 942 if (is_array($options)) { 943 $prefix = empty($options['prefix'])?null:$options['prefix']; 944 $prefix_ns = empty($options['prefix_ns'])?null:$options['prefix_ns']; 945 $id_name = empty($options['id_name'])?'Id':$options['id_name']; 946 $overwrite_id = !isset($options['overwrite'])?true:(bool)$options['overwrite']; 947 $force_uri = !isset($options['force_uri'])?false:(bool)$options['force_uri']; 948 } 949 950 $attname = $id_name; 951 if (! empty($prefix)) { 952 $attname = $prefix.':'.$attname; 953 } 954 955 $refNode = $this->createNewSignNode('Reference'); 956 $sinfoNode->appendChild($refNode); 957 958 if (! $node instanceof DOMDocument) { 959 $uri = null; 960 if (! $overwrite_id) { 961 $uri = $prefix_ns ? $node->getAttributeNS($prefix_ns, $id_name) : $node->getAttribute($id_name); 962 } 963 if (empty($uri)) { 964 $uri = XMLSecurityDSig::generateGUID(); 965 $node->setAttributeNS($prefix_ns, $attname, $uri); 966 } 967 $refNode->setAttribute("URI", '#'.$uri); 968 } elseif ($force_uri) { 969 $refNode->setAttribute("URI", ''); 970 } 971 972 $transNodes = $this->createNewSignNode('Transforms'); 973 $refNode->appendChild($transNodes); 974 975 if (is_array($arTransforms)) { 976 foreach ($arTransforms AS $transform) { 977 $transNode = $this->createNewSignNode('Transform'); 978 $transNodes->appendChild($transNode); 979 if (is_array($transform) && 980 (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116'])) && 981 (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']))) { 982 $transNode->setAttribute('Algorithm', 'http://www.w3.org/TR/1999/REC-xpath-19991116'); 983 $XPathNode = $this->createNewSignNode('XPath', $transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']); 984 $transNode->appendChild($XPathNode); 985 if (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'])) { 986 foreach ($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'] AS $prefix => $namespace) { 987 $XPathNode->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:$prefix", $namespace); 988 } 989 } 990 } else { 991 $transNode->setAttribute('Algorithm', $transform); 992 } 993 } 994 } elseif (! empty($this->canonicalMethod)) { 995 $transNode = $this->createNewSignNode('Transform'); 996 $transNodes->appendChild($transNode); 997 $transNode->setAttribute('Algorithm', $this->canonicalMethod); 998 } 999 1000 $canonicalData = $this->processTransforms($refNode, $node); 1001 $digValue = $this->calculateDigest($algorithm, $canonicalData); 1002 1003 $digestMethod = $this->createNewSignNode('DigestMethod'); 1004 $refNode->appendChild($digestMethod); 1005 $digestMethod->setAttribute('Algorithm', $algorithm); 1006 1007 $digestValue = $this->createNewSignNode('DigestValue', $digValue); 1008 $refNode->appendChild($digestValue); 1009 } 1010 1011 public function addReference($node, $algorithm, $arTransforms=null, $options=null) { 1012 if ($xpath = $this->getXPathObj()) { 1013 $query = "./secdsig:SignedInfo"; 1014 $nodeset = $xpath->query($query, $this->sigNode); 1015 if ($sInfo = $nodeset->item(0)) { 1016 $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options); 1017 } 1018 } 1019 } 1020 1021 public function addReferenceList($arNodes, $algorithm, $arTransforms=null, $options=null) { 1022 if ($xpath = $this->getXPathObj()) { 1023 $query = "./secdsig:SignedInfo"; 1024 $nodeset = $xpath->query($query, $this->sigNode); 1025 if ($sInfo = $nodeset->item(0)) { 1026 foreach ($arNodes AS $node) { 1027 $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options); 1028 } 1029 } 1030 } 1031 } 1032 1033 public function addObject($data, $mimetype=null, $encoding=null) { 1034 $objNode = $this->createNewSignNode('Object'); 1035 $this->sigNode->appendChild($objNode); 1036 if (! empty($mimetype)) { 1037 $objNode->setAttribute('MimeType', $mimetype); 1038 } 1039 if (! empty($encoding)) { 1040 $objNode->setAttribute('Encoding', $encoding); 1041 } 1042 1043 if ($data instanceof DOMElement) { 1044 $newData = $this->sigNode->ownerDocument->importNode($data, true); 1045 } else { 1046 $newData = $this->sigNode->ownerDocument->createTextNode($data); 1047 } 1048 $objNode->appendChild($newData); 1049 1050 return $objNode; 1051 } 1052 1053 public function locateKey($node=null) { 1054 if (empty($node)) { 1055 $node = $this->sigNode; 1056 } 1057 if (! $node instanceof DOMNode) { 1058 return null; 1059 } 1060 if ($doc = $node->ownerDocument) { 1061 $xpath = new DOMXPath($doc); 1062 $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); 1063 $query = "string(./secdsig:SignedInfo/secdsig:SignatureMethod/@Algorithm)"; 1064 $algorithm = $xpath->evaluate($query, $node); 1065 if ($algorithm) { 1066 try { 1067 $objKey = new XMLSecurityKey($algorithm, array('type'=>'public')); 1068 } catch (Exception $e) { 1069 return null; 1070 } 1071 return $objKey; 1072 } 1073 } 1074 return null; 1075 } 1076 1077 public function verify($objKey) { 1078 $doc = $this->sigNode->ownerDocument; 1079 $xpath = new DOMXPath($doc); 1080 $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); 1081 $query = "string(./secdsig:SignatureValue)"; 1082 $sigValue = $xpath->evaluate($query, $this->sigNode); 1083 if (empty($sigValue)) { 1084 throw new Exception("Unable to locate SignatureValue"); 1085 } 1086 return $objKey->verifySignature($this->signedInfo, base64_decode($sigValue)); 1087 } 1088 1089 public function signData($objKey, $data) { 1090 return $objKey->signData($data); 1091 } 1092 1093 public function sign($objKey, $appendToNode = null) { 1094 // If we have a parent node append it now so C14N properly works 1095 if ($appendToNode != null) { 1096 $this->resetXPathObj(); 1097 $this->appendSignature($appendToNode); 1098 $this->sigNode = $appendToNode->lastChild; 1099 } 1100 if ($xpath = $this->getXPathObj()) { 1101 $query = "./secdsig:SignedInfo"; 1102 $nodeset = $xpath->query($query, $this->sigNode); 1103 if ($sInfo = $nodeset->item(0)) { 1104 $query = "./secdsig:SignatureMethod"; 1105 $nodeset = $xpath->query($query, $sInfo); 1106 $sMethod = $nodeset->item(0); 1107 $sMethod->setAttribute('Algorithm', $objKey->type); 1108 $data = $this->canonicalizeData($sInfo, $this->canonicalMethod); 1109 $sigValue = base64_encode($this->signData($objKey, $data)); 1110 $sigValueNode = $this->createNewSignNode('SignatureValue', $sigValue); 1111 if ($infoSibling = $sInfo->nextSibling) { 1112 $infoSibling->parentNode->insertBefore($sigValueNode, $infoSibling); 1113 } else { 1114 $this->sigNode->appendChild($sigValueNode); 1115 } 1116 } 1117 } 1118 } 1119 1120 public function appendCert() { 1121 1122 } 1123 1124 public function appendKey($objKey, $parent=null) { 1125 $objKey->serializeKey($parent); 1126 } 1127 1128 1129 /** 1130 * This function inserts the signature element. 1131 * 1132 * The signature element will be appended to the element, unless $beforeNode is specified. If $beforeNode 1133 * is specified, the signature element will be inserted as the last element before $beforeNode. 1134 * 1135 * @param $node The node the signature element should be inserted into. 1136 * @param $beforeNode The node the signature element should be located before. 1137 * 1138 * @return DOMNode The signature element node 1139 */ 1140 public function insertSignature($node, $beforeNode = null) { 1141 1142 $document = $node->ownerDocument; 1143 $signatureElement = $document->importNode($this->sigNode, true); 1144 1145 if($beforeNode == null) { 1146 return $node->insertBefore($signatureElement); 1147 } else { 1148 return $node->insertBefore($signatureElement, $beforeNode); 1149 } 1150 } 1151 1152 public function appendSignature($parentNode, $insertBefore = false) { 1153 $beforeNode = $insertBefore ? $parentNode->firstChild : null; 1154 return $this->insertSignature($parentNode, $beforeNode); 1155 } 1156 1157 static function get509XCert($cert, $isPEMFormat=true) { 1158 $certs = XMLSecurityDSig::staticGet509XCerts($cert, $isPEMFormat); 1159 if (! empty($certs)) { 1160 return $certs[0]; 1161 } 1162 return ''; 1163 } 1164 1165 static function staticGet509XCerts($certs, $isPEMFormat=true) { 1166 if ($isPEMFormat) { 1167 $data = ''; 1168 $certlist = array(); 1169 $arCert = explode("\n", $certs); 1170 $inData = false; 1171 foreach ($arCert AS $curData) { 1172 if (! $inData) { 1173 if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) { 1174 $inData = true; 1175 } 1176 } else { 1177 if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) { 1178 $inData = false; 1179 $certlist[] = $data; 1180 $data = ''; 1181 continue; 1182 } 1183 $data .= trim($curData); 1184 } 1185 } 1186 return $certlist; 1187 } else { 1188 return array($certs); 1189 } 1190 } 1191 1192 static function staticAdd509Cert($parentRef, $cert, $isPEMFormat=true, $isURL=false, $xpath=null, $options=null) { 1193 if ($isURL) { 1194 $cert = file_get_contents($cert); 1195 } 1196 if (! $parentRef instanceof DOMElement) { 1197 throw new Exception('Invalid parent Node parameter'); 1198 } 1199 1200 list($parentRef, $keyInfo) = self::auxKeyInfo($parentRef, $xpath); 1201 1202 // Add all certs if there are more than one 1203 $certs = XMLSecurityDSig::staticGet509XCerts($cert, $isPEMFormat); 1204 1205 $baseDoc = $parentRef->ownerDocument; 1206 // Attach X509 data node 1207 $x509DataNode = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:X509Data'); 1208 $keyInfo->appendChild($x509DataNode); 1209 1210 $issuerSerial = false; 1211 $subjectName = false; 1212 if (is_array($options)) { 1213 if (! empty($options['issuerSerial'])) { 1214 $issuerSerial = true; 1215 } 1216 if (! empty($options['subjectName'])) { 1217 $subjectName = true; 1218 } 1219 } 1220 1221 // Attach all certificate nodes and any additional data 1222 foreach ($certs as $X509Cert){ 1223 if ($issuerSerial || $subjectName) { 1224 if ($certData = openssl_x509_parse("-----BEGIN CERTIFICATE-----\n".chunk_split($X509Cert, 64, "\n")."-----END CERTIFICATE-----\n")) { 1225 if ($subjectName && ! empty($certData['subject'])) { 1226 if (is_array($certData['subject'])) { 1227 $parts = array(); 1228 foreach ($certData['subject'] AS $key => $value) { 1229 if (is_array($value)) { 1230 foreach ($value as $valueElement) { 1231 array_unshift($parts, "$key=$valueElement"); 1232 } 1233 } else { 1234 array_unshift($parts, "$key=$value"); 1235 } 1236 } 1237 $subjectNameValue = implode(',', $parts); 1238 } else { 1239 $subjectNameValue = $certData['issuer']; 1240 } 1241 $x509SubjectNode = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:X509SubjectName', $subjectNameValue); 1242 $x509DataNode->appendChild($x509SubjectNode); 1243 } 1244 if ($issuerSerial && ! empty($certData['issuer']) && ! empty($certData['serialNumber'])) { 1245 if (is_array($certData['issuer'])) { 1246 $parts = array(); 1247 foreach ($certData['issuer'] AS $key => $value) { 1248 array_unshift($parts, "$key=$value"); 1249 } 1250 $issuerName = implode(',', $parts); 1251 } else { 1252 $issuerName = $certData['issuer']; 1253 } 1254 1255 $x509IssuerNode = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:X509IssuerSerial'); 1256 $x509DataNode->appendChild($x509IssuerNode); 1257 1258 $x509Node = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:X509IssuerName', $issuerName); 1259 $x509IssuerNode->appendChild($x509Node); 1260 $x509Node = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:X509SerialNumber', $certData['serialNumber']); 1261 $x509IssuerNode->appendChild($x509Node); 1262 } 1263 } 1264 1265 } 1266 $x509CertNode = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:X509Certificate', $X509Cert); 1267 $x509DataNode->appendChild($x509CertNode); 1268 } 1269 } 1270 1271 public function add509Cert($cert, $isPEMFormat=true, $isURL=false, $options=null) { 1272 if ($xpath = $this->getXPathObj()) { 1273 self::staticAdd509Cert($this->sigNode, $cert, $isPEMFormat, $isURL, $xpath, $options); 1274 } 1275 } 1276 1277 /** 1278 * This function appends a node to the KeyInfo. 1279 * 1280 * The KeyInfo element will be created if one does not exist in the document. 1281 * 1282 * @param DOMNode $node The node to append to the KeyInfo. 1283 * 1284 * @return DOMNode The KeyInfo element node 1285 */ 1286 public function appendToKeyInfo($node) { 1287 $parentRef = $this->sigNode; 1288 1289 $xpath = $this->getXPathObj(); 1290 1291 list($parentRef, $keyInfo) = self::auxKeyInfo($parentRef, $xpath); 1292 1293 $keyInfo->appendChild($node); 1294 1295 return $keyInfo; 1296 } 1297 1298 static function auxKeyInfo($parentRef, $xpath=null) 1299 { 1300 $baseDoc = $parentRef->ownerDocument; 1301 if (empty($xpath)) { 1302 $xpath = new DOMXPath($parentRef->ownerDocument); 1303 $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); 1304 } 1305 1306 $query = "./secdsig:KeyInfo"; 1307 $nodeset = $xpath->query($query, $parentRef); 1308 $keyInfo = $nodeset->item(0); 1309 if (! $keyInfo) { 1310 $inserted = false; 1311 $keyInfo = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:KeyInfo'); 1312 1313 $query = "./secdsig:Object"; 1314 $nodeset = $xpath->query($query, $parentRef); 1315 if ($sObject = $nodeset->item(0)) { 1316 $sObject->parentNode->insertBefore($keyInfo, $sObject); 1317 $inserted = true; 1318 } 1319 1320 if (! $inserted) { 1321 $parentRef->appendChild($keyInfo); 1322 } 1323 } 1324 return array($parentRef, $keyInfo); 1325 } 1326 1327 /* This function retrieves an associative array of the validated nodes. 1328 * 1329 * The array will contain the id of the referenced node as the key and the node itself 1330 * as the value. 1331 * 1332 * Returns: 1333 * An associative array of validated nodes or null if no nodes have been validated. 1334 */ 1335 public function getValidatedNodes() { 1336 return $this->validatedNodes; 1337 } 1338} 1339 1340 1341class XMLSecEnc { 1342 const template = "<xenc:EncryptedData xmlns:xenc='http://www.w3.org/2001/04/xmlenc#'> 1343 <xenc:CipherData> 1344 <xenc:CipherValue></xenc:CipherValue> 1345 </xenc:CipherData> 1346</xenc:EncryptedData>"; 1347 1348 const Element = 'http://www.w3.org/2001/04/xmlenc#Element'; 1349 const Content = 'http://www.w3.org/2001/04/xmlenc#Content'; 1350 const URI = 3; 1351 const XMLENCNS = 'http://www.w3.org/2001/04/xmlenc#'; 1352 1353 private $encdoc = null; 1354 private $rawNode = null; 1355 public $type = null; 1356 public $encKey = null; 1357 private $references = array(); 1358 1359 public function __construct() { 1360 $this->_resetTemplate(); 1361 } 1362 1363 private function _resetTemplate(){ 1364 $this->encdoc = new DOMDocument(); 1365 $this->encdoc->loadXML(XMLSecEnc::template); 1366 } 1367 1368 public function addReference($name, $node, $type) { 1369 if (! $node instanceOf DOMNode) { 1370 throw new Exception('$node is not of type DOMNode'); 1371 } 1372 $curencdoc = $this->encdoc; 1373 $this->_resetTemplate(); 1374 $encdoc = $this->encdoc; 1375 $this->encdoc = $curencdoc; 1376 $refuri = XMLSecurityDSig::generateGUID(); 1377 $element = $encdoc->documentElement; 1378 $element->setAttribute("Id", $refuri); 1379 $this->references[$name] = array("node" => $node, "type" => $type, "encnode" => $encdoc, "refuri" => $refuri); 1380 } 1381 1382 public function setNode($node) { 1383 $this->rawNode = $node; 1384 } 1385 1386 /** 1387 * Encrypt the selected node with the given key. 1388 * 1389 * @param XMLSecurityKey $objKey The encryption key and algorithm. 1390 * @param bool $replace Whether the encrypted node should be replaced in the original tree. Default is true. 1391 * @return DOMElement The <xenc:EncryptedData>-element. 1392 */ 1393 public function encryptNode($objKey, $replace=true) { 1394 $data = ''; 1395 if (empty($this->rawNode)) { 1396 throw new Exception('Node to encrypt has not been set'); 1397 } 1398 if (! $objKey instanceof XMLSecurityKey) { 1399 throw new Exception('Invalid Key'); 1400 } 1401 $doc = $this->rawNode->ownerDocument; 1402 $xPath = new DOMXPath($this->encdoc); 1403 $objList = $xPath->query('/xenc:EncryptedData/xenc:CipherData/xenc:CipherValue'); 1404 $cipherValue = $objList->item(0); 1405 if ($cipherValue == null) { 1406 throw new Exception('Error locating CipherValue element within template'); 1407 } 1408 switch ($this->type) { 1409 case (XMLSecEnc::Element): 1410 $data = $doc->saveXML($this->rawNode); 1411 $this->encdoc->documentElement->setAttribute('Type', XMLSecEnc::Element); 1412 break; 1413 case (XMLSecEnc::Content): 1414 $children = $this->rawNode->childNodes; 1415 foreach ($children AS $child) { 1416 $data .= $doc->saveXML($child); 1417 } 1418 $this->encdoc->documentElement->setAttribute('Type', XMLSecEnc::Content); 1419 break; 1420 default: 1421 throw new Exception('Type is currently not supported'); 1422 } 1423 1424 $encMethod = $this->encdoc->documentElement->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:EncryptionMethod')); 1425 $encMethod->setAttribute('Algorithm', $objKey->getAlgorithm()); 1426 $cipherValue->parentNode->parentNode->insertBefore($encMethod, $cipherValue->parentNode->parentNode->firstChild); 1427 1428 $strEncrypt = base64_encode($objKey->encryptData($data)); 1429 $value = $this->encdoc->createTextNode($strEncrypt); 1430 $cipherValue->appendChild($value); 1431 1432 if ($replace) { 1433 switch ($this->type) { 1434 case (XMLSecEnc::Element): 1435 if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { 1436 return $this->encdoc; 1437 } 1438 $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, true); 1439 $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode); 1440 return $importEnc; 1441 case (XMLSecEnc::Content): 1442 $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, true); 1443 while($this->rawNode->firstChild) { 1444 $this->rawNode->removeChild($this->rawNode->firstChild); 1445 } 1446 $this->rawNode->appendChild($importEnc); 1447 return $importEnc; 1448 } 1449 } else { 1450 return $this->encdoc->documentElement; 1451 } 1452 } 1453 1454 public function encryptReferences($objKey) { 1455 $curRawNode = $this->rawNode; 1456 $curType = $this->type; 1457 foreach ($this->references AS $name=>$reference) { 1458 $this->encdoc = $reference["encnode"]; 1459 $this->rawNode = $reference["node"]; 1460 $this->type = $reference["type"]; 1461 try { 1462 $encNode = $this->encryptNode($objKey); 1463 $this->references[$name]["encnode"] = $encNode; 1464 } catch (Exception $e) { 1465 $this->rawNode = $curRawNode; 1466 $this->type = $curType; 1467 throw $e; 1468 } 1469 } 1470 $this->rawNode = $curRawNode; 1471 $this->type = $curType; 1472 } 1473 1474 /** 1475 * Retrieve the CipherValue text from this encrypted node. 1476 * 1477 * @return string|null The Ciphervalue text, or null if no CipherValue is found. 1478 */ 1479 public function getCipherValue() { 1480 if (empty($this->rawNode)) { 1481 throw new Exception('Node to decrypt has not been set'); 1482 } 1483 1484 $doc = $this->rawNode->ownerDocument; 1485 $xPath = new DOMXPath($doc); 1486 $xPath->registerNamespace('xmlencr', XMLSecEnc::XMLENCNS); 1487 /* Only handles embedded content right now and not a reference */ 1488 $query = "./xmlencr:CipherData/xmlencr:CipherValue"; 1489 $nodeset = $xPath->query($query, $this->rawNode); 1490 $node = $nodeset->item(0); 1491 1492 if (!$node) { 1493 return null; 1494 } 1495 1496 return base64_decode($node->nodeValue); 1497 } 1498 1499 /** 1500 * Decrypt this encrypted node. 1501 * 1502 * The behaviour of this function depends on the value of $replace. 1503 * If $replace is false, we will return the decrypted data as a string. 1504 * If $replace is true, we will insert the decrypted element(s) into the 1505 * document, and return the decrypted element(s). 1506 * 1507 * @params XMLSecurityKey $objKey The decryption key that should be used when decrypting the node. 1508 * @params boolean $replace Whether we should replace the encrypted node in the XML document with the decrypted data. The default is true. 1509 * @return string|DOMElement The decrypted data. 1510 */ 1511 public function decryptNode($objKey, $replace=true) { 1512 if (! $objKey instanceof XMLSecurityKey) { 1513 throw new Exception('Invalid Key'); 1514 } 1515 1516 $encryptedData = $this->getCipherValue(); 1517 if ($encryptedData) { 1518 $decrypted = $objKey->decryptData($encryptedData); 1519 if ($replace) { 1520 switch ($this->type) { 1521 case (XMLSecEnc::Element): 1522 $newdoc = new DOMDocument(); 1523 $newdoc->loadXML($decrypted); 1524 if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { 1525 return $newdoc; 1526 } 1527 $importEnc = $this->rawNode->ownerDocument->importNode($newdoc->documentElement, true); 1528 $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode); 1529 return $importEnc; 1530 case (XMLSecEnc::Content): 1531 if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { 1532 $doc = $this->rawNode; 1533 } else { 1534 $doc = $this->rawNode->ownerDocument; 1535 } 1536 $newFrag = $doc->createDocumentFragment(); 1537 $newFrag->appendXML($decrypted); 1538 $parent = $this->rawNode->parentNode; 1539 $parent->replaceChild($newFrag, $this->rawNode); 1540 return $parent; 1541 default: 1542 return $decrypted; 1543 } 1544 } else { 1545 return $decrypted; 1546 } 1547 } else { 1548 throw new Exception("Cannot locate encrypted data"); 1549 } 1550 } 1551 1552 public function encryptKey($srcKey, $rawKey, $append=true) { 1553 if ((! $srcKey instanceof XMLSecurityKey) || (! $rawKey instanceof XMLSecurityKey)) { 1554 throw new Exception('Invalid Key'); 1555 } 1556 $strEncKey = base64_encode($srcKey->encryptData($rawKey->key)); 1557 $root = $this->encdoc->documentElement; 1558 $encKey = $this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:EncryptedKey'); 1559 if ($append) { 1560 $keyInfo = $root->insertBefore($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo'), $root->firstChild); 1561 $keyInfo->appendChild($encKey); 1562 } else { 1563 $this->encKey = $encKey; 1564 } 1565 $encMethod = $encKey->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:EncryptionMethod')); 1566 $encMethod->setAttribute('Algorithm', $srcKey->getAlgorithm()); 1567 if (! empty($srcKey->name)) { 1568 $keyInfo = $encKey->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo')); 1569 $keyInfo->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyName', $srcKey->name)); 1570 } 1571 $cipherData = $encKey->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:CipherData')); 1572 $cipherData->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:CipherValue', $strEncKey)); 1573 if (is_array($this->references) && count($this->references) > 0) { 1574 $refList = $encKey->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:ReferenceList')); 1575 foreach ($this->references AS $name=>$reference) { 1576 $refuri = $reference["refuri"]; 1577 $dataRef = $refList->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:DataReference')); 1578 $dataRef->setAttribute("URI", '#' . $refuri); 1579 } 1580 } 1581 return; 1582 } 1583 1584 public function decryptKey($encKey) { 1585 if (! $encKey->isEncrypted) { 1586 throw new Exception("Key is not Encrypted"); 1587 } 1588 if (empty($encKey->key)) { 1589 throw new Exception("Key is missing data to perform the decryption"); 1590 } 1591 return $this->decryptNode($encKey, false); 1592 } 1593 1594 public function locateEncryptedData($element) { 1595 if ($element instanceof DOMDocument) { 1596 $doc = $element; 1597 } else { 1598 $doc = $element->ownerDocument; 1599 } 1600 if ($doc) { 1601 $xpath = new DOMXPath($doc); 1602 $query = "//*[local-name()='EncryptedData' and namespace-uri()='".XMLSecEnc::XMLENCNS."']"; 1603 $nodeset = $xpath->query($query); 1604 return $nodeset->item(0); 1605 } 1606 return null; 1607 } 1608 1609 public function locateKey($node=null) { 1610 if (empty($node)) { 1611 $node = $this->rawNode; 1612 } 1613 if (! $node instanceof DOMNode) { 1614 return null; 1615 } 1616 if ($doc = $node->ownerDocument) { 1617 $xpath = new DOMXPath($doc); 1618 $xpath->registerNamespace('xmlsecenc', XMLSecEnc::XMLENCNS); 1619 $query = ".//xmlsecenc:EncryptionMethod"; 1620 $nodeset = $xpath->query($query, $node); 1621 if ($encmeth = $nodeset->item(0)) { 1622 $attrAlgorithm = $encmeth->getAttribute("Algorithm"); 1623 try { 1624 $objKey = new XMLSecurityKey($attrAlgorithm, array('type'=>'private')); 1625 } catch (Exception $e) { 1626 return null; 1627 } 1628 return $objKey; 1629 } 1630 } 1631 return null; 1632 } 1633 1634 static function staticLocateKeyInfo($objBaseKey=null, $node=null) { 1635 if (empty($node) || (! $node instanceof DOMNode)) { 1636 return null; 1637 } 1638 $doc = $node->ownerDocument; 1639 if (!$doc) { 1640 return null; 1641 } 1642 1643 $xpath = new DOMXPath($doc); 1644 $xpath->registerNamespace('xmlsecenc', XMLSecEnc::XMLENCNS); 1645 $xpath->registerNamespace('xmlsecdsig', XMLSecurityDSig::XMLDSIGNS); 1646 $query = "./xmlsecdsig:KeyInfo"; 1647 $nodeset = $xpath->query($query, $node); 1648 $encmeth = $nodeset->item(0); 1649 if (!$encmeth) { 1650 /* No KeyInfo in EncryptedData / EncryptedKey. */ 1651 return $objBaseKey; 1652 } 1653 1654 foreach ($encmeth->childNodes AS $child) { 1655 switch ($child->localName) { 1656 case 'KeyName': 1657 if (! empty($objBaseKey)) { 1658 $objBaseKey->name = $child->nodeValue; 1659 } 1660 break; 1661 case 'KeyValue': 1662 foreach ($child->childNodes AS $keyval) { 1663 switch ($keyval->localName) { 1664 case 'DSAKeyValue': 1665 throw new Exception("DSAKeyValue currently not supported"); 1666 case 'RSAKeyValue': 1667 $modulus = null; 1668 $exponent = null; 1669 if ($modulusNode = $keyval->getElementsByTagName('Modulus')->item(0)) { 1670 $modulus = base64_decode($modulusNode->nodeValue); 1671 } 1672 if ($exponentNode = $keyval->getElementsByTagName('Exponent')->item(0)) { 1673 $exponent = base64_decode($exponentNode->nodeValue); 1674 } 1675 if (empty($modulus) || empty($exponent)) { 1676 throw new Exception("Missing Modulus or Exponent"); 1677 } 1678 $publicKey = XMLSecurityKey::convertRSA($modulus, $exponent); 1679 $objBaseKey->loadKey($publicKey); 1680 break; 1681 } 1682 } 1683 break; 1684 case 'RetrievalMethod': 1685 $type = $child->getAttribute('Type'); 1686 if ($type !== 'http://www.w3.org/2001/04/xmlenc#EncryptedKey') { 1687 /* Unsupported key type. */ 1688 break; 1689 } 1690 $uri = $child->getAttribute('URI'); 1691 if ($uri[0] !== '#') { 1692 /* URI not a reference - unsupported. */ 1693 break; 1694 } 1695 $id = substr($uri, 1); 1696 1697 $query = "//xmlsecenc:EncryptedKey[@Id='$id']"; 1698 $keyElement = $xpath->query($query)->item(0); 1699 if (!$keyElement) { 1700 throw new Exception("Unable to locate EncryptedKey with @Id='$id'."); 1701 } 1702 1703 return XMLSecurityKey::fromEncryptedKeyElement($keyElement); 1704 case 'EncryptedKey': 1705 return XMLSecurityKey::fromEncryptedKeyElement($child); 1706 case 'X509Data': 1707 if ($x509certNodes = $child->getElementsByTagName('X509Certificate')) { 1708 if ($x509certNodes->length > 0) { 1709 $x509cert = $x509certNodes->item(0)->textContent; 1710 $x509cert = str_replace(array("\r", "\n", " "), "", $x509cert); 1711 $x509cert = "-----BEGIN CERTIFICATE-----\n".chunk_split($x509cert, 64, "\n")."-----END CERTIFICATE-----\n"; 1712 $objBaseKey->loadKey($x509cert, false, true); 1713 } 1714 } 1715 break; 1716 } 1717 } 1718 return $objBaseKey; 1719 } 1720 1721 public function locateKeyInfo($objBaseKey=null, $node=null) { 1722 if (empty($node)) { 1723 $node = $this->rawNode; 1724 } 1725 return XMLSecEnc::staticLocateKeyInfo($objBaseKey, $node); 1726 } 1727} 1728