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