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