summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.ssh.apache
diff options
context:
space:
mode:
authorThomas Wolf <twolf@apache.org>2023-07-09 20:06:37 +0200
committerThomas Wolf <twolf@apache.org>2023-07-17 04:52:30 -0400
commit23758d7a61081be4e28c9fc2a0256d7774962455 (patch)
tree9a661f915f0ad12d77d0e495f73c4b1a3c224ea1 /org.eclipse.jgit.ssh.apache
parent760bdd09b1d186d4ca4f21b7f771882513521949 (diff)
downloadjgit-23758d7a61081be4e28c9fc2a0256d7774962455.tar.gz
jgit-23758d7a61081be4e28c9fc2a0256d7774962455.zip
ssh: PKCS#11 support
Support PKCS#11 HSMs (like YubiKey PIV) for SSH authentication. Use the SunPKCS11 provider as described at [1]. This provider dynamically loads the library from the PKCS11Provider SSH configuration and creates a Java KeyStore with that provider. A Java CallbackHandler is needed to feed PIN prompts from the KeyStore into the JGit CredentialsProvider framework. Because the JGit CredentialsProvider may be specific to a SSH session but the PKCS11Provider may be used by several sessions, the CallbackHandler needs to be configurable per session. PIN prompts respect the NumberOfPasswordPrompts SSH configuration. As long as the library asks only for a PIN, we use the KeyPasswordProvider to prompt for it. This gives automatic integration in Eclipse with the Eclipse secure storage, so a user has even the option to store the PIN there. (Eclipse will then ask for the secure storage master password on first access, so the usefulness of this is debatable.) By default the provider uses the first PKCS#11 token (slot list index zero). This can be overridden by a non-standard PKCS11SlotListIndex ssh configuration entry. (For OpenSSH interoperability, also set "IgnoreUnknown PKCS11SlotListIndex" in the SSH config file then.) Once loaded, the provider and its shared library and the keys contained remain available until the application exits. Manually tested using SoftHSM. See file manual_tests.txt. Kudos to Christopher Lamb for additional manual testing with a real YubiKey, also on Windows.[2] [1] https://docs.oracle.com/en/java/javase/11/security/pkcs11-reference-guide1.html [2] https://www.eclipse.org/forums/index.php/t/1113295/ Change-Id: I544c97e1e24d05e28a9f0e803fd4b9151a76ed11 Signed-off-by: Thomas Wolf <twolf@apache.org>
Diffstat (limited to 'org.eclipse.jgit.ssh.apache')
-rw-r--r--org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit.ssh.apache/README.md54
-rw-r--r--org.eclipse.jgit.ssh.apache/manual_tests.txt45
-rw-r--r--org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties16
-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
11 files changed, 955 insertions, 33 deletions
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
index e8507e1b57..6bcbf4bcf8 100644
--- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
@@ -25,6 +25,7 @@ Export-Package: org.eclipse.jgit.internal.transport.sshd;version="6.7.0";x-inter
org.eclipse.jgit.transport",
org.eclipse.jgit.internal.transport.sshd.agent;version="6.7.0";x-internal:=true,
org.eclipse.jgit.internal.transport.sshd.auth;version="6.7.0";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.pkcs11;version="6.7.0";x-internal:=true,
org.eclipse.jgit.internal.transport.sshd.proxy;version="6.7.0";x-friends:="org.eclipse.jgit.ssh.apache.test",
org.eclipse.jgit.transport.sshd;version="6.7.0";
uses:="org.eclipse.jgit.transport,
diff --git a/org.eclipse.jgit.ssh.apache/README.md b/org.eclipse.jgit.ssh.apache/README.md
index f06b2f6071..b2911c688c 100644
--- a/org.eclipse.jgit.ssh.apache/README.md
+++ b/org.eclipse.jgit.ssh.apache/README.md
@@ -73,7 +73,8 @@ the use of an SSH agent. For the details, see the [OpenBSD ssh-config documentat
* **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
+ also listed in an `IdentityFile` property and for which the public key is available in a
+ corresponding `*.pub` file 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.)
@@ -90,6 +91,57 @@ OpenSSH does not implement ed448 keys, and neither does Apache MINA sshd, and he
not supported in JGit if its built-in SSH implementation is used. ed448 or other unsupported keys
provided by an SSH agent are ignored.
+## PKCS#11 support
+
+JGit supports using PKCS#11 HSMs (Hardware Security Modules) such as YubiKey PIV for SSH
+authentication.
+
+Using such a PKCS#11 token for SSH authentication can be configured in `~/.ssh/config` with a
+configuration
+
+```
+ PCKS11Provider /absolute/path/to/vendor/library.so
+```
+
+instead of or in addition to `IdentityFile` or `IdentityAgent`. PKCS#11 keys are considered before
+keys from an SSH agent. If `IdentitiesOnly` is also set, only keys listed in `IdentityFile` for which
+the public key is available in a corresponding `*.pub` file are considered.
+
+If `PKCS11Provider` is not set, or is set to the value `none`, no PKCS#11 library is used.
+
+This is all as in OpenSSH.
+
+Keys from PKCS#11 tokens are never added to an SSH agent; the `AddKeysToAgent` configuration has
+no effect for PKCS#11 keys in JGit. It makes only sense if someone is using agent forwarding and
+it requires the SSH agent to understand the `SSH_AGENTC_ADD_SMARTCARD_KEY` command. It is unknown
+which SSH agents support this (OpenSSH does), the SSH library used by JGit has no API for it,
+and JGit doesn't do agent forwarding anyway. (To hop through servers to a git repository use
+`ProxyJump` instead.)
+
+JGit by default uses the first token (the default `slotListIndex` zero). The Java KeyStore or
+[Provider configuration](https://docs.oracle.com/en/java/javase/11/security/pkcs11-reference-guide1.html)
+does not seem to have any support for [RFC7512](https://www.rfc-editor.org/rfc/rfc7512) URIs
+to select the token. JGit provides a custom SSH configuration `PKCS11SlotListIndex` that can be
+set to the slot index of the token wanted. The value should be a non-negative integer. If not
+set or if negative, the first token (slot list index zero) is used. (Note that the value is the
+slot *index*, not the slot ID. Slot IDs are not necessarily stable.)
+
+If you *do* set `PKCS11SlotListIndex` anywhere in your configuration file, then you should also
+set at the very top of the `~/.ssh/config` file:
+
+```
+IgnoreUnknown PKCS11SlotListIndex
+```
+
+The `IgnoreUnknown` configuration tells OpenSSH to ignore configurations it doesn't know about.
+Without this option, OpenSSH will issue an error and exit if the config file contains
+`PKCS11SlotListIndex`. The `IgnoreUnknown` option is available in OpenSSH since version 6.3
+from 2013-09-13. See the [OpenSSH documentation](https://man.openbsd.org/ssh_config.5#IgnoreUnknown)
+for details.
+
+If a token has multiple certificates and keys, a specific one can be selected by exporting
+the public key to a file and then using `IdentitiesOnly` and an `IdentityFile` configuration.
+
## Using a different SSH implementation
To use a different SSH implementation:
diff --git a/org.eclipse.jgit.ssh.apache/manual_tests.txt b/org.eclipse.jgit.ssh.apache/manual_tests.txt
new file mode 100644
index 0000000000..ea3e59cfe0
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/manual_tests.txt
@@ -0,0 +1,45 @@
+Testing PKCS11 support
+----------------------
+
+# Install SoftHSM and OpenSC
+
+I got SoftHSM via MacPorts, and OpenSC from https://github.com/OpenSC/OpenSC#downloads
+
+You need both; softhsm2-util cannot import certificates.
+
+# Initialize SoftHSM
+
+$ softhsm2-util --init-token --slot 0 --label "TestToken" --pin 1234 --so-pin 4567
+The token has been initialized and is reassigned to slot 2006661923
+
+# Create a new RSA key and certificate
+
+$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -subj "/CN=MyCertTEST" -nodes
+
+# Import the RSA key pair into the SoftHSM token
+
+$ softhsm2-util --import key.pem --slot 2006661923 --label "testkey" --id 1212 --pin 1234
+
+# Convert the certificate to DER and import it into SoftHSM token
+
+$ openssl x509 -in cert.pem -out cert.der -outform DER
+$ pkcs11-tool --module /opt/local/lib/softhsm/libsofthsm2.so -l --id 1212 --label "testcert" -y cert -w cert.der --pin 1234
+
+# Export the RSA public key convert to PEM, and show in SSH format
+# (I'm sure this could be done simpler from the original key.pem, but what the heck.)
+
+pkcs11-tool --module /opt/local/lib/softhsm/libsofthsm2.so --slot 2006661923 --read-object --type pubkey --id 1212 -o key.der
+openssl rsa -pubin -inform DER -in key.der -outform PEM -out key.pub.pem
+ssh-keygen -f key.pub.pem -m pkcs8 -i
+
+# Install that public key at Gerrit (or your git server of choice)
+
+# Have an ~/.ssh/config with a host entry for your git server using the SoftHSM library as PKCS11 provider:
+
+Host gitserver
+Hostname git.eclipse.org
+Port 29418
+User ...
+PKCS11Provider /opt/local/lib/softhsm/libsofthsm2.so
+
+# Fetch from your git server! When asked for the PIN, enter 1234.
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 c676221800..7da7181887 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
@@ -32,17 +32,17 @@ gssapiFailure=GSS-API error for mechanism OID {0}
gssapiInitFailure=GSS-API initialization failure for mechanism {0}
gssapiUnexpectedMechanism=Server {0} replied with unknown mechanism name ''{1}'' in {2} authentication
gssapiUnexpectedMessage=Received unexpected ssh message {1} in {0} authentication
-identityFileCannotDecrypt=Given passphrase cannot decrypt identity {0}
+identityFileCannotDecrypt=Given passphrase cannot read identity {0}
identityFileNoKey=No keys found in identity {0}
identityFileMultipleKeys=Multiple key pairs found in identity {0}
identityFileNotFound=Skipping identity ''{0}'': file not found
identityFileUnsupportedFormat=Unsupported format in identity {0}
invalidSignatureAlgorithm=Signature algorithm ''{0}'' is not valid for a key of type ''{1}''
kexServerKeyInvalid=Server key did not validate
-keyEncryptedMsg=Key ''{0}'' is encrypted. Enter the passphrase to decrypt it.
+keyEncryptedMsg=''{0}'' needs a passphrase to be read.
keyEncryptedPrompt=Passphrase
-keyEncryptedRetry=Encrypted key ''{0}'' could not be decrypted. Enter the passphrase again.
-keyLoadFailed=Could not load key ''{0}''
+keyEncryptedRetry=''{0}'' could not be read. Enter the passphrase again.
+keyLoadFailed=Could not load ''{0}''
knownHostsCouldNotUpdate=Could not update known hosts file {0}
knownHostsFileLockedUpdate=Could not update known hosts file (locked) {0}
knownHostsFileReadFailed=Failed to read known hosts file {0}
@@ -69,6 +69,14 @@ knownHostsUserAskCreationMsg=File {0} does not exist.
knownHostsUserAskCreationPrompt=Create file {0} ?
loginDenied=Cannot log in at {0}:{1}
passwordPrompt=Password
+pkcs11Error=ERROR: {0}
+pkcs11FailedInstantiation=HostConfig for host {0} (hostname {1}): could not instantiate {2} {3}
+pkcs11GeneralMessage=Java reported for PKCS#11 token {0}: {1}
+pkcs11NoKeys=HostConfig for host {0} (hostname {1}) {2} {3} did not provide any keys
+pkcs11NonExisting=HostConfig for host {0} (hostname {1}) {2} {3} does not exist or is not a file
+pkcs11NotAbsolute=HostConfig for host {0} (hostname {1}) {2} {3} is not an absolute path
+pkcs11Unsupported=HostConfig for host {0} (hostname {1}) {2} {3}: PKCS#11 is not supported
+pkcs11Warning=WARNING: {0}
proxyCannotAuthenticate=Cannot authenticate to proxy {0}
proxyHttpFailure=HTTP Proxy connection to {0} failed with code {1}: {2}
proxyHttpInvalidUserName=HTTP proxy connection {0} with invalid user name; must not contain colons: {1}
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.