NSS/JSS:在 Java 中加载用户导入的证书以及 PKCS#11 智能卡

NSS/JSS: load user imported cert along with PKCS#11 smartcard in Java

场景

我正在开发一个 Java Swing 项目,我必须开发一个列出证书的功能,供用户选择通过 SSL 针对服务器进行身份验证。

这些证书必须包含用户在 Firefox 中导入的证书,如果插入智能卡,卡中的证书也会被列出。环境是Linux/MacOS。在 Windows 中,Internet Explorer 会处理这一切,我们想要实现的与 Windows 中发生的事情非常相似:列出所有证书以及卡中的证书,供用户选择。


情况

在Ubuntu中使用Mozilla的NSS(Network Security Service)时,我发现自己迷路了。由于没有在 Java 中使用 JSS 的代码示例,我只能让它部分工作,具体取决于我为提供程序加载配置文件的方式。

我现在做的是:

  1. 在 firefox 中读取证书(使用 KeyStoreProviderKeyStore.Builder,将 softokn.so 作为库加载)。

  2. 使用CryptoManager从卡中加载证书并获取其所有模块。 (CryptoManager.initialize(profileDir), cm.getModules(), module.getTokens(), 等等)


问题

方法一

如果我使用 libsoftoken3.so 加载提供程序,我可以看到用户证书。但是,当我在构造 provider 之后初始化 CryptoManager 时,外部模块(例如,我的智能卡)没有在 cryptoManager.getModules().

中列出
config = "library=" + NSS_JSS_Utils.NSS_LIB_DIR + "/libsoftokn3.so\n"
            + "name=\"Soft Token\"\n"
            + "slot=2\n" //for softoken, can only be 2.
            + "attributes=compatibility\n"
            + "allowSingleThreadedModules=true\n"
            + "showInfo=true\n"
            + "nssArgs=\"configdir='" + NSS_JSS_Utils.getFireFoxProfilePath() + "' "
                + "certPrefix='' "
                + "keyPrefix='' "
                + "secmod='secmod.db' "
                + "flags='readOnly'\""
//              + "flags='noDb'\""
            + "\n";

方法二

如果我用 NNS 的 secmod.db 加载提供程序,该卡将被列出,即使它不是 present/inserted,在 keyStore 用这个 provider 构造。插入后,在上面的第二步中,我可以看到外部模块,但是该卡列出了两次,别名相同。

config = "name=\"NSS Module\"\n"
            + "attributes=compatibility\n"
            + "showInfo=true\n"
            + "allowSingleThreadedModules=true\n"
            + "nssUseSecmod=true\n"
            + "nssSecmodDirectory=" + NSS_JSS_Utils.getFireFoxProfilePath();

问题:

我已经以某种方式成功地解决了它。我80%的问题都是自己解决的…………我觉得很正常

基本上它包括构造KeyStore的两个实例和Provider的两个实例,一个用于用户证书,另一个用于智能卡。

  1. libsoftokn.so 构建一个提供者,例如我问题中的第一个 config,然后插入它。使用 KeyStore.Builder 和此提供程序,构建一个 KeyStore softKeyStore。在此密钥库中,您拥有所有用户证书。提取这些证书的信息并将它们列在JTable.

  2. 在第一次 CryptoManager 初始化之前插入智能卡。 (否则,该卡将被忽略,直到重新启动应用程序。)

  3. 初始化CryptoManager。这里有一些技巧可以打破 AlreadyInitializedException/NotInitializedException 的死循环:

我们有:

private static void initializeCryptoManager() throws Exception {
    //load the NSS modules before creating the second keyStore.

    if (cm == null) { //cm is of type CryptoManager
        while (true) { //the trick.
            try {
                cm = CryptoManager.getInstance();
            } catch (NotInitializedException e2) {
                try {
                    InitializationValues iv = new InitializationValues(NSS_JSS_Utils.getFireFoxProfilePath());
                    //TEST
                    iv.installJSSProvider = false;
                    iv.removeSunProvider = false;
                    iv.initializeJavaOnly = false; //must be false, or native C error if no provider is created.
                    iv.cooperate = false;
                    iv.readOnly = true;
                    iv.noRootInit = true;
                    iv.configDir = NSS_JSS_Utils.getFireFoxProfilePath();
                    iv.noModDB = false;
    //              iv.noCertDB = false; 
    //              CustomPasswordCallback cpc = new  CustomPasswordCallback();
    //              iv.passwordCallback = cpc; //no passwordcallback needed here.
                    iv.forceOpen = false;
                    iv.PK11Reload = false;
                    CryptoManager.initialize(iv);
                    continue; // continue to getInstance.
                } catch (KeyDatabaseException | CertDatabaseException | GeneralSecurityException e) {
                    Traza.error(e);
                    throw e;
                } catch (AlreadyInitializedException e1) {
                    continue; //if is initialized, must go on to get cm.
                }
            } 
            break; //if nothing is catched, must break to end the loop.
        }
    }
}

现在,我们可以cm.getModules()module.getTokens()来识别卡片。 **只有当卡被插入时,相关模块及其令牌才会出现。 **

  1. 当我们到达卡的令牌时,检查它是否需要登录以及是否已登录。并且,我们必须排除 InternalCryptoTokenInternalKeyStorageToken.

所以:

if (!token.isInternalCryptoToken() && !token.isInternalKeyStorageToken()){ // If not Internal Crypto service, neither Firefox CA store
    if (token.isPresent() ) { // when the card is inserted
        if (!token.isLoggedIn()){ // Try to login. 3 times.
            Traza.info("Reading the certificates from token " + token.getName() + ". Loggining... ");
            while (UtilTarjetas.tries <= 3) {
                try {
                //TEST
                    token.setLoginMode(NSS_JSS_Utils.LOGIN_MODE_ONE_TIME); 
                    token.login((PasswordCallback) new CustomPasswordCallback());
                    UtilTarjetas.prevTryFailed = false;
                    cm.setThreadToken(token);
                    break;
                } catch (IncorrectPasswordException e){
                    UtilTarjetas.prevTryFailed = true;
                    UtilTarjetas.tries ++;
                } catch (TokenException e) {
                    UtilTarjetas.prevTryFailed = true;
                    UtilTarjetas.tries ++;
                } 
            }

                // if tries > 3
                if (UtilTarjetas.tries > 3) {
                    Traza.error("The token " + token.getName() + " is locked now. ");
                    throw new IOException("You have tries 3 times and now the card is locked. ");
                }
            }

            if (token.isLoggedIn()) {
                ....
            }

登录令牌后,使用 Runtime.getRuntime().exec(command) 执行 shell 脚本以使用 NSS 附带的 modutil

在shell中是这样的:

modutil -dbdir /your/firefox/profile/dir -rawlist

此命令以可读格式向您显示 secmod.db 中包含的信息。

 name="NSS Internal PKCS #11 Module" parameters="configdir=/home/easternfox/.mozilla/firefox/5yasix1g.default-1475600224376 certPrefix= keyPrefix= secmod=secmod.db flags=readOnly " NSS="trustOrder=75 cipherOrder=100 slotParams={0x00000001=[slotFlags=RSA,RC4,RC2,DES,DH,SHA1,MD5,MD2,SSL,TLS,AES,SHA256,SHA512,Camellia,SEED,RANDOM askpw=any timeout=30 ] 0x00000002=[ askpw=any timeout=0 ] }  Flags=internal,critical"

library=/usr/lib/libpkcs11-dnie.so name="DNIe NEW"  

library=/usr/local/lib/libbit4ipki.so name="Izenpe local"  NSS="  slotParams={0x00000000=[ askpw=any timeout=0 ] }  "

因此您可以分析输出并在 module.getName() 所在的行中获取库位置。我们可以使用 StringTokenizer.

//divide the line into strings with "=".
StringTokenizer tz = new StringTokenizer(line, "=");
//get the first part, "library". 
String token = tz.nextToken(); 
//get the second part, "/usr/local/lib/libbit4ipki.so name"
token = tz.nextToken();
....
  1. 然后,根据.so驱动的文件路径,构造一个config字符串,加载另一个provider。

我们将有:

String config = "name=\"" + moduleName + "\"\n" + "library=" + libPath;

moduleName最好用“\”转义,因为它通常包含空格。 libPath 应该被转义,如果有空格的话。最好不要有空格。

插入此提供程序,并使用相同的提供程序构造一个cardKeyStore

Provider p = new SunPKCS11(new ByteArrayInputStream(config.getBytes()));
Security.insertProviderAt(p, 1);
KeyStore.Builder builder = null;
builder = KeyStore.Builder.newInstance("PKCS11", p, 
    new KeyStore.CallbackHandlerProtection(new UtilTarjetas().new CustomCallbackHandler()));
cardKeyStore = builder.getKeyStore();
  1. 在上面使用的 JTable 中列出我们从 cardKeyStore 获得的证书的别名,以及 softKeyStore 的别名。

  2. 当用户 select 在 JTable 中的一行时,获取 selected 别名并将其存储在静态字段中。

  3. 当我们需要一个密钥库来构造KeyManagerFactoryX509KeyManager用于SSL通信时,我们使用静态aliassoftKeyStore中查找它然后 cardKeyStore.

赞:

if (softKeyStore.containsAlias(alias)) {
    return softKeyStore;
} else if (cardKeyStore.containsAlias(alias)) {
    return cardKeyStore;
}
  1. SSL 握手、发送消息、接收、签名等