summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.ssh.apache/src/org
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit.ssh.apache/src/org')
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java8
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java204
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java26
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java8
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/pkcs11/Pkcs11Provider.java372
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/pkcs11/SecurityCallback.java246
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java8
7 files changed, 844 insertions, 28 deletions
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 76175cc5b8..c19a04d7e5 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
@@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.function.Supplier;
import org.apache.sshd.client.ClientBuilder;
import org.apache.sshd.client.ClientFactoryManager;
@@ -55,6 +56,7 @@ import org.eclipse.jgit.fnmatch.FileNameMatcher;
import org.eclipse.jgit.internal.transport.sshd.proxy.StatefulProxyConnector;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshConstants;
+import org.eclipse.jgit.transport.sshd.KeyPasswordProvider;
import org.eclipse.jgit.util.StringUtils;
/**
@@ -69,6 +71,12 @@ import org.eclipse.jgit.util.StringUtils;
public class JGitClientSession extends ClientSessionImpl {
/**
+ * Attribute set by {@link JGitSshClient} to make the
+ * {@link KeyPasswordProvider} factory accessible via the session.
+ */
+ public static final AttributeKey<Supplier<KeyPasswordProvider>> KEY_PASSWORD_PROVIDER_FACTORY = new AttributeKey<>();
+
+ /**
* Default setting for the maximum number of bytes to read in the initial
* protocol version exchange. 64kb is what OpenSSH &lt; 8.0 read; OpenSSH
* 8.0 changed it to 8Mb, but that seems excessive for the purpose stated in
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 e2da7991af..9f1df89e33 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, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2023 Thomas Wolf <twolf@apache.org> 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
@@ -10,8 +10,12 @@
package org.eclipse.jgit.internal.transport.sshd;
import static java.text.MessageFormat.format;
+import static org.eclipse.jgit.transport.SshConstants.NONE;
+import static org.eclipse.jgit.transport.SshConstants.PKCS11_PROVIDER;
+import static org.eclipse.jgit.transport.SshConstants.PKCS11_SLOT_LIST_INDEX;
import static org.eclipse.jgit.transport.SshConstants.PUBKEY_ACCEPTED_ALGORITHMS;
+import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
@@ -25,6 +29,7 @@ import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -49,19 +54,25 @@ 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.apache.sshd.common.util.GenericUtils;
import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
+import org.eclipse.jgit.internal.transport.sshd.pkcs11.Pkcs11Provider;
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.FS;
import org.eclipse.jgit.util.StringUtils;
/**
* Custom {@link UserAuthPublicKey} implementation for handling SSH config
- * PubkeyAcceptedAlgorithms and interaction with the SSH agent.
+ * PubkeyAcceptedAlgorithms and interaction with the SSH agent and PKCS11
+ * providers.
*/
public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
+ private static final String LOG_FORMAT = "{}"; //$NON-NLS-1$
+
private SshAgent agent;
private HostConfigEntry hostConfig;
@@ -102,7 +113,7 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
super.init(session, service);
return;
}
- log.warn(format(SshdText.get().configNoKnownAlgorithms,
+ log.warn(LOG_FORMAT, format(SshdText.get().configNoKnownAlgorithms,
PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos));
}
// TODO: remove this once we're on an sshd version that has SSHD-1272
@@ -181,7 +192,7 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
} 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(
+ log.error(LOG_FORMAT,
format(SshdText.get().pubkeyAuthAddKeyToAgentError,
keyType, fingerprint),
e);
@@ -303,13 +314,6 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
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 {
@@ -331,7 +335,8 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
}
} catch (InvalidPathException | IOException
| GeneralSecurityException e) {
- log.warn(format(SshdText.get().cannotReadPublicKey, s), e);
+ log.warn("{}", //$NON-NLS-1$
+ format(SshdText.get().cannotReadPublicKey, s), e);
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toList());
@@ -340,36 +345,40 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
@Override
protected Iterable<KeyAgentIdentity> initializeAgentIdentities(
ClientSession session) throws IOException {
- if (agent == null) {
+ Iterable<KeyAgentIdentity> allAgentKeys = getAgentIdentities();
+ if (allAgentKeys == null) {
return null;
}
- agentKeys = agent.getIdentities();
- if (hostConfig != null && hostConfig.isIdentitiesOnly()) {
- identityFiles = getExplicitKeys(hostConfig.getIdentities());
+ Collection<PublicKey> identityFiles = identitiesOnly();
+ if (GenericUtils.isEmpty(identityFiles)) {
+ return allAgentKeys;
}
+
+ // Only consider agent or PKCS11 keys that match a known public key
+ // file.
return () -> new Iterator<>() {
- private final Iterator<? extends Map.Entry<PublicKey, String>> iter = agentKeys
+ private final Iterator<KeyAgentIdentity> identities = allAgentKeys
.iterator();
- private Map.Entry<PublicKey, String> next;
+ private KeyAgentIdentity next;
@Override
public boolean hasNext() {
- while (next == null && iter.hasNext()) {
- Map.Entry<PublicKey, String> val = iter.next();
- PublicKey pk = val.getKey();
+ while (next == null && identities.hasNext()) {
+ KeyAgentIdentity val = identities.next();
+ PublicKey pk = val.getKeyIdentity().getPublic();
// 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()
+ if (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$
+ "Ignoring SSH agent or PKCS11 {} key not in explicit IdentityFile in SSH config: {}", //$NON-NLS-1$
KeyUtils.getKeyType(pk),
KeyUtils.getFingerPrint(pk));
}
@@ -382,12 +391,157 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
if (!hasNext()) {
throw new NoSuchElementException();
}
- KeyAgentIdentity result = new KeyAgentIdentity(agent,
- next.getKey(), next.getValue());
+ KeyAgentIdentity result = next;
next = null;
return result;
}
};
}
+
+ private Collection<PublicKey> identitiesOnly() {
+ if (hostConfig != null && hostConfig.isIdentitiesOnly()) {
+ return getExplicitKeys(hostConfig.getIdentities());
+ }
+ return Collections.emptyList();
+ }
+
+ private Iterable<KeyAgentIdentity> getAgentIdentities()
+ throws IOException {
+ Iterable<KeyAgentIdentity> pkcs11Keys = getPkcs11Keys();
+ if (agent == null) {
+ return pkcs11Keys;
+ }
+ Iterable<? extends Map.Entry<PublicKey, String>> agentKeys = agent
+ .getIdentities();
+ if (GenericUtils.isEmpty(agentKeys)) {
+ return pkcs11Keys;
+ }
+ Iterable<KeyAgentIdentity> fromAgent = () -> new Iterator<>() {
+
+ private final Iterator<? extends Map.Entry<PublicKey, String>> iter = agentKeys
+ .iterator();
+
+ @Override
+ public boolean hasNext() {
+ return iter.hasNext();
+ }
+
+ @Override
+ public KeyAgentIdentity next() {
+ Map.Entry<PublicKey, String> next = iter.next();
+ return new KeyAgentIdentity(agent, next.getKey(),
+ next.getValue());
+ }
+ };
+ if (GenericUtils.isEmpty(pkcs11Keys)) {
+ return fromAgent;
+ }
+ return () -> new Iterator<>() {
+
+ private final Iterator<Iterator<KeyAgentIdentity>> keyIter = List
+ .of(pkcs11Keys.iterator(), fromAgent.iterator())
+ .iterator();
+
+ private Iterator<KeyAgentIdentity> currentKeys;
+
+ private Boolean hasElement;
+
+ @Override
+ public boolean hasNext() {
+ if (hasElement != null) {
+ return hasElement.booleanValue();
+ }
+ while (currentKeys == null || !currentKeys.hasNext()) {
+ if (keyIter.hasNext()) {
+ currentKeys = keyIter.next();
+ } else {
+ currentKeys = null;
+ hasElement = Boolean.FALSE;
+ return false;
+ }
+ }
+ hasElement = Boolean.TRUE;
+ return true;
+ }
+
+ @Override
+ public KeyAgentIdentity next() {
+ if (hasElement == null && !hasNext()
+ || !hasElement.booleanValue()) {
+ throw new NoSuchElementException();
+ }
+ hasElement = null;
+ KeyAgentIdentity result;
+ try {
+ result = currentKeys.next();
+ } catch (NoSuchElementException e) {
+ result = null;
+ }
+ return result;
+ }
+ };
+ }
+
+ private Iterable<KeyAgentIdentity> getPkcs11Keys() throws IOException {
+ String value = hostConfig.getProperty(PKCS11_PROVIDER);
+ if (StringUtils.isEmptyOrNull(value) || NONE.equals(value)) {
+ return null;
+ }
+ if (value.startsWith("~/") //$NON-NLS-1$
+ || value.startsWith('~' + File.separator)) {
+ value = new File(FS.DETECTED.userHome(), value.substring(2))
+ .toString();
+ }
+ Path library = Paths.get(value);
+ if (!library.isAbsolute()) {
+ throw new IOException(format(SshdText.get().pkcs11NotAbsolute,
+ hostConfig.getHost(), hostConfig.getHostName(),
+ PKCS11_PROVIDER, value));
+ }
+ if (!Files.isRegularFile(library)) {
+ throw new IOException(format(SshdText.get().pkcs11NonExisting,
+ hostConfig.getHost(), hostConfig.getHostName(),
+ PKCS11_PROVIDER, value));
+ }
+ try {
+ int slotListIndex = OpenSshConfigFile.positive(
+ hostConfig.getProperty(PKCS11_SLOT_LIST_INDEX));
+ Pkcs11Provider provider = Pkcs11Provider.getProvider(library,
+ slotListIndex);
+ if (provider == null) {
+ throw new UnsupportedOperationException();
+ }
+ Iterable<KeyAgentIdentity> pkcs11Identities = provider
+ .getKeys(getSession());
+ if (GenericUtils.isEmpty(pkcs11Identities)) {
+ log.warn(LOG_FORMAT, format(SshdText.get().pkcs11NoKeys,
+ hostConfig.getHost(), hostConfig.getHostName(),
+ PKCS11_PROVIDER, value));
+ return null;
+ }
+ return pkcs11Identities;
+ } catch (UnsupportedOperationException e) {
+ throw new UnsupportedOperationException(format(
+ SshdText.get().pkcs11Unsupported, hostConfig.getHost(),
+ hostConfig.getHostName(), PKCS11_PROVIDER, value), e);
+ } catch (Exception e) {
+ checkCancellation(e);
+ throw new IOException(
+ format(SshdText.get().pkcs11FailedInstantiation,
+ hostConfig.getHost(), hostConfig.getHostName(),
+ PKCS11_PROVIDER, value),
+ e);
+ }
+ }
+
+ private void checkCancellation(Throwable e) {
+ Throwable t = e;
+ while (t != null) {
+ if (t instanceof AuthenticationCanceledException) {
+ throw (AuthenticationCanceledException) t;
+ }
+ t = t.getCause();
+ }
+ }
}
}
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 311cf198ae..6e9bd621d2 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
@@ -32,6 +32,7 @@ import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.sshd.client.SshClient;
@@ -58,6 +59,7 @@ import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshConstants;
import org.eclipse.jgit.transport.sshd.KeyCache;
+import org.eclipse.jgit.transport.sshd.KeyPasswordProvider;
import org.eclipse.jgit.transport.sshd.ProxyData;
import org.eclipse.jgit.transport.sshd.ProxyDataFactory;
import org.eclipse.jgit.util.StringUtils;
@@ -103,6 +105,8 @@ public class JGitSshClient extends SshClient {
private CredentialsProvider credentialsProvider;
+ private Supplier<KeyPasswordProvider> keyPasswordProviderFactory;
+
private ProxyDataFactory proxyDatabase;
@Override
@@ -277,6 +281,8 @@ public class JGitSshClient extends SshClient {
}
int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig);
PASSWORD_PROMPTS.set(session, Integer.valueOf(numberOfPasswordPrompts));
+ session.setAttribute(JGitClientSession.KEY_PASSWORD_PROVIDER_FACTORY,
+ getKeyPasswordProviderFactory());
List<Path> identities = hostConfig.getIdentities().stream()
.map(s -> {
try {
@@ -374,6 +380,26 @@ public class JGitSshClient extends SshClient {
}
/**
+ * Sets a supplier for a {@link KeyPasswordProvider} for this client.
+ *
+ * @param factory
+ * to set
+ */
+ public void setKeyPasswordProviderFactory(
+ Supplier<KeyPasswordProvider> factory) {
+ keyPasswordProviderFactory = factory;
+ }
+
+ /**
+ * Retrieves the {@link KeyPasswordProvider} factory of this client.
+ *
+ * @return a factory to create {@link KeyPasswordProvider}s
+ */
+ public Supplier<KeyPasswordProvider> getKeyPasswordProviderFactory() {
+ return keyPasswordProviderFactory;
+ }
+
+ /**
* A {@link SessionFactory} to create our own specialized
* {@link JGitClientSession}s.
*/
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 39332d9fca..34c73fcc1d 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
@@ -90,6 +90,14 @@ public final class SshdText extends TranslationBundle {
/***/ public String knownHostsUserAskCreationPrompt;
/***/ public String loginDenied;
/***/ public String passwordPrompt;
+ /***/ public String pkcs11Error;
+ /***/ public String pkcs11FailedInstantiation;
+ /***/ public String pkcs11GeneralMessage;
+ /***/ public String pkcs11NoKeys;
+ /***/ public String pkcs11NonExisting;
+ /***/ public String pkcs11NotAbsolute;
+ /***/ public String pkcs11Unsupported;
+ /***/ public String pkcs11Warning;
/***/ public String proxyCannotAuthenticate;
/***/ public String proxyHttpFailure;
/***/ public String proxyHttpInvalidUserName;
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/pkcs11/Pkcs11Provider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/pkcs11/Pkcs11Provider.java
new file mode 100644
index 0000000000..eefa3aa868
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/pkcs11/Pkcs11Provider.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2023 Thomas Wolf <twolf@apache.org> 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.pkcs11;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.Provider;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.Signature;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.security.auth.login.FailedLoginException;
+
+import org.apache.sshd.agent.SshAgent;
+import org.apache.sshd.agent.SshAgentKeyConstraint;
+import org.apache.sshd.client.auth.pubkey.KeyAgentIdentity;
+import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.signature.BuiltinSignatures;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.transport.URIish;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Bridge for using a PKCS11 HSM (Hardware Security Module) for public-key
+ * authentication.
+ */
+public class Pkcs11Provider {
+
+ private static final Logger LOG = LoggerFactory
+ .getLogger(Pkcs11Provider.class);
+
+ /**
+ * A dummy agent; exists only because
+ * {@link KeyAgentIdentity#KeyAgentIdentity(SshAgent, PublicKey, String)} requires
+ * a non-{@code null} {@link SshAgent}.
+ */
+ private static final SshAgent NULL_AGENT = new SshAgent() {
+
+ @Override
+ public boolean isOpen() {
+ return true;
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Nothing to do
+ }
+
+ @Override
+ public Iterable<? extends Entry<PublicKey, String>> getIdentities()
+ throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Entry<String, byte[]> sign(SessionContext session, PublicKey key,
+ String algo, byte[] data) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addIdentity(KeyPair key, String comment,
+ SshAgentKeyConstraint... constraints) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeIdentity(PublicKey key) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeAllIdentities() throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ };
+
+ private static final Map<String, Pkcs11Provider> PROVIDERS = new ConcurrentHashMap<>();
+
+ private static final AtomicInteger COUNT = new AtomicInteger();
+
+ /**
+ * Creates a new {@link Pkcs11Provider}.
+ *
+ * @param library
+ * {@link Path} to the library the SunPKCS11 provider shall use
+ * @param slotListIndex
+ * index identifying the token; if &lt; 0, ignored and 0 is used
+ * @return a new {@link Pkcs11Provider}, or {@code null} if SunPKCS11 is not
+ * available
+ * @throws IOException
+ * if the configuration file cannot be created
+ * @throws java.security.ProviderException
+ * if the Java {@link Provider} encounters a problem
+ * @throws UnsupportedOperationException
+ * if PKCS#11 is unsupported
+ */
+ public static Pkcs11Provider getProvider(@NonNull Path library,
+ int slotListIndex) throws IOException {
+ int slotIndex = slotListIndex < 0 ? 0 : slotListIndex;
+ Path libPath = library.toAbsolutePath();
+ String key = libPath.toString() + '/' + slotIndex;
+ return PROVIDERS.computeIfAbsent(key, sharedLib -> {
+ Provider pkcs11 = Security.getProvider("SunPKCS11"); //$NON-NLS-1$
+ if (pkcs11 == null) {
+ throw new UnsupportedOperationException();
+ }
+ // There must not be any spaces in the name.
+ String name = libPath.getFileName().toString().replaceAll("\\s", //$NON-NLS-1$
+ ""); //$NON-NLS-1$
+ name = "JGit-" + slotIndex + '-' + name; //$NON-NLS-1$
+ // SunPKCS11 has a problem with paths containing multiple successive
+ // spaces; it collapses them to a single space.
+ //
+ // However, it also performs property expansion on these paths.
+ // (Seems to be an undocumented feature, though.) A reference like
+ // ${xyz} is replaced by system property "xyz". Use that to work
+ // around the rudimentary config parsing in SunPKCS11.
+ String property = "pkcs11-" + COUNT.incrementAndGet() + '-' + name; //$NON-NLS-1$
+ System.setProperty(property, libPath.toString());
+ // Undocumented feature of the SunPKCS11 provider: if the parameter
+ // to configure() starts with two dashes, it's not a file name but
+ // the configuration directly.
+ String config = "--" //$NON-NLS-1$
+ + "name = " + name + '\n' //$NON-NLS-1$
+ + "library = ${" + property + "}\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + "slotListIndex = " + slotIndex + '\n'; //$NON-NLS-1$
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(
+ "{}: configuring provider with system property {}={} and config:{}{}", //$NON-NLS-1$
+ name, property, libPath, System.lineSeparator(),
+ config);
+ }
+ pkcs11 = pkcs11.configure(config);
+ // Produce an RFC7512 URI. Empty path, module-path must be in
+ // the query.
+ String path = "pkcs11:?module-path=" + libPath; //$NON-NLS-1$
+ if (slotListIndex > 0) {
+ // RFC7512 has nothing for the slot list index; pretend it
+ // was a vendor-specific query attribute.
+ path += "&slot-list-index=" + slotListIndex; //$NON-NLS-1$
+ }
+ SecurityCallback callback = new SecurityCallback(
+ new URIish().setPath(path));
+ return new Pkcs11Provider(pkcs11, callback);
+ });
+ }
+
+ private final Provider provider;
+
+ private final SecurityCallback prompter;
+
+ private final KeyStore.Builder builder;
+
+ private KeyStore keys;
+
+ private Pkcs11Provider(Provider pkcs11, SecurityCallback prompter) {
+ this.provider = pkcs11;
+ this.prompter = prompter;
+ this.builder = KeyStore.Builder.newInstance("PKCS11", provider, //$NON-NLS-1$
+ new KeyStore.CallbackHandlerProtection(prompter));
+ }
+
+ // Implementation note: With SoftHSM Java 11 asks for the PIN when the
+ // KeyStore is loaded, i.e., when the token is accessed. softhsm2-util,
+ // however, can list certificates and public keys without PIN entry, but
+ // needs a PIN to also list private keys. So it appears that different
+ // module libraries or possibly different KeyStore implementations may
+ // prompt either when accessing the token, or only when we try to actually
+ // sign something (i.e., when accessing a private key). It may also depend
+ // on the token itself; some tokens require early log-in.
+ //
+ // Therefore we initialize the prompter in both cases, even if it may be
+ // unused in one or the other operation.
+ //
+ // The price to pay is that sign() has to be synchronized, too, to avoid
+ // that different sessions step on each other's toes in the prompter.
+
+ private synchronized void load(SessionContext session)
+ throws GeneralSecurityException, IOException {
+ if (keys == null) {
+ int numberOfPrompts = prompter.init(session);
+ int attempt = 0;
+ while (attempt < numberOfPrompts) {
+ attempt++;
+ try {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(
+ "{}: Loading PKCS#11 KeyStore (attempt {})", //$NON-NLS-1$
+ getName(), Integer.toString(attempt));
+ }
+ keys = builder.getKeyStore();
+ prompter.passwordTried(null);
+ return;
+ } catch (GeneralSecurityException e) {
+ if (!prompter.passwordTried(e) || attempt >= numberOfPrompts
+ || !isWrongPin(e)) {
+ throw e;
+ }
+ }
+ }
+ }
+ }
+
+ synchronized byte[] sign(SessionContext session, String algorithm,
+ String alias, byte[] data)
+ throws GeneralSecurityException, IOException {
+ int numberOfPrompts = prompter.init(session);
+ int attempt = 0;
+ while (attempt < numberOfPrompts) {
+ attempt++;
+ try {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(
+ "{}: Signing with PKCS#11 key {}, algorithm {} (attempt {})", //$NON-NLS-1$
+ getName(), alias, algorithm,
+ Integer.toString(attempt));
+ }
+ Signature signer = Signature.getInstance(algorithm, provider);
+ PrivateKey privKey = (PrivateKey) keys.getKey(alias, null);
+ signer.initSign(privKey);
+ signer.update(data);
+ byte[] signature = signer.sign();
+ prompter.passwordTried(null);
+ return signature;
+ } catch (GeneralSecurityException e) {
+ if (!prompter.passwordTried(e) || attempt >= numberOfPrompts
+ || !isWrongPin(e)) {
+ throw e;
+ }
+ }
+ }
+ return null;
+ }
+
+ private boolean isWrongPin(Throwable e) {
+ Throwable t = e;
+ while (t != null) {
+ if (t instanceof FailedLoginException) {
+ return true;
+ }
+ t = t.getCause();
+ }
+ return false;
+ }
+
+ /**
+ * Retrieves an identifying name of this {@link Pkcs11Provider}.
+ *
+ * @return the name
+ */
+ public String getName() {
+ return provider.getName();
+ }
+
+ /**
+ * Obtains the identities provided by the PKCS11 library.
+ *
+ * @param session
+ * in which we to load the identities
+ * @return all the available identities
+ * @throws IOException
+ * if keys cannot be accessed
+ * @throws GeneralSecurityException
+ * if keys cannot be accessed
+ */
+ public Iterable<KeyAgentIdentity> getKeys(SessionContext session)
+ throws IOException, GeneralSecurityException {
+ // Get all public keys from the KeyStore.
+ load(session);
+ List<KeyAgentIdentity> result = new ArrayList<>(2);
+ Enumeration<String> aliases = keys.aliases();
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ Certificate certificate = keys.getCertificate(alias);
+ if (certificate == null) {
+ continue;
+ }
+ PublicKey pubKey = certificate.getPublicKey();
+ if (pubKey == null) {
+ // This should never happen
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("{}: certificate {} has no public key??", //$NON-NLS-1$
+ getName(), alias);
+ }
+ continue;
+ }
+ if (LOG.isDebugEnabled()) {
+ if (certificate instanceof X509Certificate) {
+ X509Certificate x509 = (X509Certificate) certificate;
+ // OpenSSH does not seem to check certificate validity?
+ String msg;
+ try {
+ x509.checkValidity();
+ msg = "Certificate is valid"; //$NON-NLS-1$
+ } catch (CertificateExpiredException
+ | CertificateNotYetValidException e) {
+ msg = "Certificate is INVALID"; //$NON-NLS-1$
+ }
+ // OpenSSh explicitly also considers private keys not
+ // intended for signing, see
+ // https://bugzilla.mindrot.org/show_bug.cgi?id=1736 .
+ boolean[] usage = x509.getKeyUsage();
+ if (usage != null) {
+ // We have no access to the PKCS#11 flags on the key, so
+ // report the certificate flag, if present.
+ msg += ", signing " //$NON-NLS-1$
+ + (usage[0] ? "allowed" : "NOT allowed"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ LOG.debug(
+ "{}: Loaded X.509 certificate {}, key type {}. {}.", //$NON-NLS-1$
+ getName(), alias, pubKey.getAlgorithm(), msg);
+ } else {
+ LOG.debug("{}: Loaded certificate {}, key type {}.", //$NON-NLS-1$
+ getName(), alias, pubKey.getAlgorithm());
+ }
+ }
+ result.add(new Pkcs11Identity(pubKey, alias));
+ }
+ return result;
+ }
+
+ // We use a KeyAgentIdentity because we want to hide the private key.
+ //
+ // JGit doesn't do Agent forwarding, so there will never be any reason to
+ // add a PKCS11 key/token to an agent.
+ private class Pkcs11Identity extends KeyAgentIdentity {
+
+ Pkcs11Identity(PublicKey key, String alias) {
+ super(NULL_AGENT, key, alias);
+ }
+
+ @Override
+ public Entry<String, byte[]> sign(SessionContext session, String algo,
+ byte[] data) throws Exception {
+ // Find the built-in signature factory for the algorithm
+ BuiltinSignatures factory = BuiltinSignatures.fromFactoryName(algo);
+ // Get its Java signature algorithm name from that
+ String javaSignatureName = factory.create().getAlgorithm();
+ // We cannot use the Signature created by the factory -- we need a
+ // provider-specific Signature instance.
+ return new SimpleImmutableEntry<>(algo,
+ Pkcs11Provider.this.sign(session, javaSignatureName,
+ getComment(), data));
+ }
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/pkcs11/SecurityCallback.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/pkcs11/SecurityCallback.java
new file mode 100644
index 0000000000..334a8cac81
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/pkcs11/SecurityCallback.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2023 Thomas Wolf <twolf@apache.org> 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.pkcs11;
+
+import static java.text.MessageFormat.format;
+import static org.apache.sshd.core.CoreModuleProperties.PASSWORD_PROMPTS;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.function.Supplier;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.ChoiceCallback;
+import javax.security.auth.callback.ConfirmationCallback;
+import javax.security.auth.callback.LanguageCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.TextInputCallback;
+import javax.security.auth.callback.TextOutputCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import org.apache.sshd.common.session.SessionContext;
+import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
+import org.eclipse.jgit.internal.transport.sshd.JGitClientSession;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.transport.CredentialItem;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.sshd.KeyPasswordProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A bridge to the JGit {@link CredentialsProvider}.
+ */
+public class SecurityCallback implements CallbackHandler {
+
+ private static final Logger LOG = LoggerFactory
+ .getLogger(SecurityCallback.class);
+
+ private final URIish uri;
+
+ private KeyPasswordProvider passwordProvider;
+
+ private CredentialsProvider credentialsProvider;
+
+ private int attempts = 0;
+
+ /**
+ * Creates a new {@link SecurityCallback}.
+ *
+ * @param uri
+ * {@link URIish} identifying the item the interaction is about
+ */
+ public SecurityCallback(URIish uri) {
+ this.uri = uri;
+ }
+
+ /**
+ * Initializes this {@link SecurityCallback} for the given session.
+ *
+ * @param session
+ * {@link SessionContext} of the keystore access
+ * @return the number of PIN prompts to try to log-in to the token
+ */
+ public int init(SessionContext session) {
+ int numberOfAttempts = PASSWORD_PROMPTS.getRequired(session).intValue();
+ Supplier<KeyPasswordProvider> factory = session
+ .getAttribute(JGitClientSession.KEY_PASSWORD_PROVIDER_FACTORY);
+ if (factory == null) {
+ passwordProvider = null;
+ } else {
+ passwordProvider = factory.get();
+ passwordProvider.setAttempts(numberOfAttempts);
+ }
+ attempts = 0;
+ if (session instanceof JGitClientSession) {
+ credentialsProvider = ((JGitClientSession) session)
+ .getCredentialsProvider();
+ } else {
+ credentialsProvider = null;
+ }
+ return numberOfAttempts;
+ }
+
+ /**
+ * Tells this {@link SecurityCallback} that an attempt to load something
+ * from the key store has been made.
+ *
+ * @param error
+ * an {@link Exception} that may have occurred, or {@code null}
+ * on success
+ * @return whether to try once more
+ * @throws IOException
+ * on errors
+ * @throws GeneralSecurityException
+ * on errors
+ */
+ public boolean passwordTried(Exception error)
+ throws IOException, GeneralSecurityException {
+ if (attempts > 0 && passwordProvider != null) {
+ return passwordProvider.keyLoaded(uri, attempts, error);
+ }
+ return true;
+ }
+
+ @Override
+ public void handle(Callback[] callbacks)
+ throws IOException, UnsupportedCallbackException {
+ if (callbacks.length == 1 && callbacks[0] instanceof PasswordCallback
+ && passwordProvider != null) {
+ PasswordCallback p = (PasswordCallback) callbacks[0];
+ char[] password = passwordProvider.getPassphrase(uri, attempts++);
+ if (password == null || password.length == 0) {
+ throw new AuthenticationCanceledException();
+ }
+ p.setPassword(password);
+ Arrays.fill(password, '\0');
+ } else {
+ handleGeneral(callbacks);
+ }
+ }
+
+ private void handleGeneral(Callback[] callbacks)
+ throws UnsupportedCallbackException {
+ List<CredentialItem> items = new ArrayList<>();
+ List<Runnable> updaters = new ArrayList<>();
+ for (int i = 0; i < callbacks.length; i++) {
+ Callback c = callbacks[i];
+ if (c instanceof TextOutputCallback) {
+ TextOutputCallback t = (TextOutputCallback) c;
+ String msg = getText(t.getMessageType(), t.getMessage());
+ if (credentialsProvider == null) {
+ LOG.warn("{}", format(SshdText.get().pkcs11GeneralMessage, //$NON-NLS-1$
+ uri, msg));
+ } else {
+ CredentialItem.InformationalMessage item =
+ new CredentialItem.InformationalMessage(msg);
+ items.add(item);
+ }
+ } else if (c instanceof TextInputCallback) {
+ if (credentialsProvider == null) {
+ throw new UnsupportedOperationException(
+ "No CredentialsProvider " + uri); //$NON-NLS-1$
+ }
+ TextInputCallback t = (TextInputCallback) c;
+ CredentialItem.StringType item = new CredentialItem.StringType(
+ t.getPrompt(), false);
+ String defaultValue = t.getDefaultText();
+ if (defaultValue != null) {
+ item.setValue(defaultValue);
+ }
+ items.add(item);
+ updaters.add(() -> t.setText(item.getValue()));
+ } else if (c instanceof PasswordCallback) {
+ if (credentialsProvider == null) {
+ throw new UnsupportedOperationException(
+ "No CredentialsProvider " + uri); //$NON-NLS-1$
+ }
+ // It appears that this is actually the only callback item we
+ // get from the KeyStore when it asks for the PIN.
+ PasswordCallback p = (PasswordCallback) c;
+ CredentialItem.Password item = new CredentialItem.Password(
+ p.getPrompt());
+ items.add(item);
+ updaters.add(() -> {
+ char[] password = item.getValue();
+ if (password == null || password.length == 0) {
+ throw new AuthenticationCanceledException();
+ }
+ p.setPassword(password);
+ item.clear();
+ });
+ } else if (c instanceof ConfirmationCallback) {
+ if (credentialsProvider == null) {
+ throw new UnsupportedOperationException(
+ "No CredentialsProvider " + uri); //$NON-NLS-1$
+ }
+ // JGit has only limited support for this
+ ConfirmationCallback conf = (ConfirmationCallback) c;
+ int options = conf.getOptionType();
+ int defaultOption = conf.getDefaultOption();
+ CredentialItem.YesNoType item = new CredentialItem.YesNoType(
+ getText(conf.getMessageType(), conf.getPrompt()));
+ switch (options) {
+ case ConfirmationCallback.YES_NO_OPTION:
+ if (defaultOption == ConfirmationCallback.YES) {
+ item.setValue(true);
+ }
+ updaters.add(() -> conf.setSelectedIndex(
+ item.getValue() ? ConfirmationCallback.YES
+ : ConfirmationCallback.NO));
+ break;
+ case ConfirmationCallback.OK_CANCEL_OPTION:
+ if (defaultOption == ConfirmationCallback.OK) {
+ item.setValue(true);
+ }
+ updaters.add(() -> conf.setSelectedIndex(
+ item.getValue() ? ConfirmationCallback.OK
+ : ConfirmationCallback.CANCEL));
+ break;
+ default:
+ throw new UnsupportedCallbackException(c);
+ }
+ items.add(item);
+ } else if (c instanceof ChoiceCallback) {
+ // TODO: implement? Information for the prompt, and individual
+ // YesNoItems for the choices? Might be better to hoist JGit
+ // onto the CallbackHandler interface directly, or add support
+ // for choices.
+ throw new UnsupportedCallbackException(c);
+ } else if (c instanceof LanguageCallback) {
+ ((LanguageCallback) c).setLocale(Locale.getDefault());
+ } else {
+ throw new UnsupportedCallbackException(c);
+ }
+ }
+ if (!items.isEmpty()) {
+ if (credentialsProvider.get(uri, items)) {
+ updaters.forEach(Runnable::run);
+ } else {
+ throw new AuthenticationCanceledException();
+ }
+ }
+ }
+
+ private String getText(int messageType, String text) {
+ if (messageType == TextOutputCallback.WARNING) {
+ return format(SshdText.get().pkcs11Warning, text);
+ } else if (messageType == TextOutputCallback.ERROR) {
+ return format(SshdText.get().pkcs11Error, text);
+ }
+ return text;
+ }
+}
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 a99847aa91..35c9be047a 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
@@ -210,11 +210,12 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
home, sshDir);
KeyIdentityProvider defaultKeysProvider = toKeyIdentityProvider(
getDefaultKeys(sshDir));
+ Supplier<KeyPasswordProvider> keyPasswordProvider = () -> createKeyPasswordProvider(
+ credentialsProvider);
SshClient client = ClientBuilder.builder()
.factory(JGitSshClient::new)
.filePasswordProvider(createFilePasswordProvider(
- () -> createKeyPasswordProvider(
- credentialsProvider)))
+ keyPasswordProvider))
.hostConfigEntryResolver(configFile)
.serverKeyVerifier(new JGitServerKeyVerifier(
getServerKeyDatabase(home, sshDir)))
@@ -236,6 +237,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
jgitClient.setKeyCache(getKeyCache());
jgitClient.setCredentialsProvider(credentialsProvider);
jgitClient.setProxyDatabase(proxies);
+ jgitClient.setKeyPasswordProviderFactory(keyPasswordProvider);
String defaultAuths = getDefaultPreferredAuthentications();
if (defaultAuths != null) {
jgitClient.setAttribute(
@@ -386,7 +388,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
}
/**
- * Obtains a {@link SshConfigStore}, or {@code null} if not SSH config is to
+ * Obtains a {@link SshConfigStore}, or {@code null} if no SSH config is to
* be used. The default implementation returns {@code null} if
* {@code configFile == null} and otherwise an OpenSSH-compatible store
* reading host entries from the given file.