1
2
3
4
5
6
7
8
9
10 package org.eclipse.jgit.internal.transport.sshd.agent;
11
12 import java.io.IOException;
13 import java.security.KeyPair;
14 import java.security.PrivateKey;
15 import java.security.PublicKey;
16 import java.text.MessageFormat;
17 import java.util.AbstractMap;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.Collections;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.concurrent.atomic.AtomicBoolean;
24
25 import org.apache.sshd.agent.SshAgent;
26 import org.apache.sshd.agent.SshAgentConstants;
27 import org.apache.sshd.agent.SshAgentKeyConstraint;
28 import org.apache.sshd.common.SshException;
29 import org.apache.sshd.common.config.keys.KeyUtils;
30 import org.apache.sshd.common.keyprovider.KeyPairProvider;
31 import org.apache.sshd.common.session.SessionContext;
32 import org.apache.sshd.common.util.buffer.Buffer;
33 import org.apache.sshd.common.util.buffer.BufferException;
34 import org.apache.sshd.common.util.buffer.BufferUtils;
35 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
36 import org.apache.sshd.common.util.buffer.keys.BufferPublicKeyParser;
37 import org.apache.sshd.common.util.io.der.DERParser;
38 import org.eclipse.jgit.internal.transport.sshd.SshdText;
39 import org.eclipse.jgit.transport.sshd.agent.Connector;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43
44
45
46
47
48
49
50
51
52 public class SshAgentClient implements SshAgent {
53
54 private static final Logger LOG = LoggerFactory
55 .getLogger(SshAgentClient.class);
56
57
58 private static final int MAX_NUMBER_OF_KEYS = 2048;
59
60 private final AtomicBoolean closed = new AtomicBoolean();
61
62 private final Connector connector;
63
64
65
66
67
68
69
70
71
72 public SshAgentClient(Connector connector) {
73 this.connector = connector;
74 }
75
76 private boolean open(boolean debugging) throws IOException {
77 if (closed.get()) {
78 if (debugging) {
79 LOG.debug("SSH agent connection already closed");
80 }
81 return false;
82 }
83 boolean connected;
84 try {
85 connected = connector != null && connector.connect();
86 if (!connected && debugging) {
87 LOG.debug("No SSH agent");
88 }
89 } catch (IOException e) {
90
91 if (debugging) {
92 LOG.debug("No SSH agent", e);
93 }
94 throw e;
95 }
96 return connected;
97 }
98
99 @Override
100 public void close() throws IOException {
101 if (!closed.getAndSet(true) && connector != null) {
102 connector.close();
103 }
104 }
105
106 @Override
107 public Iterable<? extends Map.Entry<PublicKey, String>> getIdentities()
108 throws IOException {
109 boolean debugging = LOG.isDebugEnabled();
110 if (!open(debugging)) {
111 return Collections.emptyList();
112 }
113 if (debugging) {
114 LOG.debug("Requesting identities from SSH agent");
115 }
116 try {
117 Buffer reply = rpc(
118 SshAgentConstants.SSH2_AGENTC_REQUEST_IDENTITIES);
119 byte cmd = reply.getByte();
120 if (cmd != SshAgentConstants.SSH2_AGENT_IDENTITIES_ANSWER) {
121 throw new SshException(MessageFormat.format(
122 SshdText.get().sshAgentReplyUnexpected,
123 SshAgentConstants.getCommandMessageName(cmd)));
124 }
125 int numberOfKeys = reply.getInt();
126 if (numberOfKeys < 0 || numberOfKeys > MAX_NUMBER_OF_KEYS) {
127 throw new SshException(MessageFormat.format(
128 SshdText.get().sshAgentWrongNumberOfKeys,
129 Integer.toString(numberOfKeys)));
130 }
131 if (numberOfKeys == 0) {
132 if (debugging) {
133 LOG.debug("SSH agent has no keys");
134 }
135 return Collections.emptyList();
136 }
137 if (debugging) {
138 LOG.debug("Got {} key(s) from the SSH agent",
139 Integer.toString(numberOfKeys));
140 }
141 boolean tracing = LOG.isTraceEnabled();
142 List<Map.Entry<PublicKey, String>> keys = new ArrayList<>(
143 numberOfKeys);
144 for (int i = 0; i < numberOfKeys; i++) {
145 PublicKey key = readKey(reply);
146 String comment = reply.getString();
147 if (key != null) {
148 if (tracing) {
149 LOG.trace("Got SSH agent {} key: {} {}",
150 KeyUtils.getKeyType(key),
151 KeyUtils.getFingerPrint(key), comment);
152 }
153 keys.add(new AbstractMap.SimpleImmutableEntry<>(key,
154 comment));
155 }
156 }
157 return keys;
158 } catch (BufferException e) {
159 throw new SshException(SshdText.get().sshAgentShortReadBuffer, e);
160 }
161 }
162
163 @Override
164 public Map.Entry<String, byte[]> sign(SessionContext session, PublicKey key,
165 String algorithm, byte[] data) throws IOException {
166 boolean debugging = LOG.isDebugEnabled();
167 String keyType = KeyUtils.getKeyType(key);
168 String signatureAlgorithm;
169 if (algorithm != null) {
170 if (!KeyUtils.getCanonicalKeyType(algorithm).equals(keyType)) {
171 throw new IllegalArgumentException(MessageFormat.format(
172 SshdText.get().invalidSignatureAlgorithm, algorithm,
173 keyType));
174 }
175 signatureAlgorithm = algorithm;
176 } else {
177 signatureAlgorithm = keyType;
178 }
179 if (!open(debugging)) {
180 return null;
181 }
182 int flags = 0;
183 switch (signatureAlgorithm) {
184 case KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS:
185 case KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS:
186 flags = 4;
187 break;
188 case KeyUtils.RSA_SHA256_KEY_TYPE_ALIAS:
189 case KeyUtils.RSA_SHA256_CERT_TYPE_ALIAS:
190 flags = 2;
191 break;
192 default:
193 break;
194 }
195 ByteArrayBuffer msg = new ByteArrayBuffer();
196 msg.putInt(0);
197 msg.putByte(SshAgentConstants.SSH2_AGENTC_SIGN_REQUEST);
198 msg.putPublicKey(key);
199 msg.putBytes(data);
200 msg.putInt(flags);
201 if (debugging) {
202 LOG.debug(
203 "sign({}): signing request to SSH agent for {} key, {} signature; flags={}",
204 session, keyType, signatureAlgorithm,
205 Integer.toString(flags));
206 }
207 Buffer reply = rpc(SshAgentConstants.SSH2_AGENTC_SIGN_REQUEST,
208 msg.getCompactData());
209 byte cmd = reply.getByte();
210 if (cmd != SshAgentConstants.SSH2_AGENT_SIGN_RESPONSE) {
211 throw new SshException(
212 MessageFormat.format(SshdText.get().sshAgentReplyUnexpected,
213 SshAgentConstants.getCommandMessageName(cmd)));
214 }
215 try {
216 Buffer signatureReply = new ByteArrayBuffer(reply.getBytes());
217 String actualAlgorithm = signatureReply.getString();
218 byte[] signature = signatureReply.getBytes();
219 if (LOG.isTraceEnabled()) {
220 LOG.trace(
221 "sign({}): signature reply from SSH agent for {} key: {} signature={}",
222 session, keyType, actualAlgorithm,
223 BufferUtils.toHex(':', signature));
224
225 } else if (LOG.isDebugEnabled()) {
226 LOG.debug(
227 "sign({}): signature reply from SSH agent for {} key, {} signature",
228 session, keyType, actualAlgorithm);
229 }
230 return new AbstractMap.SimpleImmutableEntry<>(actualAlgorithm,
231 signature);
232 } catch (BufferException e) {
233 throw new SshException(SshdText.get().sshAgentShortReadBuffer, e);
234 }
235 }
236
237 @Override
238 public void addIdentity(KeyPair key, String comment,
239 SshAgentKeyConstraint... constraints) throws IOException {
240 boolean debugging = LOG.isDebugEnabled();
241 if (!open(debugging)) {
242 return;
243 }
244
245
246
247
248
249
250
251 byte cmd = (constraints != null && constraints.length > 0)
252 ? SshAgentConstants.SSH2_AGENTC_ADD_ID_CONSTRAINED
253 : SshAgentConstants.SSH2_AGENTC_ADD_IDENTITY;
254 byte[] message = null;
255 ByteArrayBuffer msg = new ByteArrayBuffer();
256 try {
257 msg.putInt(0);
258 msg.putByte(cmd);
259 String keyType = KeyUtils.getKeyType(key);
260 if (KeyPairProvider.SSH_ED25519.equals(keyType)) {
261
262
263 putEd25519Key(msg, key);
264 } else {
265 msg.putKeyPair(key);
266 }
267 msg.putString(comment == null ? "" : comment);
268 if (constraints != null) {
269 for (SshAgentKeyConstraint constraint : constraints) {
270 constraint.put(msg);
271 }
272 }
273 if (debugging) {
274 LOG.debug(
275 "addIdentity: adding {} key {} to SSH agent; comment {}",
276 keyType, KeyUtils.getFingerPrint(key.getPublic()),
277 comment);
278 }
279 message = msg.getCompactData();
280 } finally {
281
282
283 msg.clear();
284 }
285 Buffer reply;
286 try {
287 reply = rpc(cmd, message);
288 } finally {
289 Arrays.fill(message, (byte) 0);
290 }
291 int replyLength = reply.available();
292 if (replyLength != 1) {
293 throw new SshException(MessageFormat.format(
294 SshdText.get().sshAgentReplyUnexpected,
295 MessageFormat.format(
296 SshdText.get().sshAgentPayloadLengthError,
297 Integer.valueOf(1), Integer.valueOf(replyLength))));
298
299 }
300 cmd = reply.getByte();
301 if (cmd != SshAgentConstants.SSH_AGENT_SUCCESS) {
302 throw new SshException(
303 MessageFormat.format(SshdText.get().sshAgentReplyUnexpected,
304 SshAgentConstants.getCommandMessageName(cmd)));
305 }
306 }
307
308
309
310
311
312
313
314
315
316
317
318
319
320 private static void putEd25519Key(Buffer msg, KeyPair key)
321 throws IOException {
322 Buffer tmp = new ByteArrayBuffer(36);
323 tmp.putRawPublicKeyBytes(key.getPublic());
324 byte[] publicBytes = tmp.getBytes();
325 msg.putString(KeyPairProvider.SSH_ED25519);
326 msg.putBytes(publicBytes);
327
328
329 PrivateKey pk = key.getPrivate();
330 String format = pk.getFormat();
331 if (!"PKCS#8".equalsIgnoreCase(format)) {
332 throw new IOException(MessageFormat
333 .format(SshdText.get().sshAgentEdDSAFormatError, format));
334 }
335 byte[] privateBytes = null;
336 byte[] encoded = pk.getEncoded();
337 try {
338 privateBytes = asn1Parse(encoded, 32);
339 byte[] combined = Arrays.copyOf(privateBytes, 64);
340 Arrays.fill(privateBytes, (byte) 0);
341 privateBytes = combined;
342 System.arraycopy(publicBytes, 0, privateBytes, 32, 32);
343 msg.putBytes(privateBytes);
344 } finally {
345 if (privateBytes != null) {
346 Arrays.fill(privateBytes, (byte) 0);
347 }
348 Arrays.fill(encoded, (byte) 0);
349 }
350 }
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393 private static byte[] asn1Parse(byte[] encoded, int n) throws IOException {
394 byte[] privateKey = null;
395 try (DERParser byteParser = new DERParser(encoded);
396 DERParser oneAsymmetricKey = byteParser.readObject()
397 .createParser()) {
398 oneAsymmetricKey.readObject();
399 oneAsymmetricKey.readObject();
400 privateKey = oneAsymmetricKey.readObject().getValue();
401
402 return Arrays.copyOfRange(privateKey,
403 privateKey.length - n, privateKey.length);
404 } finally {
405 if (privateKey != null) {
406 Arrays.fill(privateKey, (byte) 0);
407 }
408 }
409 }
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427 private static PublicKey readKey(Buffer buffer) throws BufferException {
428 int endOfBuffer = buffer.wpos();
429 int keyLength = buffer.getInt();
430 if (keyLength <= 0 || keyLength > buffer.available()) {
431 throw new BufferException(
432 MessageFormat.format(SshdText.get().sshAgentWrongKeyLength,
433 Integer.toString(keyLength),
434 Integer.toString(buffer.rpos()),
435 Integer.toString(endOfBuffer)));
436 }
437 int afterKey = buffer.rpos() + keyLength;
438
439 buffer.wpos(afterKey);
440 try {
441 return buffer.getRawPublicKey(BufferPublicKeyParser.DEFAULT);
442 } catch (Exception e) {
443 LOG.warn(SshdText.get().sshAgentUnknownKey, e);
444 return null;
445 } finally {
446
447 buffer.wpos(endOfBuffer);
448
449 buffer.rpos(afterKey);
450 }
451 }
452
453 private Buffer rpc(byte command, byte[] message) throws IOException {
454 return new ByteArrayBuffer(connector.rpc(command, message));
455 }
456
457 private Buffer rpc(byte command) throws IOException {
458 return new ByteArrayBuffer(connector.rpc(command));
459 }
460
461 @Override
462 public boolean isOpen() {
463 return !closed.get();
464 }
465
466 @Override
467 public void removeIdentity(PublicKey key) throws IOException {
468 throw new UnsupportedOperationException();
469 }
470
471 @Override
472 public void removeAllIdentities() throws IOException {
473 throw new UnsupportedOperationException();
474 }
475 }