如何在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个回答

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;

请查看这个相关的问题 https://stackoverflow.com/a/28295134/2413303 - EpicPandaForce

2

还有一种使用纯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);
}

这样做是不安全的,因为CN值可能会包含逗号,例如“CN=姓,名”。 - ezzadeen

1

BC使提取变得更加容易:

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

1
我在X500Name中找不到任何.getCommonName()方法。 - lapo
1
(@lapo) 你确定你没有实际使用 sun.security.x509.X500Name 吗?正如其他答案多年前指出的那样,它是未经记录的,不能依靠的。 - dave_thompson_085
1
好的,我已经链接了org.bouncycastle.asn1.x500.X500Name类的JavaDoc,但是它并没有显示那个方法... - lapo

1

可以使用Cryptacular,它是一个基于Bouncycastle的Java加密库,方便易用。

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

最好使用@Erdem Memisyazici的建议。 - Ghetolay

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

2
不行。X500Name是JDK内部类。 - peterh

1

使用Spring Security,可以使用SubjectDnX509PrincipalExtractor

X509Certificate certificate = ...;
new SubjectDnX509PrincipalExtractor().extractPrincipal(certificate).toString();

SubjectDnX509PrincipalExtractor 在内部使用正则表达式 "CN=(.*?)(?:,|$)" - Gerardo Cauich

1

如果您想要使用100%专用于此的API(尤其是如果您拥有更多“高级”证书),您可以这样使用eu.europa.esig.dss

val x509Certificate = X509CertUtils.parse(certPem)
val certToken = CertificateToken(x509Certificate)

val commonName = DSSASN1Utils.extractAttributeFromX500Principal(
    ASN1ObjectIdentifier(X520Attributes.COMMONNAME.oid),
    X500PrincipalHelper(
        x509Certificate.subjectX500Principal
    )
)

这里的优势在于X520Attributes类不仅“知道”commonName,而且几乎了解规范允许的每个可能的属性,如organizationIdentifierencryptedBusinessCategory等(目前有239个)。
esig.dss库还可以提取证书扩展和其他许多内容。例如PSD2角色:
CertificateExtensionsUtils.getQcStatements(certToken).psd2QcType.rolesOfPSP

1
在Java 17中,你可以使用方法getSubjectAlternativeNamesgetSubjectX500Principal来获取主体名称。
根据Javadoc的说明,这些方法从Java 1.4开始就存在,但我在Java 8的文档中找不到它们,所以我不知道它们是在什么时候添加的。
要获取主体名称:
System.out.println(cer.getSubjectX500Principal().getName())

如果有多个域名:
System.out.println(cer.getSubjectAlternativeNames().stream().filter(l->l.get(0).equals(2)).map(l -> String.valueOf(l.get(1))).collect(Collectors.joining(",")))

参考: Javadoc

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;
                        }
                    }
                }
            }
        }

0

正则表达式在使用时相对较耗费资源。对于这样一个简单的任务,使用它可能会过度杀伤。相反,您可以使用简单的字符串分割:

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
2
我给你点了一个踩。如果你读过RFC 2253,你会发现有一些边界情况需要考虑,例如转义逗号\,或带引号的值。 - Duncan Jones

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