Identity.php
309 lines
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * Pure-PHP ssh-agent client. |
| 5 | * |
| 6 | * {@internal See http://api.libssh.org/rfc/PROTOCOL.agent} |
| 7 | * |
| 8 | * PHP version 5 |
| 9 | * |
| 10 | * @author Jim Wigginton <terrafrost@php.net> |
| 11 | * @copyright 2009 Jim Wigginton |
| 12 | * @license http://www.opensource.org/licenses/mit-license.html MIT License |
| 13 | * @link http://phpseclib.sourceforge.net |
| 14 | */ |
| 15 | |
| 16 | declare(strict_types=1); |
| 17 | |
| 18 | namespace phpseclib3\System\SSH\Agent; |
| 19 | |
| 20 | use phpseclib3\Common\Functions\Strings; |
| 21 | use phpseclib3\Crypt\Common\PrivateKey; |
| 22 | use phpseclib3\Crypt\Common\PublicKey; |
| 23 | use phpseclib3\Crypt\DSA; |
| 24 | use phpseclib3\Crypt\EC; |
| 25 | use phpseclib3\Crypt\RSA; |
| 26 | use phpseclib3\Exception\RuntimeException; |
| 27 | use phpseclib3\Exception\UnsupportedAlgorithmException; |
| 28 | use phpseclib3\System\SSH\Agent; |
| 29 | use phpseclib3\System\SSH\Common\Traits\ReadBytes; |
| 30 | |
| 31 | /** |
| 32 | * Pure-PHP ssh-agent client identity object |
| 33 | * |
| 34 | * Instantiation should only be performed by \phpseclib3\System\SSH\Agent class. |
| 35 | * This could be thought of as implementing an interface that phpseclib3\Crypt\RSA |
| 36 | * implements. ie. maybe a Net_SSH_Auth_PublicKey interface or something. |
| 37 | * The methods in this interface would be getPublicKey and sign since those are the |
| 38 | * methods phpseclib looks for to perform public key authentication. |
| 39 | * |
| 40 | * @author Jim Wigginton <terrafrost@php.net> |
| 41 | * @internal |
| 42 | */ |
| 43 | class Identity implements PrivateKey |
| 44 | { |
| 45 | use ReadBytes; |
| 46 | |
| 47 | // Signature Flags |
| 48 | // See https://tools.ietf.org/html/draft-miller-ssh-agent-00#section-5.3 |
| 49 | public const SSH_AGENT_RSA2_256 = 2; |
| 50 | public const SSH_AGENT_RSA2_512 = 4; |
| 51 | |
| 52 | /** |
| 53 | * Key Object |
| 54 | * |
| 55 | * @var PublicKey |
| 56 | * @see self::getPublicKey() |
| 57 | */ |
| 58 | private $key; |
| 59 | |
| 60 | /** |
| 61 | * Key Blob |
| 62 | * |
| 63 | * @var string |
| 64 | * @see self::sign() |
| 65 | */ |
| 66 | private $key_blob; |
| 67 | |
| 68 | /** |
| 69 | * Socket Resource |
| 70 | * |
| 71 | * @var resource |
| 72 | * @see self::sign() |
| 73 | */ |
| 74 | private $fsock; |
| 75 | |
| 76 | /** |
| 77 | * Signature flags |
| 78 | * |
| 79 | * @var int |
| 80 | * @see self::sign() |
| 81 | * @see self::setHash() |
| 82 | */ |
| 83 | private $flags = 0; |
| 84 | |
| 85 | /** |
| 86 | * Curve Aliases |
| 87 | * |
| 88 | * @var array |
| 89 | */ |
| 90 | private static $curveAliases = [ |
| 91 | 'secp256r1' => 'nistp256', |
| 92 | 'secp384r1' => 'nistp384', |
| 93 | 'secp521r1' => 'nistp521', |
| 94 | 'Ed25519' => 'Ed25519', |
| 95 | ]; |
| 96 | |
| 97 | /** |
| 98 | * Default Constructor. |
| 99 | * |
| 100 | * @param resource $fsock |
| 101 | */ |
| 102 | public function __construct($fsock) |
| 103 | { |
| 104 | $this->fsock = $fsock; |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Set Public Key |
| 109 | * |
| 110 | * Called by \phpseclib3\System\SSH\Agent::requestIdentities() |
| 111 | */ |
| 112 | public function withPublicKey(PublicKey $key): Identity |
| 113 | { |
| 114 | if ($key instanceof EC) { |
| 115 | if (is_array($key->getCurve()) || !isset(self::$curveAliases[$key->getCurve()])) { |
| 116 | throw new UnsupportedAlgorithmException('The only supported curves are nistp256, nistp384, nistp512 and Ed25519'); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | $new = clone $this; |
| 121 | $new->key = $key; |
| 122 | return $new; |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Set Public Key |
| 127 | * |
| 128 | * Called by \phpseclib3\System\SSH\Agent::requestIdentities(). The key blob could be extracted from $this->key |
| 129 | * but this saves a small amount of computation. |
| 130 | */ |
| 131 | public function withPublicKeyBlob(string $key_blob): Identity |
| 132 | { |
| 133 | $new = clone $this; |
| 134 | $new->key_blob = $key_blob; |
| 135 | return $new; |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Get Public Key |
| 140 | * |
| 141 | * Wrapper for $this->key->getPublicKey() |
| 142 | * |
| 143 | * @param string $type optional |
| 144 | */ |
| 145 | public function getPublicKey(string $type = 'PKCS8'): PublicKey |
| 146 | { |
| 147 | return $this->key; |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Sets the hash |
| 152 | */ |
| 153 | public function withHash(string $hash): Identity |
| 154 | { |
| 155 | $new = clone $this; |
| 156 | |
| 157 | $hash = strtolower($hash); |
| 158 | |
| 159 | if ($this->key instanceof RSA) { |
| 160 | $new->flags = 0; |
| 161 | switch ($hash) { |
| 162 | case 'sha1': |
| 163 | break; |
| 164 | case 'sha256': |
| 165 | $new->flags = self::SSH_AGENT_RSA2_256; |
| 166 | break; |
| 167 | case 'sha512': |
| 168 | $new->flags = self::SSH_AGENT_RSA2_512; |
| 169 | break; |
| 170 | default: |
| 171 | throw new UnsupportedAlgorithmException('The only supported hashes for RSA are sha1, sha256 and sha512'); |
| 172 | } |
| 173 | } |
| 174 | if ($this->key instanceof EC) { |
| 175 | switch ($this->key->getCurve()) { |
| 176 | case 'secp256r1': |
| 177 | $expectedHash = 'sha256'; |
| 178 | break; |
| 179 | case 'secp384r1': |
| 180 | $expectedHash = 'sha384'; |
| 181 | break; |
| 182 | //case 'secp521r1': |
| 183 | //case 'Ed25519': |
| 184 | default: |
| 185 | $expectedHash = 'sha512'; |
| 186 | } |
| 187 | if ($hash != $expectedHash) { |
| 188 | throw new UnsupportedAlgorithmException('The only supported hash for ' . self::$curveAliases[$this->key->getCurve()] . ' is ' . $expectedHash); |
| 189 | } |
| 190 | } |
| 191 | if ($this->key instanceof DSA) { |
| 192 | if ($hash != 'sha1') { |
| 193 | throw new UnsupportedAlgorithmException('The only supported hash for DSA is sha1'); |
| 194 | } |
| 195 | } |
| 196 | return $new; |
| 197 | } |
| 198 | |
| 199 | /** |
| 200 | * Sets the padding |
| 201 | * |
| 202 | * Only PKCS1 padding is supported |
| 203 | */ |
| 204 | public function withPadding(int $padding): Identity |
| 205 | { |
| 206 | if (!$this->key instanceof RSA) { |
| 207 | throw new UnsupportedAlgorithmException('Only RSA keys support padding'); |
| 208 | } |
| 209 | if ($padding != RSA::SIGNATURE_PKCS1 && $padding != RSA::SIGNATURE_RELAXED_PKCS1) { |
| 210 | throw new UnsupportedAlgorithmException('ssh-agent can only create PKCS1 signatures'); |
| 211 | } |
| 212 | return $this; |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Determines the signature padding mode |
| 217 | * |
| 218 | * Valid values are: ASN1, SSH2, Raw |
| 219 | */ |
| 220 | public function withSignatureFormat(string $format): Identity |
| 221 | { |
| 222 | if ($this->key instanceof RSA) { |
| 223 | throw new UnsupportedAlgorithmException('Only DSA and EC keys support signature format setting'); |
| 224 | } |
| 225 | if ($format != 'SSH2') { |
| 226 | throw new UnsupportedAlgorithmException('Only SSH2-formatted signatures are currently supported'); |
| 227 | } |
| 228 | |
| 229 | return $this; |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Returns the curve |
| 234 | * |
| 235 | * Returns a string if it's a named curve, an array if not |
| 236 | * |
| 237 | * @return string|array |
| 238 | */ |
| 239 | public function getCurve() |
| 240 | { |
| 241 | if (!$this->key instanceof EC) { |
| 242 | throw new UnsupportedAlgorithmException('Only EC keys have curves'); |
| 243 | } |
| 244 | |
| 245 | return $this->key->getCurve(); |
| 246 | } |
| 247 | |
| 248 | /** |
| 249 | * Create a signature |
| 250 | * |
| 251 | * See "2.6.2 Protocol 2 private key signature request" |
| 252 | * |
| 253 | * @param string $message |
| 254 | * @throws RuntimeException on connection errors |
| 255 | * @throws UnsupportedAlgorithmException if the algorithm is unsupported |
| 256 | */ |
| 257 | public function sign($message): string |
| 258 | { |
| 259 | // the last parameter (currently 0) is for flags and ssh-agent only defines one flag (for ssh-dss): SSH_AGENT_OLD_SIGNATURE |
| 260 | $packet = Strings::packSSH2( |
| 261 | 'CssN', |
| 262 | Agent::SSH_AGENTC_SIGN_REQUEST, |
| 263 | $this->key_blob, |
| 264 | $message, |
| 265 | $this->flags |
| 266 | ); |
| 267 | $packet = Strings::packSSH2('s', $packet); |
| 268 | if (strlen($packet) != fwrite($this->fsock, $packet)) { |
| 269 | throw new RuntimeException('Connection closed during signing'); |
| 270 | } |
| 271 | |
| 272 | $length = current(unpack('N', $this->readBytes(4))); |
| 273 | $packet = $this->readBytes($length); |
| 274 | |
| 275 | [$type, $signature_blob] = Strings::unpackSSH2('Cs', $packet); |
| 276 | if ($type != Agent::SSH_AGENT_SIGN_RESPONSE) { |
| 277 | throw new RuntimeException('Unable to retrieve signature'); |
| 278 | } |
| 279 | |
| 280 | if (!$this->key instanceof RSA) { |
| 281 | return $signature_blob; |
| 282 | } |
| 283 | |
| 284 | [$type, $signature_blob] = Strings::unpackSSH2('ss', $signature_blob); |
| 285 | |
| 286 | return $signature_blob; |
| 287 | } |
| 288 | |
| 289 | /** |
| 290 | * Returns the private key |
| 291 | * |
| 292 | * @param array $options optional |
| 293 | */ |
| 294 | public function toString(string $type, array $options = []): string |
| 295 | { |
| 296 | throw new RuntimeException('ssh-agent does not provide a mechanism to get the private key'); |
| 297 | } |
| 298 | |
| 299 | /** |
| 300 | * Sets the password |
| 301 | * |
| 302 | * @return never |
| 303 | */ |
| 304 | public function withPassword(?string $password = null): PrivateKey |
| 305 | { |
| 306 | throw new RuntimeException('ssh-agent does not provide a mechanism to get the private key'); |
| 307 | } |
| 308 | } |
| 309 |