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