mirror of
https://github.com/danog/tgseclib.git
synced 2024-12-02 17:48:00 +01:00
481 lines
17 KiB
PHP
481 lines
17 KiB
PHP
<?php
|
|
|
|
/**
|
|
* XML Formatted ECDSA Key Handler
|
|
*
|
|
* More info:
|
|
*
|
|
* https://www.w3.org/TR/xmldsig-core/#sec-ECKeyValue
|
|
* http://en.wikipedia.org/wiki/XML_Signature
|
|
*
|
|
* PHP version 5
|
|
*
|
|
* @category Crypt
|
|
* @package ECDSA
|
|
* @author Jim Wigginton <terrafrost@php.net>
|
|
* @copyright 2015 Jim Wigginton
|
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
|
* @link http://phpseclib.sourceforge.net
|
|
*/
|
|
|
|
namespace phpseclib\Crypt\ECDSA\Keys;
|
|
|
|
use ParagonIE\ConstantTime\Base64;
|
|
use phpseclib\Math\BigInteger;
|
|
use phpseclib\Crypt\ECDSA\BaseCurves\Base as BaseCurve;
|
|
use phpseclib\Crypt\ECDSA\BaseCurves\Prime as PrimeCurve;
|
|
use phpseclib\Crypt\ECDSA\BaseCurves\TwistedEdwards as TwistedEdwardsCurve;
|
|
use phpseclib\Exception\UnsupportedCurveException;
|
|
|
|
/**
|
|
* XML Formatted ECDSA Key Handler
|
|
*
|
|
* @package ECDSA
|
|
* @author Jim Wigginton <terrafrost@php.net>
|
|
* @access public
|
|
*/
|
|
abstract class XML
|
|
{
|
|
use Common;
|
|
|
|
/**
|
|
* Default namespace
|
|
*
|
|
* @var string
|
|
*/
|
|
private static $namespace;
|
|
|
|
/**
|
|
* Flag for using RFC4050 syntax
|
|
*
|
|
* @var bool
|
|
*/
|
|
private static $rfc4050 = false;
|
|
|
|
/**
|
|
* Break a public or private key down into its constituent components
|
|
*
|
|
* @access public
|
|
* @param string $key
|
|
* @param string $password optional
|
|
* @return array
|
|
*/
|
|
public static function load($key, $password = '')
|
|
{
|
|
self::initialize_static_variables();
|
|
|
|
if (!is_string($key)) {
|
|
throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
|
|
}
|
|
|
|
$use_errors = libxml_use_internal_errors(true);
|
|
|
|
$temp = self::isolateNamespace($key, 'http://www.w3.org/2009/xmldsig11#');
|
|
if ($temp) {
|
|
$key = $temp;
|
|
}
|
|
|
|
$temp = self::isolateNamespace($key, 'http://www.w3.org/2001/04/xmldsig-more#');
|
|
if ($temp) {
|
|
$key = $temp;
|
|
}
|
|
|
|
$dom = new \DOMDocument();
|
|
if (substr($key, 0, 5) != '<?xml') {
|
|
$key = '<xml>' . $key . '</xml>';
|
|
}
|
|
|
|
if (!$dom->loadXML($key)) {
|
|
libxml_use_internal_errors($use_errors);
|
|
throw new \UnexpectedValueException('Key does not appear to contain XML');
|
|
}
|
|
$xpath = new \DOMXPath($dom);
|
|
libxml_use_internal_errors($use_errors);
|
|
$curve = self::loadCurveByParam($xpath);
|
|
|
|
$pubkey = self::query($xpath, 'publickey', 'Public Key is not present');
|
|
|
|
$QA = self::query($xpath, 'ecdsakeyvalue')->length ?
|
|
self::extractPointRFC4050($xpath, $curve) :
|
|
self::extractPoint("\0" . $pubkey, $curve);
|
|
|
|
libxml_use_internal_errors($use_errors);
|
|
|
|
return compact('curve', 'QA');
|
|
}
|
|
|
|
/**
|
|
* Case-insensitive xpath query
|
|
*
|
|
* @param \DOMXPath $xpath
|
|
* @param string $name
|
|
* @param string $error optional
|
|
* @param bool $decode optional
|
|
* @return \DOMNodeList
|
|
*/
|
|
private static function query($xpath, $name, $error = null, $decode = true)
|
|
{
|
|
$query = '/';
|
|
$names = explode('/', $name);
|
|
foreach ($names as $name) {
|
|
$query.= "/*[translate(local-name(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='$name']";
|
|
}
|
|
$result = $xpath->query($query);
|
|
if (!isset($error)) {
|
|
return $result;
|
|
}
|
|
|
|
if (!$result->length) {
|
|
throw new \RuntimeException($error);
|
|
}
|
|
return $decode ? self::decodeValue($result->item(0)->textContent) : $result->item(0)->textContent;
|
|
}
|
|
|
|
/**
|
|
* Finds the first element in the relevant namespace, strips the namespacing and returns the XML for that element.
|
|
*
|
|
* @param string $xml
|
|
* @param string $ns
|
|
*/
|
|
private static function isolateNamespace($xml, $ns)
|
|
{
|
|
$dom = new \DOMDocument();
|
|
if (!$dom->loadXML($xml)) {
|
|
return false;
|
|
}
|
|
$xpath = new \DOMXPath($dom);
|
|
$nodes = $xpath->query("//*[namespace::*[.='$ns'] and not(../namespace::*[.='$ns'])]");
|
|
if (!$nodes->length) {
|
|
return false;
|
|
}
|
|
$node = $nodes->item(0);
|
|
$ns_name = $node->lookupPrefix($ns);
|
|
$node->removeAttributeNS($ns, $ns_name);
|
|
return $dom->saveXML($node);
|
|
}
|
|
|
|
/**
|
|
* Decodes the value
|
|
*
|
|
* @param string $value
|
|
*/
|
|
private static function decodeValue($value)
|
|
{
|
|
return Base64::decode(str_replace(["\r", "\n", ' ', "\t"], '', $value));
|
|
}
|
|
|
|
/**
|
|
* Extract points from an XML document
|
|
*
|
|
* @param \DOMXPath $xpath
|
|
* @param \phpseclib\Crypt\ECDSA\BaseCurves\Base $curve
|
|
* @return object[]
|
|
*/
|
|
private static function extractPointRFC4050(\DOMXPath $xpath, BaseCurve $curve)
|
|
{
|
|
$x = self::query($xpath, 'publickey/x');
|
|
$y = self::query($xpath, 'publickey/y');
|
|
if (!$x->length || !$x->item(0)->hasAttribute('Value')) {
|
|
throw new \RuntimeException('Public Key / X coordinate not found');
|
|
}
|
|
if (!$y->length || !$y->item(0)->hasAttribute('Value')) {
|
|
throw new \RuntimeException('Public Key / Y coordinate not found');
|
|
}
|
|
$point = [
|
|
$curve->convertInteger(new BigInteger($x->item(0)->getAttribute('Value'))),
|
|
$curve->convertInteger(new BigInteger($y->item(0)->getAttribute('Value')))
|
|
];
|
|
if (!$curve->verifyPoint($point)) {
|
|
throw new \RuntimeException('Unable to verify that point exists on curve');
|
|
}
|
|
return $point;
|
|
}
|
|
|
|
/**
|
|
* Returns an instance of \phpseclib\Crypt\ECDSA\BaseCurves\Base based
|
|
* on the curve parameters
|
|
*
|
|
* @param \DomXPath $xpath
|
|
* @return \phpseclib\Crypt\ECDSA\BaseCurves\Base|false
|
|
*/
|
|
private static function loadCurveByParam(\DOMXPath $xpath)
|
|
{
|
|
$namedCurve = self::query($xpath, 'namedcurve');
|
|
if ($namedCurve->length == 1) {
|
|
$oid = $namedCurve->item(0)->getAttribute('URN');
|
|
$oid = preg_replace('#[^\d.]#', '', $oid);
|
|
$name = array_search($oid, self::$curveOIDs);
|
|
if ($name === false) {
|
|
throw new UnsupportedCurveException('Curve with OID of ' . $oid . ' is not supported');
|
|
}
|
|
|
|
$curve = '\phpseclib\Crypt\ECDSA\Curves\\' . $name;
|
|
if (!class_exists($curve)) {
|
|
throw new UnsupportedCurveException('Named Curve of ' . $name . ' is not supported');
|
|
}
|
|
return new $curve();
|
|
}
|
|
|
|
$params = self::query($xpath, 'explicitparams');
|
|
if ($params->length) {
|
|
return self::loadCurveByParamRFC4050($xpath);
|
|
}
|
|
|
|
$params = self::query($xpath, 'ecparameters');
|
|
if (!$params->length) {
|
|
throw new \RuntimeException('No parameters are present');
|
|
}
|
|
|
|
$fieldTypes = [
|
|
'prime-field' => ['fieldid/prime/p'],
|
|
'gnb' => ['fieldid/gnb/m'],
|
|
'tnb' => ['fieldid/tnb/k'],
|
|
'pnb' => ['fieldid/pnb/k1', 'fieldid/pnb/k2', 'fieldid/pnb/k3'],
|
|
'unknown' => []
|
|
];
|
|
|
|
foreach ($fieldTypes as $type => $queries) {
|
|
foreach ($queries as $query) {
|
|
$result = self::query($xpath, $query);
|
|
if (!$result->length) {
|
|
continue 2;
|
|
}
|
|
$param = preg_replace('#.*/#', '', $query);
|
|
$$param = self::decodeValue($result->item(0)->textContent);
|
|
}
|
|
break;
|
|
}
|
|
|
|
$a = self::query($xpath, 'curve/a', 'A coefficient is not present');
|
|
$b = self::query($xpath, 'curve/b', 'B coefficient is not present');
|
|
$base = self::query($xpath, 'base', 'Base point is not present');
|
|
$order = self::query($xpath, 'order', 'Order is not present');
|
|
|
|
switch ($type) {
|
|
case 'prime-field':
|
|
$curve = new PrimeCurve();
|
|
$curve->setModulo(new BigInteger($p, 256));
|
|
$curve->setCoefficients(
|
|
new BigInteger($a, 256),
|
|
new BigInteger($b, 256)
|
|
);
|
|
$point = self::extractPoint("\0" . $base, $curve);
|
|
$curve->setBasePoint(...$point);
|
|
$curve->setOrder(new BigInteger($order, 256));
|
|
return $curve;
|
|
case 'gnb':
|
|
case 'tnb':
|
|
case 'pnb':
|
|
default:
|
|
throw new UnsupportedCurveException('Field Type of ' . $type . ' is not supported');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an instance of \phpseclib\Crypt\ECDSA\BaseCurves\Base based
|
|
* on the curve parameters
|
|
*
|
|
* @param \DomXPath $xpath
|
|
* @return \phpseclib\Crypt\ECDSA\BaseCurves\Base|false
|
|
*/
|
|
private static function loadCurveByParamRFC4050(\DOMXPath $xpath)
|
|
{
|
|
$fieldTypes = [
|
|
'prime-field' => ['primefieldparamstype/p'],
|
|
'unknown' => []
|
|
];
|
|
|
|
foreach ($fieldTypes as $type => $queries) {
|
|
foreach ($queries as $query) {
|
|
$result = self::query($xpath, $query);
|
|
if (!$result->length) {
|
|
continue 2;
|
|
}
|
|
$param = preg_replace('#.*/#', '', $query);
|
|
$$param = $result->item(0)->textContent;
|
|
}
|
|
break;
|
|
}
|
|
|
|
$a = self::query($xpath, 'curveparamstype/a', 'A coefficient is not present', false);
|
|
$b = self::query($xpath, 'curveparamstype/b', 'B coefficient is not present', false);
|
|
$x = self::query($xpath, 'basepointparams/basepoint/ecpointtype/x', 'Base Point X is not present', false);
|
|
$y = self::query($xpath, 'basepointparams/basepoint/ecpointtype/y', 'Base Point Y is not present', false);
|
|
$order = self::query($xpath, 'order', 'Order is not present', false);
|
|
|
|
switch ($type) {
|
|
case 'prime-field':
|
|
$curve = new PrimeCurve();
|
|
|
|
$p = str_replace(["\r", "\n", ' ', "\t"], '', $p);
|
|
$curve->setModulo(new BigInteger($p));
|
|
|
|
$a = str_replace(["\r", "\n", ' ', "\t"], '', $a);
|
|
$b = str_replace(["\r", "\n", ' ', "\t"], '', $b);
|
|
$curve->setCoefficients(
|
|
new BigInteger($a),
|
|
new BigInteger($b)
|
|
);
|
|
|
|
$x = str_replace(["\r", "\n", ' ', "\t"], '', $x);
|
|
$y = str_replace(["\r", "\n", ' ', "\t"], '', $y);
|
|
$curve->setBasePoint(
|
|
new BigInteger($x),
|
|
new BigInteger($y)
|
|
);
|
|
|
|
$order = str_replace(["\r", "\n", ' ', "\t"], '', $order);
|
|
$curve->setOrder(new BigInteger($order));
|
|
return $curve;
|
|
default:
|
|
throw new UnsupportedCurveException('Field Type of ' . $type . ' is not supported');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the namespace. dsig11 is the most common one.
|
|
*
|
|
* Set to null to unset. Used only for creating public keys.
|
|
*
|
|
* @param string $namespace
|
|
*/
|
|
public static function setNamespace($namespace)
|
|
{
|
|
self::$namespace = $namespace;
|
|
}
|
|
|
|
/**
|
|
* Uses the XML syntax specified in https://tools.ietf.org/html/rfc4050
|
|
*/
|
|
public static function enableRFC4050Syntax()
|
|
{
|
|
self::$rfc4050 = true;
|
|
}
|
|
|
|
/**
|
|
* Uses the XML syntax specified in https://www.w3.org/TR/xmldsig-core/#sec-ECParameters
|
|
*/
|
|
public static function disableRFC4050Syntax()
|
|
{
|
|
self::$rfc4050 = false;
|
|
}
|
|
|
|
/**
|
|
* Convert a public key to the appropriate format
|
|
*
|
|
* @param \phpseclib\Crypt\ECDSA\BaseCurves\Base $curve
|
|
* @param \phpseclib\Math\Common\FiniteField\Integer[] $publicKey
|
|
* @return string
|
|
*/
|
|
public static function savePublicKey(BaseCurve $curve, array $publicKey)
|
|
{
|
|
self::initialize_static_variables();
|
|
|
|
if ($curve instanceof TwistedEdwardsCurve) {
|
|
throw new UnsupportedCurveException('TwistedEdwards Curves are not supported');
|
|
}
|
|
|
|
if (empty(static::$namespace)) {
|
|
$pre = $post = '';
|
|
} else {
|
|
$pre = static::$namespace . ':';
|
|
$post = ':' . static::$namespace;
|
|
}
|
|
|
|
if (self::$rfc4050) {
|
|
return '<' . $pre . 'ECDSAKeyValue xmlns' . $post . '="http://www.w3.org/2001/04/xmldsig-more#">' . "\r\n" .
|
|
self::encodeXMLParameters($curve, $pre) . "\r\n" .
|
|
'<' . $pre . 'PublicKey>' . "\r\n" .
|
|
'<' . $pre . 'X Value="' . $publicKey[0] . '" />' . "\r\n" .
|
|
'<' . $pre . 'Y Value="' . $publicKey[1] . '" />' . "\r\n" .
|
|
'</' . $pre . 'PublicKey>' . "\r\n" .
|
|
'</' . $pre . 'ECDSAKeyValue>';
|
|
}
|
|
|
|
$publicKey = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes();
|
|
|
|
return '<' . $pre . 'ECKeyValue xmlns' . $post . '="http://www.w3.org/2009/xmldsig11#">' . "\r\n" .
|
|
self::encodeXMLParameters($curve, $pre) . "\r\n" .
|
|
'<' . $pre . 'PublicKey>' . Base64::encode($publicKey) . '</' . $pre . 'PublicKey>' . "\r\n" .
|
|
'</' . $pre . 'ECKeyValue>';
|
|
}
|
|
|
|
/**
|
|
* Encode Parameters
|
|
*
|
|
* @param \phpseclib\Crypt\ECDSA\BaseCurves\Base $curve
|
|
* @param string $pre
|
|
* @return string|false
|
|
*/
|
|
private static function encodeXMLParameters(BaseCurve $curve, $pre)
|
|
{
|
|
$result = self::encodeParameters($curve, true);
|
|
|
|
if (isset($result['namedCurve'])) {
|
|
$namedCurve = '<' . $pre . 'NamedCurve URI="urn:oid:' . self::$curveOIDs[$result['namedCurve']] . '" />';
|
|
return self::$rfc4050 ?
|
|
'<DomainParameters>' . str_replace('URI', 'URN', $namedCurve) . '</DomainParameters>' :
|
|
$namedCurve;
|
|
}
|
|
|
|
if (self::$rfc4050) {
|
|
$xml = '<' . $pre . 'ExplicitParams>' . "\r\n" .
|
|
'<' . $pre . 'FieldParams>' . "\r\n";
|
|
$temp = $result['specifiedCurve'];
|
|
switch ($temp['fieldID']['fieldType']) {
|
|
case 'prime-field':
|
|
$xml.= '<' . $pre . 'PrimeFieldParamsType>' . "\r\n" .
|
|
'<' . $pre . 'P>' . $temp['fieldID']['parameters'] . '</' . $pre . 'P>' . "\r\n" .
|
|
'</' . $pre . 'PrimeFieldParamsType>' . "\r\n";
|
|
$a = $curve->getA();
|
|
$b = $curve->getB();
|
|
list($x, $y) = $curve->getBasePoint();
|
|
break;
|
|
default:
|
|
throw new UnsupportedCurveException('Field Type of ' . $temp['fieldID']['fieldType'] . ' is not supported');
|
|
}
|
|
$xml.= '</' . $pre . 'FieldParams>' . "\r\n" .
|
|
'<' . $pre . 'CurveParamsType>' . "\r\n" .
|
|
'<' . $pre . 'A>' . $a . '</' . $pre . 'A>' . "\r\n" .
|
|
'<' . $pre . 'B>' . $b . '</' . $pre . 'B>' . "\r\n" .
|
|
'</' . $pre . 'CurveParamsType>' . "\r\n" .
|
|
'<' . $pre . 'BasePointParams>' . "\r\n" .
|
|
'<' . $pre . 'BasePoint>' . "\r\n" .
|
|
'<' . $pre . 'ECPointType>' . "\r\n" .
|
|
'<' . $pre . 'X>' . $x . '</' . $pre . 'X>' . "\r\n" .
|
|
'<' . $pre . 'Y>' . $y . '</' . $pre . 'Y>' . "\r\n" .
|
|
'</' . $pre . 'ECPointType>' . "\r\n" .
|
|
'</' . $pre . 'BasePoint>' . "\r\n" .
|
|
'<' . $pre . 'Order>' . $curve->getOrder() . '</' . $pre . 'Order>' . "\r\n" .
|
|
'</' . $pre . 'BasePointParams>' . "\r\n" .
|
|
'</' . $pre . 'ExplicitParams>' . "\r\n";
|
|
|
|
return $xml;
|
|
}
|
|
|
|
if (isset($result['specifiedCurve'])) {
|
|
$xml = '<' . $pre . 'ECParameters>' . "\r\n" .
|
|
'<' . $pre . 'FieldID>' . "\r\n";
|
|
$temp = $result['specifiedCurve'];
|
|
switch ($temp['fieldID']['fieldType']) {
|
|
case 'prime-field':
|
|
$xml.= '<' . $pre . 'Prime>' . "\r\n" .
|
|
'<' . $pre . 'P>' . Base64::encode($temp['fieldID']['parameters']->toBytes()) . '</' . $pre . 'P>' . "\r\n" .
|
|
'</' . $pre . 'Prime>' . "\r\n" ;
|
|
break;
|
|
default:
|
|
throw new UnsupportedCurveException('Field Type of ' . $temp['fieldID']['fieldType'] . ' is not supported');
|
|
}
|
|
$xml.= '</' . $pre . 'FieldID>' . "\r\n" .
|
|
'<' . $pre . 'Curve>' . "\r\n" .
|
|
'<' . $pre . 'A>' . Base64::encode($temp['curve']['a']) . '</' . $pre . 'A>' . "\r\n" .
|
|
'<' . $pre . 'B>' . Base64::encode($temp['curve']['b']) . '</' . $pre . 'B>' . "\r\n" .
|
|
'</' . $pre . 'Curve>' . "\r\n" .
|
|
'<' . $pre . 'Base>' . Base64::encode($temp['base']) . '</' . $pre . 'Base>' . "\r\n" .
|
|
'<' . $pre . 'Order>' . Base64::encode($temp['order']) . '</' . $pre . 'Order>' . "\r\n" .
|
|
'</' . $pre . 'ECParameters>';
|
|
return $xml;
|
|
}
|
|
}
|
|
}
|