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