为什么我的密钥标识符不匹配?

8
我正在尝试解密一封S/MIME邮件(最初通过Outlook发送),为此我使用了bouncycastle API。然而,我遇到了一个问题。
在Windows证书存储中,我有收件人的证书。我之前使用它向对方发送了签名和加密的电子邮件,他们又用它给我发送了加密回复。然后,我将证书(带有私钥)导出为.pfx文件,并将该文件加载到Java KeyStore中。但是它不起作用,我怀疑这是因为主题密钥标识符不匹配。
以下是我用于从KeyStore获取主题密钥ID的代码:
KeyStore ks = KeyStore.getInstance("PKCS12");
char[]   pw = "password".toCharArray();

ks.load(new FileInputStream("d:\\cert_priv_key.pfx"), pw);

Enumeration en = ks.aliases();

while( en.hasMoreElements() )
{
    String alias = (String)en.nextElement();
    System.out.println(alias);

    if( ks.isKeyEntry(alias) )
    {
        Certificate[]   chain = ks.getCertificateChain(alias);
        X509Certificate cert  = (X509Certificate)chain[0];

        byte[] id = cert.getExtensionValue("2.5.29.14");

        System.out.println("  " + toHex(id));
    }
}

这将打印出以下密钥标识符:

04 16 04 14 88 ed bb 7c 64 7b 41 63 48 0a 24 40 2b 3c d0 78 72 3c 30 b3

然而,当我检查Windows证书存储时,密钥标识符不同:

88 ed bb 7c 64 7b 41 63 48 0a 24 40 2b 3c d0 78 72 3c 30 b3

KeyStore会在前面返回多余的4个字节(主题密钥标识符应为密钥的160位SHA1哈希值,因此应该是20个字节长,正确吗?)。

更加令人困惑的是,当我使用bouncycastle API解析S/MIME电子邮件并查看收件人(SMIMEEnveloped.getRecipientInfos().getRecipients())时,返回的唯一收件人(应该只有一个)具有此主题密钥标识符:

04 14 88 ed bb 7c 64 7b 41 63 48 0a 24 40 2b 3c d0 78 72 3c 30 b3

只有两个额外的字节,而不是四个,我猜这就是为什么我无法使用证书解密电子邮件的原因。

为什么这些主题关键标识符都不匹配?我做错了什么吗?

3个回答

17
所有这些答案都是一致的,如果您理解了所有规范,但是如果您不理解,则当然会感到困惑。首先要查看的地方是RFC 5280,4.2.1.2节。在这种情况下,使用方法(1)。接下来,请查看附录A.2中的KeyIdentifier定义。它被定义为OCTET STRING。现在看看ASN.1 OCTET STRING 应该如何编码。它应该以十六进制04开头,后跟字节长度(20个字节或14个十六进制),后跟实际的八位字符串(SHA1哈希)。因此扩展的内容应该是:
04 14 88 ed bb 7c 64 7b 41 63 48 0a 24 40 2b 3c d0 78 72 3c 30 b3

最后,看一下Extension的ASN.1定义。它说扩展值必须被编码为OCTET STRING。在这个特定的扩展中,其实际效果是它作为OCTET STRING连续两次编码。在这个级别上,OCTET STRING是前一个包括两个头字节04 14的OCTET STRING,因此长度为hex 16,编码为:
04 16 04 14 88 ed bb 7c 64 7b 41 63 48 0a 24 40 2b 3c d0 78 72 3c 30 b3

这是X509Extension.getExtensionValue()方法返回的值。现在,关于密钥ID有趣的部分只是以88开头的SHA1哈希值,因此这就是Windows实用程序显示的内容。显然,您使用的bouncycastle方法会在不进行额外解码的情况下显示扩展名。

我能够直接使用私钥解密电子邮件,但是我仍然无法使用非废弃方法让它正常工作。我的猜测是X509Extension.getExtensionValue()在返回值时应该会剥离一层编码。这样就能匹配了。 - Mark
我也假定你的意思是“额外解码”? - Mark
@Mark:关于去除一层的问题,我最初也是这么想的,但经过一番思考后,我现在认为 getExtensionValue() 做得对。 - President James K. Polk
@GregS:那么,您认为匹配主题密钥 ID 的代码要么是错误的(应该在证书端进行 2 次解码,在 SMIME 端进行 1 次解码),要么是 SMIME 消息编码不正确? - Mark
@Mark:这是我的理解。getExtensionValue()方法不知道你所请求的扩展的含义。源代码可能非常简单。SMIME代码更加类型感知。它知道这不仅仅是一个扩展,而是SubjectKeyIdentifier扩展,因此这个OCTET STRING实际上编码了另一个OCTET STRING。我承认我对bcmail api不太熟悉,只是猜测。 - President James K. Polk

1

GregS的答案对我帮助很大。

最终运行成功的代码是:

X509Certificate certificate = ...
byte[] encExtensionSubjectKeyIdentifier = certificate.getExtensionValue(Extension.subjectKeyIdentifier.getId());

// Unwrap first 'layer'
ASN1Primitive skiPrimitive = JcaX509ExtensionUtils.parseExtensionValue(encExtensionSubjectKeyIdentifier);

// Unwrap second 'layer'
byte[] keyIdentifier = ASN1OctetString.getInstance(skiPrimitive.getEncoded()).getOctets();

// Use keyIdentifier in e.g. CMS SignerInfo
SignerInfoGenerator signerInfoGenerator = jcaSignerInfoGeneratorBuilder.build(sha1Signer, keyIdentifier);

1

不,这似乎不是问题所在。肯定有一个SubjectKeyIdentifier,只是它与证书的内容不匹配。 - Mark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接