BouncyCastle的使用

Bouncy Castle 是用于 Java 和 C# 的最广泛使用的开源加密 API 之一。它实现了涵盖主要安全领域的一整套资源,包括密码学、公钥基础设施、数字签名、身份验证和安全通信。 对于需要高水平保证和合规性的用户,还有适用于
Java 和 C# 的 API 的 FIPS 认证版本。

它有什么功能?

Bouncy Castle 项目提供了一组库,涵盖了核心加密原语,例如密码、密钥传输、密钥协商、MAC、消息摘要和签名以及更高级别的协议。

密码的示例包括国家标准,例如 AES、ARIA、Camellia 和 SM3。签名算法包括基于椭圆曲线 (EC)、爱德华兹曲线、DSA 和 RSA 的算法。除了 RSA 和 EC 之外,还为密钥传输和密钥协商提供了额外的公钥算法,例如
ElGamal 和 Diffie-Hellman。支持NIST 标准 MAC 算法,例如 CMAC、 HMAC 以及较新的 KMAC,以及 SHA-2 系列摘要、SHA-3 系列(包括
SHAKE128/SHAKE256)以及其他摘要算法,例如作为 Whirlpool、RIPEMD 和 Tiger,以及 Blake2 和 Blake3。

为涉及 CRMF、CMC、CMP、EST 和 PKCS#10 的 X.509 证书生成和处理提供了高级 API。还有一些 API 用于支持其他协议,例如 CMS、DANE、PEM、S/MIME、时间戳协议和 OpenPGP。传输层安全性 (
TLS) API 以及 Java 的 JSSE 提供程序也可用,并提供高达 TLS 1.3 的支持,以及 PSK 和 SRP 扩展。

有关 Bouncy Castle 的功能和支持的概述,请参阅官方的互操作性

常用示例

生成密钥对

任何证书或认证请求的起点都是密钥对,使用 JCA(Java 密码体系结构)创建密钥对非常简单,一般使用 KeyPairGenerator 类完成。 需要确认的是要使用何种算法和参数,下面描述两个比较常见的算法:RSA、EC。

注:非对称加密公钥的安全性与对称加密是不同的:3072Bit的RSA密钥与128Bit的AES密钥一样安全,同样256Bit的EC密钥也是如此。

RSA密钥对

下面方法用于生成2048Bit的RSA密钥。采用RSAKeyGenParameterSpec封装密钥所需的大小和公共指数。 当前使用的RSAKeyGenParameterSpec.F4
是指费马素数是第四个(费马素数从0开始计算),它的实际值是0x10001(即65537)。

public static KeyPair generateRSAKeyPair() throws GeneralSecurityException {
    KeyPairGenerator  kpGen = KeyPairGenerator.getInstance("RSA", "BC");
    kpGen.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4));
    return kpGen.generateKeyPair();
}

根据你的操作,可以看出公共指数最少为F4或更大。

RSAKeyGenParameterSpec还提供F0(0x3) 作为选项。但是强烈建议不要使用F0。 就安全强度而言,2048Bit密钥相当于112位的安全性,这是目前密钥对的最低使用要求。

EC密钥对

EC 密钥对从所使用的曲线参数中获取其密钥大小。例如,P-256 将在 EC 域中生成有效大小为 256 位的密钥。

public static KeyPair generateECKeyPair() throws GeneralSecurityException {
    KeyPairGenerator  kpGen = KeyPairGenerator.getInstance("EC", "BC");
    kpGen.initialize(new ECGenParameterSpec("P-256"));
    return kpGen.generateKeyPair();
}

EC 对于密钥协商和签名都是有用的算法。吸引力在于相对较短的钥匙尺寸。基于 P-521 曲线的密钥,其安全性超过 256 位,比需要模数至少为 15360 位的等效 RSA 密钥小得多。

生成认证请求

认证请求的作用即使用本地的私钥颁发公钥证书。最早的标准形式是最初为 RSA 发布的标准形式 PKCS#10。一个更新的、通用的标准是 CRMF(证书请求消息格式)。
以下将仅提供请求消息的示例,因为一旦发送请求,服务器返回的内容取决于服务器。通常它是包含一个或多个证书的 CMS SignedData 消息。 尽管在 CRMF 的情况下,它也可以是 CMS EnvelopedData
消息,其中包含包含一个或多个证书的加密 CMS SigneData 消息。

PKCS10 和 CRMF 消息类都支持返回类的 ASN.1 编码版本的 getEncoded() 方法。
您还可以将 ASN.1 编码版本传递给两个类的 byte[] 构造函数。

生成 PKCS10 认证请求

PKCS10 格式最初被设计为一个标准,考虑到 RSA 的“特性”:RSA 密钥对可用于签名和加密,而签名只是使用私钥的加密操作。这种双重用途(用于签名和加密)实际上并不是一个好主意,因为它会破坏私钥的安全性。

许多标准(例如 FIPS)现在明确禁止在一般用途中双重使用,一个例外是使用 PKCS10 创建认证请求。原因是 PKCS10 的设计理念是所有权证明的主要机制是签名验证。 创建 PKCS10
请求主要涉及指定您希望最终证书关联的身份,然后对包含该信息的结构进行签名。这个想法是,如果随附的公钥可以在证书颁发机构 (CA) 稍后查看请求时验证签名,则必须使用相应的私钥对结构进行签名。 PKCS10
结构也允许一些可选数据,例如证书扩展。以下代码示例生成一个基本的 PKCS10 请求。

public static PKCS10CertificationRequest createPKCS10(
    KeyPair keyPair, String sigAlg) throws OperatorCreationException {
    X500Name subject = new X500Name("CN=Example");
    PKCS10CertificationRequestBuilder requestBuilder
            = new JcaPKCS10CertificationRequestBuilder(
                                    subject, keyPair.getPublic());
    ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
                            .setProvider("BC").build(keyPair.getPrivate());
    return requestBuilder.build(signer);
}

您可以使用前面显示的示例代码来创建密钥对。两者都可以作为 RSA 和 EC 支持签名。我们建议对 RSA 和 SHA256withECDSA 尝试算法 SHA256withRSA。

请注意,指定名称后,会使用传入的密钥对中提供的私钥创建 ContentSigner,然后使用 ContentSigner 构建包含该名称的请求。

通过在请求正文中包含属性,将可选数据添加到 PKCS10 认证请求。以下示例代码显示了如何通过添加请求以在 CA 将颁发的证书中包含 SubjectAlternativeName 扩展来向认证请求添加电子邮件地址。

public static PKCS10CertificationRequest createPKCS10WithExtensions(
        KeyPair keyPair, String sigAlg) throws OperatorCreationException, IOException {
    X500Name subject = new X500Name("CN=Example");
    PKCS10CertificationRequestBuilder requestBuilder
            = new JcaPKCS10CertificationRequestBuilder(
                                        subject, keyPair.getPublic());
    ExtensionsGenerator extGen = new ExtensionsGenerator();
    extGen.addExtension(Extension.subjectAlternativeName, false,
            new GeneralNames(
                    new GeneralName(
                            GeneralName.rfc822Name,
                            "example@primekey.com")));
    Extensions extensions = extGen.generate();
    requestBuilder.addAttribute(
            PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensions);
    ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
                            .setProvider("BC").build(keyPair.getPrivate());
    return requestBuilder.build(signer);
}

这是包含电子邮件地址的“正确”方式,尽管您会看到它们包含在构成主题的 X.500 名称中。请注意,第一个示例和这个示例之间的唯一区别是添加了生成扩展的代码和添加 ExtensionRequest 属性的代码。

生成 CRMF 认证请求

CRMF,(在RFC 4211),解决所有权证明的问题,或者用 RFC 的语言来说,所有权证明 (POP),与 PKCS10 略有不同。它允许签名,但也允许加密和密钥协议。

第一个示例可能是与PKCS10 示例最简单的等效示例。该示例假定一个可以创建和验证签名的算法。

public static byte[] generateRequestWithPOPSig(BigInteger certReqID, KeyPair kp, String sigAlg) throws CRMFException, IOException, OperatorCreationException {
    X500Name subject = new X500Name("CN=Example");
    JcaCertificateRequestMessageBuilder certReqBuild
        = new JcaCertificateRequestMessageBuilder(certReqID);
    certReqBuild
        .setPublicKey(kp.getPublic())
        .setSubject(subject)
        .setProofOfPossessionSigningKeySigner(
            new JcaContentSignerBuilder(sigAlg)
                .setProvider("BC")
                .build(kp.getPrivate()));
    return certReqBuild.build().getEncoded();
}

certReqId 只需要是一个唯一的整数,以便发送者可以匹配返回到发送的请求的响应。

在某些方面,就功能内容而言,上述示例之间唯一真正的区别是证书请求 ID 的存在,它只是证书请求者指定的整数值,并在证书响应中回显以允许证书请求者可能需要多个证书。除此之外,您可以使用之前建议的相同算法以及所需的密钥类型。

The following example shows how to create a CRMF certification request using encryption for the POP.
/**
 * Basic example for generating a CRMF certificate request with POP for
 * an encryption only algorithm like ElGamal.
 *
 * @param certReqID identity (for the client) of this certificate request.
 * @param kp key pair whose public key we are making the request for.
 */
public static byte[] generateRequestWithPOPEnc(BigInteger certReqID, KeyPair kp) throws CRMFException, IOException {
    X500Name subject = new X500Name("CN=Example");
    JcaCertificateRequestMessageBuilder certReqBuild
        = new JcaCertificateRequestMessageBuilder(certReqID);
    certReqBuild
        .setPublicKey(kp.getPublic())
        .setSubject(subject)
        .setProofOfPossessionSubsequentMessage(SubsequentMessage.encrCert);
    return certReqBuild.build().getEncoded();
}

为此,您需要使用 RSA。所有权证明步骤实际上是在 CA 的响应中。在这种情况下,CA 将发回一个加密的证书,如果请求者希望能够使用它,他们需要先对其进行解密。

在加密 POP 的情况下,证书将以 EnvelopedData 结构包装返回,私钥是支持 RecipientInfo 结构所需的密钥,该结构包含用于加密包含所请求证书的消息的对称密钥。

使用 CMP 生成 CRMF 认证请求

CRMF 消息需要包装在另一个协议中,例如证书管理协议 (CMP)。CMP 消息还包括消息发送者的附加字段,有时与证书主题字段表示的实体不同。在这种情况下,CRMF 消息可能会作为“RA 验证”发送,这意味着发送者将保证请求的真实性,而 CMP 消息可能会使用共享密钥进行验证。

以下方法显示了为“RA 验证”设置的基本认证请求消息:

public static byte[] generateRequestWithPOPRA(BigInteger certReqID, KeyPair kp) throws CRMFException, IOException, OperatorCreationException {
    X500Name subject = new X500Name("CN=Example");
    JcaCertificateRequestMessageBuilder certReqBuild
        = new JcaCertificateRequestMessageBuilder(certReqID);
    certReqBuild
        .setPublicKey(kp.getPublic())
        .setSubject(subject)
        .setProofOfPossessionRaVerified();
    return certReqBuild.build().getEncoded();
}

请注意,除了使用不同的 POP 方法外,它与前面的示例相同。

接下来,在将消息发送到 CA 之前,您需要对消息进行包装,以便拥有适合您正在使用的 CMP 服务的内容。

以下代码将包装您返回的 CRMF 消息,并在给定发件人和收件人的情况下生成 ProtectedPKIMessage:

public static ProtectedPKIMessage generatMacProtectedMessage(
    X500Name sender, X500Name recipient,
    byte[] senderNonce,
    CertificateRequestMessage crmfMessage,
    char[] password)
    throws CRMFException, CMPException
{
    return new ProtectedPKIMessageBuilder(new GeneralName(sender), new GeneralName(recipient))
                    .setMessageTime(new Date())
                    .setSenderNonce(senderNonce)
                    .setBody(new PKIBody(PKIBody.TYPE_INIT_REQ, new CertReqMessages(crmfMessage.toASN1Structure())))
                    .build(new PKMACBuilder(new JcePKMACValuesCalculator().setProvider("BC")).build(password));
}

有关使用 EJBCA 的特定示例,请参阅证书和 CRL 生成。以下代码片段显示了如何将不同的消息放在一起以及构建它们需要什么:

SecureRandom random = new SecureRandom();
BigInteger certReqId = BigInteger.valueOf(System.currentTimeMillis());
KeyPair keyPair = KeyPairGeneratorExamples.generateECKeyPair();
byte[] senderNonce = new byte[16];
random.nextBytes(senderNonce);
CertificateRequestMessage certReqMsg = new CertificateRequestMessage(
        CRMFExamples.generateRequestWithPOPRA(certReqId, keyPair));
X500Name sender = new X500Name("CN=Cert Requester");
X500Name recipient = new X500Name("CN=Certificate Issuer");
ProtectedPKIMessage pkiMessage = generatMacProtectedMessage(sender, recipient, senderNonce, certReqMsg, "secret".toCharArray());

这表明处理这些协议的复杂性更多地在于知道参数值是什么,而不是实际的 API 调用。

请注意,代码示例使用与您要为其创建证书的 CRMF 请求中使用的主题不同的发件人。由于您希望 CA 根据您的话颁发证书,因此使用共享密钥验证请求,该密钥可用于计算 MAC。这在客户端应用程序可能为多个实体颁发证书的情况下是有意义的。在客户端应用程序不同时生成私钥的情况下,应使用确认最终使用证书的实体拥有私钥的其他 POP 模式之一。

如果需要,还可以对 ProtectedPKIMessages 进行签名。

Bouncy Castle 包含一个 ASN1Dump 类,可用于转储 ASN.1 对象树。如果您有编码的,请先调用 ASN1Primitive.fromByteArray()。

生成证书和 CRL

自签证书

使用的标准的原始名称是 X.509,证书和 CRL 的生命周期有限,因此第一个实用程序方法只允许创建一个 Date 对象,该对象表示未来某个特定小时数的某个时间。

/**
 * Calculate a date in seconds (suitable for the PKIX profile - RFC 5280)
 *
 * @param hoursInFuture hours ahead of now, may be negative.
 * @return a Date set to now + (hoursInFuture * 60 * 60) seconds
 */
public static Date calculateDate(int hoursInFuture)
{
  long secs = System.currentTimeMillis() / 1000;
  return new Date((secs + (hoursInFuture * 60 * 60)) * 1000);
}

证书也有序列号。证书颁发者不应颁发两个具有相同序列号的证书。有多种计算序列号的方法,以下示例显示了使用当前时间初始化的 long 递增。

private static long serialNumberBase = System.currentTimeMillis();
/**
 * Calculate a serial number using a monotonically increasing value.
 *
 * @return a BigInteger representing the next serial number in the sequence.
 */
public static synchronized BigInteger calculateSerialNumber()
{
  return BigInteger.valueOf(serialNumberBase++);
}

我们通常识别为识别机器、个人或其他不同“事物”的证书通常从证书链或证书路径中获得其权威出处,从而导致证书提供身份。在这个链的另一端是一个信任锚,一个必须以面值接受的证书才能被接受。

以下示例代码生成一个从现在到 365 天有效的信任锚。

public static X509Certificate createTrustAnchor(
  KeyPair keyPair, String sigAlg)
  throws OperatorCreationException, CertificateException
{
  X500Name name = new X500Name("CN=Trust Anchor");
  X509v1CertificateBuilder certBldr = new JcaX509v1CertificateBuilder(
    name,
    calculateSerialNumber(),
    calculateDate(0),
    calculateDate(24 * 365),
    name,
    keyPair.getPublic());
  ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
    .setProvider("BC").build(keyPair.getPrivate());
  JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider("BC");
  return converter.getCertificate(certBldr.build(signer));
}

该证书是自签名的,因为它是代表建立信任的东西的证书,所以需要依靠密码学以外的东西来赋予它权限。

示例代码生成版本 1 证书,最初信任锚始终是版本 1 证书。如今,使用版本 3 证书更为常见,因此可以提供最大路径长度和其他约束,从而允许信任锚的相应私钥持有者对信任锚证书的使用保持一定的控制权。下一部分概述了版本 3 证书和扩展。

信任锚本身通常不足以开始颁发识别个人的证书。由于信任锚是必须从表面上接受的一件事,因此部署它们通常需要做一些工作,并且您希望将私钥锁定并很少使用。因此,在信任锚和最终实体证书之间通常存在一个或多个证书颁发机构 (CA)证书。最终实体证书是我们通常与个人、电子邮件地址或网站相关联的证书。

public static X509Certificate createCACertificate(
  X509Certificate signerCert, PrivateKey signerKey,
  String sigAlg, PublicKey certKey, int followingCACerts)
  throws GeneralSecurityException,
  OperatorCreationException, CertIOException
{
  X500Principal subject = new X500Principal("CN=Certificate Authority");
  X509v3CertificateBuilder certBldr = new JcaX509v3CertificateBuilder(
      signerCert.getSubjectX500Principal(),
      calculateSerialNumber(),
      calculateDate(0),
      calculateDate(24 * 60),
      subject,
      certKey);
  JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
  certBldr.addExtension(Extension.basicConstraints,
          true, new BasicConstraints(followingCACerts))
      .addExtension(Extension.keyUsage,
          true, new KeyUsage(KeyUsage.keyCertSign
                    | KeyUsage.cRLSign));
  ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
      .setProvider("BC").build(signerKey);
  JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider("BC");
  return converter.getCertificate(certBldr.build(signer));
}

在此示例中,使用的构建器是版本 3 证书的构建器。使用版本 3 构建器的主要原因是利用扩展。扩展很有用,因为它们为证书颁发者提供了一种机制来描述他们希望如何使用他们正在签名的主题的公钥。

示例中使用了两个扩展:

  • 第一个扩展由 BasicConstraints 类表示,用于确定此证书是否为 CA 证书以及链中可以跟随多少个 CA 证书(0 表示链以链中的下一个证书结束)。
  • 第二个扩展由 KeyUsage 类表示,并指定证书中的公钥可以用于什么。在这种情况下,扩展程序表示它可以用于签署其他证书和/或签署证书撤销列表 (CRL)(请参阅证书撤销)。

请注意,(true)也使用布尔值。此标志是“isCritical”标志,它告诉任何查看证书的人是否必须由寻求使用证书的人理解特定扩展。在这种情况下,通过将两个扩展的标志都设置为 true,颁发者告诉任何无法理解BasicConstraints和KeyUsage扩展是什么的人,他们不应该尝试使用证书。

实体证书

证书路径中的最后一个证书是最终实体证书。在证书路径的普通解释中,最终实体证书通常是您尝试使用并需要验证的证书。

public static X509Certificate createEndEntity(
    X509Certificate signerCert, PrivateKey signerKey,
    String sigAlg, PublicKey certKey)
  throws CertIOException, OperatorCreationException, CertificateException
{
  X500Principal subject = new X500Principal("CN=End Entity");
  X509v3CertificateBuilder  certBldr = new JcaX509v3CertificateBuilder(
      signerCert.getSubjectX500Principal(),
      calculateSerialNumber(),
      calculateDate(0),
      calculateDate(24 * 31),
      subject,
      certKey);
  certBldr.addExtension(Extension.basicConstraints,
          true, new BasicConstraints(false))
      .addExtension(Extension.keyUsage,
          true, new KeyUsage(KeyUsage.digitalSignature));
  ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
                  .setProvider("BC").build(signerKey);
  JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider("BC");
  return converter.getCertificate(certBldr.build(signer));
}

该代码类似于 CA 证书的代码,但具有不同的扩展值。最重要的区别是BasicConstraints简单地用布尔值调用构造函数的值false.这告诉任何查看证书的人它不能用作 CA 证书。false 值告诉观察者该证书是路径的结尾,并且后面不能有任何其他证书。

其他常见扩展

扩展经常出现在所有 X.509 结构中。扩展提供了一种简单的统一机制来包含现有标准甚至是内部专有标准所涵盖的附加信息。两个很好的例子是主题密钥 ID 扩展和授权密钥 ID 扩展。

主题密钥 ID 扩展允许您将简单的八位字节字符串(由 ASN.1 类型 SubjectKeyIdentifier 表示)与您的证书相关联。通常,此值是根据证书包含的公钥的 SHA-1 哈希计算的。在 Bouncy Castle 中,您可以按如下方式创建 SubjectKeyIdentier:

public static SubjectKeyIdentifier createSubjectKeyIdentifier(PublicKey key)
  throws NoSuchAlgorithmException
{
  JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
  return extUtils.createSubjectKeyIdentifier(key);
}
//And you can include it in your certificate using the following code:
certBldr.addExtension(Extension.subjectKeyIdentifier,
          false, createSubjectKeyIdentifier(certKey))

减少的哈希值总是比其相应的证书小得多,并且您会在各种 PKIX 协议中找到包含主题密钥 ID 而不是整个证书或仅包含公钥的位置。

public static AuthorityKeyIdentifier createAuthorityKeyIdentifier(X509Certificate issuer)
  throws NoSuchAlgorithmException, CertificateEncodingException
{
  JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
  return extUtils.createAuthorityKeyIdentifier(issuer);
}

使用 PKCS10 请求证书

通过使用 Bouncy Castle 创建证书签名请求,您现在可以从“真实” CA获得“真实”证书。

首先,您需要创建一个包含最终实体配置文件所需信息的 CSR。然后,将 CSR 包装在 JSON 中,并通过相互验证的 TLS 连接将其发送到 EJBCA。使用存储在 P12 文件中的客户端证书并使用 Java 中包含的工具来处理它。

下面显示了一个使用 Google GSON 和 Apache 的 HTTP 客户端的代码示例:

private static KeyPair generateKeyPair() throws Exception {
  final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
  keyPairGenerator.initialize(2048);
  return keyPairGenerator.generateKeyPair();
}
 
private static PKCS10CertificationRequest generateCsr(final KeyPair keyPair) throws Exception {
  final X500Name subjectDN = new X500NameBuilder(BCStyle.INSTANCE)
      .addRDN(BCStyle.CN, CN)
      .build();
  final ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator();
  extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(new GeneralName(GeneralName.dNSName, "pki-at-the-edge.com")));
  return new JcaPKCS10CertificationRequestBuilder(subjectDN, keyPair.getPublic())
    .addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate())
    .build(
      new JcaContentSignerBuilder("SHA256withRSA")
        .build(keyPair.getPrivate()));
}
 
private static void sendCsrToEjbcaAndGetCertificate(final PKCS10CertificationRequest csr) throws Exception {
  final KeyStore keyStore = KeyStore.getInstance("PKCS12", BouncyCastleProvider.PROVIDER_NAME);
  keyStore.load(new ByteArrayInputStream(Base64.decode(KEYSTORE)), "foo123".toCharArray());
  final Map<String, Object> jsonMap = new HashMap<>();
  jsonMap.put("certificate_request",
    "-----BEGIN CERTIFICATE REQUEST-----\n" +
    new String(Base64.encode(csr.getEncoded())) + "\n" +
    "-----END CERTIFICATE REQUEST-----");
  jsonMap.put("certificate_profile_name", "Workshop Certificate");
  jsonMap.put("end_entity_profile_name", "Workshop Certificate");
  jsonMap.put("certificate_authority_name", "Bouncy Castle Test CA");
  // This user will be created when requesting a certificate from EJBCA
  jsonMap.put("username", CN);
  jsonMap.put("password", "foo123");
  jsonMap.put("include_chain", false);
  final SSLContext sslContext = SSLContexts.custom()
    .loadKeyMaterial(keyStore, "foo123".toCharArray())
    .build();
  final HttpClient httpClient = HttpClients.custom()
    .setSSLContext(sslContext)
    .build();
  final HttpPost httpPost = new HttpPost(URL);
  final StringEntity entity = new StringEntity(new GsonBuilder().create().toJson(jsonMap));
  httpPost.setEntity(entity);
  httpPost.setHeader("Accept", "application/json");
  httpPost.setHeader("Content-type", "application/json");
  out.println("Posting the following payload to " + URL + ": " + System.lineSeparator() + new GsonBuilder().setPrettyPrinting().create().toJson(jsonMap));
  final HttpResponse response = httpClient.execute(httpPost);
  out.println("Received the following response from EJBCA:" + System.lineSeparator() + EntityUtils.toString(response.getEntity()));
}

使用 CRMF 申请证书

要在 EJBCA 中使用 CRMF 请求证书,您必须使用 CMP。

密钥对生成与 PKCS#10 相同:

final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
keyPairGenerator.initialize(2048);
final KeyPair keyPair = keyPairGenerator.generateKeyPair(); 

接下来,创建一个包含 CRMF 的 CertReqMessage。然后将 CertReqMessage 包装在使用 HMAC 进行身份验证的 ProtectedPKIMessage 中。

final SecureRandom random = new SecureRandom();
final BigInteger certReqId = BigInteger.valueOf(System.currentTimeMillis());
final byte[] senderNonce = new byte[16];
random.nextBytes(senderNonce);
final CertificateRequestMessage certReqMsg = new CertificateRequestMessage(new JcaCertificateRequestMessageBuilder(certReqId)
  .setPublicKey(keyPair.getPublic())
  .setSubject(subjectDn)
  .setProofOfPossessionRaVerified()
  .build()
  .getEncoded());
final X500Name sender = new X500Name("CN=BC");
final X500Name recipient = new X500Name("CN=Bouncy Castle Test CA");
final ProtectedPKIMessage pkiMessage = new ProtectedPKIMessageBuilder(new GeneralName(sender), new GeneralName(recipient))
      .setMessageTime(new Date())
      .setSenderNonce(senderNonce)
      .setBody(new PKIBody(PKIBody.TYPE_INIT_REQ, new CertReqMessages(certReqMsg.toASN1Structure())))
      .build(new PKMACBuilder(new JcePKMACValuesCalculator().setProvider(BouncyCastleProvider.PROVIDER_NAME)).build(hmacPassword.toCharArray()));

最后,使用 HTTP POST 将 ProtectedPKIMessage 发送到 EJBCA。

final HttpClient httpClient = HttpClients.createDefault();
final HttpPost httpPost = new HttpPost(URL);
final ByteArrayEntity entity = new ByteArrayEntity(pkiMessage.toASN1Structure().getEncoded());
httpPost.setEntity(entity);
httpPost.setHeader("Content-type", "application/pkixcmp");
final HttpResponse response = httpClient.execute(httpPost);
System.out.println("Received the following PKIMessage from EJBCA:");
final PKIMessage responsePkiMessage = PKIMessage.getInstance(ASN1Primitive.fromByteArray(EntityUtils.toByteArray(response.getEntity())));
System.out.println(ASN1Dump.dumpAsString(responsePkiMessage, true));

证书吊销

使用数字签名对证书进行身份验证确实留下了一个问题,即如果 CA 必须改变主意,它会做什么。如果在 6 个月内发生了会破坏与之相关联的私钥的事情,那么有效期为两年的证书将没有多大用处。

这就是证书撤销列表 (CRL) 的用武之地。CRL 是 CA 已撤销其批准的证书序列号列表。实际的序列号出现在 CRL 条目中,并且 CRL 条目通常包括撤销的原因。通常,CRL 将由颁发证书的同一实体签署,但在某些情况下,CA 会提名其他一些签署者代表其颁发 CRL。

创建空 CRL

CRL 最初不需要在其中列出任何已撤销的证书。虽然以空 CRL 开头可能看起来很奇怪,但该列表还将包含有关其更新频率的信息。更新频率决定了 CA 更新 CRL 的频率,通常它是一个初始的空 CRL 来建立它。

下面显示了创建空 CRL 的基本方法:

public static X509CRL createEmptyCRL(
  PrivateKey caKey,
  String sigAlg,
  X509Certificate caCert)
  throws IOException, GeneralSecurityException, OperatorCreationException
{
  X509v2CRLBuilder crlGen = new JcaX509v2CRLBuilder(caCert.getSubjectX500Principal(),
      calculateDate(0));
  crlGen.setNextUpdate(calculateDate(24 * 7));
  // add extensions to CRL
  JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
  crlGen.addExtension(Extension.authorityKeyIdentifier, false,
      extUtils.createAuthorityKeyIdentifier(caCert));
  ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
                .setProvider("BC").build(caKey);
  JcaX509CRLConverter converter = new JcaX509CRLConverter().setProvider("BC");
  return converter.getCRL(crlGen.build(signer));
}

调用向crlGen.setNextUpdate()接收此 CRL 的人提供有关您计划何时发布更新的 CRL 的信息。此示例使用 7 天,但值会根据对您的应用程序有意义的情况而有所不同。

在下一个更新日期之后,用户应该知道 CRL 现在可能会提供 CA 撤销的不完整视图,并且应该在对 CA 颁发的证书做出任何决定之前获取更新的副本。

添加撤销

现在您拥有一个带有已发布更新策略的 CRL,您需要向它添加撤销。

添加 CRL 条目只需将其添加到现有 CRL,然后重新生成 CRL 签名。Bouncy Castle CRL 构建器支持获取现有 CRL,然后通过添加 CRL 条目对其进行修改,如以下示例方法所示:

public X509CRL addRevocationToCRL(
    PrivateKey caKey,
    String sigAlg,
    X509CRL crl,
    X509Certificate certToRevoke)
    throws IOException, GeneralSecurityException, OperatorCreationException
{
  X509v2CRLBuilder crlGen = new JcaX509v2CRLBuilder(crl);
  crlGen.setNextUpdate(calculateDate(24 * 7));
  // add revocation
  ExtensionsGenerator extGen = new ExtensionsGenerator();
  CRLReason crlReason = CRLReason.lookup(CRLReason.privilegeWithdrawn);
  extGen.addExtension(Extension.reasonCode, false, crlReason);
  crlGen.addCRLEntry(certToRevoke.getSerialNumber(),
                    new Date(), extGen.generate());
  ContentSigner signer = new JcaContentSignerBuilder(sigAlg)
                .setProvider("BC").build(caKey);
  JcaX509CRLConverter converter = new JcaX509CRLConverter().setProvider("BC");
  return converter.getCRL(crlGen.build(signer));
}

原因通过 CRL 条目上的扩展与 CRL 条目相关联。在这个例子中,原因是“Privilege Withdrawn”,换句话说,改变了主意。对于私钥没有被泄露但已停止使用的情况,这是一个有用的原因。例如,由于与私钥证书相关的人员离职或更换角色。在这种情况下,生成的签名可以使用在吊销之前日期的吊销证书进行验证,但仍可能被接受。另一方面,如果给出的原因是“Key Compromise”(另一个有效值),您应该小心接受任何由被撤销证书的私钥签名的表面值。


BouncyCastle的使用
https://blog.cikaros.top/doc/c20cb394.html
作者
Cikaros
发布于
2022年9月6日
许可协议