1<?php 2 3/** 4 * EC Private Key 5 * 6 * @author Jim Wigginton <terrafrost@php.net> 7 * @copyright 2015 Jim Wigginton 8 * @license http://www.opensource.org/licenses/mit-license.html MIT License 9 * @link http://phpseclib.sourceforge.net 10 */ 11 12namespace phpseclib3\Crypt\EC; 13 14use phpseclib3\Common\Functions\Strings; 15use phpseclib3\Crypt\Common; 16use phpseclib3\Crypt\EC; 17use phpseclib3\Crypt\EC\BaseCurves\Montgomery as MontgomeryCurve; 18use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve; 19use phpseclib3\Crypt\EC\Curves\Curve25519; 20use phpseclib3\Crypt\EC\Curves\Ed25519; 21use phpseclib3\Crypt\EC\Formats\Keys\PKCS1; 22use phpseclib3\Crypt\EC\Formats\Signature\ASN1 as ASN1Signature; 23use phpseclib3\Crypt\Hash; 24use phpseclib3\Exception\BadConfigurationException; 25use phpseclib3\Exception\UnsupportedOperationException; 26use phpseclib3\Math\BigInteger; 27 28/** 29 * EC Private Key 30 * 31 * @author Jim Wigginton <terrafrost@php.net> 32 */ 33final class PrivateKey extends EC implements Common\PrivateKey 34{ 35 use Common\Traits\PasswordProtected; 36 37 /** 38 * Private Key dA 39 * 40 * sign() converts this to a BigInteger so one might wonder why this is a FiniteFieldInteger instead of 41 * a BigInteger. That's because a FiniteFieldInteger, when converted to a byte string, is null padded by 42 * a certain amount whereas a BigInteger isn't. 43 * 44 * @var object 45 */ 46 protected $dA; 47 48 /** 49 * @var string 50 */ 51 protected $secret; 52 53 /** 54 * Multiplies an encoded point by the private key 55 * 56 * Used by ECDH 57 * 58 * @param string $coordinates 59 * @return string 60 */ 61 public function multiply($coordinates) 62 { 63 if (self::$forcedEngine === 'OpenSSL') { 64 throw new BadConfigurationException('Engine OpenSSL is not supported for the multiplication operation'); 65 } 66 67 if (self::$forcedEngine === 'libsodium' && !$this->curve instanceof Curve25519) { 68 throw new BadConfigurationException('Engine libsodium is only supported for Curve25519'); 69 } 70 71 if ($this->curve instanceof Curve25519 && self::$forcedEngine !== 'PHP') { 72 if (self::$forcedEngine === 'libsodium' && !function_exists('sodium_crypto_scalarmult')) { 73 throw new BadConfigurationException('Engine libsodium is forced but unsupported for Curve25519'); 74 } 75 if (function_exists('sodium_crypto_scalarmult')) { 76 $dA = str_pad($this->dA->toBytes(), 32, "\0", STR_PAD_LEFT); 77 return sodium_crypto_scalarmult($dA, $coordinates); 78 } 79 } 80 81 if ($this->curve instanceof MontgomeryCurve) { 82 $point = [$this->curve->convertInteger(new BigInteger(strrev($coordinates), 256))]; 83 $point = $this->curve->multiplyPoint($point, $this->dA); 84 return strrev($point[0]->toBytes(true)); 85 } 86 87 if (!$this->curve instanceof TwistedEdwardsCurve) { 88 $coordinates = "\0$coordinates"; 89 } 90 91 $point = PKCS1::extractPoint($coordinates, $this->curve); 92 $point = $this->curve->multiplyPoint($point, $this->dA); 93 if ($this->curve instanceof TwistedEdwardsCurve) { 94 return $this->curve->encodePoint($point); 95 } 96 if (empty($point)) { 97 throw new \RuntimeException('The infinity point is invalid'); 98 } 99 return "\4" . $point[0]->toBytes(true) . $point[1]->toBytes(true); 100 } 101 102 /** 103 * Create a signature 104 * 105 * @see self::verify() 106 * @param string $message 107 * @return mixed 108 */ 109 public function sign($message) 110 { 111 if ($this->curve instanceof MontgomeryCurve) { 112 throw new UnsupportedOperationException('Montgomery Curves cannot be used to create signatures'); 113 } 114 115 $dA = $this->dA; 116 $order = $this->curve->getOrder(); 117 118 $shortFormat = $this->shortFormat; 119 $format = $this->sigFormat; 120 if ($format === false) { 121 return false; 122 } 123 124 if (self::$forcedEngine === 'libsodium' && !$this->curve instanceof Ed25519) { 125 throw new BadConfigurationException('Engine libsodium is only supported for Ed25519'); 126 } 127 128 // at this point either self::$forcedEngine is NOT libsodium or the curve is Ed25519 129 130 if ($this->curve instanceof Ed25519 && self::$forcedEngine !== 'PHP' && self::$forcedEngine !== 'OpenSSL') { 131 if (self::$forcedEngine === 'libsodium') { 132 if (!function_exists('sodium_crypto_sign_detached')) { 133 throw new BadConfigurationException('Engine libsodium is forced but unsupported for Ed25519 / Ed448'); 134 } 135 if (isset($this->context)) { 136 throw new BadConfigurationException('Engine libsodium is forced but unsupported for Ed25519ctx (context)'); 137 } 138 } 139 if (function_exists('sodium_crypto_sign_detached') && !isset($this->context)) { 140 $result = sodium_crypto_sign_detached($message, $this->withPassword()->toString('libsodium')); 141 return $shortFormat == 'SSH2' ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $result) : $result; 142 } 143 } 144 145 // at this point self::$forcedEngine CAN'T be libsodium so we won't check for it henceforth 146 147 if ($this->curve instanceof TwistedEdwardsCurve) { 148 if (self::$forcedEngine !== 'PHP') { 149 $keyTypeConstant = $this->curve instanceof Ed25519 ? 'OPENSSL_KEYTYPE_ED25519' : 'OPENSSL_KEYTYPE_ED448'; 150 if (self::$forcedEngine === 'OpenSSL') { 151 if (!defined($keyTypeConstant)) { 152 throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for Ed25519 / Ed448'); 153 } 154 // OpenSSL supports Ed25519/Ed448 but not Ed25519ctx (context), so skip if context is set 155 if (isset($this->context)) { 156 throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for Ed25519 / Ed448 curves with context\'s'); 157 } 158 } 159 if (defined($keyTypeConstant) && !isset($this->context)) { 160 $result = ''; 161 // algorithm 0 is used because EdDSA has a built-in hash 162 openssl_sign($message, $result, $this->withPassword()->toString('PKCS8'), 0); 163 if ($result) { 164 $signature = $shortFormat == 'SSH2' 165 ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $result) 166 : $result; 167 return $signature; 168 } 169 if (self::$forcedEngine === 'OpenSSL') { 170 throw new BadConfigurationException('Engine OpenSSL is forced but was unable to create signature because of ' . openssl_error_string()); 171 } 172 } 173 } 174 175 // contexts (Ed25519ctx) are supported but prehashing (Ed25519ph) is not. 176 // quoting https://tools.ietf.org/html/rfc8032#section-8.5 , 177 // "The Ed25519ph and Ed448ph variants ... SHOULD NOT be used" 178 $A = $this->curve->encodePoint($this->QA); 179 $curve = $this->curve; 180 $hash = new Hash($curve::HASH); 181 182 $secret = substr($hash->hash($this->secret), $curve::SIZE); 183 184 if ($curve instanceof Ed25519) { 185 $dom = !isset($this->context) ? '' : 186 'SigEd25519 no Ed25519 collisions' . "\0" . chr(strlen($this->context)) . $this->context; 187 } else { 188 $context = isset($this->context) ? $this->context : ''; 189 $dom = 'SigEd448' . "\0" . chr(strlen($context)) . $context; 190 } 191 // SHA-512(dom2(F, C) || prefix || PH(M)) 192 $r = $hash->hash($dom . $secret . $message); 193 $r = strrev($r); 194 $r = new BigInteger($r, 256); 195 list(, $r) = $r->divide($order); 196 $R = $curve->multiplyPoint($curve->getBasePoint(), $r); 197 $R = $curve->encodePoint($R); 198 $k = $hash->hash($dom . $R . $A . $message); 199 $k = strrev($k); 200 $k = new BigInteger($k, 256); 201 list(, $k) = $k->divide($order); 202 $S = $k->multiply($dA)->add($r); 203 list(, $S) = $S->divide($order); 204 $S = str_pad(strrev($S->toBytes()), $curve::SIZE, "\0"); 205 return $shortFormat == 'SSH2' ? Strings::packSSH2('ss', 'ssh-' . strtolower($this->getCurve()), $R . $S) : $R . $S; 206 } 207 208 if (self::$forcedEngine === 'OpenSSL' && !function_exists('openssl_get_md_methods')) { 209 throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for ECDSA'); 210 } 211 212 // at this point $forcedEngine is either PHP or null. either that OR openssl_get_md_methods() exists 213 214 if (self::$forcedEngine !== 'PHP') { 215 if (in_array($this->hash->getHash(), openssl_get_md_methods())) { 216 $signature = ''; 217 // altho PHP's OpenSSL bindings only supported EC key creation in PHP 7.1 they've long 218 // supported signing / verification 219 // we use specified curves to avoid issues with OpenSSL possibly not supporting a given named curve; 220 // doing this may mean some curve-specific optimizations can't be used but idk if OpenSSL even 221 // has curve-specific optimizations 222 $result = openssl_sign($message, $signature, $this->withPassword()->toString('PKCS8', ['namedCurve' => false]), $this->hash->getHash()); 223 224 if ($result) { 225 if ($shortFormat == 'ASN1') { 226 return $signature; 227 } 228 229 $loaded = ASN1Signature::load($signature); 230 $r = $loaded['r']; 231 $s = $loaded['s']; 232 233 return $this->formatSignature($r, $s); 234 } elseif (self::$forcedEngine === 'OpenSSL') { 235 throw new BadConfigurationException('Engine OpenSSL is forced but was unable to create signature because of ' . openssl_error_string()); 236 } 237 } elseif (self::$forcedEngine === 'OpenSSL') { 238 throw new BadConfigurationException('Engine OpenSSL is forced but unsupported for ECDSA / ' . $this->hash->getHash()); 239 } 240 } 241 242 $e = $this->hash->hash($message); 243 $e = new BigInteger($e, 256); 244 245 $Ln = $this->hash->getLength() - $order->getLength(); 246 $z = $Ln > 0 ? $e->bitwise_rightShift($Ln) : $e; 247 248 while (true) { 249 $k = BigInteger::randomRange(self::$one, $order->subtract(self::$one)); 250 list($x, $y) = $this->curve->multiplyPoint($this->curve->getBasePoint(), $k); 251 $x = $x->toBigInteger(); 252 list(, $r) = $x->divide($order); 253 if ($r->equals(self::$zero)) { 254 continue; 255 } 256 $kinv = $k->modInverse($order); 257 $temp = $z->add($dA->multiply($r)); 258 $temp = $kinv->multiply($temp); 259 list(, $s) = $temp->divide($order); 260 if (!$s->equals(self::$zero)) { 261 break; 262 } 263 } 264 265 // the following is an RFC6979 compliant implementation of deterministic ECDSA 266 // it's unused because it's mainly intended for use when a good CSPRNG isn't 267 // available. if phpseclib's CSPRNG isn't good then even key generation is 268 // suspect 269 /* 270 // if this were actually being used it'd probably be better if this lived in load() and createKey() 271 $this->q = $this->curve->getOrder(); 272 $dA = $this->dA->toBigInteger(); 273 $this->x = $dA; 274 275 $h1 = $this->hash->hash($message); 276 $k = $this->computek($h1); 277 list($x, $y) = $this->curve->multiplyPoint($this->curve->getBasePoint(), $k); 278 $x = $x->toBigInteger(); 279 list(, $r) = $x->divide($this->q); 280 $kinv = $k->modInverse($this->q); 281 $h1 = $this->bits2int($h1); 282 $temp = $h1->add($dA->multiply($r)); 283 $temp = $kinv->multiply($temp); 284 list(, $s) = $temp->divide($this->q); 285 */ 286 287 return $this->formatSignature($r, $s); 288 } 289 290 /** 291 * Returns the private key 292 * 293 * @param string $type 294 * @param array $options optional 295 * @return string 296 */ 297 public function toString($type, array $options = []) 298 { 299 $type = self::validatePlugin('Keys', $type, 'savePrivateKey'); 300 301 return $type::savePrivateKey($this->dA, $this->curve, $this->QA, $this->secret, $this->password, $options); 302 } 303 304 /** 305 * Returns the public key 306 * 307 * @see self::getPrivateKey() 308 * @return mixed 309 */ 310 public function getPublicKey() 311 { 312 $format = 'PKCS8'; 313 if ($this->curve instanceof MontgomeryCurve) { 314 $format = 'MontgomeryPublic'; 315 } 316 317 $type = self::validatePlugin('Keys', $format, 'savePublicKey'); 318 319 $key = $type::savePublicKey($this->curve, $this->QA); 320 $key = EC::loadFormat($format, $key); 321 if ($this->curve instanceof MontgomeryCurve) { 322 return $key; 323 } 324 $key = $key 325 ->withHash($this->hash->getHash()) 326 ->withSignatureFormat($this->shortFormat); 327 if ($this->curve instanceof TwistedEdwardsCurve) { 328 $key = $key->withContext($this->context); 329 } 330 return $key; 331 } 332 333 /** 334 * Returns a signature in the appropriate format 335 * 336 * @return string 337 */ 338 private function formatSignature(BigInteger $r, BigInteger $s) 339 { 340 $format = $this->sigFormat; 341 342 $temp = new \ReflectionMethod($format, 'save'); 343 $paramCount = $temp->getNumberOfRequiredParameters(); 344 345 // @codingStandardsIgnoreStart 346 switch ($paramCount) { 347 case 2: return $format::save($r, $s); 348 case 3: return $format::save($r, $s, $this->getCurve()); 349 case 4: return $format::save($r, $s, $this->getCurve(), $this->getLength()); 350 } 351 // @codingStandardsIgnoreEnd 352 353 // presumably the only way you could get to this is if you were using a custom plugin 354 throw new UnsupportedOperationException("$format::save() has $paramCount parameters - the only valid parameter counts are 2 or 3"); 355 } 356} 357