Як витягти CN із сертифіката X509 на Java?


92

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

На даний момент я телефоную, cert.getSubjectX500Principal().getName()але це, звичайно, дає мені загальний відформатований DN клієнта. Мене чомусь просто цікавить CN=theclientчастина DN. Чи є спосіб витягти цю частину DN без синтаксичного аналізу рядка?



2
@AhmadAbdelghany Ви зрозуміли, що моє запитання приблизно на 1,5 року старше зв’язаного? Отже, якщо що, інший - дублікат мого :-)
Мартін С.

Справедливий момент. Я позначу інший.
Ахмад Абдельгані

рішення Stream Abhijit Sarkar введіть опис посилання тут працює чудово!
Крістіан М.,

Відповіді:


90

Ось код для нового не застарілого 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());

10
@grak, мені цікаво, як ти зрозумів це рішення. Звичайно, просто переглянувши документацію API, я ніколи не зміг цього зрозуміти.
Елліот Варгас,

5
так, я поділяю ці настрої ... Мені довелося запитати у списку розсилки.
gtrak

7
Зверніть увагу, що цей код на поточному (23 жовтня 2012 р.) BouncyCastle (1.47) також вимагає розповсюдження bcpkix.
EwyynTomato

Сертифікат може мати кілька CN. Замість того, щоб просто повертати cn.getFirst (), вам слід переглядати всі та повертати список CN.
varrunr

5
Схоже, IETFUtils.valueToStringщо не дає правильного результату. У мене є CN, який включає деякі знаки рівності через кодування бази 64 (наприклад AAECAwQFBgcICQoLDA0ODw==). valueToStringМетод додає зворотний косу риску до результату. Замість цього використання, toStringздається, працює. Важко визначити, що насправді це правильне використання API.
Кріс

94

ось інший спосіб. ідея полягає в тому, що отриманий вами DN має формат rfc2253, який є таким самим, як і для DN LDAP. То чому б не використовувати 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 / ...
Хосейн Aqajani

Принаймні для випадку, коли я працюю над CN, всередині багатоатрибутного RDN. Іншими словами: запропоноване рішення не перебирає атрибути RDN. Це повинно!
Петер

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

Примітка. Хоча це виглядає чудовим рішенням, у нього є деякі проблеми. Я користувався цим декілька років, поки не виявив проблеми з декодуванням із "нестандартними" полями. Для полів з типами, як добре відомі типи, наприклад CN(aka 2.5.4.3) Rdn#getValue()містить a String. Однак для користувацьких типів результат є byte[](можливо, заснований на внутрішньому закодованому поданні, починаючи з #). Ofc, byte[]-> Stringможливо, але містить додаткові (непередбачувані) символи. Я вирішив це за допомогою рішень @laz на базі BC, оскільки він правильно обробляє та декодує це в String.
knalli

12

Якщо додавання залежностей не є проблемою, ви можете зробити це за допомогою 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.


здається, що X509Name застарілий у Bouncycastle 1.46, і вони мають намір використовувати x500Name. Знаєте щось про це або передбачувану альтернативу робити те саме?
gtrak

Ого, дивлячись на новий API, мені важко зрозуміти, як досягти тієї ж мети, що і вищевказаний код. Можливо, архіви списку розсилки Bouncycastle можуть отримати відповідь. Я оновлю цю відповідь, якщо зрозумію.
laz

У мене така сама проблема. Будь ласка, дайте мені знати, якщо ви щось придумаєте. Це наскільки я отримав: x500name = X500Name.getInstance (PrincipalUtil.getIssuerX509Principal (сертифікат)); RDN cn = x500name.getRDN (BCStyle.CN) [0];
gtrak

Я знайшов, як це зробити, обговоривши список розсилки, і створив відповідь, яка показує, як.
gtrak

Добре знайти gtrak. Я витратив 10 хвилин, намагаючись зрозуміти це в один момент, і ніколи не повертався до цього.
лаз

9

Як альтернатива коду 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 :-(


Це саме та сама причина, по якій я задумав це рішення: перенесення на Android ...
Айвін,

8
Не впевнений, коли це змінилося, але це тепер працює: X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(за допомогою Java 8)
trichner

будь ласка погляд на моє запитання: stackoverflow.com/questions/40613147 / ...
Хосейн 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 та 1.2.x для Java 8. Хоча дуже хороша бібліотека!
Markus L

6

Усі відповіді, опубліковані на даний момент, мають певні проблеми: більшість використовують внутрішню X500Nameабо зовнішню залежність замку Баунті. Далі спирається на відповідь @ Jakub і використовує лише загальнодоступний API JDK, але також витягує 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(", "))

У моєму випадку CN знаходиться в межах багатоатрибутного RDN. Я думаю, вам доведеться вдосконалити це рішення, щоб для кожного RDN ви перебирали атрибути RDN, а не просто дивилися на перший атрибут RDN, що, на мою думку, ви неявно робите тут.
Петер

4

Ось як це зробити, використовуючи регулярний вираз cert.getSubjectX500Principal().getName(), якщо ви не хочете брати залежність від BouncyCastle.

Цей регулярний вираз проаналізує видатне ім'я, дасть групи nameта valгрупи захоплення для кожного збігу.

Коли рядки 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

ОНОВЛЕННЯ: Цей клас знаходиться в пакеті "сонце", і ви повинні використовувати його з обережністю. Дякую Емілю за коментар :)

Просто хотів поділитися, щоб отримати CN, я роблю:

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

Щодо коментаря Еміля Лундберга див .: " Чому розробники не повинні писати програми, які називають" сонячними "пакетами


Це моя улюблена серед поточних відповідей, оскільки вона проста, читається і використовує лише те, що входить до складу JDK.
Еміль Лундберг

Погодьтеся з тим, що ви сказали про використання класів JDK :)
Рад,

3
Слід зазначити, однак, що javac попереджає про X500Nameте, що це внутрішній власний API, який може бути вилучений у наступних версіях.
Еміль Лундберг

Так, після прочитання зв’язаного FAQ мені потрібно відкликати свій перший коментар. Вибачте.
Еміль Лундберг

1
Ніяких проблем взагалі. Те, на що ви вказали, дійсно важливо. Дякую :) Насправді, я більше не використовую цей клас: P
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;

Перевірте відповідне питання stackoverflow.com/a/28295134/2413303
EpicPandaForce

1

Можна використовувати криптовалюту, яка є криптографічною бібліотекою Java, побудованою поверх Bouncycastle для зручного використання.

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

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 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 , то побачите, що вам доводиться розглядати крайні випадки, наприклад, екрановані коми \,або вказані значення.
Дункан Джонс,

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

Е. Значно полегшив видобуток:

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

Я не можу знайти жодного .getCommonName()методу в X500Name .
лапо

(@lapo) Ви впевнені, що насправді не використовуєте sun.security.x509.X500Name- що, як зазначено в інших відповідях кількома роками раніше, є бездокументарним і на нього не можна покладатися?
dave_thompson_085

Що ж, я пов’язав org.bouncycastle.asn1.x500.X500Nameклас JavaDoc , який не показує цього методу…
Лапо

0

Для багатозначних атрибутів - за допомогою API LDAP ...

        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;
                        }
                    }
                }
            }
        }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.