diff options
author | Matthias Sohn <matthias.sohn@sap.com> | 2022-07-06 17:29:23 +0200 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2022-07-06 19:07:45 +0200 |
commit | 3fc0ec18228d392aecf5d15198e001815726b5d5 (patch) | |
tree | eec583d62bdde6f49101024e5f20878f90bde5cd /org.eclipse.jgit.ssh.apache | |
parent | c6a64ecae7ea5519bb6460b38d7ec65a83dc6303 (diff) | |
parent | f6935d8cd2e00d5923917ff869354e443da66014 (diff) | |
download | jgit-3fc0ec18228d392aecf5d15198e001815726b5d5.tar.gz jgit-3fc0ec18228d392aecf5d15198e001815726b5d5.zip |
Merge branch 'master' into next
* master: (193 commits)
Add aarch64 environment to target platform configuration
JGit blame very slow for large merge commits that rename files
UploadPack: don't prematurely terminate timer in case of error
Do not create reflog for remote tracking branches during clone
UploadPack: do not check reachability of visible SHA1s
Fix warnings about non-externalized string literals
[sshd] Correct signature for RSA keys from an SSH agent
Run tests that checks araxis output only on Linux
Add missing package import javax.management to org.eclipse.jgit
Add 4.25 target platform for Eclipse 2022-09
Suppress API errors raised for new API introduced in 5.13.1
Eclipse 4.24 was released, adapt p2 URL accordingly
Fix DefaultCharset bug pattern flagged by error prone
Use SystemReader#getDefaultCharset to read console input
Annotate the exception with the possible failure reason when Bitmaps are not enabled.
Prepare 5.13.2-SNAPSHOT builds
JGit v5.13.1.202206130422-r
AmazonS3: Add support for AWS API signature version 4
Fix typo in DiffTools#compare javadoc
Update jgit-last-release-version to 6.2.0.202206071550-r
...
Change-Id: I561a0178f6bc512e8ce7d75f1870a044cb051fac
Diffstat (limited to 'org.eclipse.jgit.ssh.apache')
18 files changed, 1231 insertions, 190 deletions
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF index 3bec647dfa..f24ff80218 100644 --- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF @@ -35,54 +35,57 @@ Export-Package: org.eclipse.jgit.internal.transport.sshd;version="7.0.0";x-inter org.apache.sshd.client.keyverifier", org.eclipse.jgit.transport.sshd.agent;version="7.0.0" Import-Package: net.i2p.crypto.eddsa;version="[0.3.0,0.4.0)", - org.apache.sshd.agent;version="[2.7.0,2.8.0)", - org.apache.sshd.client;version="[2.7.0,2.8.0)", - org.apache.sshd.client.auth;version="[2.7.0,2.8.0)", - org.apache.sshd.client.auth.keyboard;version="[2.7.0,2.8.0)", - org.apache.sshd.client.auth.password;version="[2.7.0,2.8.0)", - org.apache.sshd.client.auth.pubkey;version="[2.7.0,2.8.0)", - org.apache.sshd.client.channel;version="[2.7.0,2.8.0)", - org.apache.sshd.client.config.hosts;version="[2.7.0,2.8.0)", - org.apache.sshd.client.config.keys;version="[2.7.0,2.8.0)", - org.apache.sshd.client.future;version="[2.7.0,2.8.0)", - org.apache.sshd.client.keyverifier;version="[2.7.0,2.8.0)", - org.apache.sshd.client.session;version="[2.7.0,2.8.0)", - org.apache.sshd.client.session.forward;version="[2.7.0,2.8.0)", - org.apache.sshd.common;version="[2.7.0,2.8.0)", - org.apache.sshd.common.auth;version="[2.7.0,2.8.0)", - org.apache.sshd.common.channel;version="[2.7.0,2.8.0)", - org.apache.sshd.common.compression;version="[2.7.0,2.8.0)", - org.apache.sshd.common.config.keys;version="[2.7.0,2.8.0)", - org.apache.sshd.common.config.keys.loader;version="[2.7.0,2.8.0)", - org.apache.sshd.common.config.keys.loader.openssh.kdf;version="[2.7.0,2.8.0)", - org.apache.sshd.common.digest;version="[2.7.0,2.8.0)", - org.apache.sshd.common.forward;version="[2.7.0,2.8.0)", - org.apache.sshd.common.future;version="[2.7.0,2.8.0)", - org.apache.sshd.common.helpers;version="[2.7.0,2.8.0)", - org.apache.sshd.common.io;version="[2.7.0,2.8.0)", - org.apache.sshd.common.kex;version="[2.7.0,2.8.0)", - org.apache.sshd.common.kex.extension;version="[2.7.0,2.8.0)", - org.apache.sshd.common.kex.extension.parser;version="[2.7.0,2.8.0)", - org.apache.sshd.common.keyprovider;version="[2.7.0,2.8.0)", - org.apache.sshd.common.mac;version="[2.7.0,2.8.0)", - org.apache.sshd.common.random;version="[2.7.0,2.8.0)", - org.apache.sshd.common.session;version="[2.7.0,2.8.0)", - org.apache.sshd.common.session.helpers;version="[2.7.0,2.8.0)", - org.apache.sshd.common.signature;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.buffer;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.closeable;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.io;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.io.functors;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.io.resource;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.logging;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.net;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.security;version="[2.7.0,2.8.0)", - org.apache.sshd.core;version="[2.7.0,2.8.0)", - org.apache.sshd.server.auth;version="[2.7.0,2.8.0)", - org.apache.sshd.sftp;version="[2.7.0,2.8.0)", - org.apache.sshd.sftp.client;version="[2.7.0,2.8.0)", - org.apache.sshd.sftp.common;version="[2.7.0,2.8.0)", + org.apache.sshd.agent;version="[2.8.0,2.9.0)", + org.apache.sshd.client;version="[2.8.0,2.9.0)", + org.apache.sshd.client.auth;version="[2.8.0,2.9.0)", + org.apache.sshd.client.auth.keyboard;version="[2.8.0,2.9.0)", + org.apache.sshd.client.auth.password;version="[2.8.0,2.9.0)", + org.apache.sshd.client.auth.pubkey;version="[2.8.0,2.9.0)", + org.apache.sshd.client.channel;version="[2.8.0,2.9.0)", + org.apache.sshd.client.config.hosts;version="[2.8.0,2.9.0)", + org.apache.sshd.client.config.keys;version="[2.8.0,2.9.0)", + org.apache.sshd.client.future;version="[2.8.0,2.9.0)", + org.apache.sshd.client.keyverifier;version="[2.8.0,2.9.0)", + org.apache.sshd.client.session;version="[2.8.0,2.9.0)", + org.apache.sshd.client.session.forward;version="[2.8.0,2.9.0)", + org.apache.sshd.common;version="[2.8.0,2.9.0)", + org.apache.sshd.common.auth;version="[2.8.0,2.9.0)", + org.apache.sshd.common.channel;version="[2.8.0,2.9.0)", + org.apache.sshd.common.compression;version="[2.8.0,2.9.0)", + org.apache.sshd.common.config.keys;version="[2.8.0,2.9.0)", + org.apache.sshd.common.config.keys.loader;version="[2.8.0,2.9.0)", + org.apache.sshd.common.config.keys.loader.openssh.kdf;version="[2.8.0,2.9.0)", + org.apache.sshd.common.config.keys.u2f;version="[2.8.0,2.9.0)", + org.apache.sshd.common.digest;version="[2.8.0,2.9.0)", + org.apache.sshd.common.forward;version="[2.8.0,2.9.0)", + org.apache.sshd.common.future;version="[2.8.0,2.9.0)", + org.apache.sshd.common.helpers;version="[2.8.0,2.9.0)", + org.apache.sshd.common.io;version="[2.8.0,2.9.0)", + org.apache.sshd.common.kex;version="[2.8.0,2.9.0)", + org.apache.sshd.common.kex.extension;version="[2.8.0,2.9.0)", + org.apache.sshd.common.kex.extension.parser;version="[2.8.0,2.9.0)", + org.apache.sshd.common.keyprovider;version="[2.8.0,2.9.0)", + org.apache.sshd.common.mac;version="[2.8.0,2.9.0)", + org.apache.sshd.common.random;version="[2.8.0,2.9.0)", + org.apache.sshd.common.session;version="[2.8.0,2.9.0)", + org.apache.sshd.common.session.helpers;version="[2.8.0,2.9.0)", + org.apache.sshd.common.signature;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.buffer;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.buffer.keys;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.closeable;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.io;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.io.der;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.io.functors;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.io.resource;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.logging;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.net;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.security;version="[2.8.0,2.9.0)", + org.apache.sshd.core;version="[2.8.0,2.9.0)", + org.apache.sshd.server.auth;version="[2.8.0,2.9.0)", + org.apache.sshd.sftp;version="[2.8.0,2.9.0)", + org.apache.sshd.sftp.client;version="[2.8.0,2.9.0)", + org.apache.sshd.sftp.common;version="[2.8.0,2.9.0)", org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)", org.eclipse.jgit.errors;version="[7.0.0,7.1.0)", org.eclipse.jgit.fnmatch;version="[7.0.0,7.1.0)", diff --git a/org.eclipse.jgit.ssh.apache/README.md b/org.eclipse.jgit.ssh.apache/README.md index cba87ac9cc..f06b2f6071 100644 --- a/org.eclipse.jgit.ssh.apache/README.md +++ b/org.eclipse.jgit.ssh.apache/README.md @@ -43,6 +43,53 @@ FetchCommand fetch = git.fetch() .call(); ``` +## Support for SSH agents + +There exist two IETF draft RFCs for communication with an SSH agent: + +* an older [SSH1 protocol](https://tools.ietf.org/html/draft-ietf-secsh-agent-02) that can deal only with DSA and RSA keys, and +* a newer [SSH2 protocol](https://tools.ietf.org/html/draft-miller-ssh-agent-04) (from OpenSSH). + +JGit only supports the newer OpenSSH protocol. + +Communication with an SSH agent can occur over any transport protocol, and different +SSH agents may use different transports for local communication. JGit provides some +transports via the [org.eclipse.jgit.ssh.apache.agent](../org.eclipse.jgit.ssh.apache.agent/README.md) +fragment, which are discovered from `org.eclipse.jgit.ssh.apache` also via the `ServiceLoader` mechanism; +the SPI (service provider interface) is `org.eclipse.jgit.transport.sshd.agent.ConnectorFactory`. + +If such a `ConnectorFactory` implementation is found, JGit may use an SSH agent. If none +is available, JGit cannot communicate with an SSH agent, and will not attempt to use one. + +### SSH configurations for SSH agents + +There are several SSH properties that can be used in the `~/.ssh/config` file to configure +the use of an SSH agent. For the details, see the [OpenBSD ssh-config documentation](https://man.openbsd.org/ssh_config.5). + +* **AddKeysToAgent** can be set to `no`, `yes`, or `ask`. If set to `yes`, keys will be added + to the agent if they're not yet in the agent. If set to `ask`, the user will be prompted + before doing so, and can opt out of adding the key. JGit also supports the additional + settings `confirm` and key lifetimes. +* **IdentityAgent** can be set to choose which SSH agent to use, if there are several running. + It can also be set to `none` to explicitly switch off using an SSH agent at all. +* **IdentitiesOnly** if set to `yes` and an SSH agent is used, only keys from the agent that are + also listed in an `IdentityFile` property will be considered. (It'll also switch off trying + default key names, such as `~/.ssh/id_rsa` or `~/.ssh/id_ed25519`; only keys listed explicitly + will be used.) + +### Limitations + +As mentioned above JGit only implements the newer OpenSSH protocol. OpenSSH fully implements this, +but some other SSH agents only offer partial implementations. In particular on Windows, neither +Pageant nor Win32-OpenSSH implement the `confirm` or lifetime constraints for `AddKeysToAgent`. With +such SSH agents, these settings should not be used in `~/.ssh/config`. GPG's gpg-agent can be run +with option `enable_putty_support` and can then be used as a Pageant replacement. gpg-agent appears +to support these key constraints. + +OpenSSH does not implement ed448 keys, and neither does Apache MINA sshd, and hence such keys are +not supported in JGit if its built-in SSH implementation is used. ed448 or other unsupported keys +provided by an SSH agent are ignored. + ## Using a different SSH implementation To use a different SSH implementation: diff --git a/org.eclipse.jgit.ssh.apache/pom.xml b/org.eclipse.jgit.ssh.apache/pom.xml index 024dae8524..3ea8b63ab7 100644 --- a/org.eclipse.jgit.ssh.apache/pom.xml +++ b/org.eclipse.jgit.ssh.apache/pom.xml @@ -154,6 +154,9 @@ <includes> <include>org.eclipse.jgit.*</include> </includes> + <excludes> + <exclude>*.internal.*</exclude> + </excludes> <accessModifier>public</accessModifier> <breakBuildOnModifications>false</breakBuildOnModifications> <breakBuildOnBinaryIncompatibleModifications>false</breakBuildOnBinaryIncompatibleModifications> @@ -207,6 +210,9 @@ <includes> <include>org.eclipse.jgit.*</include> </includes> + <excludes> + <exclude>*.internal.*</exclude> + </excludes> <accessModifier>public</accessModifier> <breakBuildOnModifications>false</breakBuildOnModifications> <breakBuildOnBinaryIncompatibleModifications>false</breakBuildOnBinaryIncompatibleModifications> diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties index 2bba736aad..c676221800 100644 --- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties +++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties @@ -1,5 +1,23 @@ authenticationCanceled=SSH authentication canceled: no password given authenticationOnClosedSession=Authentication canceled: session is already closing or closed +authGssApiAttempt={0}: trying mechanism OID {1} +authGssApiExhausted={0}: no more mechanisms to try +authGssApiFailure={0}: server refused authentication; mechanism {1} +authGssApiNotTried={0}: not tried +authGssApiPartialSuccess={0}: partial success with mechanism OID {1}, continue with authentication methods {2} +authPasswordAttempt={0}: attempt {1} +authPasswordChangeAttempt={0}: attempt {1} with password change +authPasswordExhausted={0}: no more attempts +authPasswordFailure={0}: server refused (wrong password) +authPasswordNotTried={0}: not tried +authPasswordPartialSuccess={0}: partial success, continue with authentication methods {1} +authPubkeyAttempt={0}: trying {1} key {2} with signature type {3} +authPubkeyAttemptAgent={0}: trying {1} key {2} from SSH agent with signature type {3} +authPubkeyExhausted={0}: no more keys to try +authPubkeyFailure={0}: server refused {1} key {2} +authPubkeyNoKeys={0}: no keys to try +authPubkeyPartialSuccess={0}: partial success for {1} key {2}, continue with authentication methods {3} +cannotReadPublicKey=Cannot read public key from file {0} closeListenerFailed=Ssh session close listener failed configInvalidPath=Invalid path in ssh config key {0}: {1} configInvalidPattern=Invalid pattern in ssh config key {0}: {1} @@ -77,6 +95,8 @@ proxySocksPasswordTooLong=Password for proxy {0} must be at most 255 bytes long, proxySocksUnexpectedMessage=Unexpected message received from SOCKS5 proxy {0}; client state {1}: {2} proxySocksUnexpectedVersion=Expected SOCKS version 5, got {0} proxySocksUsernameTooLong=User name for proxy {0} must be at most 255 bytes long, is {1} bytes: {2} +pubkeyAuthAddKeyToAgentError=Could not add {0} key with fingerprint {1} to the SSH agent +pubkeyAuthAddKeyToAgentQuestion=Add the {0} key with fingerprint {1} to the SSH agent? pubkeyAuthWrongCommand=Public key authentication received unknown SSH command {0} from {1} ({2}) pubkeyAuthWrongKey=Public key authentication received wrong key; sent {0}, got back {1} from {2} ({3}) pubkeyAuthWrongSignatureAlgorithm=Public key authentication requested signature type {0} but got back {1} from {2} ({3}) @@ -85,9 +105,13 @@ serverIdTooLong=Server identification is longer than 255 characters (including l serverIdWithNul=Server identification contains a NUL character: {0} sessionCloseFailed=Closing the session failed sessionWithoutUsername=SSH session created without user name; cannot authenticate +sshAgentEdDSAFormatError=Cannot add ed25519 key to the SSH agent because it is encoded as {0} instead of PKCS#8 +sshAgentPayloadLengthError=Expected {0,choice,0#no bytes|1#one byte|1<{0} bytes} but got {1} sshAgentReplyLengthError=Invalid SSH agent reply message length {0} after command {1} sshAgentReplyUnexpected=Unexpected reply from ssh-agent: {0} sshAgentShortReadBuffer=Short read from SSH agent +sshAgentUnknownKey=SSH agent delivered a key that cannot be handled +sshAgentWrongKeyLength=SSH agent delivered illogical key length {0} at offset {1} in buffer of length {2} sshAgentWrongNumberOfKeys=Invalid number of SSH agent keys: {0} sshClosingDown=Apache MINA sshd session factory is closing down; cannot create new ssh sessions on this factory sshCommandTimeout={0} timed out after {1} seconds while opening the channel diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java new file mode 100644 index 0000000000..add79b35c9 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.transport.sshd; + +import static org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider.getKeyId; + +import java.security.KeyPair; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter; +import org.apache.sshd.client.auth.password.UserAuthPassword; +import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter; +import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.config.keys.KeyUtils; + +/** + * Provides a log of authentication attempts for a {@link ClientSession}. + */ +public class AuthenticationLogger { + + private final List<String> messages = new ArrayList<>(); + + // We're interested in this log only in the failure case, so we don't need + // to log authentication success. + + private final PublicKeyAuthenticationReporter pubkeyLogger = new PublicKeyAuthenticationReporter() { + + private boolean hasAttempts; + + @Override + public void signalAuthenticationAttempt(ClientSession session, + String service, KeyPair identity, String signature) + throws Exception { + hasAttempts = true; + String message; + if (identity.getPrivate() == null) { + // SSH agent key + message = MessageFormat.format( + SshdText.get().authPubkeyAttemptAgent, + UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity), + getKeyId(session, identity), signature); + } else { + message = MessageFormat.format( + SshdText.get().authPubkeyAttempt, + UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity), + getKeyId(session, identity), signature); + } + messages.add(message); + } + + @Override + public void signalAuthenticationExhausted(ClientSession session, + String service) throws Exception { + String message; + if (hasAttempts) { + message = MessageFormat.format( + SshdText.get().authPubkeyExhausted, + UserAuthPublicKey.NAME); + } else { + message = MessageFormat.format(SshdText.get().authPubkeyNoKeys, + UserAuthPublicKey.NAME); + } + messages.add(message); + hasAttempts = false; + } + + @Override + public void signalAuthenticationFailure(ClientSession session, + String service, KeyPair identity, boolean partial, + List<String> serverMethods) throws Exception { + String message; + if (partial) { + message = MessageFormat.format( + SshdText.get().authPubkeyPartialSuccess, + UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity), + getKeyId(session, identity), serverMethods); + } else { + message = MessageFormat.format( + SshdText.get().authPubkeyFailure, + UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity), + getKeyId(session, identity)); + } + messages.add(message); + } + }; + + private final PasswordAuthenticationReporter passwordLogger = new PasswordAuthenticationReporter() { + + private int attempts; + + @Override + public void signalAuthenticationAttempt(ClientSession session, + String service, String oldPassword, boolean modified, + String newPassword) throws Exception { + attempts++; + String message; + if (modified) { + message = MessageFormat.format( + SshdText.get().authPasswordChangeAttempt, + UserAuthPassword.NAME, Integer.valueOf(attempts)); + } else { + message = MessageFormat.format( + SshdText.get().authPasswordAttempt, + UserAuthPassword.NAME, Integer.valueOf(attempts)); + } + messages.add(message); + } + + @Override + public void signalAuthenticationExhausted(ClientSession session, + String service) throws Exception { + String message; + if (attempts > 0) { + message = MessageFormat.format( + SshdText.get().authPasswordExhausted, + UserAuthPassword.NAME); + } else { + message = MessageFormat.format( + SshdText.get().authPasswordNotTried, + UserAuthPassword.NAME); + } + messages.add(message); + attempts = 0; + } + + @Override + public void signalAuthenticationFailure(ClientSession session, + String service, String password, boolean partial, + List<String> serverMethods) throws Exception { + String message; + if (partial) { + message = MessageFormat.format( + SshdText.get().authPasswordPartialSuccess, + UserAuthPassword.NAME, serverMethods); + } else { + message = MessageFormat.format( + SshdText.get().authPasswordFailure, + UserAuthPassword.NAME); + } + messages.add(message); + } + }; + + private final GssApiWithMicAuthenticationReporter gssLogger = new GssApiWithMicAuthenticationReporter() { + + private boolean hasAttempts; + + @Override + public void signalAuthenticationAttempt(ClientSession session, + String service, String mechanism) { + hasAttempts = true; + String message = MessageFormat.format( + SshdText.get().authGssApiAttempt, + GssApiWithMicAuthFactory.NAME, mechanism); + messages.add(message); + } + + @Override + public void signalAuthenticationExhausted(ClientSession session, + String service) { + String message; + if (hasAttempts) { + message = MessageFormat.format( + SshdText.get().authGssApiExhausted, + GssApiWithMicAuthFactory.NAME); + } else { + message = MessageFormat.format( + SshdText.get().authGssApiNotTried, + GssApiWithMicAuthFactory.NAME); + } + messages.add(message); + hasAttempts = false; + } + + @Override + public void signalAuthenticationFailure(ClientSession session, + String service, String mechanism, boolean partial, + List<String> serverMethods) { + String message; + if (partial) { + message = MessageFormat.format( + SshdText.get().authGssApiPartialSuccess, + GssApiWithMicAuthFactory.NAME, mechanism, + serverMethods); + } else { + message = MessageFormat.format( + SshdText.get().authGssApiFailure, + GssApiWithMicAuthFactory.NAME, mechanism); + } + messages.add(message); + } + }; + + /** + * Creates a new {@link AuthenticationLogger} and configures the + * {@link ClientSession} to report authentication attempts through this + * instance. + * + * @param session + * to configure + */ + public AuthenticationLogger(ClientSession session) { + session.setPublicKeyAuthenticationReporter(pubkeyLogger); + session.setPasswordAuthenticationReporter(passwordLogger); + session.setAttribute( + GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER, + gssLogger); + // TODO: keyboard-interactive? sshd 2.8.0 has no callback + // interface for it. + } + + /** + * Retrieves the log messages for the authentication attempts. + * + * @return the messages as an unmodifiable list + */ + public List<String> getLog() { + return Collections.unmodifiableList(messages); + } + + /** + * Drops all previously recorded log messages. + */ + public void clear() { + messages.clear(); + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java index 79b3637caa..cbd6a64140 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -11,6 +11,7 @@ package org.eclipse.jgit.internal.transport.sshd; import static java.text.MessageFormat.format; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -19,18 +20,24 @@ import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.PrivateKey; +import java.security.PublicKey; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.CancellationException; import javax.security.auth.DestroyFailedException; +import org.apache.sshd.common.AttributeRepository.AttributeKey; +import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.NamedResource; import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.keyprovider.FileKeyPairProvider; import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.util.io.resource.IoResource; @@ -43,6 +50,14 @@ import org.eclipse.jgit.transport.sshd.KeyCache; public class CachingKeyPairProvider extends FileKeyPairProvider implements Iterable<KeyPair> { + /** + * An attribute set on the {@link SessionContext} recording loaded keys by + * fingerprint. This enables us to provide nicer output by showing key + * paths, if possible. Users can identify key identities used easier by + * filename than by fingerprint. + */ + public static final AttributeKey<Map<String, Path>> KEY_PATHS_BY_FINGERPRINT = new AttributeKey<>(); + private final KeyCache cache; /** @@ -78,6 +93,33 @@ public class CachingKeyPairProvider extends FileKeyPairProvider return () -> iterator(session); } + static String getKeyId(ClientSession session, KeyPair identity) { + String fingerprint = KeyUtils.getFingerPrint(identity.getPublic()); + Map<String, Path> registered = session + .getAttribute(KEY_PATHS_BY_FINGERPRINT); + if (registered != null) { + Path path = registered.get(fingerprint); + if (path != null) { + Path home = session + .resolveAttribute(JGitSshClient.HOME_DIRECTORY); + if (home != null && path.startsWith(home)) { + try { + path = home.relativize(path); + String pathString = path.toString(); + if (!pathString.isEmpty()) { + return "~" + File.separator + pathString; //$NON-NLS-1$ + } + } catch (IllegalArgumentException e) { + // Cannot be relativized. Ignore, and work with the + // original path + } + } + return path.toString(); + } + } + return fingerprint; + } + private KeyPair loadKey(SessionContext session, Path path) throws IOException, GeneralSecurityException { if (!Files.exists(path)) { @@ -123,13 +165,23 @@ public class CachingKeyPairProvider extends FileKeyPairProvider SshdText.get().identityFileUnsupportedFormat, path)); } KeyPair result = keys.next(); + PublicKey pk = result.getPublic(); + if (pk != null) { + Map<String, Path> registered = session + .getAttribute(KEY_PATHS_BY_FINGERPRINT); + if (registered == null) { + registered = new HashMap<>(); + session.setAttribute(KEY_PATHS_BY_FINGERPRINT, registered); + } + registered.put(KeyUtils.getFingerPrint(pk), path); + } if (keys.hasNext()) { log.warn(format(SshdText.get().identityFileMultipleKeys, path)); keys.forEachRemaining(k -> { - PrivateKey pk = k.getPrivate(); - if (pk != null) { + PrivateKey priv = k.getPrivate(); + if (priv != null) { try { - pk.destroy(); + priv.destroy(); } catch (DestroyFailedException e) { // Ignore } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java index c3cac0c1df..df01db316b 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -18,6 +18,7 @@ import java.net.SocketAddress; import java.net.UnknownHostException; import java.util.Collection; import java.util.Iterator; +import java.util.List; import org.apache.sshd.client.auth.AbstractUserAuth; import org.apache.sshd.client.session.ClientSession; @@ -71,7 +72,10 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth { if (context != null) { close(false); } + GssApiWithMicAuthenticationReporter reporter = session.getAttribute( + GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER); if (!nextMechanism.hasNext()) { + reporter.signalAuthenticationExhausted(session, service); return false; } state = ProtocolState.STARTED; @@ -79,6 +83,7 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth { // RFC 4462 states that SPNEGO must not be used with ssh while (GssApiMechanisms.SPNEGO.equals(currentMechanism)) { if (!nextMechanism.hasNext()) { + reporter.signalAuthenticationExhausted(session, service); return false; } currentMechanism = nextMechanism.next(); @@ -102,6 +107,10 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth { state = ProtocolState.FAILED; return false; } + if (reporter != null) { + reporter.signalAuthenticationAttempt(session, service, + currentMechanism.toString()); + } Buffer buffer = session .createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST); buffer.putString(session.getUsername()); @@ -246,4 +255,26 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth { return false; } + @Override + public void signalAuthMethodSuccess(ClientSession session, String service, + Buffer buffer) throws Exception { + GssApiWithMicAuthenticationReporter reporter = session.getAttribute( + GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER); + if (reporter != null) { + reporter.signalAuthenticationSuccess(session, service, + currentMechanism.toString()); + } + } + + @Override + public void signalAuthMethodFailure(ClientSession session, String service, + boolean partial, List<String> serverMethods, Buffer buffer) + throws Exception { + GssApiWithMicAuthenticationReporter reporter = session.getAttribute( + GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER); + if (reporter != null) { + reporter.signalAuthenticationFailure(session, service, + currentMechanism.toString(), partial, serverMethods); + } + } } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java new file mode 100644 index 0000000000..201a131650 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.transport.sshd; + +import java.util.List; + +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.AttributeRepository.AttributeKey; + +/** + * Callback interface for recording authentication state in + * {@link GssApiWithMicAuthentication}. + */ +public interface GssApiWithMicAuthenticationReporter { + + /** + * An {@link AttributeKey}Â for a {@link ClientSession} holding the + * {@link GssApiWithMicAuthenticationReporter}. + */ + static final AttributeKey<GssApiWithMicAuthenticationReporter> GSS_AUTHENTICATION_REPORTER = new AttributeKey<>(); + + /** + * Called when a new authentication attempt is made. + * + * @param session + * the {@link ClientSession} + * @param service + * the name of the requesting SSH service name + * @param mechanism + * the OID of the mechanism used + */ + default void signalAuthenticationAttempt(ClientSession session, + String service, String mechanism) { + // nothing + } + + /** + * Called when there are no more mechanisms to try. + * + * @param session + * the {@link ClientSession} + * @param service + * the name of the requesting SSH service name + */ + default void signalAuthenticationExhausted(ClientSession session, + String service) { + // nothing + } + + /** + * Called when authentication was succeessful. + * + * @param session + * the {@link ClientSession} + * @param service + * the name of the requesting SSH service name + * @param mechanism + * the OID of the mechanism used + */ + default void signalAuthenticationSuccess(ClientSession session, + String service, String mechanism) { + // nothing + } + + /** + * Called when the authentication was not successful. + * + * @param session + * the {@link ClientSession} + * @param service + * the name of the requesting SSH service name + * @param mechanism + * the OID of the mechanism used + * @param partial + * {@code true} if authentication was partially successful, + * meaning one continues with additional authentication methods + * given by {@code serverMethods} + * @param serverMethods + * the {@link List} of authentication methods that can continue + */ + default void signalAuthenticationFailure(ClientSession session, + String service, String mechanism, boolean partial, + List<String> serverMethods) { + // nothing + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java index e2dbb4c466..5100bc9e54 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java @@ -42,7 +42,6 @@ import org.apache.sshd.common.io.IoSession; import org.apache.sshd.common.io.IoWriteFuture; import org.apache.sshd.common.kex.BuiltinDHFactories; import org.apache.sshd.common.kex.DHFactory; -import org.apache.sshd.common.kex.KexProposalOption; import org.apache.sshd.common.kex.KeyExchangeFactory; import org.apache.sshd.common.kex.extension.KexExtensionHandler; import org.apache.sshd.common.kex.extension.KexExtensionHandler.AvailabilityPhase; @@ -199,24 +198,6 @@ public class JGitClientSession extends ClientSessionImpl { } } - @Override - protected Map<KexProposalOption, String> setNegotiationResult( - Map<KexProposalOption, String> guess) { - Map<KexProposalOption, String> result = super.setNegotiationResult( - guess); - // This should be doable with a SessionListener, too, but I don't see - // how to add a listener in time to catch the negotiation end for sure - // given that the super-constructor already starts KEX. - // - // TODO: This override can be removed once we use sshd 2.8.0. - if (log.isDebugEnabled()) { - result.forEach((option, value) -> log.debug( - "setNegotiationResult({}) Kex: {} = {}", this, //$NON-NLS-1$ - option.getDescription(), value)); - } - return result; - } - Set<String> getAllAvailableSignatureAlgorithms() { Set<String> allAvailable = new HashSet<>(); BuiltinSignatures.VALUES.forEach(s -> allAvailable.add(s.getName())); diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java index ff8caaacc0..33c3c608f6 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -11,13 +11,11 @@ package org.eclipse.jgit.internal.transport.sshd; import static org.apache.sshd.core.CoreModuleProperties.PASSWORD_PROMPTS; -import org.apache.sshd.client.auth.keyboard.UserInteraction; import org.apache.sshd.client.auth.password.UserAuthPassword; import org.apache.sshd.client.session.ClientSession; /** - * A password authentication handler that uses the {@link JGitUserInteraction} - * to ask the user for the password. It also respects the + * A password authentication handler that respects the * {@code NumberOfPasswordPrompts} ssh config. */ public class JGitPasswordAuthentication extends UserAuthPassword { @@ -35,30 +33,11 @@ public class JGitPasswordAuthentication extends UserAuthPassword { } @Override - protected boolean sendAuthDataRequest(ClientSession session, String service) - throws Exception { + protected String resolveAttemptedPassword(ClientSession session, + String service) throws Exception { if (++attempts > maxAttempts) { - return false; + return null; } - UserInteraction interaction = session.getUserInteraction(); - if (!interaction.isInteractionAllowed(session)) { - return false; - } - String password = getPassword(session, interaction); - if (password == null) { - throw new AuthenticationCanceledException(); - } - // sendPassword takes a buffer as first argument, but actually doesn't - // use it and creates its own buffer... - sendPassword(null, session, password, password); - return true; - } - - private String getPassword(ClientSession session, - UserInteraction interaction) { - String[] results = interaction.interactive(session, null, null, "", //$NON-NLS-1$ - new String[] { SshdText.get().passwordPrompt }, - new boolean[] { false }); - return (results == null || results.length == 0) ? null : results[0]; + return super.resolveAttemptedPassword(session, service); } } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java index c082a9a963..e1036c6283 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -12,25 +12,68 @@ package org.eclipse.jgit.internal.transport.sshd; import static java.text.MessageFormat.format; import static org.eclipse.jgit.transport.SshConstants.PUBKEY_ACCEPTED_ALGORITHMS; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.stream.Collectors; +import org.apache.sshd.agent.SshAgent; +import org.apache.sshd.agent.SshAgentFactory; +import org.apache.sshd.agent.SshAgentKeyConstraint; import org.apache.sshd.client.auth.pubkey.KeyAgentIdentity; import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity; import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey; +import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyIterator; import org.apache.sshd.client.config.hosts.HostConfigEntry; import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.FactoryManager; import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; +import org.apache.sshd.common.config.keys.u2f.SecurityKeyPublicKey; import org.apache.sshd.common.signature.Signature; +import org.apache.sshd.common.signature.SignatureFactoriesManager; +import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.SshConstants; +import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.StringUtils; /** * Custom {@link UserAuthPublicKey} implementation for handling SSH config - * PubkeyAcceptedAlgorithms. + * PubkeyAcceptedAlgorithms and interaction with the SSH agent. */ public class JGitPublicKeyAuthentication extends UserAuthPublicKey { + private SshAgent agent; + + private HostConfigEntry hostConfig; + + private boolean addKeysToAgent; + + private boolean askBeforeAdding; + + private String skProvider; + + private SshAgentKeyConstraint[] constraints; + JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) { super(factories); } @@ -43,7 +86,7 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey { + rawSession.getClass().getCanonicalName()); } JGitClientSession session = (JGitClientSession) rawSession; - HostConfigEntry hostConfig = session.getHostConfigEntry(); + hostConfig = session.getHostConfigEntry(); // Set signature algorithms for public key authentication String pubkeyAlgos = hostConfig.getProperty(PUBKEY_ACCEPTED_ALGORITHMS); if (!StringUtils.isEmptyOrNull(pubkeyAlgos)) { @@ -56,54 +99,304 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey { log.debug(PUBKEY_ACCEPTED_ALGORITHMS + ' ' + signatures); } setSignatureFactoriesNames(signatures); - } else { - log.warn(format(SshdText.get().configNoKnownAlgorithms, - PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos)); + super.init(session, service); + return; } + log.warn(format(SshdText.get().configNoKnownAlgorithms, + PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos)); + } + // TODO: remove this once we're on an sshd version that has SSHD-1272 + // fixed + List<NamedFactory<Signature>> localFactories = getSignatureFactories(); + if (localFactories == null || localFactories.isEmpty()) { + setSignatureFactoriesNames(session.getSignatureFactoriesNames()); } - // If we don't set signature factories here, the default ones from the - // session will be used. super.init(session, service); - // In sshd 2.7.0, we end up now with a key iterator that uses keys - // provided by an ssh-agent even if IdentitiesOnly is true. So if - // needed, filter out any KeyAgentIdentity. - if (hostConfig.isIdentitiesOnly()) { - Iterator<PublicKeyIdentity> original = keys; - // The original iterator will already have gotten the identities - // from the agent. Unfortunately there's nothing we can do about - // that; it'll have to be fixed upstream. (As will, ultimately, - // respecting isIdentitiesOnly().) At least we can simply not - // use the keys the agent provided. - // - // See https://issues.apache.org/jira/browse/SSHD-1218 - keys = new Iterator<>() { - - private PublicKeyIdentity value; + } + + @Override + protected Iterator<PublicKeyIdentity> createPublicKeyIterator( + ClientSession session, SignatureFactoriesManager manager) + throws Exception { + agent = getAgent(session); + if (agent != null) { + parseAddKeys(hostConfig); + if (addKeysToAgent) { + skProvider = hostConfig.getProperty(SshConstants.SECURITY_KEY_PROVIDER); + } + } + return new KeyIterator(session, manager); + } + + @Override + protected PublicKeyIdentity resolveAttemptedPublicKeyIdentity( + ClientSession session, String service) throws Exception { + PublicKeyIdentity result = getNextKey(session, service); + // This fixes SSHD-1231. Can be removed once we're using Apache MINA + // sshd > 2.8.0. + // + // See https://issues.apache.org/jira/browse/SSHD-1231 + currentAlgorithms.clear(); + return result; + } + + private PublicKeyIdentity getNextKey(ClientSession session, String service) + throws Exception { + PublicKeyIdentity id = super.resolveAttemptedPublicKeyIdentity(session, + service); + if (addKeysToAgent && id != null && !(id instanceof KeyAgentIdentity)) { + KeyPair key = id.getKeyIdentity(); + if (key != null && key.getPublic() != null + && key.getPrivate() != null) { + // We've just successfully loaded a key that wasn't in the + // agent. Add it to the agent. + // + // Keys are added after loading, as in OpenSSH. The alternative + // might be to add a key only after (partially) successful + // authentication? + PublicKey pk = key.getPublic(); + String fingerprint = KeyUtils.getFingerPrint(pk); + String keyType = KeyUtils.getKeyType(key); + try { + // Check that the key is not in the agent already. + if (agentHasKey(pk)) { + return id; + } + if (askBeforeAdding + && (session instanceof JGitClientSession)) { + CredentialsProvider provider = ((JGitClientSession) session) + .getCredentialsProvider(); + CredentialItem.YesNoType question = new CredentialItem.YesNoType( + format(SshdText + .get().pubkeyAuthAddKeyToAgentQuestion, + keyType, fingerprint)); + boolean result = provider != null + && provider.supports(question) + && provider.get(getUri(), question); + if (!result || !question.getValue()) { + // Don't add the key. + return id; + } + } + SshAgentKeyConstraint[] rules = constraints; + if (pk instanceof SecurityKeyPublicKey && !StringUtils.isEmptyOrNull(skProvider)) { + rules = Arrays.copyOf(rules, rules.length + 1); + rules[rules.length - 1] = + new SshAgentKeyConstraint.FidoProviderExtension(skProvider); + } + // Unfortunately a comment associated with the key is lost + // by Apache MINA sshd, and there is also no way to get the + // original file name for keys loaded from a file. So add it + // without comment. + agent.addIdentity(key, null, rules); + } catch (IOException e) { + // Do not re-throw: we don't want authentication to fail if + // we cannot add the key to the agent. + log.error( + format(SshdText.get().pubkeyAuthAddKeyToAgentError, + keyType, fingerprint), + e); + // Note that as of Win32-OpenSSH 8.6 and Pageant 0.76, + // neither can handle key constraints. Pageant fails + // gracefully, not adding the key and returning + // SSH_AGENT_FAILURE. Win32-OpenSSH closes the connection + // without even returning a failure message, which violates + // the SSH agent protocol and makes all subsequent requests + // to the agent fail. + } + } + } + return id; + } + + private boolean agentHasKey(PublicKey pk) throws IOException { + Iterable<? extends Map.Entry<PublicKey, String>> ids = agent + .getIdentities(); + if (ids == null) { + return false; + } + Iterator<? extends Map.Entry<PublicKey, String>> iter = ids.iterator(); + while (iter.hasNext()) { + if (KeyUtils.compareKeys(iter.next().getKey(), pk)) { + return true; + } + } + return false; + } + + private URIish getUri() { + String uri = SshConstants.SSH_SCHEME + "://"; //$NON-NLS-1$ + String userName = hostConfig.getUsername(); + if (!StringUtils.isEmptyOrNull(userName)) { + uri += userName + '@'; + } + uri += hostConfig.getHost(); + int port = hostConfig.getPort(); + if (port > 0 && port != SshConstants.SSH_DEFAULT_PORT) { + uri += ":" + port; //$NON-NLS-1$ + } + try { + return new URIish(uri); + } catch (URISyntaxException e) { + log.error(e.getLocalizedMessage(), e); + } + return new URIish(); + } + + private SshAgent getAgent(ClientSession session) throws Exception { + FactoryManager manager = Objects.requireNonNull( + session.getFactoryManager(), "No session factory manager"); //$NON-NLS-1$ + SshAgentFactory factory = manager.getAgentFactory(); + if (factory == null) { + return null; + } + return factory.createClient(session, manager); + } + + private void parseAddKeys(HostConfigEntry config) { + String value = config.getProperty(SshConstants.ADD_KEYS_TO_AGENT); + if (StringUtils.isEmptyOrNull(value)) { + addKeysToAgent = false; + return; + } + String[] values = value.split(","); //$NON-NLS-1$ + List<SshAgentKeyConstraint> rules = new ArrayList<>(2); + switch (values[0]) { + case "yes": //$NON-NLS-1$ + addKeysToAgent = true; + break; + case "no": //$NON-NLS-1$ + addKeysToAgent = false; + break; + case "ask": //$NON-NLS-1$ + addKeysToAgent = true; + askBeforeAdding = true; + break; + case "confirm": //$NON-NLS-1$ + addKeysToAgent = true; + rules.add(SshAgentKeyConstraint.CONFIRM); + if (values.length > 1) { + int seconds = OpenSshConfigFile.timeSpec(values[1]); + if (seconds > 0) { + rules.add(new SshAgentKeyConstraint.LifeTime(seconds)); + } + } + break; + default: + int seconds = OpenSshConfigFile.timeSpec(values[0]); + if (seconds > 0) { + addKeysToAgent = true; + rules.add(new SshAgentKeyConstraint.LifeTime(seconds)); + } + break; + } + constraints = rules.toArray(new SshAgentKeyConstraint[0]); + } + + @Override + protected void releaseKeys() throws IOException { + addKeysToAgent = false; + askBeforeAdding = false; + skProvider = null; + constraints = null; + try { + if (agent != null) { + try { + agent.close(); + } finally { + agent = null; + } + } + } finally { + super.releaseKeys(); + } + } + + private class KeyIterator extends UserAuthPublicKeyIterator { + + private Iterable<? extends Map.Entry<PublicKey, String>> agentKeys; + + // If non-null, all the public keys from explicitly given key files. Any + // agent key not matching one of these public keys will be ignored in + // getIdentities(). + private Collection<PublicKey> identityFiles; + + public KeyIterator(ClientSession session, + SignatureFactoriesManager manager) + throws Exception { + super(session, manager); + } + + private List<PublicKey> getExplicitKeys( + Collection<String> explicitFiles) { + if (explicitFiles == null) { + return null; + } + return explicitFiles.stream().map(s -> { + try { + Path p = Paths.get(s + ".pub"); //$NON-NLS-1$ + if (Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) { + return AuthorizedKeyEntry.readAuthorizedKeys(p).get(0) + .resolvePublicKey(null, + PublicKeyEntryResolver.IGNORING); + } + } catch (InvalidPathException | IOException + | GeneralSecurityException e) { + log.warn(format(SshdText.get().cannotReadPublicKey, s), e); + } + return null; + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + protected Iterable<KeyAgentIdentity> initializeAgentIdentities( + ClientSession session) throws IOException { + if (agent == null) { + return null; + } + agentKeys = agent.getIdentities(); + if (hostConfig != null && hostConfig.isIdentitiesOnly()) { + identityFiles = getExplicitKeys(hostConfig.getIdentities()); + } + return () -> new Iterator<>() { + + private final Iterator<? extends Map.Entry<PublicKey, String>> iter = agentKeys + .iterator(); + + private Map.Entry<PublicKey, String> next; @Override public boolean hasNext() { - if (value != null) { - return true; - } - PublicKeyIdentity next = null; - while (original.hasNext()) { - next = original.next(); - if (!(next instanceof KeyAgentIdentity)) { - value = next; + while (next == null && iter.hasNext()) { + Map.Entry<PublicKey, String> val = iter.next(); + PublicKey pk = val.getKey(); + // This checks against all explicit keys for any agent + // key, but since identityFiles.size() is typically 1, + // it should be fine. + if (identityFiles == null || identityFiles.stream() + .anyMatch(k -> KeyUtils.compareKeys(k, pk))) { + next = val; return true; } + if (log.isTraceEnabled()) { + log.trace( + "Ignoring SSH agent {} key not in explicit IdentityFile in SSH config: {}", //$NON-NLS-1$ + KeyUtils.getKeyType(pk), + KeyUtils.getFingerPrint(pk)); + } } - return false; + return next != null; } @Override - public PublicKeyIdentity next() { - if (hasNext()) { - PublicKeyIdentity result = value; - value = null; - return result; + public KeyAgentIdentity next() { + if (!hasNext()) { + throw new NoSuchElementException(); } - throw new NoSuchElementException(); + KeyAgentIdentity result = new KeyAgentIdentity(agent, + next.getKey(), next.getValue()); + next = null; + return result; } }; } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java index 71e8e61585..72f0bdb6ee 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -87,6 +87,11 @@ public class JGitSshClient extends SshClient { public static final AttributeKey<String> PREFERRED_AUTHENTICATIONS = new AttributeKey<>(); /** + * An attribute key for the home directory. + */ + public static final AttributeKey<Path> HOME_DIRECTORY = new AttributeKey<>(); + + /** * An attribute key for storing an alternate local address to connect to if * a local forward from a ProxyJump ssh config is present. If set, * {@link #connect(HostConfigEntry, AttributeRepository, SocketAddress)} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java index c51a75bc6f..2a725ea16a 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -120,15 +120,16 @@ public class JGitUserInteraction implements UserInteraction { return null; }).filter(s -> s != null).toArray(String[]::new); } - // TODO What to throw to abort the connection/authentication process? - // In UserAuthKeyboardInteractive.getUserResponses() it's clear that - // returning null is valid and signifies "an error"; we'll try the - // next authentication method. But if the user explicitly canceled, - // then we don't want to try the next methods... - // - // Probably not a serious issue with the typical order of public-key, - // keyboard-interactive, password. - return null; + throw new AuthenticationCanceledException(); + } + + @Override + public String resolveAuthPasswordAttempt(ClientSession session) + throws Exception { + String[] results = interactive(session, null, null, "", //$NON-NLS-1$ + new String[] { SshdText.get().passwordPrompt }, + new boolean[] { false }); + return (results == null || results.length == 0) ? null : results[0]; } @Override diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java index 00ee62d6dd..39332d9fca 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -29,7 +29,25 @@ public final class SshdText extends TranslationBundle { // @formatter:off /***/ public String authenticationCanceled; /***/ public String authenticationOnClosedSession; + /***/ public String authGssApiAttempt; + /***/ public String authGssApiExhausted; + /***/ public String authGssApiFailure; + /***/ public String authGssApiNotTried; + /***/ public String authGssApiPartialSuccess; + /***/ public String authPasswordAttempt; + /***/ public String authPasswordChangeAttempt; + /***/ public String authPasswordExhausted; + /***/ public String authPasswordFailure; + /***/ public String authPasswordNotTried; + /***/ public String authPasswordPartialSuccess; + /***/ public String authPubkeyAttempt; + /***/ public String authPubkeyAttemptAgent; + /***/ public String authPubkeyExhausted; + /***/ public String authPubkeyFailure; + /***/ public String authPubkeyNoKeys; + /***/ public String authPubkeyPartialSuccess; /***/ public String closeListenerFailed; + /***/ public String cannotReadPublicKey; /***/ public String configInvalidPath; /***/ public String configInvalidPattern; /***/ public String configInvalidPositive; @@ -98,6 +116,8 @@ public final class SshdText extends TranslationBundle { /***/ public String proxySocksUnexpectedMessage; /***/ public String proxySocksUnexpectedVersion; /***/ public String proxySocksUsernameTooLong; + /***/ public String pubkeyAuthAddKeyToAgentError; + /***/ public String pubkeyAuthAddKeyToAgentQuestion; /***/ public String pubkeyAuthWrongCommand; /***/ public String pubkeyAuthWrongKey; /***/ public String pubkeyAuthWrongSignatureAlgorithm; @@ -106,9 +126,13 @@ public final class SshdText extends TranslationBundle { /***/ public String serverIdWithNul; /***/ public String sessionCloseFailed; /***/ public String sessionWithoutUsername; + /***/ public String sshAgentEdDSAFormatError; + /***/ public String sshAgentPayloadLengthError; /***/ public String sshAgentReplyLengthError; /***/ public String sshAgentReplyUnexpected; /***/ public String sshAgentShortReadBuffer; + /***/ public String sshAgentUnknownKey; + /***/ public String sshAgentWrongKeyLength; /***/ public String sshAgentWrongNumberOfKeys; /***/ public String sshClosingDown; /***/ public String sshCommandTimeout; diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java index 1ed2ab9d78..a0ffd540f2 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java @@ -17,10 +17,14 @@ import java.util.List; import org.apache.sshd.agent.SshAgent; import org.apache.sshd.agent.SshAgentFactory; import org.apache.sshd.agent.SshAgentServer; +import org.apache.sshd.client.config.hosts.HostConfigEntry; import org.apache.sshd.common.FactoryManager; import org.apache.sshd.common.channel.ChannelFactory; import org.apache.sshd.common.session.ConnectionService; +import org.apache.sshd.common.session.Session; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.internal.transport.sshd.JGitClientSession; +import org.eclipse.jgit.transport.SshConstants; import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; /** @@ -49,18 +53,24 @@ public class JGitSshAgentFactory implements SshAgentFactory { @Override public List<ChannelFactory> getChannelForwardingFactories( FactoryManager manager) { - // No agent forwarding supported yet. + // No agent forwarding supported. return Collections.emptyList(); } @Override - public SshAgent createClient(FactoryManager manager) throws IOException { - // sshd 2.8.0 will pass us the session here. At that point, we can get - // the HostConfigEntry and extract and handle the IdentityAgent setting. - // For now, pass null to let the ConnectorFactory do its default - // behavior (Pageant on Windows, SSH_AUTH_SOCK on Unixes with the - // jgit-builtin factory). - return new SshAgentClient(factory.create(null, homeDir)); + public SshAgent createClient(Session session, FactoryManager manager) + throws IOException { + String identityAgent = null; + if (session instanceof JGitClientSession) { + HostConfigEntry hostConfig = ((JGitClientSession) session) + .getHostConfigEntry(); + identityAgent = hostConfig.getProperty(SshConstants.IDENTITY_AGENT, + null); + } + if (SshConstants.NONE.equals(identityAgent)) { + return null; + } + return new SshAgentClient(factory.create(identityAgent, homeDir)); } @Override diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java index 08483e4c20..cbcb4d240e 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java @@ -11,10 +11,12 @@ package org.eclipse.jgit.internal.transport.sshd.agent; import java.io.IOException; import java.security.KeyPair; +import java.security.PrivateKey; import java.security.PublicKey; import java.text.MessageFormat; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -22,21 +24,27 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.apache.sshd.agent.SshAgent; import org.apache.sshd.agent.SshAgentConstants; +import org.apache.sshd.agent.SshAgentKeyConstraint; import org.apache.sshd.common.SshException; import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.BufferException; import org.apache.sshd.common.util.buffer.BufferUtils; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.apache.sshd.common.util.buffer.keys.BufferPublicKeyParser; +import org.apache.sshd.common.util.io.der.DERParser; import org.eclipse.jgit.internal.transport.sshd.SshdText; import org.eclipse.jgit.transport.sshd.agent.Connector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * A client for an SSH2 agent. This client supports only querying identities and - * signature requests. + * A client for an SSH2 agent. This client supports querying identities, + * signature requests, and adding keys to an agent (with or without + * constraints). Removing keys is not supported, and the older SSH1 protocol is + * not supported. * * @see <a href="https://tools.ietf.org/html/draft-miller-ssh-agent-04">SSH * Agent Protocol, RFC draft</a> @@ -72,11 +80,18 @@ public class SshAgentClient implements SshAgent { } return false; } - boolean connected = connector != null && connector.connect(); - if (!connected) { + boolean connected; + try { + connected = connector != null && connector.connect(); + if (!connected && debugging) { + LOG.debug("No SSH agent"); //$NON-NLS-1$ + } + } catch (IOException e) { + // Agent not running? if (debugging) { - LOG.debug("No SSH agent (SSH_AUTH_SOCK not set)"); //$NON-NLS-1$ + LOG.debug("No SSH agent", e); //$NON-NLS-1$ } + throw e; } return connected; } @@ -127,14 +142,17 @@ public class SshAgentClient implements SshAgent { List<Map.Entry<PublicKey, String>> keys = new ArrayList<>( numberOfKeys); for (int i = 0; i < numberOfKeys; i++) { - PublicKey key = reply.getPublicKey(); + PublicKey key = readKey(reply); String comment = reply.getString(); - if (tracing) { - LOG.trace("Got SSH agent {} key: {} {}", //$NON-NLS-1$ - KeyUtils.getKeyType(key), - KeyUtils.getFingerPrint(key), comment); + if (key != null) { + if (tracing) { + LOG.trace("Got SSH agent {} key: {} {}", //$NON-NLS-1$ + KeyUtils.getKeyType(key), + KeyUtils.getFingerPrint(key), comment); + } + keys.add(new AbstractMap.SimpleImmutableEntry<>(key, + comment)); } - keys.add(new AbstractMap.SimpleImmutableEntry<>(key, comment)); } return keys; } catch (BufferException e) { @@ -216,6 +234,222 @@ public class SshAgentClient implements SshAgent { } } + @Override + public void addIdentity(KeyPair key, String comment, + SshAgentKeyConstraint... constraints) throws IOException { + boolean debugging = LOG.isDebugEnabled(); + if (!open(debugging)) { + return; + } + + // Neither Pageant 0.76 nor Win32-OpenSSH 8.6 support command + // SSH2_AGENTC_ADD_ID_CONSTRAINED. Adding a key with constraints will + // fail. The only work-around for users is not to use "confirm" or "time + // spec" with AddKeysToAgent, and not to use sk-* keys. + // + // With a true OpenSSH SSH agent, key constraints work. + byte cmd = (constraints != null && constraints.length > 0) + ? SshAgentConstants.SSH2_AGENTC_ADD_ID_CONSTRAINED + : SshAgentConstants.SSH2_AGENTC_ADD_IDENTITY; + byte[] message = null; + ByteArrayBuffer msg = new ByteArrayBuffer(); + try { + msg.putInt(0); + msg.putByte(cmd); + String keyType = KeyUtils.getKeyType(key); + if (KeyPairProvider.SSH_ED25519.equals(keyType)) { + // Apache MINA sshd 2.8.0 lacks support for writing ed25519 + // private keys to a buffer. + putEd25519Key(msg, key); + } else { + msg.putKeyPair(key); + } + msg.putString(comment == null ? "" : comment); //$NON-NLS-1$ + if (constraints != null) { + for (SshAgentKeyConstraint constraint : constraints) { + constraint.put(msg); + } + } + if (debugging) { + LOG.debug( + "addIdentity: adding {} key {} to SSH agent; comment {}", //$NON-NLS-1$ + keyType, KeyUtils.getFingerPrint(key.getPublic()), + comment); + } + message = msg.getCompactData(); + } finally { + // The message contains the private key data, so clear intermediary + // data ASAP. + msg.clear(); + } + Buffer reply; + try { + reply = rpc(cmd, message); + } finally { + Arrays.fill(message, (byte) 0); + } + int replyLength = reply.available(); + if (replyLength != 1) { + throw new SshException(MessageFormat.format( + SshdText.get().sshAgentReplyUnexpected, + MessageFormat.format( + SshdText.get().sshAgentPayloadLengthError, + Integer.valueOf(1), Integer.valueOf(replyLength)))); + + } + cmd = reply.getByte(); + if (cmd != SshAgentConstants.SSH_AGENT_SUCCESS) { + throw new SshException( + MessageFormat.format(SshdText.get().sshAgentReplyUnexpected, + SshAgentConstants.getCommandMessageName(cmd))); + } + } + + /** + * Writes an ed25519 {@link KeyPair} to a {@link Buffer}. OpenSSH specifies + * that it expects the 32 public key bytes, followed by 64 bytes formed by + * concatenating the 32 private key bytes with the 32 public key bytes. + * + * @param msg + * {@link Buffer} to write to + * @param key + * {@link KeyPair} to write + * @throws IOException + * if the private key cannot be written + */ + private static void putEd25519Key(Buffer msg, KeyPair key) + throws IOException { + Buffer tmp = new ByteArrayBuffer(36); + tmp.putRawPublicKeyBytes(key.getPublic()); + byte[] publicBytes = tmp.getBytes(); + msg.putString(KeyPairProvider.SSH_ED25519); + msg.putBytes(publicBytes); + // Next is the concatenation of the 32 byte private key value with the + // 32 bytes of the public key. + PrivateKey pk = key.getPrivate(); + String format = pk.getFormat(); + if (!"PKCS#8".equalsIgnoreCase(format)) { //$NON-NLS-1$ + throw new IOException(MessageFormat + .format(SshdText.get().sshAgentEdDSAFormatError, format)); + } + byte[] privateBytes = null; + byte[] encoded = pk.getEncoded(); + try { + privateBytes = asn1Parse(encoded, 32); + byte[] combined = Arrays.copyOf(privateBytes, 64); + Arrays.fill(privateBytes, (byte) 0); + privateBytes = combined; + System.arraycopy(publicBytes, 0, privateBytes, 32, 32); + msg.putBytes(privateBytes); + } finally { + if (privateBytes != null) { + Arrays.fill(privateBytes, (byte) 0); + } + Arrays.fill(encoded, (byte) 0); + } + } + + /** + * Extracts the private key bytes from an encoded ed25519 private key by + * parsing the bytes as ASN.1 according to RFC 5958 (PKCS #8 encoding): + * + * <pre> + * OneAsymmetricKey ::= SEQUENCE { + * version Version, + * privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, + * privateKey PrivateKey, + * ... + * } + * + * Version ::= INTEGER + * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier + * PrivateKey ::= OCTET STRING + * + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, + * parameters ANY DEFINED BY algorithm OPTIONAL + * } + * </pre> + * <p> + * and RFC 8410: "... when encoding a OneAsymmetricKey object, the private + * key is wrapped in a CurvePrivateKey object and wrapped by the OCTET + * STRING of the 'privateKey' field." + * </p> + * + * <pre> + * CurvePrivateKey ::= OCTET STRING + * </pre> + * + * @param encoded + * encoded private key to extract the private key bytes from + * @param n + * number of bytes expected + * @return the extracted private key bytes; of length {@code n} + * @throws IOException + * if the private key cannot be extracted + * @see <a href="https://tools.ietf.org/html/rfc5958">RFC 5958</a> + * @see <a href="https://tools.ietf.org/html/rfc8410">RFC 8410</a> + */ + private static byte[] asn1Parse(byte[] encoded, int n) throws IOException { + byte[] privateKey = null; + try (DERParser byteParser = new DERParser(encoded); + DERParser oneAsymmetricKey = byteParser.readObject() + .createParser()) { + oneAsymmetricKey.readObject(); // skip version + oneAsymmetricKey.readObject(); // skip algorithm identifier + privateKey = oneAsymmetricKey.readObject().getValue(); + // The last n bytes of this must be the private key bytes + return Arrays.copyOfRange(privateKey, + privateKey.length - n, privateKey.length); + } finally { + if (privateKey != null) { + Arrays.fill(privateKey, (byte) 0); + } + } + } + + /** + * A safe version of {@link Buffer#getPublicKey()}. Upon return the + * buffers's read position is always after the key blob; any exceptions + * thrown by trying to read the key are logged and <em>not</em> propagated. + * <p> + * This is needed because an SSH agent might contain and deliver keys that + * we cannot handle (for instance ed448 keys). + * </p> + * + * @param buffer + * to read the key from + * @return the {@link PublicKey}, or {@code null} if the key could not be + * read + * @throws BufferException + * if the length of the key blob cannot be read or is corrupted + */ + private static PublicKey readKey(Buffer buffer) throws BufferException { + int endOfBuffer = buffer.wpos(); + int keyLength = buffer.getInt(); + int afterKey = buffer.rpos() + keyLength; + if (keyLength <= 0 || afterKey > endOfBuffer) { + throw new BufferException( + MessageFormat.format(SshdText.get().sshAgentWrongKeyLength, + Integer.toString(keyLength), + Integer.toString(buffer.rpos()), + Integer.toString(endOfBuffer))); + } + // Limit subsequent reads to the public key blob + buffer.wpos(afterKey); + try { + return buffer.getRawPublicKey(BufferPublicKeyParser.DEFAULT); + } catch (Exception e) { + LOG.warn(SshdText.get().sshAgentUnknownKey, e); + return null; + } finally { + // Restore real buffer end + buffer.wpos(endOfBuffer); + // Set the read position to after this key, even if failed + buffer.rpos(afterKey); + } + } + private Buffer rpc(byte command, byte[] message) throws IOException { return new ByteArrayBuffer(connector.rpc(command, message)); } @@ -230,11 +464,6 @@ public class SshAgentClient implements SshAgent { } @Override - public void addIdentity(KeyPair key, String comment) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override public void removeIdentity(PublicKey key) throws IOException { throw new UnsupportedOperationException(); } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java index c270b44956..b94ccc6d4f 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -51,6 +51,9 @@ import org.apache.sshd.sftp.client.SftpClientFactory; import org.apache.sshd.sftp.common.SftpException; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; +import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException; +import org.eclipse.jgit.internal.transport.sshd.AuthenticationLogger; import org.eclipse.jgit.internal.transport.sshd.JGitSshClient; import org.eclipse.jgit.internal.transport.sshd.SshdText; import org.eclipse.jgit.transport.FtpChannel; @@ -118,6 +121,7 @@ public class SshdSession implements RemoteSession2 { ClientSession resultSession = null; ClientSession proxySession = null; PortForwardingTracker portForward = null; + AuthenticationLogger authLog = null; try { if (!hops.isEmpty()) { URIish hop = hops.remove(0); @@ -138,7 +142,11 @@ public class SshdSession implements RemoteSession2 { JGitSshClient.LOCAL_FORWARD_ADDRESS, portForward.getBoundAddress()); } - resultSession = connect(hostConfig, context, timeout); + int timeoutInSec = OpenSshConfigFile.timeSpec( + hostConfig.getProperty(SshConstants.CONNECT_TIMEOUT)); + resultSession = connect(hostConfig, context, + timeoutInSec > 0 ? Duration.ofSeconds(timeoutInSec) + : timeout); if (proxySession != null) { final PortForwardingTracker tracker = portForward; final ClientSession pSession = proxySession; @@ -160,6 +168,7 @@ public class SshdSession implements RemoteSession2 { resultSession.addCloseFutureListener(listener); } // Authentication timeout is by default 2 minutes. + authLog = new AuthenticationLogger(resultSession); resultSession.auth().verify(resultSession.getAuthTimeout()); return resultSession; } catch (IOException e) { @@ -168,15 +177,32 @@ public class SshdSession implements RemoteSession2 { close(resultSession, e); if (e instanceof SshException && ((SshException) e) .getDisconnectCode() == SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) { - // Ensure the user gets to know on which URI the authentication - // was denied. + String message = format(SshdText.get().loginDenied, host, + Integer.toString(port)); throw new TransportException(target, - format(SshdText.get().loginDenied, host, - Integer.toString(port)), - e); + withAuthLog(message, authLog), e); + } else if (e instanceof SshException && e + .getCause() instanceof AuthenticationCanceledException) { + String message = e.getCause().getMessage(); + throw new TransportException(target, + withAuthLog(message, authLog), e.getCause()); } throw e; + } finally { + if (authLog != null) { + authLog.clear(); + } + } + } + + private String withAuthLog(String message, AuthenticationLogger authLog) { + if (authLog != null) { + String log = String.join(System.lineSeparator(), authLog.getLog()); + if (!log.isEmpty()) { + return message + System.lineSeparator() + log; + } } + return message; } private ClientSession connect(HostConfigEntry config, diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java index 58cf8e1ddd..c792c1889c 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -13,6 +13,7 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.security.KeyPair; import java.time.Duration; @@ -34,7 +35,6 @@ import org.apache.sshd.client.auth.UserAuthFactory; import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory; import org.apache.sshd.client.config.hosts.HostConfigEntryResolver; import org.apache.sshd.common.NamedFactory; -import org.apache.sshd.common.SshException; import org.apache.sshd.common.compression.BuiltinCompressions; import org.apache.sshd.common.config.keys.FilePasswordProvider; import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions; @@ -44,7 +44,6 @@ import org.apache.sshd.common.signature.Signature; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; -import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException; import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider; import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory; import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory; @@ -243,6 +242,12 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { JGitSshClient.PREFERRED_AUTHENTICATIONS, defaultAuths); } + try { + jgitClient.setAttribute(JGitSshClient.HOME_DIRECTORY, + home.getAbsoluteFile().toPath()); + } catch (SecurityException | InvalidPathException e) { + // Ignore + } // Other things? return client; }); @@ -255,13 +260,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { if (e instanceof TransportException) { throw (TransportException) e; } - Throwable cause = e; - if (e instanceof SshException && e - .getCause() instanceof AuthenticationCanceledException) { - // Results in a nicer error message - cause = e.getCause(); - } - throw new TransportException(uri, cause.getMessage(), cause); + throw new TransportException(uri, e.getMessage(), e); } } |