翻译:通过Java编程创建X.509格式的数字签名证书

本文翻译自此篇文章,如有余力可直接阅读原文。

我所需要解决的问题很简单:创建一个只需要配置很少字段的X.509协议的证书,再使用已有的CA私钥/证书进行签名,最后导出为PKCS12格式的签名证书。把这个过程变得复杂化的原因是:我需要在一台小型设备(PDA)上,通过Java编程实现。

这个看似简单的工作花费了我两天的时间。我想我该分享我的成果,以期能够帮助比人解决类似的问题。

更新:似乎通过这篇博客,我找到了一些同道中人,我经常收到一些邮件来咨询相关细节。请各位不要感到不好意思,但是我确实需要一段时间来回复给你,因为我有太多的邮件需要一一回复。

为什么通过Java编程创建数字签名证书

有很多命令行工具可以创建X.509协议的证书,例如Sun JDK发布版中自带的keytool和可以适配多种平台的openssl。那么我为什么还要通过java编程实现创建数字签名证书的过程呢?我深入地考虑过使用Runtime.Exec去调用比较成熟的、系统中自带的openssl工具包,只需要传入合适的参数和标准输入即可。但是这种方法在不使用J2SE的平台上,例如J2ME JVMs在小型设备(PDA,例如CDC)和手机(例如CLDC),就会出现很大的问题。有些情况下,CDC支持执行系统命令,但是CLDC却并非如此。而且,openssl还需要一个配置文件,这个配置文件需要满足一下条件:
1. 设备中需要已经有可用的配置文件(Java中用到的openssl操作指令必须要恰当,更何况如果设备中如果没有安装openssl,还需要安装openssl以及编写合适的配置文件。)
2. 通过java程序,有权限去管理配置文件,包括创建、修改和移除。这就会带来额外的复杂性(译者注:主要是权限方面)。
因此,openssl可以稳定地运行在类桌面的平台(J2SE)中,但是要想运行在更多的嵌入式平台中就会困难重重。我决定大胆冒险,直接使用Java编程实现创建数字签名证书的过程。

调研有哪些可用的Java加密接口

很明显,如果使用Java进行一些加密性的工作,首选使用近期版本的J2SE中自带的JCE/JSSE框架。由于历史上以及当下的美国出口法的原因,JCE被分割成了基本架构和provider两部分。J2SE的1.4版本开始默认装载SunJCE作为provider。但是,类似于使用Runtime.Exec的情况,在以嵌入式形式使用的JRE上很难提供JCE,所以我们在应用设计之初,不能够乐观地假定在小型设备(PDA)上可以使用JCE。更糟糕的是,JCE再也不能够被第三方库替换了,所以应用本身不大可能装载JCE库。
另一个进行加密操作的方案时使用Bouncycastle库,它也可以作为一个JCE的provider使用,但是它同时也提供了一个轻量级API可以用于加密。这个接口是可以独立于JCE框架而独立使用的。Bouncycastle类库使用了非常自由的许可协议,被允许在任何应用中使用。因此它提供了一个独立于平台自身拥有功能的加密支持。所以我决定使用Bouncycastle轻量级API(下文将简称为BC)来完成数字签名证书的生成任务。
注意:很不幸的是,org.bouncycastle.x509.X509Util类在官方的JAR中并不是public类型的,在JAR外部无法直接使用这个类。但是生成数字签名证书的过程中必须使用这个类,那么必须想办法将这个类变成public类型。可以使用修改源代码并重新打包成JAR文件的方法。我将Buncycastle类中的一部分代码拷贝到我的代码中,保证他们他们能在J2ME中运行,同时精简代码的依赖关系以节省应用所占用的空间。另一个方法就是使用最新的OpenUAT源码,使用BC的类使用它。最后一个方法就是下载BC的源代码,包含在自己的工程中,做适当的修改(译者注:我是用的是最后这种方法)。

第一步:创建新证书所需的公私钥对

创建证书所需的公私钥对,下面分别是使用JCE和BC创建公私钥对的方法:

KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(1024, sr);
KeyPair keypair = keyGen.generateKeyPair();
privKey = keypair.getPrivate();
pubKey = keypair.getPublic();

以上是使用JCE创建公私钥对。

RSAKeyPairGenerator gen = new RSAKeyPairGenerator();
gen.init(new RSAKeyGenerationParameters(BigInteger.valueOf(3), sr, 1024, 80));
AsymmetricCipherKeyPair keypair = gen.generateKeyPair();
RSAKeyParameters publicKey = (RSAKeyParameters) keypair.getPublic();
RSAPrivateCrtKeyParameters privateKey = (RSAPrivateCrtKeyParameters) keypair.getPrivate();
// used to get proper encoding for the certificate
RSAPublicKeyStructure pkStruct = new RSAPublicKeyStructure(publicKey.getModulus(), publicKey.getExponent());
// JCE format needed for the certificate - because getEncoded() is necessary...
pubKey = KeyFactory.getInstance("RSA").generatePublic(
        new RSAPublicKeySpec(publicKey.getModulus(), publicKey.getExponent()));
// and this one for the KeyStore
privKey = KeyFactory.getInstance("RSA").generatePrivate(
        new RSAPrivateCrtKeySpec(publicKey.getModulus(), publicKey.getExponent(),
                privateKey.getExponent(), privateKey.getP(), privateKey.getQ(), 
                privateKey.getDP(), privateKey.getDQ(), privateKey.getQInv()));

以上是使用BC创建公私钥对。需要注意的是,BC和JCE生成的公私钥对的数据结构是不同的,这其中有一些额外的代码是将BC的数据结构转成JCE的数据结构,主要为了便于输出成为PKCS12格式。我希望能够不借助JCE实现这个过程,这样就减少了数据结构转换的过程。

第二步:创建一个新证书结构

上一步已经有了一个公钥,可以将它填入到X.509协议的证书结构体中,BC提供了X509v3CertificateGenerator类来实现这个过程,但是它非常依赖于JCE框架,所以最终没有使用这个类。我下面粘贴了一些代码。在这段代码中仅仅使用BC的API完成创建证书结构的过程,这样为了逐渐摆脱对JCE框架的依赖。

Calendar expiry = Calendar.getInstance();
expiry.add(Calendar.DAY_OF_YEAR, validityDays);

X509Name x509Name = new X509Name("CN=" + dn);

V3TBSCertificateGenerator certGen = new V3TBSCertificateGenerator();
certGen.setSerialNumber(new DERInteger(BigInteger.valueOf(System.currentTimeMillis())));
certGen.setIssuer(PrincipalUtil.getSubjectX509Principal(caCert));
certGen.setSubject(x509Name);
DERObjectIdentifier sigOID = X509Util.getAlgorithmOID("SHA1WithRSAEncryption");
AlgorithmIdentifier sigAlgId = new AlgorithmIdentifier(sigOID, new DERNull());
certGen.setSignature(sigAlgId);
certGen.setSubjectPublicKeyInfo(new SubjectPublicKeyInfo((ASN1Sequence)new ASN1InputStream(
        new ByteArrayInputStream(pubKey.getEncoded())).readObject()));
certGen.setStartDate(new Time(new Date(System.currentTimeMillis())));
certGen.setEndDate(new Time(expiry.getTime()));
TBSCertificateStructure tbsCert = certGen.generateTBSCertificate();

在这段代码片段中,有一些小技巧。
1. issuer name是否正确非常重要。如果它的值有一个字节的差别,在认证证书的证书链时就会失败。我之前使用new X509Name(caCert.getSubjectDN().getName())的方法设定这个值,但是得到的确实一个错误值。PrincipalUtil类可以提供正确值。
2. identifier对象描述的是使用的签名算法类型,是硬编码的。需要与后边的签名过程中保持一致。
3. 正确的编码证书中的公钥也是很难的,开始的时候我使用如下的方法:

certGen.setSubjectPublicKeyInfo(new SubjectPublicKeyInfo(sigAlgId, pkStruct.toASN1Object()));

但是这种方法产生的值,openssl无法读取。我还没有搞明白为什么只有JCE框架提供的getEncoded()方法可以得到正确的编码值,换句话说,正确的编码方法是什么?
4. Subject Name可以设置成完成的X.509协议中规定的名称,(例如:O=My organization, OE=My organizational unit, C=AT, CN=Rene Mayrhofer/[email protected]),但是我选择仅仅使用一部分内容,使用Common Name(CN)。仅仅使用Commone Name标识证书就已经可以满足我的需求了。

第三步:导入签名根证书的私钥和数字证书

以下代码实现的前提是,一个完整的CA中,包括一个自签名数字证书和对应的私钥,并且是PKCS12格式的文件。这样的根证书可以使用JCE中的KeyStore类来读取。

KeyStore caKs = KeyStore.getInstance("PKCS12");
caKs.load(new FileInputStream(new File(caFile)), caPassword.toCharArray());

// load the key entry from the keystore
Key key = caKs.getKey(caAlias, caPassword.toCharArray());
if (key == null) {
    throw new RuntimeException("Got null key from keystore!"); 
}
RSAPrivateCrtKey privKey = (RSAPrivateCrtKey) key;
caPrivateKey = new RSAPrivateCrtKeyParameters(privKey.getModulus(), privKey.getPublicExponent(), privKey.getPrivateExponent(),
        privKey.getPrimeP(), privKey.getPrimeQ(), privKey.getPrimeExponentP(), privKey.getPrimeExponentQ(), privKey.getCrtCoefficient());
// and get the certificate
caCert = (X509Certificate) caKs.getCertificate(caAlias);
if (caCert == null) {
    throw new RuntimeException("Got null cert from keystore!"); 
}
caCert.verify(caCert.getPublicKey());

我还没有找到不通过JCE类而实现这个过程的方法。

第四步:为新证书签名

使用CA的私钥为刚刚创建的证书结构体签名。这个过程因为引入了块状内容编码而变得非常复杂。

SHA1Digest digester = new SHA1Digest();
AsymmetricBlockCipher rsa = new PKCS1Encoding(new RSAEngine());
ByteArrayOutputStream   bOut = new ByteArrayOutputStream();
DEROutputStream         dOut = new DEROutputStream(bOut);
dOut.writeObject(tbsCert);
byte[] signature;
byte[] certBlock = bOut.toByteArray();
// first create digest
digester.update(certBlock, 0, certBlock.length);
byte[] hash = new byte[digester.getDigestSize()];
digester.doFinal(hash, 0);
// and sign that
rsa.init(true, caPrivateKey);
DigestInfo dInfo = new DigestInfo( new AlgorithmIdentifier(X509ObjectIdentifiers.id_SHA1, null), hash);
byte[] digest = dInfo.getEncoded(ASN1Encodable.DER);
signature = rsa.processBlock(digest, 0, digest.length);

v.add(tbsCert);
v.add(sigAlgId);
v.add(new DERBitString(signature));

正如你看到的,使用SHA1和RSA时,需要硬编码到块数据中,以匹配在证书结构体中定义的签名方法。比较复杂的部分是在签名之前使用PKCS1编码RSA签名,使用ASN1 DER编码摘要。通常可以通过返回值判断操作是否成功,这会很简单,但是BC和JCE的provider实现并没有设定返回值。

第五步:将新创建的数字签名证书保存为PKCS12格式的文件

目前的数字签名证书还是程序中保存在内存中的Java对象而已,最后一步,需要将数据签名证书保存为文件。这样就可以将证书导入到其他的机器或者仅仅是导入到其他的软件包中了。

X509CertificateObject clientCert = new X509CertificateObject(new X509CertificateStructure(new DERSequence(v))); 
clientCert.verify(caCert.getPublicKey());

PKCS12BagAttributeCarrier bagCert = clientCert;
bagCert.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName,
        new DERBMPString("My frindly name for the new certificate"));
bagCert.setBagAttribute(
        PKCSObjectIdentifiers.pkcs_9_at_localKeyId,
        new SubjectKeyIdentifierStructure(pubKey));

KeyStore store = KeyStore.getInstance("PKCS12");
store.load(null, null);

X509Certificate[] chain = new X509Certificate[2];
// first the client, then the CA certificate
chain[0] = clientCert;
chain[1] = caCert;

store.setKeyEntry("My friendly name for the new private key", privKey, exportPassword.toCharArray(), chain);

FileOutputStream fOut = new FileOutputStream(exportFile);
store.store(fOut, exportPassword.toCharArray());

这段代码严重依赖于JCE框架,我需要能够使用BC替换掉它。这看起来并不难,唯一的难点在于设置localKeyId参数,以便导入代码可以分配证书和私钥的空间。

如果谁知道如何不通过JCE可以读写PKCS12格式的文件,以及如何使用BC来正确的编码证书中的公钥,请联系告诉我。

完成代码

实现创建X.509格式的数字签名证书的Java实现类可以在点击这里查看。代码中同时实现了JCE和BC的方法,因为JCE的提供者方法要比BC的方法更快,因为JCE使用了Java中更加底层的代码(native code)。代码的应用规则遵循GPL协议,同时如果你对代码进行了扩展、提升和bug修复,我非常希望你能够通知我,我将会非常感激。我会更新我的实现代码,以便别人更好的使用它。

本文最后修订于2012年12月18日。

译者注:
JCE(Java Cryptography Extension)是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现。
它提供对对称、不对称、块和流密码的加密支持,它还支持安全流和密封的对象。它不对外出口,用它开发完成封装后将无法调用。
JCE,Java Cryptography Extension,在早期JDK版本中,由于受美国的密码出口条例约束,Java中涉及加解密功能的API被限制出口,所以Java中安全组件被分成了两部分: 不含加密功能的JCA(Java Cryptography Architecture )和含加密功能的JCE(Java Cryptography Extension)。在JDK1.1-1.3版本期间,JCE属于扩展包,仅供美国和加拿大的用户下载,JDK1.4+版本后,随JDK核心包一起分发。

根据文章显示,这篇文章写于2012年12月18日之前,通过推测和实验发现,文中使用的bcprov-jdk15on为1.46版本。该版本的源代码可以在下文提供的网站中下载,为了防止该网站年久失修,无法下载1.46版本的源码,我下载下来,存储到了CSDN的下载上,读者也可以从点击此处下载。

代码运行需要另外两个包:log4j和commons-codec,如果使用Maven管理代码,可以下pom.xml中添加:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.16</version>
</dependency>
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.8</version>
</dependency>

bouncycastle官网网站
https://www.bouncycastle.org/latest_releases.html

实现本文介绍的源码的下载地址
https://www.mayrhofer.eu.org/downloads/research-notes/X509CertificateGenerator.java
如果源码地址无法使用,可以点击此处下载

不同版本bcprov-jdk15on的源码包和运行包的下载地址,包括1.46(2012年)至1.52(2015年)
http://grepcode.com/project/repo1.maven.org/maven2/org.bouncycastle/bcprov-jdk15on/

猜你喜欢

转载自blog.csdn.net/xkjcf/article/details/78757505