如何从Java的X509Certificate中提取CN?


91

我正在使用SslServerSocket和客户端证书,并希望从客户端的SubjectDN中提取CN X509Certificate

目前,我打电话,cert.getSubjectX500Principal().getName()但这当然给了我客户端的总格式化DN。由于某种原因,我只对CN=theclientDN感兴趣。有没有一种方法可以提取DN的这一部分而无需自己解析String?



2
@AhmadAbdelghany您意识到,我的问题比链接的问题大1.5年?所以,如果有的话,另一个就是我的副本:-)
Martin C.

有道理。我将标记另一个。
艾哈迈德·阿卜杜勒加尼

Stream解决方案Abhijit Sarkar输入链接描述,这里工作正常!
Christian M.

Answers:


89

这是新的不推荐使用的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());

9
@grak,我对您如何解决此问题感兴趣。当然,仅通过查看API文档,我就永远无法弄清楚这一点。
艾略特·巴尔加斯

5
是的,我也有同感...我必须在邮件列表中询问。
gtrak,2012年

7
请注意,当前(2012年10月23日)BouncyCastle(1.47)上的此代码也需要bcpkix分发。
EwyynTomato 2012年

一个证书可以具有多个CN。不仅要返回cn.getFirst(),还应该遍历所有对象并返回CN列表。
varrunr 2014年

5
IETFUtils.valueToString不会出现产生正确的结果。由于使用base 64编码(例如AAECAwQFBgcICQoLDA0ODw==),我有一个包含一些等号的CN 。该valueToString方法将反斜杠添加到结果中。取而代之的是,使用toString似乎很有效。很难确定这实际上是api的正确用法。
克里斯(Chris)

94

这是另一种方式。这个想法是您获得的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

:请在我的问题有一个视线stackoverflow.com/questions/40613147/...
Hosein Aqajani

至少对于我正在使用CN的情况,它位于多属性RDN中。换句话说:建议的解决方案不会遍历RDN的属性。这应该!
彼得

String commonName = new LdapName(certificate.getSubjectX500Principal().getName()).getRdns().stream() .filter(i -> i.getType().equalsIgnoreCase("CN")).findFirst().get().getValue().toString();
RetoHöhener,

注意:尽管它看起来不错,但是仍然存在一些问题。我使用这个工具已经有好几年了,直到发现“非标准”字段的解码问题。对于具有类似众所周知的类型的字段,例如CN(aka 2.5.4.3Rdn#getValue()包含一个String。但是,对于自定义类型,结果为byte[](可能基于以开头的内部编码表示形式#)。Ofc,byte[]->String是可能的,但包含其他(不可预测的)字符。我已使用基于BC的@laz解决方案解决了此问题,因为它在中正确处理和解码了此问题String
knalli

12

如果添加依赖关系不是问题,则可以使用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的更新代码


在Bouncycastle 1.46中似乎已弃用X509Name,并且他们打算使用x500Name。对此有所了解或打算做同样的事情吗?
gtrak 2011年

哇,看着新的API,我很难确定如何实现与上述代码相同的目标。也许Bouncycastle邮件列表档案可能有答案。如果知道了,我将更新此答案。
laz 2011年

我有同样的问题。如果您有任何建议,请告诉我。据我所知:x500name = X500Name.getInstance(PrincipalUtil.getIssuerX509Principal(cert)); RDN cn = x500name.getRDNs(BCStyle.CN)[0];
gtrak 2011年

我通过邮件列表讨论找到了解决方法,并创建了一个答案来说明如何进行。
gtrak

很好找到gtrak。我花了10分钟试图一口气弄清楚它,却再也没有解决。
laz 2012年

9

作为不需要``bcmail''的gtrak代码的替代方法:

    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 :-(


这与我提出此解决方案的原因完全相同:移植到Android ...
Ivin 2012年

8
不确定何时更改,但是现在可以X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();使用:(使用Java 8)
trichner 2014年

:请在我的问题有一个视线stackoverflow.com/questions/40613147/...
Hosein Aqajani

IETFUtils.valueToString返回的值逃脱形式。我发现只是调用.toString()而是为我工作。
holmis83

7

http://www.cryptacular.org一行

CertUtil.subjectCN(certificate);

JavaDoc:http ://www.cryptacular.org/javadocs/org/cryptacular/util/CertUtil.html#subjectCN(java.security.cert.X509Certificate

Maven依赖项:

<dependency>
    <groupId>org.cryptacular</groupId>
    <artifactId>cryptacular</artifactId>
    <version>1.1.0</version>
</dependency>

请注意,Cryptacular 1.1.x系列适用于Java 7,而Java 4.1.2.x适用于Java 8。
Markus L

6

到目前为止发布的所有答案都有一个问题:大多数使用内部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的第一个属性,我认为这就是您在这里隐式所做的事情。
彼得

4

cert.getSubjectX500Principal().getName()如果您不想依赖BouncyCastle,可以使用正则表达式over进行以下操作。

此正则表达式将解析一个专有名称,给予nameval每场比赛的捕获组。

当DN字符串包含逗号时,它们应加引号-此正则表达式可以正确处理带引号和不带引号的字符串,还可以处理带引号的字符串中的转义引号:

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

这是很好的格式:

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

这是一个链接,因此您可以查看它的运行情况:https : //regex101.com/r/zfZX3f/2

如果您想让正则表达式获取CN,则此改编版本可以做到:

(?:^|,\s?)(?:CN=(?<val>"(?:[^"]|"")+"|[^,]+))


最可靠的答案。另外,如果您要甚至支持由其编号指定的OID(例如OID.2.5.4.97),则允许的字符应从[AZ]扩展为[AZ,0-9 ,.]
yurislav

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();

我遇到了反斜杠问题,因此解决了我的问题。
琥珀色

3

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

我只想分享,以获得CN,我这样做:

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

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


这是当前答案中我最喜欢的,因为它简单,易读并且仅使用JDK中捆绑的内容。
埃米尔·伦德伯格

同意您关于使用JDK类的说法:)
Rad Rad

3
但是,应该注意的是,javac警告说它X500Name是一种内部专有API,将来的发行版中可能会删除它。
Emil Lundberg

是的,阅读链接的常见问题解答后,我需要撤消我的第一条评论。抱歉。
埃米尔·伦德伯格

1
没问题 您指出的内容非常重要。谢谢:)事实上,我不再使用该类了:P
Rad Rad

2

确实,由于gtrak似乎获得了客户证书并提取了CN,这很有可能起作用。

    X509Certificate[] certs = (X509Certificate[]) httpServletRequest
        .getAttribute("javax.servlet.request.X509Certificate");
    X509Certificate cert = certs[0];
    X509CertificateHolder x509CertificateHolder = new X509CertificateHolder(cert.getEncoded());
    X500Name x500Name = x509CertificateHolder.getSubject();
    RDN[] rdns = x500Name.getRDNs(BCStyle.CN);
    RDN rdn = rdns[0];
    String name = IETFUtils.valueToString(rdn.getFirst().getValue());
    return name;


1

可以使用cryptacular,这是在bouncycastle之上构建的Java密码库,易于使用。

RDNSequence dn = new NameReader(cert).readSubject();
return dn.getValue(StandardAttributeType.CommonName);

最好使用@Erdem Memisyazici建议。
Ghetolay 2015年


1

从证书中获取CN并不是那么简单。下面的代码一定会对您有所帮助。

String certificateURL = "C://XYZ.cer";      //just pass location

CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate testCertificate = (X509Certificate)cf.generateCertificate(new FileInputStream(certificateURL));
String certificateName = X500Name.asX500Name((new X509CertImpl(testCertificate.getEncoded()).getSubjectX500Principal())).getCommonName();

1

使用纯Java的另一种方法:

public static String getCommonName(X509Certificate certificate) {
    String name = certificate.getSubjectX500Principal().getName();
    int start = name.indexOf("CN=");
    int end = name.indexOf(",", start);
    if (end == -1) {
        end = name.length();
    }
    return name.substring(start + 3, end);
}

0

正则表达式非常昂贵。对于这样一个简单的任务,它可能会被淘汰。相反,您可以使用简单的String拆分:

String dn = ((X509Certificate) certificate).getIssuerDN().getName();
String CN = getValByAttributeTypeFromIssuerDN(dn,"CN=");

private String getValByAttributeTypeFromIssuerDN(String dn, String attributeType)
{
    String[] dnSplits = dn.split(","); 
    for (String dnSplit : dnSplits) 
    {
        if (dnSplit.contains(attributeType)) 
        {
            String[] cnSplits = dnSplit.trim().split("=");
            if(cnSplits[1]!= null)
            {
                return cnSplits[1].trim();
            }
        }
    }
    return "";
}

我很喜欢!平台和库独立。这真是太酷了!
user2007447 2014年

2
不赞成我。如果阅读RFC 2253,则会看到必须考虑一些极端情况,例如转义的逗号\,或带引号的值。
邓肯·琼斯

0

X500Name是JDK的内部实现,但是您可以使用反射。

public String getCN(String formatedDN) throws Exception{
    Class<?> x500NameClzz = Class.forName("sun.security.x509.X500Name");
    Constructor<?> constructor = x500NameClzz.getConstructor(String.class);
    Object x500NameInst = constructor.newInstance(formatedDN);
    Method method = x500NameClzz.getMethod("getCommonName", null);
    return (String)method.invoke(x500NameInst, null);
}

0

BC使提取更加容易:

X500Principal principal = x509Certificate.getSubjectX500Principal();
X500Name x500name = new X500Name(principal.getName());
String cn = x500name.getCommonName();

.getCommonName()X500Name中找不到任何方法。
lapo

(@lapo)您确定不是真正在使用sun.security.x509.X500Name-几年前其他答案指出的这一点没有记载,因此不能依靠吗?
dave_thompson_085 '18

好吧,我确实链接了org.bouncycastle.asn1.x500.X500Name类的JavaDoc ,但没有显示该方法……
lapo

0

对于多值属性-使用LDAP API ...

        X509Certificate testCertificate = ....

        X500Principal principal = testCertificate.getSubjectX500Principal(); // return subject DN
        String dn = null;
        if (principal != null)
        {
            String value = principal.getName(); // return String representation of DN in RFC 2253
            if (value != null && value.length() > 0)
            {
                dn = value;
            }
        }

        if (dn != null)
        {
            LdapName ldapDN = new LdapName(dn);
            for (Rdn rdn : ldapDN.getRdns())
            {
                Attributes attributes = rdn != null
                    ? rdn.toAttributes()
                    : null;

                Attribute attribute = attributes != null
                    ? attributes.get("CN")
                    : null;
                if (attribute != null)
                {
                    NamingEnumeration<?> values = attribute.getAll();
                    while (values != null && values.hasMoreElements())
                    {
                        Object o = values.next();
                        if (o != null && o instanceof String)
                        {
                            String cnValue = (String) o;
                        }
                    }
                }
            }
        }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.