Wie extrahiere ich CN aus X509Certificate in Java?


88

Ich verwende ein SslServerSocketund Client-Zertifikate und möchte den CN aus dem SubjectDN aus dem Client extrahieren X509Certificate.

Im Moment rufe ich an, cert.getSubjectX500Principal().getName()aber dies gibt mir natürlich den gesamten formatierten DN des Clients. Aus irgendeinem Grund interessiert mich nur der CN=theclientTeil des DN. Gibt es eine Möglichkeit, diesen Teil des DN zu extrahieren, ohne den String selbst zu analysieren?



2
@AhmadAbdelghany Sie haben festgestellt, dass meine Frage etwa 1,5 Jahre älter ist als die verknüpfte? Also, wenn überhaupt, ist das andere ein Duplikat von mir :-)
Martin C.

Gutes Argument. Ich werde den anderen markieren.
Ahmad Abdelghany

Die Stream-Lösung Abhijit Sarkar Link Beschreibung hier eingeben funktioniert gut!
Christian M.

Antworten:


86

Hier ist ein Code für die neue nicht veraltete BouncyCastle-API. Sie benötigen sowohl bcmail- als auch bcprov-Distributionen.

X509Certificate cert = ...;

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

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

9
@grak, ich bin daran interessiert, wie Sie diese Lösung herausgefunden haben. Nur durch das Betrachten der API-Dokumentation würde ich das nie herausfinden können.
Elliot Vargas

4
Ja, ich teile dieses Gefühl ... Ich musste auf der Mailingliste nachfragen.
Gtrak

7
Beachten Sie, dass dieser Code in BouncyCastle (1.47) (23. Oktober 2012) auch eine bcpkix-Verteilung erfordert.
EwyynTomato

Ein Zertifikat kann mehrere CNs haben. Anstatt nur cn.getFirst () zurückzugeben, sollten Sie alle durchlaufen und eine Liste von CNs zurückgeben.
varrunr

4
Das IETFUtils.valueToStringscheint kein korrektes Ergebnis zu liefern. Ich habe einen CN, der aufgrund der Basis-64-Codierung (z AAECAwQFBgcICQoLDA0ODw==. B. ) einige Gleichheitszeichen enthält . Die valueToStringMethode fügt dem Ergebnis Schrägstriche hinzu. Stattdessen toStringscheint die Verwendung zu funktionieren. Es ist schwierig festzustellen, dass dies tatsächlich eine korrekte Verwendung der API ist.
Chris

94

Hier ist ein anderer Weg. Die Idee ist, dass der DN, den Sie erhalten, im Format rfc2253 vorliegt, das dem für LDAP-DN verwendeten entspricht. Warum also nicht die LDAP-API wiederverwenden?

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
Eine nützliche Verknüpfung, wenn Sie spring verwenden: LdapUtils.getStringValue (ldapDN, "cn");
Berthier Lemieux

Bitte haben Sie einen Blick auf meine Frage: stackoverflow.com/questions/40613147/…
Hosein Aqajani

Zumindest für den Fall, dass ich an der CN arbeite, befindet sie sich in einer RDN mit mehreren Attributen. Mit anderen Worten: Die vorgeschlagene Lösung iteriert nicht über Attribute des RDN. Es sollte!
Peterh

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

Hinweis: Obwohl es nach einer guten Lösung aussieht, gibt es einige Probleme. Ich habe dieses für einige Jahre verwendet, bis ich Dekodierungsprobleme mit "nicht standardmäßigen" Feldern entdeckte. Für Felder mit Typen wie bekannten Typen enthält CN(aka 2.5.4.3) Rdn#getValue()a String. Für benutzerdefinierte Typen ist das Ergebnis jedoch byte[](möglicherweise basierend auf einer internen codierten Darstellung, beginnend mit #). Ofc, byte[]-> Stringist möglich, enthält aber zusätzliche (unvorhersehbare) Zeichen. Ich habe dies mit @ laz-Lösungen gelöst, die auf BC basieren, weil es dies korrekt verarbeitet und dekodiert String.
Knalli

12

Wenn das Hinzufügen von Abhängigkeiten kein Problem darstellt, können Sie dies mit der API von Bouncy Castle für die Arbeit mit X.509-Zertifikaten tun :

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

Aktualisieren

Zum Zeitpunkt dieser Veröffentlichung war dies der Weg, dies zu tun. Wie gtrak in den Kommentaren erwähnt, ist dieser Ansatz jetzt veraltet. Siehe den aktualisierten Code von gtrak , der die neue Bouncy Castle-API verwendet.


Es scheint, dass X509Name in Bouncycastle 1.46 veraltet ist und sie beabsichtigen, x500Name zu verwenden. Wissen Sie etwas darüber oder über die beabsichtigte Alternative, um dasselbe zu tun?
Gtrak

Wow, wenn ich mir die neue API anschaue, fällt es mir schwer herauszufinden, wie ich das gleiche Ziel wie mit dem obigen Code erreichen kann. Vielleicht haben die Archive der Bouncycastle-Mailingliste eine Antwort. Ich werde diese Antwort aktualisieren, wenn ich es herausfinde.
Laz

Ich habe das gleiche Problem. Bitte lassen Sie mich wissen, wenn Sie etwas finden. Dies ist soweit ich gekommen bin: x500name = X500Name.getInstance (PrincipalUtil.getIssuerX509Principal (cert)); RDN cn = x500name.getRDNs (BCStyle.CN) [0];
gtrak

Ich habe über eine Mailinglistendiskussion herausgefunden, wie es geht. Ich habe eine Antwort erstellt, die zeigt, wie.
gtrak

Gute Entdeckung gtrak. Ich habe 10 Minuten damit verbracht, es irgendwann herauszufinden, und bin nie wieder darauf zurückgekommen.
Laz

9

Als Alternative zu gtraks Code, der kein "bcmail" benötigt:

    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: Ich habe Ihre Lösung verwendet, bis meine SW auf Android ausgeführt werden musste. Und Android implementiert javax.naming.ldap nicht :-(


Das ist genau der gleiche Grund, warum ich mich für diese Lösung entschieden habe: Portierung auf Android ...
Ivin

8
Ich X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();
bin

Bitte haben Sie einen Blick auf meine Frage: stackoverflow.com/questions/40613147/…
Hosein Aqajani

Das IETFUtils.valueToStringgibt den Wert in maskierter Form zurück. Ich fand einfach aufrufen .toString()statt für mich arbeiten.
holmis83


5

Alle bisher veröffentlichten Antworten haben ein Problem: Die meisten verwenden die interne X500Nameoder externe Bounty Castle-Abhängigkeit. Das Folgende baut auf der Antwort von @ Jakub auf und verwendet nur die öffentliche JDK-API, extrahiert aber auch den CN, wie vom OP gefordert. Es wird auch Java 8 verwendet, das Mitte 2017 stehen sollte.

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(", "))

In meinem Fall befindet sich der CN innerhalb eines RDN mit mehreren Attributen. Ich denke, Sie müssen diese Lösung so verbessern, dass Sie für jede RDN über RDN-Attribute iterieren, anstatt nur das erste Attribut der RDN zu betrachten, was Sie meiner Meinung nach hier implizit tun.
Peterh

4

Hier erfahren Sie cert.getSubjectX500Principal().getName(), wie Sie einen regulären Ausdruck verwenden , falls Sie keine Abhängigkeit von BouncyCastle übernehmen möchten.

Diese Regex analysiert für jedes Match einen eindeutigen Namen, eine Angabe nameund valeine Erfassungsgruppe.

Wenn DN-Zeichenfolgen Kommas enthalten, sollen sie in Anführungszeichen gesetzt werden. Dieser reguläre Ausdruck behandelt sowohl Zeichenfolgen in Anführungszeichen als auch Zeichenfolgen in Anführungszeichen korrekt und behandelt maskierte Anführungszeichen in Zeichenfolgen in Anführungszeichen:

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

Hier ist schön formatiert:

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

Hier ist ein Link, damit Sie ihn in Aktion sehen können: https://regex101.com/r/zfZX3f/2

Wenn Sie möchten, dass ein Regex nur den CN erhält , dann wird diese angepasste Version dies tun:

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


Die robusteste Antwort, die es gibt. Wenn Sie sogar OIDs unterstützen möchten, die durch ihre Nummer angegeben sind (z. B. OID.2.5.4.97), sollten zulässige Zeichen von [AZ] auf [AZ, 0-9,] erweitert werden
yurislav

3

Ich habe BouncyCastle 1.49 und die Klasse, die es jetzt hat, ist org.bouncycastle.asn1.x509.Certificate. Ich habe mir den Code von IETFUtils.valueToString()- es ist etwas Besonderes, mit Backslashes zu entkommen. Für einen Domainnamen würde es nichts Schlechtes tun, aber ich denke, wir können es besser machen. In den Fällen, die ich mir angesehen habe, werden cn.getFirst().getValue()verschiedene Arten von Zeichenfolgen zurückgegeben, die alle die ASN1String-Schnittstelle implementieren, die eine getString () -Methode bereitstellt. Was für mich zu funktionieren scheint, ist

Certificate c = ...;
RDN cn = c.getSubject().getRDNs(BCStyle.CN)[0];
return ((ASN1String)cn.getFirst().getValue()).getString();

Ich bin auf das Backslash-Problem gestoßen, wodurch mein Problem behoben wurde.
Amber

3

UPDATE: Diese Klasse befindet sich im "sun" -Paket und sollte mit Vorsicht verwendet werden. Danke Emil für den Kommentar :)

Ich wollte nur teilen, um die CN zu bekommen, ich mache:

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

Zu Emil Lundbergs Kommentar siehe: Warum Entwickler keine Programme schreiben sollten, die "Sun" -Pakete nennen


Dies ist mein Favorit unter den aktuellen Antworten, da es einfach und lesbar ist und nur das verwendet, was im JDK gebündelt ist.
Emil Lundberg

Stimmen Sie dem zu, was Sie über die Verwendung von JDK-Klassen gesagt haben :)
Rad

3
Man sollte jedoch beachten, dass javac davor warnt X500Name, eine interne proprietäre API zu sein, die in zukünftigen Versionen möglicherweise entfernt wird.
Emil Lundberg

Ja, nachdem ich die verlinkten FAQ gelesen habe, muss ich meinen ersten Kommentar widerrufen. Es tut uns leid.
Emil Lundberg

1
Überhaupt kein Problem. Was Sie betont haben, ist wirklich wichtig. Danke :) Tatsächlich benutze ich diese Klasse nicht mehr: P
Rad

2

In der Tat gtrakfunktioniert dies höchstwahrscheinlich, um das Client-Zertifikat zu erhalten und den CN zu extrahieren.

    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;

Überprüfen Sie diese relevante Frage stackoverflow.com/a/28295134/2413303
EpicPandaForce

1

Könnte Cryptacular verwenden, eine kryptografische Java-Bibliothek, die zur einfachen Verwendung auf Bouncycastle aufgebaut ist.

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

Verwenden Sie besser den Vorschlag von @Erdem Memisyazici.
Ghetolay

1

Das Abrufen von CN aus dem Zertifikat ist nicht so einfach. Der folgende Code wird Ihnen definitiv helfen.

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

Eine weitere Möglichkeit, mit einfachem Java umzugehen:

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

Regex-Ausdrücke sind ziemlich teuer in der Verwendung. Für solch eine einfache Aufgabe wird es wahrscheinlich ein Over-Kill sein. Stattdessen können Sie einen einfachen String-Split verwenden:

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

Ich mag es wirklich! Plattform- und bibliotheksunabhängig. Das ist wirklich cool!
user2007447

2
Ich stimme ab. Wenn Sie RFC 2253 lesen , werden Sie feststellen , dass Sie Randfälle berücksichtigen müssen, z. B. Escapezeichen \,oder Anführungszeichen.
Duncan Jones

0

X500Name ist eine interne Implementierung von JDK. Sie können jedoch Reflection verwenden.

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 machte die Extraktion viel einfacher:

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

Ich kann keine .getCommonName()Methode in X500Name finden .
Lapo

(@lapo) Sind Sie sicher, dass Sie nicht wirklich verwenden sun.security.x509.X500Name- was, wie andere Antworten einige Jahre zuvor festgestellt haben, nicht dokumentiert ist und auf das man sich nicht verlassen kann?
Dave_thompson_085

Nun, ich habe das JavaDoc der org.bouncycastle.asn1.x500.X500NameKlasse verlinkt , das diese Methode nicht zeigt ...
Lapo

0

Für mehrwertige Attribute - mit 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;
                        }
                    }
                }
            }
        }
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.