]> source.dussan.org Git - jgit.git/blob
96da0cccdd86f581efe2292a5e017f6dcea30aa3
[jgit.git] /
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 }