如何在Java中从X509证书中提取CN?

112

我正在使用SslServerSocket和客户端证书,想要从客户端的X509Certificate中提取SubjectDN中的CN。

目前我调用cert.getSubjectX500Principal().getName(),但这当然给了我客户端的总格式化DN。由于某种原因,我只对DN中的CN=theclient部分感兴趣。是否有方法可以在不自己解析字符串的情况下提取DN的此部分?


可能是从证书DN中解析CN的重复问题。 - Ahmad Abdelghany
2
@AhmadAbdelghany 你意识到我的问题比链接的那个早了1.5年吗?所以如果有重复的话,那个问题是我的副本 :-) - Martin C.
公正的观点。我会标记另一个。 - Ahmad Abdelghany
22个回答

108

这里有一些新的非弃用的BouncyCastle API代码。您需要bcmail和bcprov两个发行版。

X509Certificate cert = ...;

X500Name x500name = new JcaX509CertificateHolder(cert).getSubject();
RDN cn = x500name.getRDNs(BCStyle.CN)[0];

return IETFUtils.valueToString(cn.getFirst().getValue());

11
@grak,我对你是如何找出这个解决方案感兴趣。仅仅从API文档中看,我是不可能想出这个解决方法的。 - Elliot Vargas
8
是的,我有同感... 我不得不在邮件列表上询问。 - gtrak
7
请注意,这段代码在当前的BouncyCastle版本(1.47,2012年10月)中还需要bcpkix分发版。 - EwyynTomato
2
一个证书可以有多个CN。你不应该只返回cn.getFirst(),而是应该遍历所有的CN并返回一个CN列表。 - varrunr
6
IETFUtils.valueToString 方法似乎不能正确输出结果。我的 CN 包含一些等号,因为它是通过 Base64 编码得到的(例如 AAECAwQFBgcICQoLDA0ODw==)。valueToString 方法会在结果中添加反斜杠。而使用 toString 方法似乎可以正常工作。很难确定这是否实际上是 API 的正确用法。 - Chris
显示剩余3条评论

102

这里还有另一种方法。其思路是,你获得的DN采用的是rfc2253格式,这个格式和LDAP DN使用的格式相同。那么为什么不重用LDAP API呢?

import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;

String dn = x509cert.getSubjectX500Principal().getName();
LdapName ldapDN = new LdapName(dn);
for(Rdn rdn: ldapDN.getRdns()) {
    System.out.println(rdn.getType() + " -> " + rdn.getValue());
}

1
如果您正在使用Spring,一个有用的快捷方式是:LdapUtils.getStringValue(ldapDN, "cn"); - Berthier Lemieux
至少对于我正在处理的情况,CN位于多属性RDN内。换句话说:所提出的解决方案不会迭代RDN的属性。但它应该这样做! - peterh
1
String commonName = new LdapName(certificate.getSubjectX500Principal().getName()).getRdns().stream() .filter(i -> i.getType().equalsIgnoreCase("CN")).findFirst().get().getValue().toString(); - Reto Höhener
2
注意:尽管它看起来是一个不错的解决方案,但它有一些问题。我使用这个方案已经有几年了,直到我发现了“非标准”字段的解码问题。对于像CN(又名2.5.4.3)这样的众所周知的类型字段,Rdn#getValue()包含一个String。然而,对于自定义类型,结果是byte[](可能基于以#开头的内部编码表示)。当然,byte[]->String是可能的,但包含额外的(不可预测的)字符。我已经通过基于BC的@laz解决方案解决了这个问题,因为它可以正确地在String中处理和解码。 - knalli

14

如果添加依赖项不是问题,您可以使用Bouncy Castle的API来处理X.509证书:

import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.PrincipalUtil;
import org.bouncycastle.jce.X509Principal;

...

final X509Principal principal = PrincipalUtil.getSubjectX509Principal(cert);
final Vector<?> values = principal.getValues(X509Name.CN);
final String cn = (String) values.get(0);

更新

在此帖发表时,这是实现此功能的方法。但是如gtrak在评论中提到的那样,这种方法现在已过时。请参见gtrak的已更新代码,该代码使用了新的Bouncy Castle API。


1
似乎在Bouncycastle 1.46版中,X509Name已被弃用,他们打算使用x500Name。您了解这方面的信息或替代方法吗? - gtrak
哇,看着这个新的 API,我很难想象如何完成与上面代码相同的目标。也许在 Bouncycastle 邮件列表归档中可以找到答案。如果我找到了解决方案,我会更新这个答案。 - laz
我通过一封邮件列表讨论找到了如何做到它,我创建了一篇回答来展示如何操作。 - gtrak
不错的发现 gtrak。我曾经花了10分钟试图找到它的解决方法,但最终没有再回来处理它。 - laz
请查看我的问题:http://stackoverflow.com/questions/40613147/how-to-get-the-policy-identifier-and-the-subject-type-of-basic-constraints-in-a - Hosein Aqajani
显示剩余4条评论

12

作为不需要''bcmail''的替代方案,参考以下代码:

    X509Certificate cert = ...;
    X500Principal principal = cert.getSubjectX500Principal();

    X500Name x500name = new X500Name( principal.getName() );
    RDN cn = x500name.getRDNs(BCStyle.CN)[0]);

    return IETFUtils.valueToString(cn.getFirst().getValue());

@Jakub:我一直使用你的解决方案,直到我的软件需要在Android上运行。但是Android没有实现javax.naming.ldap :-(


1
这正是我想到这个解决方案的原因:移植到Android... - Ivin
10
不确定何时更改,但此代码现在可行:X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(使用Java 8) - trichner
请查看我的问题:http://stackoverflow.com/questions/40613147/how-to-get-the-policy-identifier-and-the-subject-type-of-basic-constraints-in-a - Hosein Aqajani
IETFUtils.valueToString转义形式 返回值。我发现简单地调用 .toString() 对我而言更有效。 - holmis83

11

到目前为止发布的所有答案都存在一些问题:大多数使用内部X500Name或外部Bounty Castle依赖项。以下建立在@Jakub的答案上,只使用公共JDK API,并且根据OP的要求提取CN。它还使用Java 8,在2017年中期,你真的应该使用。

Stream.of(certificate)
    .map(cert -> cert.getSubjectX500Principal().getName())
    .flatMap(name -> {
        try {
            return new LdapName(name).getRdns().stream()
                    .filter(rdn -> rdn.getType().equalsIgnoreCase("cn"))
                    .map(rdn -> rdn.getValue().toString());
        } catch (InvalidNameException e) {
            log.warn("Failed to get certificate CN.", e);
            return Stream.empty();
        }
    })
    .collect(joining(", "))

在我的情况下,CN位于多属性RDN内。我认为您需要增强此解决方案,以便对于每个RDN,您将迭代RDN属性,而不仅仅是查看RDN的第一个属性,这是您在此隐式执行的操作。 - peterh

8

请注意,Cryptacular 1.1.x 系列适用于 Java 7,而 1.2.x 适用于 Java 8。虽然非常好的库! - Markus L

7

如果您不想依赖BouncyCastle,可以使用正则表达式对cert.getSubjectX500Principal().getName()进行解析。该正则表达式将解析显式名称,并为每个匹配项捕获组nameval

由于DN字符串包含逗号时应进行引用,因此该正则表达式可以正确处理带引号和不带引号的字符串,并且还可以处理带引号字符串中的转义引号:

(?:^|,\s?)(?:(?<name>[A-Z]+)=(?<val>"(?:[^"]|"")+"|[^,]+))+

以下是格式良好的正则表达式:

(?:^|,\s?)
(?:
    (?<name>[A-Z]+)=
    (?<val>"(?:[^"]|"")+"|[^,]+)
)+

这是一个链接,你可以查看它的运行情况: https://regex101.com/r/zfZX3f/2 如果您想获得仅包含CN的正则表达式,则此调整版本将实现该目的:
(?:^|,\s?)(?:CN=(?"(?:[^"]|"")+"|[^,]+))

最强大的答案。此外,如果您想支持甚至按其编号指定的OID(例如OID.2.5.4.97),允许的字符应从[A-Z]扩展到[A-Z、0-9、.]。 - yurislav
这对我来说似乎有点棘手,因为出现了逗号转义的情况。例如:"O=Acme Stuff, and more" 将被解析为 "Acme Stuff"。 - appmattus

5

使用正则表达式获取证书的通用名称,不使用任何库。

要获取名称:

String name = x509Certificate.getSubjectDN().getName();

从全名中提取常用名称。
    String name = "CN=Go Daddy Root Certificate Authority - G2, O=\"GoDaddy.com, Inc.\", L=Scottsdale, ST=Arizona, C=US";
    Pattern pattern = Pattern.compile("CN=(.*?)(?:,|\$)");
    Matcher matcher = pattern.matcher(name);
    if (matcher.find()) {
        System.out.println(matcher.group(1));
    }

希望这能对任何人有所帮助。(-_-)


由于简单,我点了赞。然而,正则表达式在美元符号前面不应该有反斜杠。 - SamwellTarly

4

更新:此类位于“sun”软件包中,您应该谨慎使用。感谢Emil的评论 :)

只是想分享一下,获取CN的方法如下:

X500Name.asX500Name(cert.getSubjectX500Principal()).getCommonName();

关于Emil Lundberg的评论,请参见:为什么开发人员不应编写调用“sun”包的程序


1
这是我目前最喜欢的答案,因为它简单易懂,可读性强,并且仅使用了JDK中提供的内容。 - Emil Lundberg
同意你关于使用JDK类的说法 :) - Rad
4
然而应该注意的是,javac警告X500Name是一个内部专有API,可能会在未来的版本中被移除。 - Emil Lundberg
是的,在阅读了链接FAQ之后,我需要撤回我的第一条评论。抱歉。 - Emil Lundberg
1
没问题。你指出的确实很重要。谢谢 :) 实际上,我不再使用那个类了 :P - Rad

3
我有BouncyCastle 1.49版本,其中包含的类是org.bouncycastle.asn1.x509.Certificate。我查看了IETFUtils.valueToString()代码 - 它使用反斜杠进行一些炫酷的转义。对于域名不会出现任何问题,但我认为我们可以做得更好。在我查看的情况下,cn.getFirst().getValue()返回了不同类型的字符串,它们都实现了ASN1String接口,该接口提供了一个getString()方法。因此,对我来说似乎有效的方法是:
Certificate c = ...;
RDN cn = c.getSubject().getRDNs(BCStyle.CN)[0];
return ((ASN1String)cn.getFirst().getValue()).getString();

我遇到了反斜杠问题,所以这解决了我的问题。 - Amber

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