在我的上一篇文章《Milo库OPCUA协议java实现》中发现比较多人留言说到比较困惑在创建客户端过程中的证书的生成和使用。下面我就跟大家说一下。
我自己做的一个相关例程:证书处理的例程
在上一篇文章中我们可以看到,OPC UA客户端对象的创建是需要一个X509Certificate证书对象,和一个KeyPair密钥对。
下面的代码用到了Milo库中的工具对象来读取或者创建证书文件,代码会有点长,请耐心看下去。(我会在下面代码相应的地方作注释说明)。
class KeyStoreLoader {
private static final Pattern IP_ADDR_PATTERN = Pattern.compile(
"^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
//证书别名
private static final String CLIENT_ALIAS = "client-ai";
//获取私钥的密码
private static final char[] PASSWORD = "password".toCharArray();
private final Logger logger = LoggerFactory.getLogger(getClass());
//证书对象
private X509Certificate clientCertificate;
//密钥对对象
private KeyPair clientKeyPair;
KeyStoreLoader load(Path baseDir) throws Exception {
//创建一个使用`PKCS12`加密标准的KeyStore。KeyStore在后面将作为读取和生成证书的对象。
KeyStore keyStore = KeyStore.getInstance("PKCS12");
//PKCS12的加密标准的文件后缀是.pfx,其中包含了公钥和私钥。
//而其他如.der等的格式只包含公钥,私钥在另外的文件中。
Path serverKeyStore = baseDir.resolve("example-client.pfx");
("Loading KeyStore at {}", serverKeyStore);
//如果文件不存在则创建.pfx证书文件。
if (!Files.exists(serverKeyStore)) {
keyStore.load(null, PASSWORD);
//用2048位的RAS算法。`SelfSignedCertificateGenerator`为Milo库的对象。
KeyPair keyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048);
//`SelfSignedCertificateBuilder`也是Milo库的对象,用来生成证书。
//中间所设置的证书属性可以自行修改。
SelfSignedCertificateBuilder builder = new SelfSignedCertificateBuilder(keyPair)
.setCommonName("Eclipse Milo Example Client")
.setOrganization("digitalpetri")
.setOrganizationalUnit("dev")
.setLocalityName("Folsom")
.setStateName("CA")
.setCountryCode("US")
.setApplicationUri("urn:eclipse:milo:examples:client")
.addDnsName("localhost")
.addIpAddress("127.0.0.1");
// Get as many hostnames and IP addresses as we can listed in the certificate.
for (String hostname : HostnameUtil.getHostnames("0.0.0.0")) {
if (IP_ADDR_PATTERN.matcher(hostname).matches()) {
builder.addIpAddress(hostname);
} else {
builder.addDnsName(hostname);
}
}
//创建证书
X509Certificate certificate = builder.build();
//设置对应私钥的别名,密码,证书链
keyStore.setKeyEntry(CLIENT_ALIAS, keyPair.getPrivate(), PASSWORD, new X509Certificate[]{certificate});
try (OutputStream out = Files.newOutputStream(serverKeyStore)) {
//保存证书到输出流
keyStore.store(out, PASSWORD);
}
} else {
try (InputStream in = Files.newInputStream(serverKeyStore)) {
//如果文件存在则读取
keyStore.load(in, PASSWORD);
}
}
//用密码获取对应别名的私钥。
Key serverPrivateKey = keyStore.getKey(CLIENT_ALIAS, PASSWORD);
if (serverPrivateKey instanceof PrivateKey) {
//获取对应别名的证书对象。
clientCertificate = (X509Certificate) keyStore.getCertificate(CLIENT_ALIAS);
//获取公钥
PublicKey serverPublicKey = clientCertificate.getPublicKey();
//创建Keypair对象。
clientKeyPair = new KeyPair(serverPublicKey, (PrivateKey) serverPrivateKey);
}
return this;
}
//返回证书
X509Certificate getClientCertificate() {
return clientCertificate;
}
//返回密钥对
KeyPair getClientKeyPair() {
return clientKeyPair;
}
}
上面所返回的证书和密钥对就可以在创建OPC UA客户端对象中使用了。
上面例子来自Milo例程:Milo证书例程
这里我们先讨论怎样获取.der,.pem文件的证书和密钥,关于该类型证书的生成我们下面再来一起讨论。
假设如果我们原本有.der和.pem文件的话,想要读取证书和私钥就会稍稍有点复杂,创建OPC客户端对象需要证书和密钥对,那么自然要获取证书对象,和公钥,私钥对象。读取.der证书不是什么难事,难的在于解析.pem文件。私钥就是存放在.pem文件中。
关于.pem文件的解析我们借助bouncycastle包来帮助我们解析,首先需要添加依赖:
maven:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.57</version>
</dependency>
gradle:
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.57'
获取证书的代码如下:
X509Certificate c = null;
File derFilee = new File("\\...");
FileInputStream in = new FileInputStream(derFile);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
c = (X509Certificate) cf.generateCertificate(in);
密钥的获取就需要用到bouncycastle包了。
获取密匙对的代码如下:
KeyPair keyPair = null;
File pemFile = new File("\\...");
FileInputStream privatekeyfile = new FileInputStream(pemFile);
//添加BouncyCastleProvider
Security.addProvider(new BouncyCastleProvider());
PEMParser pemParser = new PEMParser(new InputStreamReader(privatekeyfile));
Object object = pemParser.readObject();
PEMDecryptorProvider pemDecryptorProvider = new
//其中password 是读取私钥所需要的密码
JcePEMDecryptorProviderBuilder().build("password".toCharArray());
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
//获取KeyPair对象。
keyPair = converter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(pemDecryptorProvider));
至此我们就成功获取到证书对象和密钥对了,就可以使用这两个对象创建OPC客户端了。
上一篇我文章也说过,OPC Foundation在Github上也是有OPC UA的java库的,只是因为相比于Milo学习资源不多,官方的例程也做得不够Milo好,所以我就没有选用OPC Foundation的java实现。
但是既然现在说到了关于证书的生成和使用问题,那么就来看一下OPC Foundation 是怎样来创建证书和获取证书的。
//以下程序需要添加OPC Foundation的java实现的依赖
private static KeyPair getOPCCert(String applicationName)
throws ServiceResultException
{
File certFile = new File(applicationName + ".der");
File privKeyFile = new File(applicationName+ ".pem");
//尝试获取证书和密钥对。
try {
Cert myServerCertificate = Cert.load( certFile );
PrivKey myServerPrivateKey = PrivKey.load(privKeyFile, "password");
KeyPair k = new KeyPair(myServerCertificate, myServerPrivateKey);
return k;
} catch (CertificateException e) {
throw new ServiceResultException( e );
} catch (IOException e) {
//如果文件不存在的话则创建一个`.der`和`.pem`文件
try {
String hostName = InetAddress.getLocalHost().getHostName();
String applicationUri = "urn:"+hostName+":"+applicationName;
KeyPair keys = CertificateUtils.createApplicationInstanceCertificate(applicationName, null, applicationUri, 3650, hostName);
keys.getCertificate().save(certFile);
PrivKey privKeySecure = keys.getPrivateKey();
//设置私钥访问密码
privKeySecure.save(privKeyFile, "password");
return keys;
} catch (Exception e1) {
throw new ServiceResultException( e1 );
}
} catch (NoSuchAlgorithmException e) {
throw new ServiceResultException( e );
} catch (Exception e) {
throw new ServiceResultException( e );
}
}
上面的例程是OPC Foundation 中的一个获取和创建证书的例子,可以看到在OPC Foundation 中正是读取或者创建.der和.pem,而且代码更少,更简单。
为什么OPC Foundation 读取.pem这么简单轻松?正是因为上述例程中KeyPair对象的包名是org.opcfoundation.ua.transport.security,也即是由OPC Foundation重新封装过的KeyPair对象。而在Milo库中使用的则是在包为java.security中的KeyPair对象。所以是不能直接用到Milo库中的。
这两个对象的转化如下:
//k 为org.opcfoundation.ua.transport.security包中的KeyPair
java.security.KeyPair keyPair = new java.security.KeyPair(k.getCertificate().getCertificate().getPublicKey(),k.getPrivateKey().getPrivateKey());
以上就是我目前来说所用到的证书处理方法,本人知识有限,如果还有更好的方法或者有什么不对的可以提出。最好的方法还是之间去看源码,什么都一目了然了。