View Javadoc
1   /*
2    * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
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   * Custom {@link UserAuthPublicKey} implementation for handling SSH config
61   * PubkeyAcceptedAlgorithms and interaction with the SSH agent.
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: " //$NON-NLS-1$
86  					+ rawSession.getClass().getCanonicalName());
87  		}
88  		JGitClientSession session = (JGitClientSession) rawSession;
89  		hostConfig = session.getHostConfigEntry();
90  		// Set signature algorithms for public key authentication
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 		// If we don't set signature factories here, the default ones from the
108 		// session will be used.
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 		// This fixes SSHD-1231. Can be removed once we're using Apache MINA
131 		// sshd > 2.8.0.
132 		//
133 		// See https://issues.apache.org/jira/browse/SSHD-1231
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 				// We've just successfully loaded a key that wasn't in the
147 				// agent. Add it to the agent.
148 				//
149 				// Keys are added after loading, as in OpenSSH. The alternative
150 				// might be to add a key only after (partially) successful
151 				// authentication?
152 				PublicKey pk = key.getPublic();
153 				String fingerprint = KeyUtils.getFingerPrint(pk);
154 				String keyType = KeyUtils.getKeyType(key);
155 				try {
156 					// Check that the key is not in the agent already.
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 							// Don't add the key.
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 					// Unfortunately a comment associated with the key is lost
183 					// by Apache MINA sshd, and there is also no way to get the
184 					// original file name for keys loaded from a file. So add it
185 					// without comment.
186 					agent.addIdentity(key, null, rules);
187 				} catch (IOException e) {
188 					// Do not re-throw: we don't want authentication to fail if
189 					// we cannot add the key to the agent.
190 					log.error(
191 							format(SshdText.get().pubkeyAuthAddKeyToAgentError,
192 									keyType, fingerprint),
193 							e);
194 					// Note that as of Win32-OpenSSH 8.6 and Pageant 0.76,
195 					// neither can handle key constraints. Pageant fails
196 					// gracefully, not adding the key and returning
197 					// SSH_AGENT_FAILURE. Win32-OpenSSH closes the connection
198 					// without even returning a failure message, which violates
199 					// the SSH agent protocol and makes all subsequent requests
200 					// to the agent fail.
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; //$NON-NLS-1$
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"); //$NON-NLS-1$
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(","); //$NON-NLS-1$
258 		List<SshAgentKeyConstraint> rules = new ArrayList<>(2);
259 		switch (values[0]) {
260 		case "yes": //$NON-NLS-1$
261 			addKeysToAgent = true;
262 			break;
263 		case "no": //$NON-NLS-1$
264 			addKeysToAgent = false;
265 			break;
266 		case "ask": //$NON-NLS-1$
267 			addKeysToAgent = true;
268 			askBeforeAdding = true;
269 			break;
270 		case "confirm": //$NON-NLS-1$
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 		// If non-null, all the public keys from explicitly given key files. Any
315 		// agent key not matching one of these public keys will be ignored in
316 		// getIdentities().
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"); //$NON-NLS-1$
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 						// This checks against all explicit keys for any agent
369 						// key, but since identityFiles.size() is typically 1,
370 						// it should be fine.
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: {}", //$NON-NLS-1$
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 }