จะแยก CN จาก X509Certificate ใน Java ได้อย่างไร


92

ฉันใช้ a SslServerSocketและใบรับรองไคลเอ็นต์และต้องการแยก CN จาก SubjectDN ออกจากไฟล์X509Certificate.

ในขณะที่ฉันโทรไปcert.getSubjectX500Principal().getName()แต่แน่นอนว่านี่ให้ DN ที่จัดรูปแบบทั้งหมดของไคลเอ็นต์ ด้วยเหตุผลบางอย่างฉันแค่สนใจในCN=theclientส่วนของ DN มีวิธีแยกส่วนนี้ของ DN โดยไม่ต้องแยกวิเคราะห์ String ด้วยตัวเองหรือไม่?



2
@AhmadAbdelghany คุณรู้ไหมว่าคำถามของฉันเก่ากว่าคำถามที่เชื่อมโยงประมาณ 1.5 ปี? ดังนั้นถ้ามีสิ่งอื่นซ้ำกับของฉัน :-)
Martin C.

จุดยุติธรรม ฉันจะฟันธงอีกคนหนึ่ง
Ahmad Abdelghany

โซลูชันสตรีม Abhijit Sarkar ป้อนคำอธิบายลิงก์ที่นี่ใช้งานได้ดี!
Christian M.

คำตอบ:


90

นี่คือโค้ดบางส่วนสำหรับ BouncyCastle API ใหม่ที่ยังไม่เลิกใช้ คุณจะต้องมีการแจกแจง 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 ฉันจะไม่สามารถเข้าใจสิ่งนี้ได้
Elliot Vargas

5
ใช่ฉันแบ่งปันความรู้สึกนั้น ... ฉันต้องถามในรายชื่อผู้รับจดหมาย
gtrak

7
โปรดทราบว่ารหัสนี้ในปัจจุบัน (23 ต.ค. 2555) BouncyCastle (1.47) ยังต้องการการแจกแจง bcpkix
EwyynTomato

ใบรับรองสามารถมี CN ได้หลายรายการ แทนที่จะส่งคืน cn.getFirst () คุณควรวนซ้ำทั้งหมดและส่งคืนรายการ CNs
varrunr

5
IETFUtils.valueToStringไม่ปรากฏในการผลิตผลที่ถูกต้อง ฉันมี CN ที่มีเครื่องหมายเท่ากับเนื่องจากการเข้ารหัส 64 ฐาน (เช่นAAECAwQFBgcICQoLDA0ODw==) valueToStringวิธีการเพิ่มเครื่องหมายทับกลับไปยังผลที่ตามมา แทนการใช้งานtoStringดูเหมือนจะใช้งานได้ เป็นการยากที่จะระบุว่านี่เป็นการใช้ api ที่ถูกต้อง
คริส

94

นี่เป็นอีกวิธีหนึ่ง แนวคิดคือ DN ที่คุณได้รับอยู่ในรูปแบบ rfc2253 ซึ่งเหมือนกับที่ใช้สำหรับ LDAP DN ทำไมไม่ใช้ 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
ทางลัดหนึ่งที่มีประโยชน์หากคุณใช้สปริง: LdapUtils.getStringValue (ldapDN, "cn");
Berthier Lemieux

โปรดดูคำถามของฉัน: stackoverflow.com/questions/40613147/…
Hosein Aqajani

อย่างน้อยสำหรับกรณีที่ฉันกำลังทำงานกับ CN อยู่ในหลายแอตทริบิวต์ RDN กล่าวอีกนัยหนึ่ง: โซลูชันที่เสนอจะไม่วนซ้ำคุณสมบัติของ RDN มันควรจะ!
peterh

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()มีไฟล์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 ที่ใช้ Bouncy Castle API ใหม่


ดูเหมือนว่า X509Name เลิกใช้แล้วใน Bouncycastle 1.46 และพวกเขาตั้งใจจะใช้ x500Name รู้อะไรเกี่ยวกับสิ่งนั้นหรือทางเลือกที่ตั้งใจจะทำสิ่งเดียวกันหรือไม่?
gtrak

ว้าวดู API ใหม่ฉันมีช่วงเวลาที่ยากลำบากในการหาวิธีบรรลุเป้าหมายเดียวกันกับโค้ดด้านบน บางทีที่เก็บรายชื่ออีเมลของ Bouncycastle อาจมีคำตอบ ฉันจะอัปเดตคำตอบนี้หากฉันเข้าใจ
laz

ฉันมีปัญหาเดียวกัน โปรดแจ้งให้เราทราบหากคุณคิดอะไร เท่าที่ฉันได้รับ: x500name = X500Name.getInstance (PrincipalUtil.getIssuerX509Principal (ใบรับรอง)); RDN cn = x500name.getRDNs (BCStyle.CN) [0];
gtrak

ฉันพบวิธีการทำผ่านการสนทนารายชื่ออีเมลฉันสร้างคำตอบที่แสดงให้เห็นว่า
gtrak

หา gtrak ดีๆ ฉันใช้เวลา 10 นาทีในการพยายามคิดออกจนถึงจุดหนึ่งและไม่เคยกลับไปหามันเลย
laz

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 ...
Ivin

8
ไม่แน่ใจว่าสิ่งนี้เปลี่ยนแปลงไปเมื่อใด แต่ตอนนี้ใช้งานได้: X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(ใช้ java 8)
trichner

โปรดดูคำถามของฉัน: stackoverflow.com/questions/40613147/…
Hosein 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 series ใช้สำหรับ Java 7 และ 1.2.x สำหรับ Java 8 ไลบรารีที่ดีมาก!
Markus L

6

คำตอบทั้งหมดที่โพสต์จนถึงขณะนี้มีปัญหา: ส่วนใหญ่ใช้การX500Nameพึ่งพา Bounty Castle ภายในหรือภายนอก ต่อไปนี้สร้างจากคำตอบของ @ Jakub และใช้ JDK API สาธารณะเท่านั้น แต่ยังแยก CN ตามที่ OP ขอด้วย นอกจากนี้ยังใช้ Java 8 ซึ่งยืนอยู่ในกลางปี ​​2560 คุณควร

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 ซึ่งฉันคิดว่าเป็นสิ่งที่คุณทำโดยปริยายที่นี่
peterh

4

นี่คือวิธีดำเนินการโดยใช้ regex cert.getSubjectX500Principal().getName()ในกรณีที่คุณไม่ต้องการพึ่งพา BouncyCastle

regex นี้จะแยกวิเคราะห์ชื่อที่แตกต่างการตั้งชื่อnameและvalกลุ่มการจับสำหรับแต่ละการแข่งขัน

เมื่อสตริง DN ประกอบด้วยเครื่องหมายจุลภาคพวกเขาควรจะถูกยกมา - regex นี้จัดการทั้งสตริงที่ยกมาและไม่ได้คำพูดอย่างถูกต้องและยังจัดการกับเครื่องหมายคำพูดที่ใช้ Escape ในสตริงที่ยกมา:

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

นี่คือรูปแบบที่สวยงาม:

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

นี่คือลิงค์เพื่อให้คุณสามารถดูการทำงานได้: https://regex101.com/r/zfZX3f/2

หากคุณต้องการให้ regex รับเฉพาะ 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

UPDATE: ชั้นเรียนนี้อยู่ในแพ็คเกจ "sun" และคุณควรใช้ด้วยความระมัดระวัง ขอบคุณ Emil สำหรับความคิดเห็น :)

แค่อยากจะแบ่งปันเพื่อรับ CN ฉันทำ:

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

เกี่ยวกับความคิดเห็นของ Emil Lundberg โปรดดู: เหตุใดนักพัฒนาจึงไม่ควรเขียนโปรแกรมที่เรียกแพ็คเกจ 'sun'


นี่เป็นคำตอบที่ฉันชอบที่สุดในบรรดาคำตอบในปัจจุบันเนื่องจากง่ายอ่านได้และใช้เฉพาะสิ่งที่รวมอยู่ใน JDK
Emil Lundberg

เห็นด้วยกับสิ่งที่คุณพูดเกี่ยวกับการใช้คลาส JDK :)
Rad

3
อย่างไรก็ตามสิ่งหนึ่งที่ควรทราบคือ javac เตือนเกี่ยวกับX500Nameการเป็น API ที่เป็นกรรมสิทธิ์ภายในซึ่งอาจถูกลบออกในอนาคต
Emil Lundberg

ใช่หลังจากอ่านคำถามที่พบบ่อยที่เชื่อมโยงแล้วฉันต้องเพิกถอนความคิดเห็นแรกของฉัน ขออภัย.
Emil Lundberg

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

สามารถใช้ cryptacular ซึ่งเป็นไลบรารีการเข้ารหัสของ 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

นิพจน์ Regex ค่อนข้างแพงที่จะใช้ สำหรับงานง่ายๆเช่นนี้มันอาจจะเป็นการฆ่ามากเกินไป แต่คุณสามารถใช้การแยกสตริงแบบธรรมดา:

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คุณจะเห็นว่ามีกรณีขอบที่คุณต้องพิจารณาเช่นเครื่องหมายจุลภาคที่ใช้ Escape \,หรือค่าที่ยกมา
Duncan Jones

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

BC ทำให้การสกัดง่ายขึ้นมาก:

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

ฉันไม่สามารถหาใด ๆ.getCommonName()วิธีการในX500Name
Lapo

(@lapo) คุณแน่ใจหรือไม่ว่าคุณไม่ได้ใช้งานจริงsun.security.x509.X500Nameซึ่งเนื่องจากคำตอบอื่น ๆ ที่ระบุไว้เมื่อหลายปีก่อนนั้นไม่มีเอกสารและไม่สามารถพึ่งพาได้
dave_thompson_085

ฉันเชื่อม JavaDoc ของorg.bouncycastle.asn1.x500.X500Nameคลาสซึ่งไม่แสดงวิธีการนั้น ...
lapo

0

สำหรับแอตทริบิวต์หลายค่า - โดยใช้ 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;
                        }
                    }
                }
            }
        }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.