Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read IdentityFile configuration directive to limit authentication attempts using agent #13947

Merged
merged 4 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 19 additions & 26 deletions ssh/src/main/java/ch/cyberduck/core/sftp/SFTPSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.exception.ConnectionRefusedException;
import ch.cyberduck.core.exception.InteroperabilityException;
import ch.cyberduck.core.exception.LocalAccessDeniedException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.exception.LoginFailureException;
import ch.cyberduck.core.features.*;
Expand All @@ -41,7 +40,6 @@
import ch.cyberduck.core.sftp.openssh.OpenSSHAgentAuthenticator;
import ch.cyberduck.core.sftp.openssh.OpenSSHCredentialsConfigurator;
import ch.cyberduck.core.sftp.openssh.OpenSSHHostnameConfigurator;
import ch.cyberduck.core.sftp.openssh.OpenSSHIdentitiesOnlyConfigurator;
import ch.cyberduck.core.sftp.openssh.OpenSSHIdentityAgentConfigurator;
import ch.cyberduck.core.sftp.openssh.OpenSSHJumpHostConfigurator;
import ch.cyberduck.core.sftp.openssh.OpenSSHPreferredAuthenticationsConfigurator;
Expand Down Expand Up @@ -277,30 +275,25 @@ private void authenticate(final SSHClient client, final Host host, final LoginCa
// Ordered list of preferred authentication methods
final List<AuthenticationProvider<Boolean>> defaultMethods = new ArrayList<>();
if(preferences.getBoolean("ssh.authentication.agent.enable")) {
if(new OpenSSHIdentitiesOnlyConfigurator().isIdentitiesOnly(host.getHostname())) {
log.warn("Skip reading keys from SSH agent with IdentitiesOnly configuration");
}
else {
switch(Factory.Platform.getDefault()) {
case windows:
defaultMethods.add(new SFTPAgentAuthentication(client, new PageantAuthenticator()));
try {
defaultMethods.add(new SFTPAgentAuthentication(client, new WindowsOpenSSHAgentAuthenticator()));
}
catch(AgentProxyException e) {
log.warn(String.format("Agent proxy failed with %s", e));
}
break;
default:
try {
defaultMethods.add(new SFTPAgentAuthentication(client, new OpenSSHAgentAuthenticator(
new OpenSSHIdentityAgentConfigurator().getIdentityAgent(host.getHostname()))));
}
catch(AgentProxyException e) {
log.warn(String.format("Agent proxy failed with %s", e));
}
break;
}
switch(Factory.Platform.getDefault()) {
case windows:
defaultMethods.add(new SFTPAgentAuthentication(client, new PageantAuthenticator()));
try {
defaultMethods.add(new SFTPAgentAuthentication(client, new WindowsOpenSSHAgentAuthenticator()));
}
catch(AgentProxyException e) {
log.warn(String.format("Agent proxy failed with %s", e));
}
break;
default:
try {
defaultMethods.add(new SFTPAgentAuthentication(client, new OpenSSHAgentAuthenticator(
new OpenSSHIdentityAgentConfigurator().getIdentityAgent(host.getHostname()))));
}
catch(AgentProxyException e) {
log.warn(String.format("Agent proxy failed with %s", e));
}
break;
}
}
defaultMethods.add(new SFTPPublicKeyAuthentication(client));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,44 @@
*/

import ch.cyberduck.core.AuthenticationProvider;
import ch.cyberduck.core.Credentials;
import ch.cyberduck.core.DefaultIOExceptionMappingService;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.Local;
import ch.cyberduck.core.LocalFactory;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.exception.AccessDeniedException;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.sftp.SFTPExceptionMappingService;
import ch.cyberduck.core.sftp.openssh.OpenSSHCredentialsConfigurator;
import ch.cyberduck.core.sftp.openssh.OpenSSHIdentitiesOnlyConfigurator;
import ch.cyberduck.core.threading.CancelCallback;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyFileUtil;
import com.jcraft.jsch.agentproxy.CustomIdentity;
import com.jcraft.jsch.agentproxy.Identity;
import com.jcraft.jsch.agentproxy.sshj.AuthAgent;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.Base64;
import net.schmizz.sshj.common.Buffer;
import net.schmizz.sshj.transport.TransportException;
import net.schmizz.sshj.userauth.UserAuthException;
import net.schmizz.sshj.userauth.keyprovider.KeyFormat;
import net.schmizz.sshj.userauth.keyprovider.KeyProviderUtil;

public class SFTPAgentAuthentication implements AuthenticationProvider<Boolean> {
private static final Logger log = LogManager.getLogger(SFTPAgentAuthentication.class);
Expand All @@ -50,12 +67,36 @@ public SFTPAgentAuthentication(final SSHClient client, final AgentAuthenticator
}

@Override
public Boolean authenticate(final Host bookmark, final LoginCallback prompt, final CancelCallback cancel)
throws BackgroundException {
public Boolean authenticate(final Host bookmark, final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException {
if(log.isDebugEnabled()) {
log.debug(String.format("Login using agent %s for %s", agent, bookmark));
}
for(Identity identity : this.filterIdentities(bookmark, agent.getIdentities())) {
final Collection<Identity> identities;
if(new OpenSSHIdentitiesOnlyConfigurator().isIdentitiesOnly(bookmark.getHostname())) {
final Credentials configuration = new OpenSSHCredentialsConfigurator().configure(bookmark);
if(configuration.isPublicKeyAuthentication()) {
try {
final Local identity = configuration.getIdentity();
if(log.isWarnEnabled()) {
log.warn(String.format("Only read specific key %s from SSH agent with IdentitiesOnly configuration", identity));
}
identities = this.isPrivateKey(identity) ?
this.identityFromPrivateKey(identity) :
this.identityFromPublicKey(identity);
}
catch(IOException e) {
throw new DefaultIOExceptionMappingService().map(e);
}
}
else {
log.warn(String.format("Missing IdentityFile configuration for %s", bookmark));
identities = Collections.emptyList();
}
}
else {
identities = this.filter(bookmark.getCredentials(), agent.getIdentities());
}
for(Identity identity : identities) {
try {
client.auth(bookmark.getCredentials().getUsername(), new AuthAgent(agent.getProxy(), identity));
// Successfully authenticated
Expand All @@ -80,9 +121,9 @@ public String getMethod() {
return "publickey";
}

protected Collection<Identity> filterIdentities(final Host bookmark, final Collection<Identity> identities) {
if(bookmark.getCredentials().isPublicKeyAuthentication()) {
final Local selected = bookmark.getCredentials().getIdentity();
protected Collection<Identity> filter(final Credentials credentials, final Collection<Identity> identities) {
if(credentials.isPublicKeyAuthentication()) {
final Local selected = credentials.getIdentity();
for(Identity identity : identities) {
if(identity.getComment() != null) {
final String candidate = new String(identity.getComment(), StandardCharsets.UTF_8);
Expand All @@ -97,4 +138,33 @@ protected Collection<Identity> filterIdentities(final Host bookmark, final Colle
}
return identities;
}
}

private boolean isPrivateKey(final Local identity) throws AccessDeniedException, IOException {
final KeyFormat format = KeyProviderUtil.detectKeyFileFormat(
new InputStreamReader(identity.getInputStream()), true);
return format != KeyFormat.Unknown;
}

private Collection<Identity> identityFromPrivateKey(final Local identity) throws IOException, AccessDeniedException {
final File pubKey = OpenSSHKeyFileUtil.getPublicKeyFile(new File(identity.getAbsolute()));
if(pubKey != null) {
return this.identityFromPublicKey(LocalFactory.get(pubKey.getAbsolutePath()));
}
log.warn(String.format("Unable to find public key file for identity %s", identity));
return Collections.emptyList();
}

private Collection<Identity> identityFromPublicKey(final Local identity) throws IOException, AccessDeniedException {
final List<String> lines = IOUtils.readLines(identity.getInputStream(), Charset.defaultCharset());
for(String line : lines) {
final String keydata = line.trim();
if(StringUtils.isNotBlank(keydata)) {
String[] parts = keydata.split("\\s+");
if(parts.length >= 2) {
return Collections.singletonList(new CustomIdentity(Base64.decode(parts[1])));
}
}
}
throw new IOException(String.format("Failure reading public key %s", identity));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.jcraft.jsch.agentproxy;

/*
* Copyright (c) 2002-2022 iterate GmbH. All rights reserved.
* https://cyberduck.io/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/

public final class CustomIdentity extends Identity {
public CustomIdentity(byte[] blob) {
super(blob, blob);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
*/

import ch.cyberduck.core.Credentials;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.Local;
import ch.cyberduck.core.sftp.SFTPProtocol;
import ch.cyberduck.core.sftp.openssh.OpenSSHAgentAuthenticator;

import org.apache.commons.lang3.StringUtils;
Expand All @@ -44,12 +42,12 @@ public class SFTPAgentAuthenticationTest {
@Test
public void filterIdentitiesMatch() {
final SFTPAgentAuthentication authentication = new SFTPAgentAuthentication(new SSHClient(), new OpenSSHAgentAuthenticator(new AgentProxy(null)));
final Host bookmark = new Host(new SFTPProtocol(), StringUtils.EMPTY, new Credentials("user").withIdentity(new Local("mykey") {
final Credentials credentials = new Credentials("user").withIdentity(new Local("mykey") {
@Override
public boolean exists() {
return true;
}
}));
});

final List<Identity> identities = new ArrayList<>();
final Identity nomatch = mock(Identity.class);
Expand All @@ -60,20 +58,20 @@ public boolean exists() {
identities.add(nomatch);
identities.add(match);

final Collection<Identity> filtered = authentication.filterIdentities(bookmark, identities);
final Collection<Identity> filtered = authentication.filter(credentials, identities);
assertEquals(1, filtered.size());
assertArrayEquals(match.getComment(), filtered.iterator().next().getComment());
}

@Test
public void filterIdentitiesNoMatch() {
final SFTPAgentAuthentication authentication = new SFTPAgentAuthentication(new SSHClient(), new OpenSSHAgentAuthenticator(new AgentProxy(null)));
final Host bookmark = new Host(new SFTPProtocol(), StringUtils.EMPTY, new Credentials("user").withIdentity(new Local("mykey") {
final Credentials credentials = new Credentials("user").withIdentity(new Local("mykey") {
@Override
public boolean exists() {
return true;
}
}));
});

final List<Identity> identities = new ArrayList<>();
final Identity nomatch = mock(Identity.class);
Expand All @@ -82,14 +80,14 @@ public boolean exists() {
identities.add(nomatch);
identities.add(nomatch);

final Collection<Identity> filtered = authentication.filterIdentities(bookmark, identities);
final Collection<Identity> filtered = authentication.filter(credentials, identities);
assertEquals(2, filtered.size());
}

@Test
public void filterIdentitiesNoKeySet() {
final SFTPAgentAuthentication authentication = new SFTPAgentAuthentication(new SSHClient(), new OpenSSHAgentAuthenticator(new AgentProxy(null)));
final Host bookmark = new Host(new SFTPProtocol(), StringUtils.EMPTY, new Credentials("user"));
final Credentials credentials = new Credentials("user");

final List<Identity> identities = new ArrayList<>();
final Identity nomatch = mock(Identity.class);
Expand All @@ -98,7 +96,7 @@ public void filterIdentitiesNoKeySet() {
identities.add(nomatch);
identities.add(nomatch);

final Collection<Identity> filtered = authentication.filterIdentities(bookmark, identities);
final Collection<Identity> filtered = authentication.filter(credentials, identities);
assertEquals(2, filtered.size());
}
}