1
2
3
4
5
6
7
8
9
10 package org.eclipse.jgit.internal.transport.sshd;
11
12 import static java.text.MessageFormat.format;
13 import static org.eclipse.jgit.transport.SshConstants.PUBKEY_ACCEPTED_ALGORITHMS;
14
15 import java.io.IOException;
16 import java.net.URISyntaxException;
17 import java.nio.file.Files;
18 import java.nio.file.InvalidPathException;
19 import java.nio.file.LinkOption;
20 import java.nio.file.Path;
21 import java.nio.file.Paths;
22 import java.security.GeneralSecurityException;
23 import java.security.KeyPair;
24 import java.security.PublicKey;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.Collection;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.NoSuchElementException;
32 import java.util.Objects;
33 import java.util.stream.Collectors;
34
35 import org.apache.sshd.agent.SshAgent;
36 import org.apache.sshd.agent.SshAgentFactory;
37 import org.apache.sshd.agent.SshAgentKeyConstraint;
38 import org.apache.sshd.client.auth.pubkey.KeyAgentIdentity;
39 import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity;
40 import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
41 import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyIterator;
42 import org.apache.sshd.client.config.hosts.HostConfigEntry;
43 import org.apache.sshd.client.session.ClientSession;
44 import org.apache.sshd.common.FactoryManager;
45 import org.apache.sshd.common.NamedFactory;
46 import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
47 import org.apache.sshd.common.config.keys.KeyUtils;
48 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
49 import org.apache.sshd.common.config.keys.u2f.SecurityKeyPublicKey;
50 import org.apache.sshd.common.signature.Signature;
51 import org.apache.sshd.common.signature.SignatureFactoriesManager;
52 import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
53 import org.eclipse.jgit.transport.CredentialItem;
54 import org.eclipse.jgit.transport.CredentialsProvider;
55 import org.eclipse.jgit.transport.SshConstants;
56 import org.eclipse.jgit.transport.URIish;
57 import org.eclipse.jgit.util.StringUtils;
58
59
60
61
62
63 public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
64
65 private SshAgent agent;
66
67 private HostConfigEntry hostConfig;
68
69 private boolean addKeysToAgent;
70
71 private boolean askBeforeAdding;
72
73 private String skProvider;
74
75 private SshAgentKeyConstraint[] constraints;
76
77 JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
78 super(factories);
79 }
80
81 @Override
82 public void init(ClientSession rawSession, String service)
83 throws Exception {
84 if (!(rawSession instanceof JGitClientSession)) {
85 throw new IllegalStateException("Wrong session type: "
86 + rawSession.getClass().getCanonicalName());
87 }
88 JGitClientSession session = (JGitClientSession) rawSession;
89 hostConfig = session.getHostConfigEntry();
90
91 String pubkeyAlgos = hostConfig.getProperty(PUBKEY_ACCEPTED_ALGORITHMS);
92 if (!StringUtils.isEmptyOrNull(pubkeyAlgos)) {
93 List<String> signatures = session.getSignatureFactoriesNames();
94 signatures = session.modifyAlgorithmList(signatures,
95 session.getAllAvailableSignatureAlgorithms(), pubkeyAlgos,
96 PUBKEY_ACCEPTED_ALGORITHMS);
97 if (!signatures.isEmpty()) {
98 if (log.isDebugEnabled()) {
99 log.debug(PUBKEY_ACCEPTED_ALGORITHMS + ' ' + signatures);
100 }
101 setSignatureFactoriesNames(signatures);
102 } else {
103 log.warn(format(SshdText.get().configNoKnownAlgorithms,
104 PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos));
105 }
106 }
107
108
109 super.init(session, service);
110 }
111
112 @Override
113 protected Iterator<PublicKeyIdentity> createPublicKeyIterator(
114 ClientSession session, SignatureFactoriesManager manager)
115 throws Exception {
116 agent = getAgent(session);
117 if (agent != null) {
118 parseAddKeys(hostConfig);
119 if (addKeysToAgent) {
120 skProvider = hostConfig.getProperty(SshConstants.SECURITY_KEY_PROVIDER);
121 }
122 }
123 return new KeyIterator(session, manager);
124 }
125
126 @Override
127 protected PublicKeyIdentity resolveAttemptedPublicKeyIdentity(
128 ClientSession session, String service) throws Exception {
129 PublicKeyIdentity result = getNextKey(session, service);
130
131
132
133
134 currentAlgorithms.clear();
135 return result;
136 }
137
138 private PublicKeyIdentity getNextKey(ClientSession session, String service)
139 throws Exception {
140 PublicKeyIdentity id = super.resolveAttemptedPublicKeyIdentity(session,
141 service);
142 if (addKeysToAgent && id != null && !(id instanceof KeyAgentIdentity)) {
143 KeyPair key = id.getKeyIdentity();
144 if (key != null && key.getPublic() != null
145 && key.getPrivate() != null) {
146
147
148
149
150
151
152 PublicKey pk = key.getPublic();
153 String fingerprint = KeyUtils.getFingerPrint(pk);
154 String keyType = KeyUtils.getKeyType(key);
155 try {
156
157 if (agentHasKey(pk)) {
158 return id;
159 }
160 if (askBeforeAdding
161 && (session instanceof JGitClientSession)) {
162 CredentialsProvider provider = ((JGitClientSession) session)
163 .getCredentialsProvider();
164 CredentialItem.YesNoType question = new CredentialItem.YesNoType(
165 format(SshdText
166 .get().pubkeyAuthAddKeyToAgentQuestion,
167 keyType, fingerprint));
168 boolean result = provider != null
169 && provider.supports(question)
170 && provider.get(getUri(), question);
171 if (!result || !question.getValue()) {
172
173 return id;
174 }
175 }
176 SshAgentKeyConstraint[] rules = constraints;
177 if (pk instanceof SecurityKeyPublicKey && !StringUtils.isEmptyOrNull(skProvider)) {
178 rules = Arrays.copyOf(rules, rules.length + 1);
179 rules[rules.length - 1] =
180 new SshAgentKeyConstraint.FidoProviderExtension(skProvider);
181 }
182
183
184
185
186 agent.addIdentity(key, null, rules);
187 } catch (IOException e) {
188
189
190 log.error(
191 format(SshdText.get().pubkeyAuthAddKeyToAgentError,
192 keyType, fingerprint),
193 e);
194
195
196
197
198
199
200
201 }
202 }
203 }
204 return id;
205 }
206
207 private boolean agentHasKey(PublicKey pk) throws IOException {
208 Iterable<? extends Map.Entry<PublicKey, String>> ids = agent
209 .getIdentities();
210 if (ids == null) {
211 return false;
212 }
213 Iterator<? extends Map.Entry<PublicKey, String>> iter = ids.iterator();
214 while (iter.hasNext()) {
215 if (KeyUtils.compareKeys(iter.next().getKey(), pk)) {
216 return true;
217 }
218 }
219 return false;
220 }
221
222 private URIish getUri() {
223 String uri = SshConstants.SSH_SCHEME + "://"; //$NON-NLS-1$
224 String userName = hostConfig.getUsername();
225 if (!StringUtils.isEmptyOrNull(userName)) {
226 uri += userName + '@';
227 }
228 uri += hostConfig.getHost();
229 int port = hostConfig.getPort();
230 if (port > 0 && port != SshConstants.SSH_DEFAULT_PORT) {
231 uri += ":" + port;
232 }
233 try {
234 return new URIish(uri);
235 } catch (URISyntaxException e) {
236 log.error(e.getLocalizedMessage(), e);
237 }
238 return new URIish();
239 }
240
241 private SshAgent getAgent(ClientSession session) throws Exception {
242 FactoryManager manager = Objects.requireNonNull(
243 session.getFactoryManager(), "No session factory manager");
244 SshAgentFactory factory = manager.getAgentFactory();
245 if (factory == null) {
246 return null;
247 }
248 return factory.createClient(session, manager);
249 }
250
251 private void parseAddKeys(HostConfigEntry config) {
252 String value = config.getProperty(SshConstants.ADD_KEYS_TO_AGENT);
253 if (StringUtils.isEmptyOrNull(value)) {
254 addKeysToAgent = false;
255 return;
256 }
257 String[] values = value.split(",");
258 List<SshAgentKeyConstraint> rules = new ArrayList<>(2);
259 switch (values[0]) {
260 case "yes":
261 addKeysToAgent = true;
262 break;
263 case "no":
264 addKeysToAgent = false;
265 break;
266 case "ask":
267 addKeysToAgent = true;
268 askBeforeAdding = true;
269 break;
270 case "confirm":
271 addKeysToAgent = true;
272 rules.add(SshAgentKeyConstraint.CONFIRM);
273 if (values.length > 1) {
274 int seconds = OpenSshConfigFile.timeSpec(values[1]);
275 if (seconds > 0) {
276 rules.add(new SshAgentKeyConstraint.LifeTime(seconds));
277 }
278 }
279 break;
280 default:
281 int seconds = OpenSshConfigFile.timeSpec(values[0]);
282 if (seconds > 0) {
283 addKeysToAgent = true;
284 rules.add(new SshAgentKeyConstraint.LifeTime(seconds));
285 }
286 break;
287 }
288 constraints = rules.toArray(new SshAgentKeyConstraint[0]);
289 }
290
291 @Override
292 protected void releaseKeys() throws IOException {
293 addKeysToAgent = false;
294 askBeforeAdding = false;
295 skProvider = null;
296 constraints = null;
297 try {
298 if (agent != null) {
299 try {
300 agent.close();
301 } finally {
302 agent = null;
303 }
304 }
305 } finally {
306 super.releaseKeys();
307 }
308 }
309
310 private class KeyIterator extends UserAuthPublicKeyIterator {
311
312 private Iterable<? extends Map.Entry<PublicKey, String>> agentKeys;
313
314
315
316
317 private Collection<PublicKey> identityFiles;
318
319 public KeyIterator(ClientSession session,
320 SignatureFactoriesManager manager)
321 throws Exception {
322 super(session, manager);
323 }
324
325 private List<PublicKey> getExplicitKeys(
326 Collection<String> explicitFiles) {
327 if (explicitFiles == null) {
328 return null;
329 }
330 return explicitFiles.stream().map(s -> {
331 try {
332 Path p = Paths.get(s + ".pub");
333 if (Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) {
334 return AuthorizedKeyEntry.readAuthorizedKeys(p).get(0)
335 .resolvePublicKey(null,
336 PublicKeyEntryResolver.IGNORING);
337 }
338 } catch (InvalidPathException | IOException
339 | GeneralSecurityException e) {
340 log.warn(format(SshdText.get().cannotReadPublicKey, s), e);
341 }
342 return null;
343 }).filter(Objects::nonNull).collect(Collectors.toList());
344 }
345
346 @Override
347 protected Iterable<KeyAgentIdentity> initializeAgentIdentities(
348 ClientSession session) throws IOException {
349 if (agent == null) {
350 return null;
351 }
352 agentKeys = agent.getIdentities();
353 if (hostConfig != null && hostConfig.isIdentitiesOnly()) {
354 identityFiles = getExplicitKeys(hostConfig.getIdentities());
355 }
356 return () -> new Iterator<>() {
357
358 private final Iterator<? extends Map.Entry<PublicKey, String>> iter = agentKeys
359 .iterator();
360
361 private Map.Entry<PublicKey, String> next;
362
363 @Override
364 public boolean hasNext() {
365 while (next == null && iter.hasNext()) {
366 Map.Entry<PublicKey, String> val = iter.next();
367 PublicKey pk = val.getKey();
368
369
370
371 if (identityFiles == null || identityFiles.stream()
372 .anyMatch(k -> KeyUtils.compareKeys(k, pk))) {
373 next = val;
374 return true;
375 }
376 if (log.isTraceEnabled()) {
377 log.trace(
378 "Ignoring SSH agent {} key not in explicit IdentityFile in SSH config: {}",
379 KeyUtils.getKeyType(pk),
380 KeyUtils.getFingerPrint(pk));
381 }
382 }
383 return next != null;
384 }
385
386 @Override
387 public KeyAgentIdentity next() {
388 if (!hasNext()) {
389 throw new NoSuchElementException();
390 }
391 KeyAgentIdentity result = new KeyAgentIdentity(agent,
392 next.getKey(), next.getValue());
393 next = null;
394 return result;
395 }
396 };
397 }
398 }
399 }