设想

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

这些证书必须包含用户在Firefox中导入的证书,如果插入了智能卡,则还将列出该卡中的证书。环境是Linux/MacOS。在Windows中,Internet Explorer可以处理所有事情,我们想要实现的功能与Windows中的情况非常相似:列出所有证书以及卡中的证书,以供用户选择。

情况

在Ubuntu中使用Mozilla的NSS(网络安全服务)时,我迷路了。由于没有用于在Java中使用JSS的代码示例,所以只能使其部分工作,具体取决于我如何为提供程序加载配置文件。

我现在要做的是:

  • 读取firefox中的证书(带有KeyStoreProviderKeyStore.Builder,将softokn.so加载为库)。
  • 使用CryptoManager从卡中加载证书,并获取其所有模块。
    (CryptoManager.initialize(profileDir)cm.getModules()module.getTokens()等)


  • 问题

    方法1

    如果我使用libsoftoken3.so加载提供程序,则可以看到用户证书。但是,当我在构造CryptoManager后初始化provider时,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";
    

    方法2

    如果我将NNS的secmod.db加载到提供程序,则该卡将在使用此keyStore构建的provider中列出,即使不存在/未插入,也将列出。插入后,在上面的第二步中,我可以看到外部模块,但是随后该卡以相同的别名列出了两次。
    config = "name=\"NSS Module\"\n"
                + "attributes=compatibility\n"
                + "showInfo=true\n"
                + "allowSingleThreadedModules=true\n"
                + "nssUseSecmod=true\n"
                + "nssSecmodDirectory=" + NSS_JSS_Utils.getFireFoxProfilePath();
    

    问题:
  • 如何能以简单的方式轻松地加载所有证书,而不是与JSS分开加载?
  • 如果不可能,如何配置提供程序以分别但不重复地加载它们?
  • 最佳答案

    我已经以某种方式成功解决了它。我有80%的问题是由我自己解决的.....我认为这很正常。

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

  • 使用libsoftokn.so构造一个提供程序,例如,我问题中的第一个config,然后将其插入。使用KeyStore.Builder和此提供程序,构建一个KeyStore softKeyStore。在此 keystore 中,您拥有所有用户证书。提取这些证书的信息,并在JTable中列出它们。
  • 在首次初始化CryptoManager之前插入智能卡。 (否则,卡将被忽略,直到重新启动应用程序为止。)
  • 初始化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()来识别卡。 **只有插入卡后,相关模块及其 token 才会出现。 **
  • 当我们到达卡的 token 时,检查它是否需要登录以及是否已登录。并且,我们必须排除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()) {
                    ....
                }
    

    登录 token 后,使用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();
    ....
    
  • 然后,使用.so驱动程序的文件路径,构造一个config字符串以加载另一个提供程序。

  • 我们将有:
    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();
    
  • 列出我们从cardKeyStore获得的证书的别名(与上面使用的JTable相同),以及softKeyStore的证书的别名。
  • 当用户在JTable中选择一行时,获取所选别名并将其存储在静态字段中。
  • 当我们需要一个 keystore 来构造KeyManagerFactoryX509KeyManager进行SSL通信时,使用静态alias我们先在softKeyStore中查找它,然后在cardKeyStore中查找它。

  • 喜欢:
    if (softKeyStore.containsAlias(alias)) {
        return softKeyStore;
    } else if (cardKeyStore.containsAlias(alias)) {
        return cardKeyStore;
    }
    
  • SSL握手,发送消息,接收,签名等。
  • 09-28 15:03