Skip to content

Commit

Permalink
Fix the CKR_USER_NOT_LOGGED_IN error when using the API for signing m…
Browse files Browse the repository at this point in the history
…ultiple times with the YUBIKEY storetype (Fixes #230)
  • Loading branch information
ebourg committed Jun 28, 2024
1 parent c0bbf18 commit 39a94c2
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 23 deletions.
21 changes: 0 additions & 21 deletions jsign-core/src/main/java/net/jsign/SignerHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.security.AuthProvider;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
Expand All @@ -45,9 +44,6 @@
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.login.LoginException;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
Expand Down Expand Up @@ -471,23 +467,6 @@ public void sign(File file) throws SignerException {
signer = build();
}

// logout and login again to avoid the CKR_USER_NOT_LOGGED_IN error with the Yubikey PKCS#11 provider
Provider provider = ksparams.provider();
if (provider instanceof AuthProvider) {
try {
((AuthProvider) provider).logout();
((AuthProvider) provider).login(null, callbacks -> {
for (Callback callback : callbacks) {
if (callback instanceof PasswordCallback) {
((PasswordCallback) callback).setPassword(ksparams.storepass().toCharArray());
}
}
});
} catch (LoginException e) {
// ignore the CKR_USER_NOT_LOGGED_IN error thrown when the user isn't logged in
}
}

log.info("Adding Authenticode signature to " + file);
signer.sign(signable);

Expand Down
5 changes: 4 additions & 1 deletion jsign-crypto/src/main/java/net/jsign/YubiKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.io.File;
import java.io.IOException;
import java.security.AuthProvider;
import java.security.Provider;
import java.security.ProviderException;
import java.util.ArrayList;
Expand All @@ -27,6 +28,8 @@
import sun.security.pkcs11.wrapper.PKCS11;
import sun.security.pkcs11.wrapper.PKCS11Exception;

import net.jsign.jca.AutoLoginProvider;

/**
* Helper class for working with YubiKeys.
*
Expand All @@ -42,7 +45,7 @@ class YubiKey {
* @since 4.0
*/
static Provider getProvider() {
return ProviderUtils.createSunPKCS11Provider(getSunPKCS11Configuration());
return new AutoLoginProvider((AuthProvider) ProviderUtils.createSunPKCS11Provider(getSunPKCS11Configuration()));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@

package net.jsign.jca;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.security.KeyStoreSpi;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
Expand Down Expand Up @@ -100,6 +103,6 @@ public void engineStore(OutputStream stream, char[] password) {
}

@Override
public void engineLoad(InputStream stream, char[] password) {
public void engineLoad(InputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException {
}
}
121 changes: 121 additions & 0 deletions jsign-crypto/src/main/java/net/jsign/jca/AutoLoginProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Copyright 2024 Emmanuel Bourg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.jsign.jca;

import java.io.IOException;
import java.io.InputStream;
import java.security.AuthProvider;
import java.security.Key;
import java.security.KeyStoreSpi;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Enumeration;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.login.LoginException;

/**
* Security provider calling automatically the <code>login()</code> method of the underlying provider password before
* signing. It is designed to avoid the <code>CKR_USER_NOT_LOGGED_IN</code> error when signing multiple times with the
* ykcs11 PKCS#11 module for the Yubikey.
*
* @since 7.0
*/
public class AutoLoginProvider extends Provider {

private final AuthProvider provider;

private char[] storepass;

public AutoLoginProvider(AuthProvider provider) {
super(provider.getName(), provider.getVersion(), provider.getInfo() + " with auto login");
this.provider = provider;
}

@Override
public Service getService(String type, String algorithm) {
if ("KeyStore".equals(type)) {
return new PasswordInterceptorService(provider.getService(type, algorithm));
} else if ("Signature".equals(type) && storepass != null) {
login();
}

return provider.getService(type, algorithm);
}

private void login() {
// logout and login again to avoid the CKR_USER_NOT_LOGGED_IN error with the Yubikey PKCS#11 provider
try {
provider.logout();
provider.login(null, callbacks -> {
for (Callback callback : callbacks) {
if (callback instanceof PasswordCallback) {
((PasswordCallback) callback).setPassword(storepass);
}
}
});
} catch (LoginException e) {
// ignore the CKR_USER_NOT_LOGGED_IN error thrown when the user isn't logged in
}
}

class PasswordInterceptorService extends Service {
private final Service service;

public PasswordInterceptorService(Service service) {
super(provider, service.getType(), service.getAlgorithm(), service.getClassName(), null, null);
this.service = service;
}

@Override
public Object newInstance(Object constructorParameter) throws NoSuchAlgorithmException {
return new PasswordInterceptorKeyStoreSpi((KeyStoreSpi) service.newInstance(constructorParameter));
}
}

class PasswordInterceptorKeyStoreSpi extends AbstractKeyStoreSpi {
private final KeyStoreSpi instance;

public PasswordInterceptorKeyStoreSpi(KeyStoreSpi instance) {
this.instance = instance;
}

@Override
public void engineLoad(InputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException {
storepass = password;
instance.engineLoad(stream, password);
}

@Override
public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException {
return instance.engineGetKey(alias, password);
}

@Override
public Certificate[] engineGetCertificateChain(String alias) {
return instance.engineGetCertificateChain(alias);
}

@Override
public Enumeration<String> engineAliases() {
return instance.engineAliases();
}
}
}
34 changes: 34 additions & 0 deletions jsign-crypto/src/test/java/net/jsign/YubikeyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
package net.jsign;

import java.io.File;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Signature;

import org.junit.Assume;
import org.junit.Test;
Expand Down Expand Up @@ -47,4 +50,35 @@ public void testGetLibrary() {
assertNotNull("native library", library);
assertTrue("native library not found", library.exists());
}

@Test
public void testAutoLogin() throws Exception {
assumeYubikey();

Provider provider = YubiKey.getProvider();
KeyStore keystore = KeyStore.getInstance("PKCS11", provider);
keystore.load(() -> new KeyStore.PasswordProtection("123456".toCharArray()));

String alias = keystore.aliases().nextElement();

PrivateKey key = (PrivateKey) keystore.getKey(alias, "123456".toCharArray());

// first signature
Signature signature = Signature.getInstance("SHA256withRSA", provider);
signature.initSign(key);
signature.update("Hello World".getBytes());
byte[] s1 = signature.sign();

assertNotNull("signature null", s1);

key = (PrivateKey) keystore.getKey(alias, "123456".toCharArray());

// second signature
signature = Signature.getInstance("SHA256withRSA", provider);
signature.initSign(key);
signature.update("Hello World".getBytes());
byte[] s2 = signature.sign();

assertArrayEquals("signature", s1, s2);
}
}

0 comments on commit 39a94c2

Please sign in to comment.