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 
43 class 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 
520 class 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 
1341 class 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