JGitPublicKeyAuthentication.java

  1. /*
  2.  * Copyright (C) 2018, 2022 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. import static java.text.MessageFormat.format;
  12. import static org.eclipse.jgit.transport.SshConstants.PUBKEY_ACCEPTED_ALGORITHMS;

  13. import java.io.IOException;
  14. import java.net.URISyntaxException;
  15. import java.nio.file.Files;
  16. import java.nio.file.InvalidPathException;
  17. import java.nio.file.LinkOption;
  18. import java.nio.file.Path;
  19. import java.nio.file.Paths;
  20. import java.security.GeneralSecurityException;
  21. import java.security.KeyPair;
  22. import java.security.PublicKey;
  23. import java.util.ArrayList;
  24. import java.util.Arrays;
  25. import java.util.Collection;
  26. import java.util.Iterator;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.NoSuchElementException;
  30. import java.util.Objects;
  31. import java.util.stream.Collectors;

  32. import org.apache.sshd.agent.SshAgent;
  33. import org.apache.sshd.agent.SshAgentFactory;
  34. import org.apache.sshd.agent.SshAgentKeyConstraint;
  35. import org.apache.sshd.client.auth.pubkey.KeyAgentIdentity;
  36. import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity;
  37. import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
  38. import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyIterator;
  39. import org.apache.sshd.client.config.hosts.HostConfigEntry;
  40. import org.apache.sshd.client.session.ClientSession;
  41. import org.apache.sshd.common.FactoryManager;
  42. import org.apache.sshd.common.NamedFactory;
  43. import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
  44. import org.apache.sshd.common.config.keys.KeyUtils;
  45. import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
  46. import org.apache.sshd.common.config.keys.u2f.SecurityKeyPublicKey;
  47. import org.apache.sshd.common.signature.Signature;
  48. import org.apache.sshd.common.signature.SignatureFactoriesManager;
  49. import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
  50. import org.eclipse.jgit.transport.CredentialItem;
  51. import org.eclipse.jgit.transport.CredentialsProvider;
  52. import org.eclipse.jgit.transport.SshConstants;
  53. import org.eclipse.jgit.transport.URIish;
  54. import org.eclipse.jgit.util.StringUtils;

  55. /**
  56.  * Custom {@link UserAuthPublicKey} implementation for handling SSH config
  57.  * PubkeyAcceptedAlgorithms and interaction with the SSH agent.
  58.  */
  59. public class JGitPublicKeyAuthentication extends UserAuthPublicKey {

  60.     private SshAgent agent;

  61.     private HostConfigEntry hostConfig;

  62.     private boolean addKeysToAgent;

  63.     private boolean askBeforeAdding;

  64.     private String skProvider;

  65.     private SshAgentKeyConstraint[] constraints;

  66.     JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
  67.         super(factories);
  68.     }

  69.     @Override
  70.     public void init(ClientSession rawSession, String service)
  71.             throws Exception {
  72.         if (!(rawSession instanceof JGitClientSession)) {
  73.             throw new IllegalStateException("Wrong session type: " //$NON-NLS-1$
  74.                     + rawSession.getClass().getCanonicalName());
  75.         }
  76.         JGitClientSession session = (JGitClientSession) rawSession;
  77.         hostConfig = session.getHostConfigEntry();
  78.         // Set signature algorithms for public key authentication
  79.         String pubkeyAlgos = hostConfig.getProperty(PUBKEY_ACCEPTED_ALGORITHMS);
  80.         if (!StringUtils.isEmptyOrNull(pubkeyAlgos)) {
  81.             List<String> signatures = session.getSignatureFactoriesNames();
  82.             signatures = session.modifyAlgorithmList(signatures,
  83.                     session.getAllAvailableSignatureAlgorithms(), pubkeyAlgos,
  84.                     PUBKEY_ACCEPTED_ALGORITHMS);
  85.             if (!signatures.isEmpty()) {
  86.                 if (log.isDebugEnabled()) {
  87.                     log.debug(PUBKEY_ACCEPTED_ALGORITHMS + ' ' + signatures);
  88.                 }
  89.                 setSignatureFactoriesNames(signatures);
  90.                 super.init(session, service);
  91.                 return;
  92.             }
  93.             log.warn(format(SshdText.get().configNoKnownAlgorithms,
  94.                     PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos));
  95.         }
  96.         // TODO: remove this once we're on an sshd version that has SSHD-1272
  97.         // fixed
  98.         List<NamedFactory<Signature>> localFactories = getSignatureFactories();
  99.         if (localFactories == null || localFactories.isEmpty()) {
  100.             setSignatureFactoriesNames(session.getSignatureFactoriesNames());
  101.         }
  102.         super.init(session, service);
  103.     }

  104.     @Override
  105.     protected Iterator<PublicKeyIdentity> createPublicKeyIterator(
  106.             ClientSession session, SignatureFactoriesManager manager)
  107.             throws Exception {
  108.         agent = getAgent(session);
  109.         if (agent != null) {
  110.             parseAddKeys(hostConfig);
  111.             if (addKeysToAgent) {
  112.                 skProvider = hostConfig.getProperty(SshConstants.SECURITY_KEY_PROVIDER);
  113.             }
  114.         }
  115.         return new KeyIterator(session, manager);
  116.     }

  117.     @Override
  118.     protected PublicKeyIdentity resolveAttemptedPublicKeyIdentity(
  119.             ClientSession session, String service) throws Exception {
  120.         PublicKeyIdentity id = super.resolveAttemptedPublicKeyIdentity(session,
  121.                 service);
  122.         if (addKeysToAgent && id != null && !(id instanceof KeyAgentIdentity)) {
  123.             KeyPair key = id.getKeyIdentity();
  124.             if (key != null && key.getPublic() != null
  125.                     && key.getPrivate() != null) {
  126.                 // We've just successfully loaded a key that wasn't in the
  127.                 // agent. Add it to the agent.
  128.                 //
  129.                 // Keys are added after loading, as in OpenSSH. The alternative
  130.                 // might be to add a key only after (partially) successful
  131.                 // authentication?
  132.                 PublicKey pk = key.getPublic();
  133.                 String fingerprint = KeyUtils.getFingerPrint(pk);
  134.                 String keyType = KeyUtils.getKeyType(key);
  135.                 try {
  136.                     // Check that the key is not in the agent already.
  137.                     if (agentHasKey(pk)) {
  138.                         return id;
  139.                     }
  140.                     if (askBeforeAdding
  141.                             && (session instanceof JGitClientSession)) {
  142.                         CredentialsProvider provider = ((JGitClientSession) session)
  143.                                 .getCredentialsProvider();
  144.                         CredentialItem.YesNoType question = new CredentialItem.YesNoType(
  145.                                 format(SshdText
  146.                                         .get().pubkeyAuthAddKeyToAgentQuestion,
  147.                                         keyType, fingerprint));
  148.                         boolean result = provider != null
  149.                                 && provider.supports(question)
  150.                                 && provider.get(getUri(), question);
  151.                         if (!result || !question.getValue()) {
  152.                             // Don't add the key.
  153.                             return id;
  154.                         }
  155.                     }
  156.                     SshAgentKeyConstraint[] rules = constraints;
  157.                     if (pk instanceof SecurityKeyPublicKey && !StringUtils.isEmptyOrNull(skProvider)) {
  158.                         rules = Arrays.copyOf(rules, rules.length + 1);
  159.                         rules[rules.length - 1] =
  160.                                 new SshAgentKeyConstraint.FidoProviderExtension(skProvider);
  161.                     }
  162.                     // Unfortunately a comment associated with the key is lost
  163.                     // by Apache MINA sshd, and there is also no way to get the
  164.                     // original file name for keys loaded from a file. So add it
  165.                     // without comment.
  166.                     agent.addIdentity(key, null, rules);
  167.                 } catch (IOException e) {
  168.                     // Do not re-throw: we don't want authentication to fail if
  169.                     // we cannot add the key to the agent.
  170.                     log.error(
  171.                             format(SshdText.get().pubkeyAuthAddKeyToAgentError,
  172.                                     keyType, fingerprint),
  173.                             e);
  174.                     // Note that as of Win32-OpenSSH 8.6 and Pageant 0.76,
  175.                     // neither can handle key constraints. Pageant fails
  176.                     // gracefully, not adding the key and returning
  177.                     // SSH_AGENT_FAILURE. Win32-OpenSSH closes the connection
  178.                     // without even returning a failure message, which violates
  179.                     // the SSH agent protocol and makes all subsequent requests
  180.                     // to the agent fail.
  181.                 }
  182.             }
  183.         }
  184.         return id;
  185.     }

  186.     private boolean agentHasKey(PublicKey pk) throws IOException {
  187.         Iterable<? extends Map.Entry<PublicKey, String>> ids = agent
  188.                 .getIdentities();
  189.         if (ids == null) {
  190.             return false;
  191.         }
  192.         Iterator<? extends Map.Entry<PublicKey, String>> iter = ids.iterator();
  193.         while (iter.hasNext()) {
  194.             if (KeyUtils.compareKeys(iter.next().getKey(), pk)) {
  195.                 return true;
  196.             }
  197.         }
  198.         return false;
  199.     }

  200.     private URIish getUri() {
  201.         String uri = SshConstants.SSH_SCHEME + "://"; //$NON-NLS-1$
  202.         String userName = hostConfig.getUsername();
  203.         if (!StringUtils.isEmptyOrNull(userName)) {
  204.             uri += userName + '@';
  205.         }
  206.         uri += hostConfig.getHost();
  207.         int port = hostConfig.getPort();
  208.         if (port > 0 && port != SshConstants.SSH_DEFAULT_PORT) {
  209.             uri += ":" + port; //$NON-NLS-1$
  210.         }
  211.         try {
  212.             return new URIish(uri);
  213.         } catch (URISyntaxException e) {
  214.             log.error(e.getLocalizedMessage(), e);
  215.         }
  216.         return new URIish();
  217.     }

  218.     private SshAgent getAgent(ClientSession session) throws Exception {
  219.         FactoryManager manager = Objects.requireNonNull(
  220.                 session.getFactoryManager(), "No session factory manager"); //$NON-NLS-1$
  221.         SshAgentFactory factory = manager.getAgentFactory();
  222.         if (factory == null) {
  223.             return null;
  224.         }
  225.         return factory.createClient(session, manager);
  226.     }

  227.     private void parseAddKeys(HostConfigEntry config) {
  228.         String value = config.getProperty(SshConstants.ADD_KEYS_TO_AGENT);
  229.         if (StringUtils.isEmptyOrNull(value)) {
  230.             addKeysToAgent = false;
  231.             return;
  232.         }
  233.         String[] values = value.split(","); //$NON-NLS-1$
  234.         List<SshAgentKeyConstraint> rules = new ArrayList<>(2);
  235.         switch (values[0]) {
  236.         case "yes": //$NON-NLS-1$
  237.             addKeysToAgent = true;
  238.             break;
  239.         case "no": //$NON-NLS-1$
  240.             addKeysToAgent = false;
  241.             break;
  242.         case "ask": //$NON-NLS-1$
  243.             addKeysToAgent = true;
  244.             askBeforeAdding = true;
  245.             break;
  246.         case "confirm": //$NON-NLS-1$
  247.             addKeysToAgent = true;
  248.             rules.add(SshAgentKeyConstraint.CONFIRM);
  249.             if (values.length > 1) {
  250.                 int seconds = OpenSshConfigFile.timeSpec(values[1]);
  251.                 if (seconds > 0) {
  252.                     rules.add(new SshAgentKeyConstraint.LifeTime(seconds));
  253.                 }
  254.             }
  255.             break;
  256.         default:
  257.             int seconds = OpenSshConfigFile.timeSpec(values[0]);
  258.             if (seconds > 0) {
  259.                 addKeysToAgent = true;
  260.                 rules.add(new SshAgentKeyConstraint.LifeTime(seconds));
  261.             }
  262.             break;
  263.         }
  264.         constraints = rules.toArray(new SshAgentKeyConstraint[0]);
  265.     }

  266.     @Override
  267.     protected void releaseKeys() throws IOException {
  268.         addKeysToAgent = false;
  269.         askBeforeAdding = false;
  270.         skProvider = null;
  271.         constraints = null;
  272.         try {
  273.             if (agent != null) {
  274.                 try {
  275.                     agent.close();
  276.                 } finally {
  277.                     agent = null;
  278.                 }
  279.             }
  280.         } finally {
  281.             super.releaseKeys();
  282.         }
  283.     }

  284.     private class KeyIterator extends UserAuthPublicKeyIterator {

  285.         private Iterable<? extends Map.Entry<PublicKey, String>> agentKeys;

  286.         // If non-null, all the public keys from explicitly given key files. Any
  287.         // agent key not matching one of these public keys will be ignored in
  288.         // getIdentities().
  289.         private Collection<PublicKey> identityFiles;

  290.         public KeyIterator(ClientSession session,
  291.                 SignatureFactoriesManager manager)
  292.                 throws Exception {
  293.             super(session, manager);
  294.         }

  295.         private List<PublicKey> getExplicitKeys(
  296.                 Collection<String> explicitFiles) {
  297.             if (explicitFiles == null) {
  298.                 return null;
  299.             }
  300.             return explicitFiles.stream().map(s -> {
  301.                 try {
  302.                     Path p = Paths.get(s + ".pub"); //$NON-NLS-1$
  303.                     if (Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) {
  304.                         return AuthorizedKeyEntry.readAuthorizedKeys(p).get(0)
  305.                                 .resolvePublicKey(null,
  306.                                         PublicKeyEntryResolver.IGNORING);
  307.                     }
  308.                 } catch (InvalidPathException | IOException
  309.                         | GeneralSecurityException e) {
  310.                     log.warn(format(SshdText.get().cannotReadPublicKey, s), e);
  311.                 }
  312.                 return null;
  313.             }).filter(Objects::nonNull).collect(Collectors.toList());
  314.         }

  315.         @Override
  316.         protected Iterable<KeyAgentIdentity> initializeAgentIdentities(
  317.                 ClientSession session) throws IOException {
  318.             if (agent == null) {
  319.                 return null;
  320.             }
  321.             agentKeys = agent.getIdentities();
  322.             if (hostConfig != null && hostConfig.isIdentitiesOnly()) {
  323.                 identityFiles = getExplicitKeys(hostConfig.getIdentities());
  324.             }
  325.             return () -> new Iterator<>() {

  326.                 private final Iterator<? extends Map.Entry<PublicKey, String>> iter = agentKeys
  327.                         .iterator();

  328.                 private Map.Entry<PublicKey, String> next;

  329.                 @Override
  330.                 public boolean hasNext() {
  331.                     while (next == null && iter.hasNext()) {
  332.                         Map.Entry<PublicKey, String> val = iter.next();
  333.                         PublicKey pk = val.getKey();
  334.                         // This checks against all explicit keys for any agent
  335.                         // key, but since identityFiles.size() is typically 1,
  336.                         // it should be fine.
  337.                         if (identityFiles == null || identityFiles.stream()
  338.                                 .anyMatch(k -> KeyUtils.compareKeys(k, pk))) {
  339.                             next = val;
  340.                             return true;
  341.                         }
  342.                         if (log.isTraceEnabled()) {
  343.                             log.trace(
  344.                                     "Ignoring SSH agent {} key not in explicit IdentityFile in SSH config: {}", //$NON-NLS-1$
  345.                                     KeyUtils.getKeyType(pk),
  346.                                     KeyUtils.getFingerPrint(pk));
  347.                         }
  348.                     }
  349.                     return next != null;
  350.                 }

  351.                 @Override
  352.                 public KeyAgentIdentity next() {
  353.                     if (!hasNext()) {
  354.                         throw new NoSuchElementException();
  355.                     }
  356.                     KeyAgentIdentity result = new KeyAgentIdentity(agent,
  357.                             next.getKey(), next.getValue());
  358.                     next = null;
  359.                     return result;
  360.                 }
  361.             };
  362.         }
  363.     }
  364. }