Files

9.4 KiB
Raw Permalink Blame History

1、前言

我们是如何发现这个问题的呢?

国内有家航司的维护子公司找到我们。他们说某飞机制造商要求航司自研的系统安装到他们生产的飞机上要用硬件Keystore。以前是用软件的Keystore,毕竟不完全。硬件的Keystore,给的文档只有YubiHSM2 FIPS。YubiHSM小巧,性价比高,广受国外各种厂商认可。

他们向我们咨询YubiHSM2的集成配置,后来他们发现文档要求是YubiHSM2 FIPS,找飞机制造商确认,他们没有正面回复YubiHSM2是否可以用,只是强调FIPS版本更强大。至少最终我们把签名的流程用YubiHSM调通了,就不知道符合飞机制造商的要求不。

飞机制造商提供的Loadable Software Airplane Part (LSAP) Librarian Suite系统就是Java写的,其中CSCT模块通过PKCS#11对调用YubiHSM2的dll,进行签名。吐槽下,在配置中遇到和种问题。这么顶级的厂商的文档也不能全信的,文档里提的明明支持的版本,测试下来就是不支持。想着之前掉飞机的事,安全生产之路还任重道远。

我们遇到的报错:

2023-09-25 23:48:02,340 [ModalContext] ERROR com.xxxx.xxxx.xxx - logMsg() internalSignSingleItem() Could not initialize the keystore credentials provider:
Could not find SunPKCS11 constructor (sun.security.pkcs11.SunPKCS11.<init>(java.lang.String)).
Click Tools | Settings | Keystores and fill in the keystore information section.
java.security.ProviderException: Could not find SunPKCS11 constructor

2、Java 9对Sun PKCS 11的变更

PKCS11 Povider支持不再的Java版本。为了与智能卡或令牌设备进行身份验证或签名,PKCS#11是实现这一目标的标准之一。Java提供了SunPKCS11 Provider程序,这是一种包装器(wrapper),我们可以用它来与这类设备进行通信的本地库进行交互。基本上,我们在操作系统中安装一个库(dll、so或dylib文件),该库通常由设备供应商提供,然后通过SunPKCS11 Provider程序,我们指定设备的PKCS11库文件,PKCS11提供程序负责与设备进行交互。

因此,Java有现成的支持来与这些设备进行交互。然而,在Java 9中,对如何初始化SunPKCS11 Provider程序进行了一些变更,这些变量与Java的早期版本不再兼容。

3、SunPKCS11在Java 8及更早的版本

在Java 8和之前的版本中创建和配置SunPKCS11实例的步骤如下:

import sun.security.pkcs11.SunPKCS11;

String configName = "/opt/bar/cfg/pkcs11.cfg";
Provider p = new SunPKCS11(configName);
Security.addProvider(p);

基本上,给定一个包含该提供程序配置(库文件、名称等)的文件/opt/bar/cfg/pkcs11.cfg,我们能够仅使用构造函数就能初始化和配置SunPKCS11。最后,新创建的Provider程序被添加到安全上下文中。

4、SunPKCS11在Java 9及更高版本

以下是在Java 9或更高版本中创建和配置SunPKCS11实例的步骤。

import sun.security.pkcs11.SunPKCS11

String configName = "/opt/bar/cfg/pkcs11.cfg";
Provider p = Security.getProvider("SunPKCS11");
p = p.configure(configName);
Security.addProvider(p);

在这种情况下,SunPKCS11类没有可用的构造函数来实例化它,配置是在一个单独的步骤中完成的,通过configure(String configFile)方法。最后,新的Provider实例(配置后)被添加到安全上下文中。

通过安全上下文,使用getProvider方法,可以获得SunPKCS11的一个实例。它返回一个作为“模板”的“空(empty)”实例,然后我们使用configure方法。

SunPKCS11没有公共构造函数的原因是因为Java 9中添加了模块封装。因为它属于sun.*包,它们默认受到代码访问的限制。

5、跨版本解决方案

如我们所见,两种方法都改变了如何初始化和配置它。由于两者的定义(构造函数和方法)都不同,因此两种方法都不兼容。如果我们有一个可能在Java 8和更新版本上运行的应用程序,该如何实现呢?

解决方案是使用Java反射API。

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.Provider;
import java.security.Security;

private static final String SUN_PKCS11_CLASSNAME = "sun.security.pkcs11.SunPKCS11";
private static final String SUN_PKCS11_PROVIDER_NAME = "SunPKCS11";

try {
  int javaVersion = JavaVersionUtil.getVersion();
  if(javaVersion >= 9){
    Provider prototype = Security.getProvider(SUN_PKCS11_PROVIDER_NAME);
    Class<?> sunPkcs11ProviderClass = Class.forName(SUN_PKCS11_CLASSNAME);
    Method configureMethod = sunPkcs11ProviderClass.getMethod("configure", String.class);
    return (Provider) configureMethod.invoke(prototype, configFile);
  } else {
    Class<?> sunPkcs11ProviderClass = Class.forName(SUN_PKCS11_CLASSNAME);
    Constructor<?> constructor = sunPkcs11ProviderClass.getConstructor(String.class);
    return (Provider) constructor.newInstance(configFile);
  }
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException 
            | IllegalArgumentException | InvocationTargetException 
            | NoSuchMethodException | SecurityException ex) {
     //Handle any error, log message or throw an exception
}

基本上,它是在运行时检测正在运行的Java版本,并根据其值访问SunPKCS11类在每个版本上的不同方法。在Java 8及之前的版本中,使用构造函数进行配置,或者在Java 9或更高版本中使用“configure”方法。

6、在运行时查找Java的版本

这部分没有什么高深的科学,它只是检查系统属性java.version上的值。

public class JavaVersionUtil {
    public static int getVersion() {
        String version = System.getProperty("java.version");
        if(version.startsWith("1.")) {
            version = version.substring(2, 3);
        } else {
            int dot = version.indexOf(".");
            if(dot != -1) { version = version.substring(0, dot); }
        } return Integer.parseInt(version);
    }

7、启用加密模块和包

从Java 9到15,这些包仍然可以被访问,但在Java 16之后,它们默认不是公开的,因此我们需要添加一些模块子句来进行访问。这可以通过在我们的jar文件的Manifest.MF文件中添加以下几行来完成。

Add-Exports jdk.crypto.cryptoki/sun.security.pkcs11 jdk.crypto.cryptoki/sun.security.pkcs11.wrapper java.base/sun.security.action java.base/sun.security.rsa
Add-Opens java.base/java.security java.base/sun.security.util

我是通过使用maven-jar-plugin来实现这一点的。

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.0.2</version>
  <configuration>
    <archive>
      <manifest>
        .....
      </manifest>
      <manifestEntries>
        ....
        <Add-Exports>jdk.crypto.cryptoki/sun.security.pkcs11 jdk.crypto.cryptoki/sun.security.pkcs11.wrapper java.base/sun.security.action java.base/sun.security.rsa</Add-Exports>
        <Add-Opens>java.base/java.security java.base/sun.security.util</Add-Opens>                                   
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

8、结论

希望这段代码能帮助其他面临同样情况的人,即他们需要支持不同版本的Java(例如Java桌面应用程序),并希望提供一个跨版本的解决方案。

翻译自原文:

PKCS 11 Provider across multiple Java versions

原文参考:

Java PKCS#11 Reference Guide

Security Developer’s Guide

https://en.wikipedia.org/wiki/PKCS_11

Reflection in Java - GeeksforGeeks

我找到的关于此PKCS11版本问题的文章还有:

net.jsign.SignerException. java code examples | Tabnine

https://github.com/apache/ranger/blob/master/kms/src/main/java/org/apache/hadoop/crypto/key/RangerSafenetKeySecure.java

发布于 2023-09-28 16:54・IP 属地美国