Load bundled opensc-pkcs11.so.

This commit is contained in:
David Kocher
2026-05-19 10:27:53 +02:00
parent 81952143e2
commit 8f91e7c7c6
4 changed files with 113 additions and 34 deletions
+7
View File
@@ -128,5 +128,12 @@
<version>${jna-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>opensc</groupId>
<artifactId>opensc-pkcs11</artifactId>
<type>so</type>
<version>0.27.1</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,36 @@
package ch.cyberduck.core.ssl;
/*
* Copyright (c) 2002-2026 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.
*/
import ch.cyberduck.core.CertificateIdentityCallback;
import ch.cyberduck.core.DisabledCertificateStore;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.TestProtocol;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class PKCS11CertificateStoreX509KeyManagerTest {
@Test
public void testList() {
final PKCS11CertificateStoreX509KeyManager manager = new PKCS11CertificateStoreX509KeyManager(CertificateIdentityCallback.noop,
new Host(new TestProtocol()), new DisabledCertificateStore(), LoginCallback.noop, "opensc-pkcs11.so");
assertTrue(manager.list().isEmpty());
}
}
@@ -19,24 +19,32 @@ import ch.cyberduck.core.CertificateIdentityCallback;
import ch.cyberduck.core.CertificateStore;
import ch.cyberduck.core.Credentials;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.LocalFactory;
import ch.cyberduck.core.LocaleFactory;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.preferences.HostPreferencesFactory;
import ch.cyberduck.core.preferences.PreferencesFactory;
import org.apache.commons.lang3.concurrent.ConcurrentException;
import org.apache.commons.lang3.concurrent.LazyInitializer;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.security.auth.login.LoginException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.security.cert.CertificateException;
public class PKCS11CertificateStoreX509KeyManager extends CertificateStoreX509KeyManager {
private static final Logger log = LogManager.getLogger(PKCS11CertificateStoreX509KeyManager.class);
@@ -46,23 +54,39 @@ public class PKCS11CertificateStoreX509KeyManager extends CertificateStoreX509Ke
this(prompt, bookmark, store, login, HostPreferencesFactory.get(bookmark).getProperty("connection.ssl.keystore.pkcs11.library"));
}
/**
*
* @param library Native PKCS11 library name or path
*/
public PKCS11CertificateStoreX509KeyManager(final CertificateIdentityCallback prompt, final Host bookmark,
final CertificateStore store, final LoginCallback login,
final String libraryPath) {
super(prompt, bookmark, store, buildKeyStore(libraryPath, bookmark, login));
final String library) {
super(prompt, bookmark, store, buildKeyStore(library, bookmark, login));
}
private static LazyInitializer<KeyStore> buildKeyStore(final String libraryPath, final Host bookmark, final LoginCallback login) {
private static LazyInitializer<KeyStore> buildKeyStore(final String library, final Host bookmark, final LoginCallback login) {
return new LazyInitializer<KeyStore>() {
@Override
protected KeyStore initialize() throws ConcurrentException {
try {
log.info("Load PKCS11 store from library {}", libraryPath);
log.info("Load PKCS11 store from library {}", library);
// SunPKCS11 names the configured provider as "SunPKCS11-{name}"
final String providerName = "SunPKCS11-Cyberduck";
final String providerName = String.format("SunPKCS11-%s", PreferencesFactory.get().getProperty("application.name"));
Provider provider = Security.getProvider(providerName);
if(provider == null) {
provider = configurePkcs11Provider(libraryPath);
try {
if(LocalFactory.get(library).exists()) {
provider = load(library);
}
else {
provider = load(LocalFactory.get(System.getProperty("java.library.path"), library).getAbsolute());
}
}
catch(ReflectiveOperationException e) {
log.error("Failed to load PKCS11 provider from {}: {}", library, ExceptionUtils.getRootCause(e).getMessage());
throw new ConcurrentException(e);
}
log.debug("Loaded PKCS11 provider {}", provider.getName());
// Register globally so JSSE can resolve RSASSA-PSS from this provider
// during TLS 1.3 CertificateVerify — required for RSA keys on hardware tokens
Security.addProvider(provider);
@@ -72,60 +96,72 @@ public class PKCS11CertificateStoreX509KeyManager extends CertificateStoreX509Ke
log.debug("Reusing existing PKCS11 provider {}", providerName);
}
final KeyStore store = KeyStore.getInstance("PKCS11", provider);
char[] pin = null;
while(true) {
try {
store.load(null, pin);
break;
}
catch(IOException e) {
if(e.getCause() instanceof LoginException) {
// Token requires PIN or provided PIN was incorrect — prompt and retry
log.debug("Token requires PIN: {}", e.getCause().getMessage());
final Credentials credentials = login.prompt(bookmark,
try {
store.load(null, null);
}
catch(IOException e) {
if(ExceptionUtils.getRootCause(e) instanceof LoginException) {
// Token requires PIN or provided PIN was incorrect — prompt and retry
log.debug("Token requires PIN: {}", e.getCause().getMessage());
final Credentials credentials;
try {
credentials = login.prompt(bookmark,
bookmark.getCredentials().getUsername(),
LocaleFactory.localizedString("Provide additional login credentials", "Credentials"),
LocaleFactory.localizedString("Enter PIN for PKCS11 token", "Credentials"),
new LoginOptions().user(false).password(true).keychain(false).icon(bookmark.getProtocol().disk())
);
pin = credentials.getPassword().toCharArray();
}
else {
throw e;
catch(LoginCanceledException ex) {
log.info("PIN prompt canceled for {}", library);
throw new ConcurrentException(e);
}
// Retry with PIN entry
store.load(null, credentials.getPassword().toCharArray());
}
else {
log.error("Failed to initialize PKCS11 keystore from {}: {}", library, e.getMessage());
throw new ConcurrentException(e);
}
}
catch(CertificateException | NoSuchAlgorithmException e) {
log.error("Failed to initialize PKCS11 keystore from {}: {}", library, e.getMessage());
throw new ConcurrentException(e);
}
return store;
}
catch(LoginCanceledException e) {
log.info("PIN prompt canceled for {}", libraryPath);
throw new ConcurrentException(e);
}
catch(Exception e) {
log.error("Failed to initialize PKCS11 keystore from {}: {}", libraryPath, e.getMessage());
throw new ConcurrentException(e);
catch(IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException e) {
throw new RuntimeException(e);
}
}
};
}
private static Provider configurePkcs11Provider(final String libraryPath) throws Exception {
/**
* Load and configure PKCS11 provider
*
* @param libraryPath Path to native library
* @return Null when loading library fails
*/
private static Provider load(final String libraryPath) throws ReflectiveOperationException {
final String config = String.format("--\nname=%s\nlibrary=%s\n",
PreferencesFactory.get().getProperty("application.name"), libraryPath);
// Java 9+: standard JCA Provider.configure(String) with inline config (prefix --)
final Provider base = Security.getProvider("SunPKCS11");
if(base != null) {
try {
final String config = "--\nname=Cyberduck\nlibrary=" + libraryPath + "\n";
return (Provider) Provider.class.getMethod("configure", String.class).invoke(base, config);
final Method configure = Provider.class.getMethod("configure", String.class);
return (Provider) configure.invoke(base, config);
}
catch(NoSuchMethodException ignored) {
// Java 8 does not have Provider.configure() — fall through
log.warn("Fall through to reflection to load PKCS11 provider");
}
}
// Java 8: sun.security.pkcs11.SunPKCS11(InputStream) — accessed via reflection so
// the source compiles without a direct sun.* reference on Java 9+/21
final String config = "name=Cyberduck\nlibrary=" + libraryPath + "\n";
final Class<?> cls = Class.forName("sun.security.pkcs11.SunPKCS11");
return (Provider) cls.getConstructor(java.io.InputStream.class)
return (Provider) cls.getConstructor(InputStream.class)
.newInstance(new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8)));
}
}
@@ -629,8 +629,8 @@ connection.ssl.keystore.provider=
connection.ssl.keystore.pkcs11.library=/opt/homebrew/lib/opensc-pkcs11.so
# OpenSC via Homebrew (Intel)
#connection.ssl.keystore.pkcs11.library=/usr/local/lib/opensc-pkcs11.so
# Override with PKCS11Provider directive in ~/.ssh/config
#connection.ssl.keystore.pkcs11.library=/usr/lib/ssh-keychain.dylib
# OpenSC Bundled
#connection.ssl.keystore.pkcs11.library=opensc-pkcs11.so
# Transfer read buffer size
connection.chunksize=32768