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