Bagaimana cara mengekstrak CN dari X509Certificate di Java?


92

Saya menggunakan SslServerSocketsertifikat dan klien dan ingin mengekstrak CN dari SubjectDN dari klien X509Certificate.

Saat ini saya menelepon cert.getSubjectX500Principal().getName()tetapi ini tentu saja memberi saya DN klien yang diformat total. Untuk beberapa alasan saya hanya tertarik pada CN=theclientbagian DN. Adakah cara untuk mengekstrak bagian DN ini tanpa mengurai String sendiri?



2
@AhmadAbdelghany Anda menyadari, bahwa pertanyaan saya kira-kira 1,5 tahun lebih tua dari yang ditautkan? Jadi jika ada, yang lain adalah duplikat milik saya :-)
Martin C.

Poin yang adil. Saya akan menandai yang lainnya.
Ahmad Abdelghany

solusi Streaming Abhijit Sarkar masukkan deskripsi tautan di sini berfungsi dengan baik!
Christian M.

Jawaban:


90

Berikut beberapa kode untuk BouncyCastle API baru yang tidak digunakan lagi. Anda membutuhkan distribusi bcmail dan bcprov.

X509Certificate cert = ...;

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

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

10
@ Grak, saya tertarik dengan cara Anda menemukan solusi ini. Tentunya hanya dengan melihat dokumentasi API saya tidak akan pernah bisa memahami hal ini.
Elliot Vargas

5
ya, saya berbagi sentimen itu ... Saya harus bertanya di milis.
gtrak

7
Perhatikan bahwa kode ini pada saat ini (23 Okt 2012) BouncyCastle (1.47) juga membutuhkan distribusi bcpkix.
EwyynTomato

Sertifikat dapat memiliki banyak CN. Alih-alih hanya menampilkan cn.getFirst (), Anda harus mengulang semua dan mengembalikan daftar CN.
varrunr

5
The IETFUtils.valueToStringtidak muncul untuk menghasilkan hasil yang benar. Saya memiliki CN yang menyertakan beberapa tanda sama dengan karena pengkodean basis 64 (misalnya AAECAwQFBgcICQoLDA0ODw==). The valueToStringMetode menambahkan kembali garis miring ke hasilnya. Sebagai gantinya, penggunaan toStringtampaknya berhasil. Sulit untuk menentukan bahwa ini sebenarnya penggunaan api yang benar.
Chris

94

ini cara lain. Idenya adalah bahwa DN yang Anda peroleh dalam format rfc2253, yang sama seperti yang digunakan untuk DN LDAP. Jadi mengapa tidak menggunakan kembali 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());
}

1
Satu pintasan berguna jika Anda menggunakan spring: LdapUtils.getStringValue (ldapDN, "cn");
Berthier Lemieux


Setidaknya untuk kasus yang saya kerjakan CN berada dalam RDN multi-atribut. Dengan kata lain: solusi yang diusulkan tidak mengulangi atribut RDN. Itu harus!
peterh

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

Catatan: Meskipun terlihat seperti solusi yang bagus, ini memiliki beberapa masalah. Saya menggunakan yang ini selama beberapa tahun sampai saya menemukan masalah decoding dengan bidang "non standar". Untuk bidang dengan tipe seperti tipe terkenal seperti CN(alias 2.5.4.3) Rdn#getValue()berisi file String. Namun, untuk tipe kustom, hasilnya adalah byte[](mungkin berdasarkan representasi internal yang dienkode dimulai dengan #). Ofc, byte[]-> Stringdimungkinkan, tetapi berisi karakter tambahan (tidak dapat diprediksi). Saya telah menyelesaikan ini dengan solusi @laz berdasarkan BC, karena menangani dan menerjemahkannya dengan benar String.
knalli

12

Jika menambahkan dependensi tidak menjadi masalah, Anda dapat melakukannya dengan API Bouncy Castle untuk bekerja dengan sertifikat 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);

Memperbarui

Pada saat posting ini, ini adalah cara untuk melakukan ini. Namun, seperti yang disebutkan gtrak di komentar, pendekatan ini sekarang sudah tidak digunakan lagi. Lihat kode terupdate gtrak yang menggunakan Bouncy Castle API baru.


sepertinya X509Name sudah tidak digunakan lagi di Bouncycastle 1.46, dan mereka bermaksud menggunakan x500Name. Tahu sesuatu tentang itu atau alternatif yang dimaksudkan untuk melakukan hal yang sama?
gtrak

Wow, melihat API baru, saya mengalami kesulitan mencari cara untuk mencapai tujuan yang sama seperti kode di atas. Mungkin arsip milis Bouncycastle punya jawabannya. Saya akan memperbarui jawaban ini jika saya mengetahuinya.
laz

Saya mempunyai masalah yang sama. Tolong beri tahu saya jika Anda menemukan sesuatu. Sejauh yang saya dapatkan: x500name = X500Name.getInstance (PrincipalUtil.getIssuerX509Principal (cert)); RDN cn = x500name.getRDNs (BCStyle.CN) [0];
gtrak

Saya menemukan cara melakukannya melalui diskusi milis, saya membuat jawaban yang menunjukkan caranya.
gtrak

Bagus temukan gtrak. Saya menghabiskan 10 menit mencoba memahaminya pada satu titik dan tidak pernah kembali ke sana.
laz

9

Sebagai alternatif dari kode gtrak yang tidak memerlukan '' 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: Saya telah menggunakan solusi Anda sampai SW saya harus dijalankan di Android. Dan Android tidak mengimplementasikan javax.naming.ldap :-(


Itulah alasan yang persis sama saya bergabung dengan solusi ini: porting ke Android ...
Ivin

8
Tidak yakin kapan ini berubah, tetapi ini sekarang berfungsi: X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(menggunakan java 8)
trichner


The IETFUtils.valueToStringmengembalikan nilai dalam melarikan diri bentuk. Saya menemukan bahwa hanya memohon .toString()bekerja untuk saya.
holmis83

7

Satu baris dengan http://www.cryptacular.org

CertUtil.subjectCN(certificate);

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

Ketergantungan Maven:

<dependency>
    <groupId>org.cryptacular</groupId>
    <artifactId>cryptacular</artifactId>
    <version>1.1.0</version>
</dependency>

Perhatikan bahwa seri Cryptacular 1.1.x untuk Java 7 dan 1.2.x untuk Java 8. Namun, perpustakaan yang sangat bagus!
Markus L

6

Semua jawaban yang diposting sejauh ini memiliki beberapa masalah: Sebagian besar menggunakan X500Namedependensi Bounty Castle internal atau eksternal. Kalimat berikut ini dibangun di atas jawaban @ Jakub dan hanya menggunakan JDK API publik, tetapi juga mengekstrak CN seperti yang diminta oleh OP. Ini juga menggunakan Java 8, yang berdiri di pertengahan 2017, Anda benar-benar harus.

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

Dalam kasus saya, CN berada dalam RDN multi-atribut. Saya pikir Anda harus meningkatkan solusi ini sehingga untuk setiap RDN Anda akan mengulangi atribut RDN, daripada hanya melihat atribut pertama dari RDN, yang menurut saya adalah apa yang Anda lakukan secara implisit di sini.
peterh

4

Berikut cara melakukannya menggunakan regex over cert.getSubjectX500Principal().getName(), jika Anda tidak ingin bergantung pada BouncyCastle.

Regex ini akan mengurai nama yang dibedakan, memberi namedan valmenangkap grup untuk setiap pertandingan.

Jika string DN berisi koma, itu dimaksudkan untuk dikutip - ekspresi reguler ini menangani string yang dikutip dan tidak dikutip dengan benar, dan juga menangani kutipan yang lolos dalam string yang dikutip:

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

Ini diformat dengan baik:

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

Berikut tautannya sehingga Anda dapat melihatnya beraksi: https://regex101.com/r/zfZX3f/2

Jika Anda ingin regex hanya mendapatkan CN, versi adaptasi ini akan melakukannya:

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


Jawaban paling kuat. Selain itu, jika Anda ingin mendukung bahkan OID yang ditentukan oleh nomornya (mis. OID.2.5.4.97), karakter yang diizinkan harus diperpanjang dari [AZ] hingga [AZ, 0-9,]
yurislav

3

Saya memiliki BouncyCastle 1,49, dan kelas yang dimilikinya sekarang adalah org.bouncycastle.asn1.x509.Certificate. Saya melihat ke dalam kode IETFUtils.valueToString()- itu melakukan beberapa melarikan diri mewah dengan garis miring terbalik. Untuk nama domain itu tidak akan melakukan sesuatu yang buruk, tapi saya rasa kami bisa melakukan yang lebih baik. Dalam kasus-kasus, saya telah melihat cn.getFirst().getValue()mengembalikan berbagai jenis string yang semuanya mengimplementasikan antarmuka ASN1String, yang ada untuk menyediakan metode getString (). Jadi, apa yang tampaknya berhasil bagi saya adalah

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

Saya mengalami masalah garis miring terbalik, jadi ini memperbaiki masalah saya.
Amber

3

UPDATE: Kelas ini ada dalam paket "sun" dan Anda harus menggunakannya dengan hati-hati. Terima kasih Emil atas komentarnya :)

Hanya ingin berbagi, untuk mendapatkan CN, saya lakukan:

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

Mengenai komentar Emil Lundberg lihat: Mengapa Pengembang Tidak Harus Menulis Program Yang Menyebut Paket 'sun'


Ini adalah favorit saya di antara jawaban saat ini karena sederhana, dapat dibaca dan hanya menggunakan apa yang dibundel dalam JDK.
Emil Lundberg

Setuju dengan apa yang Anda katakan tentang menggunakan kelas JDK :)
Rad

3
Namun, perlu diperhatikan bahwa javac memperingatkan tentang X500Namemenjadi API kepemilikan internal yang dapat dihapus dalam rilis mendatang.
Emil Lundberg

Ya, setelah membaca FAQ terkait saya perlu mencabut komentar pertama saya. Maaf.
Emil Lundberg

1
Tidak masalah sama sekali. Apa yang Anda tunjukkan sangat penting. Terima kasih :) Sebenarnya, saya tidak lagi menggunakan kelas itu: P
Rad

2

Memang, berkat gtraktampaknya untuk mendapatkan sertifikat klien dan mengekstrak CN, kemungkinan besar ini berfungsi.

    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;

Periksa pertanyaan yang relevan ini stackoverflow.com/a/28295134/2413303
EpicPandaForce

1

Bisa menggunakan cryptacular yang merupakan perpustakaan kriptografi Java yang dibangun di atas bouncycastle agar mudah digunakan.

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

Lebih baik gunakan saran @Erdem Memisyazici.
Ghetolay


1

Mengambil CN dari sertifikat tidak sesederhana itu. Kode di bawah ini pasti akan membantu Anda.

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

Satu cara lagi untuk dilakukan dengan Java biasa:

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

Ekspresi ekspresi reguler, agak mahal untuk digunakan. Untuk tugas sesederhana itu mungkin akan menjadi over kill. Sebagai gantinya Anda bisa menggunakan pemisahan String sederhana:

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

Aku benar-benar menyukainya! Platform dan perpustakaan independen. Ini sangat keren!
pengguna2007447

2
Tidak memilih dari saya. Jika Anda membaca RFC 2253 , Anda akan melihat ada kasus tepi yang harus Anda pertimbangkan, misalnya koma yang dihilangkan \,atau nilai yang dikutip.
Duncan Jones

0

X500Name adalah implementasi internal JDK, namun Anda dapat menggunakan refleksi.

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 membuat ekstraksi jauh lebih mudah:

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

Saya tidak dapat menemukan .getCommonName()metode apa pun di X500Name .
lapo

(@lapo) Anda yakin tidak benar-benar menggunakan sun.security.x509.X500Name - yang karena jawaban lain yang dicatat beberapa tahun sebelumnya tidak berdokumen dan tidak dapat diandalkan?
dave_thompson_085

Ya, saya menautkan JavaDoc org.bouncycastle.asn1.x500.X500Namekelas, yang tidak menunjukkan metode itu…
lapo

0

Untuk atribut multi-nilai - menggunakan 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;
                        }
                    }
                }
            }
        }
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.