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