Jak wyodrębnić CN z X509Certificate w Javie?

91

Używam SslServerSocketcertyfikatów a i klienta i chcę wyodrębnić CN z SubjectDN z klienta X509Certificate.

W tej chwili dzwonię, cert.getSubjectX500Principal().getName()ale to oczywiście daje mi całkowitą sformatowaną nazwę DN klienta. Z jakiegoś powodu interesuje mnie tylko CN=theclientczęść DN. Czy istnieje sposób na wyodrębnienie tej części nazwy wyróżniającej bez samodzielnego analizowania ciągu?

Martin C.
źródło
Możliwy duplikat Parsowanie CN z certyfikatu DN
Ahmad Abdelghany
2
@AhmadAbdelghany Uświadomiłeś sobie, że moje pytanie jest około 1,5 roku starsze od tego, do którego prowadzi link? Więc jeśli już, ten drugi jest moim duplikatem :-)
Martin C.
Słuszna uwaga. Oznaczę drugą.
Ahmad Abdelghany
rozwiązanie Stream Abhijit Sarkar, wprowadź tutaj opis linku, działa dobrze!
Christian M.,

Odpowiedzi:

90

Oto fragment kodu nowego niezastąpionego interfejsu API BouncyCastle. Będziesz potrzebować dystrybucji bcmail i bcprov.

X509Certificate cert = ...;

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

return IETFUtils.valueToString(cn.getFirst().getValue());
gtrak
źródło
9
@grak, interesuje mnie, jak wymyśliłeś to rozwiązanie. Z pewnością po przejrzeniu dokumentacji API nigdy nie byłem w stanie tego rozgryźć.
Elliot Vargas
5
tak, podzielam to zdanie ... musiałem zapytać na liście mailingowej.
gtrak
7
Zwróć uwagę, że ten kod w bieżącej (23 października 2012 r.) BouncyCastle (1.47) wymaga również dystrybucji bcpkix.
EwyynTomato
Certyfikat może mieć wiele numerów CN. Zamiast po prostu zwracać cn.getFirst (), powinieneś iterować przez wszystkie i zwrócić listę CN.
varrunr
5
Wydaje się, IETFUtils.valueToStringże nie daje prawidłowego wyniku. Mam CN, która zawiera kilka znaków równości z powodu kodowania podstawowego 64 (np AAECAwQFBgcICQoLDA0ODw==.). valueToStringMetoda dodaje ukośniki z powrotem do wyniku. Zamiast tego toStringwydaje się , że używanie działa. Trudno jest ustalić, czy w rzeczywistości jest to prawidłowe użycie interfejsu API.
Chris
94

tutaj jest inny sposób. pomysł polega na tym, że otrzymana nazwa wyróżniająca ma format rfc2253, który jest taki sam, jak używany w przypadku nazwy wyróżniającej LDAP. Dlaczego więc nie wykorzystać ponownie interfejsu 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());
}
Kuba
źródło
1
Jeden użyteczny skrót, jeśli używasz spring: LdapUtils.getStringValue (ldapDN, "cn");
Berthier Lemieux
proszę
spojrzeć
Przynajmniej w przypadku, w którym pracuję nad CN, znajduje się w ramach wieloatrybutowej RDN. Innymi słowy: proponowane rozwiązanie nie obejmuje atrybutów RDN. Powinno!
peterh
String commonName = new LdapName(certificate.getSubjectX500Principal().getName()).getRdns().stream() .filter(i -> i.getType().equalsIgnoreCase("CN")).findFirst().get().getValue().toString();
Reto Höhener
Uwaga: chociaż wygląda na dobre rozwiązanie, ma pewne problemy. Używałem tego przez kilka lat, dopóki nie odkryłem problemów z dekodowaniem w „niestandardowych” polach. W przypadku pól z typami, takimi jak dobrze znane typy, takie jak CN(aka 2.5.4.3), Rdn#getValue()zawiera String. Jednak w przypadku typów niestandardowych wynik jest byte[](może być oparty na wewnętrznej zakodowanej reprezentacji zaczynającej się od #). Ofc, byte[]-> Stringjest możliwe, ale zawiera dodatkowe (nieprzewidywalne) znaki. Rozwiązałem to za pomocą rozwiązań @laz opartych na BC, ponieważ obsługuje i dekoduje to poprawnie w String.
knalli
12

Jeśli dodanie zależności nie stanowi problemu, możesz to zrobić za pomocą API Bouncy Castle do pracy z certyfikatami 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);

Aktualizacja

W chwili pisania tego posta był to sposób na zrobienie tego. Jak jednak gtrak wspomina w komentarzach, to podejście jest obecnie przestarzałe. Zobacz zaktualizowany kod gtrak, który używa nowego interfejsu API Bouncy Castle.

laz
źródło
wygląda na to, że X509Name jest przestarzałe w Bouncycastle 1.46 i zamierzają używać x500Name. Wiesz coś o tym lub o planowanej alternatywie zrobienia tego samego?
gtrak
Wow, patrząc na nowe API, ciężko mi jest zrozumieć, jak osiągnąć ten sam cel, co powyższy kod. Być może archiwa list mailingowych Bouncycastle mogą znaleźć odpowiedź. Zaktualizuję tę odpowiedź, jeśli to rozgryzę.
laz
Mam ten sam problem. Daj mi znać, jeśli coś wpadniesz. O ile udało mi się uzyskać: x500name = X500Name.getInstance (PrincipalUtil.getIssuerX509Principal (cert)); RDN cn = x500name.getRDNs (BCStyle.CN) [0];
gtrak
Dowiedziałem się, jak to zrobić, poprzez dyskusję na liście mailingowej, stworzyłem odpowiedź, która pokazuje, jak to zrobić.
gtrak
Dobre znalezisko gtrak. Spędziłem 10 minut próbując to rozgryźć w pewnym momencie i nigdy nie wróciłem do tego.
laz
9

Jako alternatywa dla kodu gtrak, który nie potrzebuje '' 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: Korzystałem z twojego rozwiązania, dopóki moje oprogramowanie nie musiało być uruchomione na Androidzie. A Android nie implementuje javax.naming.ldap :-(

Ivin
źródło
To jest dokładnie ten sam powód, dla którego korzystam z tego rozwiązania: przenoszę na Androida ...
Ivin
8
Nie jestem pewien, kiedy to się zmieniło, ale to teraz działa: X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(używając Java 8)
trichner
proszę
spojrzeć
IETFUtils.valueToStringZwraca wartość uciekł formie. Zauważyłem, że po prostu wywoływanie .toString()zamiast tego działa dla mnie.
holmis83
7

Jedna linia z http://www.cryptacular.org

CertUtil.subjectCN(certificate);

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

Zależność Mavena:

<dependency>
    <groupId>org.cryptacular</groupId>
    <artifactId>cryptacular</artifactId>
    <version>1.1.0</version>
</dependency>
Erdem Memisyazici
źródło
Zwróć uwagę, że seria Cryptacular 1.1.x jest przeznaczona dla Javy 7 i 1.2.x dla Javy 8. Jednak bardzo dobra biblioteka!
Markus L
6

Wszystkie opublikowane do tej pory odpowiedzi mają pewien problem: większość korzysta z wewnętrznej X500Namelub zewnętrznej zależności Bounty Castle. Poniższy tekst opiera się na odpowiedzi @ Jakub i używa tylko publicznego interfejsu API JDK, ale także wyodrębnia CN zgodnie z prośbą OP. Wykorzystuje również Javę 8, którą w połowie 2017 roku naprawdę powinieneś.

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(", "))
Abhijit Sarkar
źródło
W moim przypadku CN znajduje się w wieloatrybutowej RDN. Myślę, że będziesz musiał ulepszyć to rozwiązanie, aby dla każdej RDN iterować po atrybutach RDN, zamiast po prostu spojrzeć na pierwszy atrybut RDN, co, jak sądzę, jest tym, co robisz domyślnie.
peterh
4

Oto jak to zrobić, używając wyrażenia regularnego cert.getSubjectX500Principal().getName(), na wypadek, gdybyś nie chciał przyjmować zależności od BouncyCastle.

To wyrażenie regularne przeanalizuje nazwę wyróżniającą, podając namei valprzechwytuj grupy dla każdego dopasowania.

Kiedy łańcuchy DN zawierają przecinki, należy je cytować - to wyrażenie regularne poprawnie obsługuje zarówno cytowane, jak i niecytowane ciągi, a także obsługuje cudzysłowy w cudzysłowach:

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

Oto ładnie sformatowany:

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

Oto link, dzięki któremu możesz zobaczyć to w akcji: https://regex101.com/r/zfZX3f/2

Jeśli chcesz, aby wyrażenie regularne otrzymywało tylko CN, zrobi to ta dostosowana wersja:

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

Cocowalla
źródło
Najbardziej solidna odpowiedź. Ponadto, jeśli chcesz obsługiwać nawet OID określone przez jego numer (np. OID.2.5.4.97), dozwolone znaki powinny zostać rozszerzone z [AZ] na [AZ, 0-9 ,.]
yurislav
3

Mam BouncyCastle 1.49, a klasa, którą ma teraz, to org.bouncycastle.asn1.x509.Certificate. Zajrzałem do kodu IETFUtils.valueToString()- robi trochę wymyślnej ucieczki z ukośnikiem odwrotnym. W przypadku nazwy domeny nie zrobiłoby to nic złego, ale czuję, że możemy zrobić to lepiej. W przypadkach, w których patrzę, cn.getFirst().getValue()zwraca różne rodzaje ciągów, z których wszystkie implementują interfejs ASN1String, który ma zapewnić metodę getString (). Więc to, co wydaje mi się działać, to

Certificate c = ...;
RDN cn = c.getSubject().getRDNs(BCStyle.CN)[0];
return ((ASN1String)cn.getFirst().getValue()).getString();
GL
źródło
Napotkałem problem z ukośnikiem odwrotnym, więc to rozwiązało mój problem.
Amber
3

AKTUALIZACJA: Ta klasa jest w pakiecie "słońce" i należy jej używać z rozwagą. Dzięki Emilowi ​​za komentarz :)

Chciałem się tylko udostępnić, aby dostać CN, robię:

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

Jeśli chodzi o komentarz Emila Lundberga, zobacz: Dlaczego programiści nie powinni pisać programów, które nazywają pakiety „sun”

Rad
źródło
To jest moja ulubiona spośród obecnych odpowiedzi, ponieważ jest prosta, czytelna i wykorzystuje tylko to, co jest zawarte w JDK.
Emil Lundberg
Zgadzam się z tym, co powiedziałeś o używaniu klas JDK :)
Rad,
3
Należy jednak pamiętać, że javac ostrzega przed X500Namebyciem wewnętrznym zastrzeżonym API, które może zostać usunięte w przyszłych wydaniach.
Emil Lundberg
Tak, po przeczytaniu linku do FAQ muszę odwołać swój pierwszy komentarz. Przepraszam.
Emil Lundberg
1
Żaden problem. To, co wskazałeś, jest naprawdę ważne. Dzięki :) Właściwie nie używam już tej klasy: P
Rad
2

Rzeczywiście, dzięki gtraktemu wydaje się, że aby uzyskać certyfikat klienta i wyodrębnić CN, najprawdopodobniej działa.

    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;
EpicPandaForce
źródło
Sprawdź to odpowiednie pytanie stackoverflow.com/a/28295134/2413303
EpicPandaForce
1

Przydałoby się cryptacular, który jest biblioteką kryptograficzną Javy zbudowaną na szczycie bouncycastle, aby była łatwa w użyciu.

RDNSequence dn = new NameReader(cert).readSubject();
return dn.getValue(StandardAttributeType.CommonName);
Ghetolay
źródło
Lepiej skorzystaj z sugestii @Erdem Memisyazici.
Ghetolay
1

Możesz spróbować użyć getName (X500Principal.RFC2253, oidMap) lub getName(X500Principal.CANONICAL, oidMap)sprawdzić, który z nich najlepiej formatuje łańcuch nazwy wyróżniającej. Może jedna z oidMapwartości mapy będzie żądanym ciągiem.

Gilbert Le Blanc
źródło
1

Pobieranie CN z certyfikatu nie jest takie proste. Poniższy kod na pewno Ci pomoże.

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();
vinayaka cn
źródło
1

Jeszcze jeden sposób na zwykłą Javę:

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);
}
barth
źródło
0

Wyrażenia Regex są dość drogie w użyciu. W przypadku tak prostego zadania prawdopodobnie będzie to przesada. Zamiast tego możesz użyć prostego podziału 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 "";
}
AivarsDa
źródło
Naprawdę to lubie! Niezależność od platformy i biblioteki. To jest naprawdę fajne!
user2007447
2
Ode mnie głos przeciw. Jeśli przeczytasz RFC 2253 , zobaczysz, że istnieją przypadki skrajne , które musisz wziąć pod uwagę, np. Przecinki ze znakami ucieczki \,lub cudzysłowy.
Duncan Jones
0

X500Name to wewnętrzna implementacja JDK, jednak możesz użyć odbicia.

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);
}
bro.xian
źródło
0

BC znacznie ułatwił ekstrakcję:

X500Principal principal = x509Certificate.getSubjectX500Principal();
X500Name x500name = new X500Name(principal.getName());
String cn = x500name.getCommonName();
s1m0nw1
źródło
Nie mogę znaleźć żadnej .getCommonName()metody w X500Name .
Lapo
(@lapo) Czy na pewno nie używasz sun.security.x509.X500Name- które, jak zauważyły ​​inne odpowiedzi kilka lat wcześniej, jest nieudokumentowane i nie można na nich polegać?
dave_thompson_085
Cóż, połączyłem org.bouncycastle.asn1.x500.X500Nameklasę JavaDoc , która nie pokazuje tej metody…
lapo
0

W przypadku atrybutów wielowartościowych - korzystanie z interfejsu 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;
                        }
                    }
                }
            }
        }
Dzisiaj Zgadnij co
źródło