Как извлечь CN из X509Certificate в Java?

Я использую сертификаты SslServerSocket и клиента и хочу извлечь CN из SubjectDN из клиента X509Certificate.

В данный момент я вызываю cert.getSubjectX500Principal().getName(), но это, конечно, дает мне полное форматированное DN клиента. По какой-то причине меня просто интересует часть CN=theclient DN. Есть ли способ извлечь эту часть DN без синтаксического разбора строки?

Ответ 1

Вот код для нового не устаревшего API BouncyCastle. Вам понадобятся как bcmail, так и bcprov.

X509Certificate cert = ...;

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

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

Ответ 2

вот еще один способ. идея заключается в том, что получаемый вами DN находится в формате rfc2253, который аналогичен используемому для LDAP DN. Итак, почему бы не повторно использовать API LDAP?

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

Ответ 3

Если добавление зависимостей не является проблемой, вы можете сделать это с помощью API Bouncy Castle для работа с сертификатами 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 обновленный код, в котором используется новый API Bouncy Castle.

Ответ 4

В качестве альтернативы gtrak-коду, который не нужен "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: Я использовал ваше решение, пока мой SW не будет запущен на Android. И Android не реализует javax.naming.ldap: - (

Ответ 6

Все ответы, опубликованные до сих пор, имеют некоторые проблемы: большинство из них использует внутреннюю X500Name или внешнюю зависимость Замка Баунти. Следующая сборка на @Jakub отвечает и использует только открытый JDK API, но также извлекает CN, как запрошено OP. Он также использует 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(", "))

Ответ 7

У меня есть 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();

Ответ 8

Вот как это сделать с помощью регулярного выражения над cert.getSubjectX500Principal().getName(), если вы не хотите получать зависимость от BouncyCastle.

Это регулярное выражение будет анализировать отличительное имя, давая name и val групп захвата для каждого совпадения.

Когда строки DN содержат запятые, они должны быть заключены в кавычки - это регулярное выражение правильно обрабатывает как строки в кавычках, так и строки в кавычках, а также обрабатывает экранированные кавычки в строках в кавычках:

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

Вот красиво отформатировано:

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

Вот ссылка, чтобы вы могли увидеть ее в действии: https://regex101.com/r/zfZX3f/2

Если вы хотите, чтобы регулярное выражение получало только CN, то эта адаптированная версия сделает это:

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

Ответ 9

В самом деле, благодаря 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;

Ответ 10

Для удобства использования cryptacular можно использовать для создания криптографической библиотеки Java поверх bouncycastle.

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

Ответ 11

UPDATE: этот класс находится в пакете "sun", и вы должны использовать его с осторожностью. Спасибо Emil за комментарий:)

Просто хотел поделиться, чтобы получить CN, я делаю:

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

Относительно комментария Эмиля Лундберга: Почему разработчики не должны писать программы, которые называют "sun" Packages

Ответ 12

Получение 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();

Ответ 13

Вы можете попробовать использовать getName (X500Principal.RFC2253, oidMap) или getName(X500Principal.CANONICAL, oidMap), чтобы увидеть, какой из них форматирует строку DN лучше. Возможно, одним из значений карты oidMap будет строка, которую вы хотите.

Ответ 14

Выражения регулярных выражений довольно дороги в использовании. Для такой простой задачи это, вероятно, будет больше, чем убийство. Вместо этого вы можете использовать простое разделение строк:

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

Ответ 15

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

Ответ 16

BC сделала извлечение намного проще:

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

Ответ 17

Для многозначных атрибутов - используя 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;
                        }
                    }
                }
            }
        }