xref: /dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/XML.php (revision 850e662095111529d7d330745fee3207907c4aee)
1<?php
2
3/**
4 * XML Formatted EC Key Handler
5 *
6 * More info:
7 *
8 * https://www.w3.org/TR/xmldsig-core/#sec-ECKeyValue
9 * http://en.wikipedia.org/wiki/XML_Signature
10 *
11 * PHP version 5
12 *
13 * @author    Jim Wigginton <terrafrost@php.net>
14 * @copyright 2015 Jim Wigginton
15 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
16 * @link      http://phpseclib.sourceforge.net
17 */
18
19namespace phpseclib3\Crypt\EC\Formats\Keys;
20
21use phpseclib3\Common\Functions\Strings;
22use phpseclib3\Crypt\EC\BaseCurves\Base as BaseCurve;
23use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve;
24use phpseclib3\Crypt\EC\BaseCurves\Prime as PrimeCurve;
25use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve;
26use phpseclib3\Exception\BadConfigurationException;
27use phpseclib3\Exception\UnsupportedCurveException;
28use phpseclib3\Math\BigInteger;
29
30/**
31 * XML Formatted EC Key Handler
32 *
33 * @author  Jim Wigginton <terrafrost@php.net>
34 */
35abstract class XML
36{
37    use Common;
38
39    /**
40     * Default namespace
41     *
42     * @var string
43     */
44    private static $namespace;
45
46    /**
47     * Flag for using RFC4050 syntax
48     *
49     * @var bool
50     */
51    private static $rfc4050 = false;
52
53    /**
54     * Break a public or private key down into its constituent components
55     *
56     * @param string $key
57     * @param string $password optional
58     * @return array
59     */
60    public static function load($key, $password = '')
61    {
62        self::initialize_static_variables();
63
64        if (!Strings::is_stringable($key)) {
65            throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
66        }
67
68        if (!class_exists('DOMDocument')) {
69            throw new BadConfigurationException('The dom extension is not setup correctly on this system');
70        }
71
72        $use_errors = libxml_use_internal_errors(true);
73
74        $temp = self::isolateNamespace($key, 'http://www.w3.org/2009/xmldsig11#');
75        if ($temp) {
76            $key = $temp;
77        }
78
79        $temp = self::isolateNamespace($key, 'http://www.w3.org/2001/04/xmldsig-more#');
80        if ($temp) {
81            $key = $temp;
82        }
83
84        $dom = new \DOMDocument();
85        if (substr($key, 0, 5) != '<?xml') {
86            $key = '<xml>' . $key . '</xml>';
87        }
88
89        if (!$dom->loadXML($key)) {
90            libxml_use_internal_errors($use_errors);
91            throw new \UnexpectedValueException('Key does not appear to contain XML');
92        }
93        $xpath = new \DOMXPath($dom);
94        libxml_use_internal_errors($use_errors);
95        $curve = self::loadCurveByParam($xpath);
96
97        $pubkey = self::query($xpath, 'publickey', 'Public Key is not present');
98
99        $QA = self::query($xpath, 'ecdsakeyvalue')->length ?
100            self::extractPointRFC4050($xpath, $curve) :
101            self::extractPoint("\0" . $pubkey, $curve);
102
103        libxml_use_internal_errors($use_errors);
104
105        return compact('curve', 'QA');
106    }
107
108    /**
109     * Case-insensitive xpath query
110     *
111     * @param \DOMXPath $xpath
112     * @param string $name
113     * @param string $error optional
114     * @param bool $decode optional
115     * @return \DOMNodeList
116     */
117    private static function query(\DOMXPath $xpath, $name, $error = null, $decode = true)
118    {
119        $query = '/';
120        $names = explode('/', $name);
121        foreach ($names as $name) {
122            $query .= "/*[translate(local-name(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$name']";
123        }
124        $result = $xpath->query($query);
125        if (!isset($error)) {
126            return $result;
127        }
128
129        if (!$result->length) {
130            throw new \RuntimeException($error);
131        }
132        return $decode ? self::decodeValue($result->item(0)->textContent) : $result->item(0)->textContent;
133    }
134
135    /**
136     * Finds the first element in the relevant namespace, strips the namespacing and returns the XML for that element.
137     *
138     * @param string $xml
139     * @param string $ns
140     */
141    private static function isolateNamespace($xml, $ns)
142    {
143        $dom = new \DOMDocument();
144        if (!$dom->loadXML($xml)) {
145            return false;
146        }
147        $xpath = new \DOMXPath($dom);
148        $nodes = $xpath->query("//*[namespace::*[.='$ns'] and not(../namespace::*[.='$ns'])]");
149        if (!$nodes->length) {
150            return false;
151        }
152        $node = $nodes->item(0);
153        $ns_name = $node->lookupPrefix($ns);
154        if ($ns_name) {
155            $node->removeAttributeNS($ns, $ns_name);
156        }
157        return $dom->saveXML($node);
158    }
159
160    /**
161     * Decodes the value
162     *
163     * @param string $value
164     */
165    private static function decodeValue($value)
166    {
167        return Strings::base64_decode(str_replace(["\r", "\n", ' ', "\t"], '', $value));
168    }
169
170    /**
171     * Extract points from an XML document
172     *
173     * @param \DOMXPath $xpath
174     * @param BaseCurve $curve
175     * @return object[]
176     */
177    private static function extractPointRFC4050(\DOMXPath $xpath, BaseCurve $curve)
178    {
179        $x = self::query($xpath, 'publickey/x');
180        $y = self::query($xpath, 'publickey/y');
181        if (!$x->length || !$x->item(0)->hasAttribute('Value')) {
182            throw new \RuntimeException('Public Key / X coordinate not found');
183        }
184        if (!$y->length || !$y->item(0)->hasAttribute('Value')) {
185            throw new \RuntimeException('Public Key / Y coordinate not found');
186        }
187        $point = [
188            $curve->convertInteger(new BigInteger($x->item(0)->getAttribute('Value'))),
189            $curve->convertInteger(new BigInteger($y->item(0)->getAttribute('Value')))
190        ];
191        if (!$curve->verifyPoint($point)) {
192            throw new \RuntimeException('Unable to verify that point exists on curve');
193        }
194        return $point;
195    }
196
197    /**
198     * Returns an instance of \phpseclib3\Crypt\EC\BaseCurves\Base based
199     * on the curve parameters
200     *
201     * @param \DomXPath $xpath
202     * @return BaseCurve|false
203     */
204    private static function loadCurveByParam(\DOMXPath $xpath)
205    {
206        $namedCurve = self::query($xpath, 'namedcurve');
207        if ($namedCurve->length == 1) {
208            $oid = $namedCurve->item(0)->getAttribute('URN');
209            $oid = preg_replace('#[^\d.]#', '', $oid);
210            $name = array_search($oid, self::$curveOIDs);
211            if ($name === false) {
212                throw new UnsupportedCurveException('Curve with OID of ' . $oid . ' is not supported');
213            }
214
215            $curve = '\phpseclib3\Crypt\EC\Curves\\' . $name;
216            if (!class_exists($curve)) {
217                throw new UnsupportedCurveException('Named Curve of ' . $name . ' is not supported');
218            }
219            return new $curve();
220        }
221
222        $params = self::query($xpath, 'explicitparams');
223        if ($params->length) {
224            return self::loadCurveByParamRFC4050($xpath);
225        }
226
227        $params = self::query($xpath, 'ecparameters');
228        if (!$params->length) {
229            throw new \RuntimeException('No parameters are present');
230        }
231
232        $fieldTypes = [
233            'prime-field' => ['fieldid/prime/p'],
234            'gnb' => ['fieldid/gnb/m'],
235            'tnb' => ['fieldid/tnb/k'],
236            'pnb' => ['fieldid/pnb/k1', 'fieldid/pnb/k2', 'fieldid/pnb/k3'],
237            'unknown' => []
238        ];
239
240        foreach ($fieldTypes as $type => $queries) {
241            foreach ($queries as $query) {
242                $result = self::query($xpath, $query);
243                if (!$result->length) {
244                    continue 2;
245                }
246                $param = preg_replace('#.*/#', '', $query);
247                $$param = self::decodeValue($result->item(0)->textContent);
248            }
249            break;
250        }
251
252        $a = self::query($xpath, 'curve/a', 'A coefficient is not present');
253        $b = self::query($xpath, 'curve/b', 'B coefficient is not present');
254        $base = self::query($xpath, 'base', 'Base point is not present');
255        $order = self::query($xpath, 'order', 'Order is not present');
256
257        switch ($type) {
258            case 'prime-field':
259                $curve = new PrimeCurve();
260                $curve->setModulo(new BigInteger($p, 256));
261                $curve->setCoefficients(
262                    new BigInteger($a, 256),
263                    new BigInteger($b, 256)
264                );
265                $point = self::extractPoint("\0" . $base, $curve);
266                $curve->setBasePoint(...$point);
267                $curve->setOrder(new BigInteger($order, 256));
268                return $curve;
269            case 'gnb':
270            case 'tnb':
271            case 'pnb':
272            default:
273                throw new UnsupportedCurveException('Field Type of ' . $type . ' is not supported');
274        }
275    }
276
277    /**
278     * Returns an instance of \phpseclib3\Crypt\EC\BaseCurves\Base based
279     * on the curve parameters
280     *
281     * @param \DomXPath $xpath
282     * @return BaseCurve|false
283     */
284    private static function loadCurveByParamRFC4050(\DOMXPath $xpath)
285    {
286        $fieldTypes = [
287            'prime-field' => ['primefieldparamstype/p'],
288            'unknown' => []
289        ];
290
291        foreach ($fieldTypes as $type => $queries) {
292            foreach ($queries as $query) {
293                $result = self::query($xpath, $query);
294                if (!$result->length) {
295                    continue 2;
296                }
297                $param = preg_replace('#.*/#', '', $query);
298                $$param = $result->item(0)->textContent;
299            }
300            break;
301        }
302
303        $a = self::query($xpath, 'curveparamstype/a', 'A coefficient is not present', false);
304        $b = self::query($xpath, 'curveparamstype/b', 'B coefficient is not present', false);
305        $x = self::query($xpath, 'basepointparams/basepoint/ecpointtype/x', 'Base Point X is not present', false);
306        $y = self::query($xpath, 'basepointparams/basepoint/ecpointtype/y', 'Base Point Y is not present', false);
307        $order = self::query($xpath, 'order', 'Order is not present', false);
308
309        switch ($type) {
310            case 'prime-field':
311                $curve = new PrimeCurve();
312
313                $p = str_replace(["\r", "\n", ' ', "\t"], '', $p);
314                $curve->setModulo(new BigInteger($p));
315
316                $a = str_replace(["\r", "\n", ' ', "\t"], '', $a);
317                $b = str_replace(["\r", "\n", ' ', "\t"], '', $b);
318                $curve->setCoefficients(
319                    new BigInteger($a),
320                    new BigInteger($b)
321                );
322
323                $x = str_replace(["\r", "\n", ' ', "\t"], '', $x);
324                $y = str_replace(["\r", "\n", ' ', "\t"], '', $y);
325                $curve->setBasePoint(
326                    new BigInteger($x),
327                    new BigInteger($y)
328                );
329
330                $order = str_replace(["\r", "\n", ' ', "\t"], '', $order);
331                $curve->setOrder(new BigInteger($order));
332                return $curve;
333            default:
334                throw new UnsupportedCurveException('Field Type of ' . $type . ' is not supported');
335        }
336    }
337
338    /**
339     * Sets the namespace. dsig11 is the most common one.
340     *
341     * Set to null to unset. Used only for creating public keys.
342     *
343     * @param string $namespace
344     */
345    public static function setNamespace($namespace)
346    {
347        self::$namespace = $namespace;
348    }
349
350    /**
351     * Uses the XML syntax specified in https://tools.ietf.org/html/rfc4050
352     */
353    public static function enableRFC4050Syntax()
354    {
355        self::$rfc4050 = true;
356    }
357
358    /**
359     * Uses the XML syntax specified in https://www.w3.org/TR/xmldsig-core/#sec-ECParameters
360     */
361    public static function disableRFC4050Syntax()
362    {
363        self::$rfc4050 = false;
364    }
365
366    /**
367     * Convert a public key to the appropriate format
368     *
369     * @param BaseCurve $curve
370     * @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey
371     * @param array $options optional
372     * @return string
373     */
374    public static function savePublicKey(BaseCurve $curve, array $publicKey, array $options = [])
375    {
376        self::initialize_static_variables();
377
378        if ($curve instanceof TwistedEdwardsCurve || $curve instanceof MontgomeryCurve) {
379            throw new UnsupportedCurveException('TwistedEdwards and Montgomery Curves are not supported');
380        }
381
382        if (empty(static::$namespace)) {
383            $pre = $post = '';
384        } else {
385            $pre = static::$namespace . ':';
386            $post = ':' . static::$namespace;
387        }
388
389        if (self::$rfc4050) {
390            return '<' . $pre . 'ECDSAKeyValue xmlns' . $post . '="http://www.w3.org/2001/04/xmldsig-more#">' . "\r\n" .
391                   self::encodeXMLParameters($curve, $pre, $options) . "\r\n" .
392                   '<' . $pre . 'PublicKey>' . "\r\n" .
393                   '<' . $pre . 'X Value="' . $publicKey[0] . '" />' . "\r\n" .
394                   '<' . $pre . 'Y Value="' . $publicKey[1] . '" />' . "\r\n" .
395                   '</' . $pre . 'PublicKey>' . "\r\n" .
396                   '</' . $pre . 'ECDSAKeyValue>';
397        }
398
399        $publicKey = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes();
400
401        return '<' . $pre . 'ECDSAKeyValue xmlns' . $post . '="http://www.w3.org/2009/xmldsig11#">' . "\r\n" .
402               self::encodeXMLParameters($curve, $pre, $options) . "\r\n" .
403               '<' . $pre . 'PublicKey>' . Strings::base64_encode($publicKey) . '</' . $pre . 'PublicKey>' . "\r\n" .
404               '</' . $pre . 'ECDSAKeyValue>';
405    }
406
407    /**
408     * Encode Parameters
409     *
410     * @param BaseCurve $curve
411     * @param string $pre
412     * @param array $options optional
413     * @return string|false
414     */
415    private static function encodeXMLParameters(BaseCurve $curve, $pre, array $options = [])
416    {
417        $result = self::encodeParameters($curve, true, $options);
418
419        if (isset($result['namedCurve'])) {
420            $namedCurve = '<' . $pre . 'NamedCurve URI="urn:oid:' . self::$curveOIDs[$result['namedCurve']] . '" />';
421            return self::$rfc4050 ?
422                '<DomainParameters>' . str_replace('URI', 'URN', $namedCurve) . '</DomainParameters>' :
423                $namedCurve;
424        }
425
426        if (self::$rfc4050) {
427            $xml = '<' . $pre . 'ExplicitParams>' . "\r\n" .
428                  '<' . $pre . 'FieldParams>' . "\r\n";
429            $temp = $result['specifiedCurve'];
430            switch ($temp['fieldID']['fieldType']) {
431                case 'prime-field':
432                    $xml .= '<' . $pre . 'PrimeFieldParamsType>' . "\r\n" .
433                           '<' . $pre . 'P>' . $temp['fieldID']['parameters'] . '</' . $pre . 'P>' . "\r\n" .
434                           '</' . $pre . 'PrimeFieldParamsType>' . "\r\n";
435                    $a = $curve->getA();
436                    $b = $curve->getB();
437                    list($x, $y) = $curve->getBasePoint();
438                    break;
439                default:
440                    throw new UnsupportedCurveException('Field Type of ' . $temp['fieldID']['fieldType'] . ' is not supported');
441            }
442            $xml .= '</' . $pre . 'FieldParams>' . "\r\n" .
443                   '<' . $pre . 'CurveParamsType>' . "\r\n" .
444                   '<' . $pre . 'A>' . $a . '</' . $pre . 'A>' . "\r\n" .
445                   '<' . $pre . 'B>' . $b . '</' . $pre . 'B>' . "\r\n" .
446                   '</' . $pre . 'CurveParamsType>' . "\r\n" .
447                   '<' . $pre . 'BasePointParams>' . "\r\n" .
448                   '<' . $pre . 'BasePoint>' . "\r\n" .
449                   '<' . $pre . 'ECPointType>' . "\r\n" .
450                   '<' . $pre . 'X>' . $x . '</' . $pre . 'X>' . "\r\n" .
451                   '<' . $pre . 'Y>' . $y . '</' . $pre . 'Y>' . "\r\n" .
452                   '</' . $pre . 'ECPointType>' . "\r\n" .
453                   '</' . $pre . 'BasePoint>' . "\r\n" .
454                   '<' . $pre . 'Order>' . $curve->getOrder() . '</' . $pre . 'Order>' . "\r\n" .
455                   '</' . $pre . 'BasePointParams>' . "\r\n" .
456                   '</' . $pre . 'ExplicitParams>' . "\r\n";
457
458            return $xml;
459        }
460
461        if (isset($result['specifiedCurve'])) {
462            $xml = '<' . $pre . 'ECParameters>' . "\r\n" .
463                   '<' . $pre . 'FieldID>' . "\r\n";
464            $temp = $result['specifiedCurve'];
465            switch ($temp['fieldID']['fieldType']) {
466                case 'prime-field':
467                    $xml .= '<' . $pre . 'Prime>' . "\r\n" .
468                           '<' . $pre . 'P>' . Strings::base64_encode($temp['fieldID']['parameters']->toBytes()) . '</' . $pre . 'P>' . "\r\n" .
469                           '</' . $pre . 'Prime>' . "\r\n" ;
470                    break;
471                default:
472                    throw new UnsupportedCurveException('Field Type of ' . $temp['fieldID']['fieldType'] . ' is not supported');
473            }
474            $xml .= '</' . $pre . 'FieldID>' . "\r\n" .
475                   '<' . $pre . 'Curve>' . "\r\n" .
476                   '<' . $pre . 'A>' . Strings::base64_encode($temp['curve']['a']) . '</' . $pre . 'A>' . "\r\n" .
477                   '<' . $pre . 'B>' . Strings::base64_encode($temp['curve']['b']) . '</' . $pre . 'B>' . "\r\n" .
478                   '</' . $pre . 'Curve>' . "\r\n" .
479                   '<' . $pre . 'Base>' . Strings::base64_encode($temp['base']) . '</' . $pre . 'Base>' . "\r\n" .
480                   '<' . $pre . 'Order>' . Strings::base64_encode($temp['order']) . '</' . $pre . 'Order>' . "\r\n" .
481                   '</' . $pre . 'ECParameters>';
482            return $xml;
483        }
484    }
485}
486