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