Làm thế nào để trích xuất CN từ X509Certificate trong Java?


91

Tôi đang sử dụng một SslServerSocketvà các chứng chỉ máy khách và muốn trích xuất CN từ SubjectDN từ máy khách X509Certificate.

Tại thời điểm tôi gọi cert.getSubjectX500Principal().getName()nhưng điều này tất nhiên cho tôi tổng số DN được định dạng của khách hàng. Vì một số lý do tôi chỉ quan tâm đến CN=theclientmột phần của DN. Có cách nào để trích xuất phần DN này mà không cần tự phân tích chuỗi không?



2
@AhmadAbdelghany Bạn nhận ra rằng câu hỏi của tôi cũ hơn câu được liên kết khoảng 1,5 năm? Vì vậy, nếu bất cứ điều gì, người kia là một bản sao của tôi :-)
Martin C.

Điểm công bằng. Tôi sẽ gắn cờ cái còn lại.
Ahmad Abdelghany

giải pháp Luồng Abhijit Sarkar nhập mô tả liên kết ở đây hoạt động tốt!
Christian M.

Câu trả lời:


89

Đây là một số mã cho API BouncyCastle mới không còn được dùng nữa. Bạn sẽ cần cả bản phân phối bcmail và bcprov.

X509Certificate cert = ...;

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

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

9
@grak, tôi quan tâm đến cách bạn tìm ra giải pháp này. Chắc chắn chỉ từ việc xem tài liệu API, tôi sẽ không bao giờ có thể tìm ra điều này.
Elliot Vargas

5
yea, tôi chia sẻ tình cảm đó ... Tôi đã phải hỏi trong danh sách gửi thư.
gtrak

7
Lưu ý rằng mã này hiện tại (23 tháng 10 năm 2012) BouncyCastle (1.47) cũng yêu cầu phân phối bcpkix.
EwyynTomato 23/10/12

Một chứng chỉ có thể có nhiều CN. Thay vì chỉ trả về cn.getFirst (), bạn nên lặp lại tất cả và trả về một danh sách các CN.
varrunr

5
Các IETFUtils.valueToStringkhông xuất hiện để tạo ra một kết quả chính xác. Tôi có một CN bao gồm một số dấu bằng vì mã hóa cơ số 64 (ví dụ AAECAwQFBgcICQoLDA0ODw==:). Các valueToStringphương pháp thêm dấu gạch chéo lên tới kết quả. Thay vào đó, việc sử dụng toStringdường như đang hoạt động. Rất khó để xác định rằng đây thực sự là cách sử dụng đúng của api.
Chris

94

đây là một cách khác. ý tưởng là DN bạn nhận được ở định dạng rfc2253, giống như được sử dụng cho LDAP DN. Vậy tại sao không sử dụng lại 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
Một phím tắt hữu ích nếu bạn đang sử dụng spring: LdapUtils.getStringValue (ldapDN, "cn");
Berthier Lemieux

vui lòng xem câu hỏi của tôi: stackoverflow.com/questions/40613147/…
Hosein Aqajani

Ít nhất đối với trường hợp tôi đang làm việc trên CN nằm trong một RDN đa thuộc tính. Nói cách khác: giải pháp được đề xuất không lặp lại các thuộc tính của RDN. Nó nên!
peterh

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

Lưu ý: Mặc dù nó có vẻ là một giải pháp tốt nhưng nó có một số vấn đề. Tôi đã sử dụng cái này trong một số năm cho đến khi tôi phát hiện ra các vấn đề giải mã với các trường "không chuẩn". Đối với các trường có kiểu như kiểu nổi tiếng như CN(aka 2.5.4.3) Rdn#getValue()chứa a String. Tuy nhiên, đối với các loại tùy chỉnh, kết quả là byte[](có thể dựa trên biểu diễn được mã hóa bên trong bắt đầu bằng #). Ofc, byte[]-> Stringlà có thể, nhưng chứa các ký tự bổ sung (không thể đoán trước). Tôi đã giải quyết vấn đề này bằng các giải pháp @laz dựa trên BC, vì nó xử lý và giải mã điều này một cách chính xác trong String.
knalli

12

Nếu việc thêm các phần phụ thuộc không phải là vấn đề, bạn có thể thực hiện việc này với API của Bouncy Castle để làm việc với các chứng chỉ 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);

Cập nhật

Tại thời điểm đăng bài này, đây là cách để làm điều này. Tuy nhiên, như gtrak đã đề cập trong các nhận xét, phương pháp này hiện không được dùng nữa. Xem mã cập nhật của gtrak sử dụng API Bouncy Castle mới.


có vẻ như X509Name không được chấp nhận trong Bouncycastle 1.46 và họ dự định sử dụng x500Name. Biết bất cứ điều gì về điều đó hoặc dự định thay thế để làm điều tương tự?
gtrak

Chà, nhìn vào API mới, tôi đang gặp khó khăn trong việc tìm cách hoàn thành mục tiêu giống như đoạn mã trên. Có lẽ kho lưu trữ danh sách gửi thư Bouncycastle có thể có câu trả lời. Tôi sẽ cập nhật câu trả lời này nếu tôi tìm ra nó.
laz

Im có cùng một vấn đề. Vui lòng cho tôi biết nếu bạn có bất cứ điều gì. Điều này theo như tôi đã nhận được: x500name = X500Name.getInstance (PrincipalUtil.getIssuerX509Principal (cert)); RDN cn = x500name.getRDNs (BCStyle.CN) [0];
gtrak

Tôi đã tìm thấy cách thực hiện thông qua thảo luận về danh sách gửi thư, tôi đã tạo câu trả lời cho biết cách thực hiện.
gtrak

Tốt tìm thấy gtrak. Tôi đã dành 10 phút để tìm ra nó tại một thời điểm và không bao giờ quay trở lại với nó.
laz

9

Để thay thế cho mã của gtrak không cần '' 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: Tôi đã sử dụng giải pháp của bạn cho đến khi SW của tôi phải chạy trên Android. Và Android không triển khai javax.naming.ldap :-(


Đó chính là lý do tương tự tôi cam lên với giải pháp này: porting Android ...
Ivin

8
Bạn không chắc chắn khi điều này đã thay đổi, nhưng điều này hiện đang làm việc: X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(sử dụng java 8)
trichner

xin vui lòng có một cái nhìn tại câu hỏi của tôi: stackoverflow.com/questions/40613147/...
Hosein Aqajani

Các IETFUtils.valueToStringtrả về giá trị trong thoát form. Tôi thấy chỉ đơn giản là kêu gọi .toString()thay vì làm việc cho tôi.
holmis83

7

Một dòng với http://www.cryptacular.org

CertUtil.subjectCN(certificate);

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

Maven phụ thuộc:

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

Lưu ý rằng loạt Cryptacular 1.1.x dành cho Java 7 và 1.2.x dành cho Java 8. Tuy nhiên, thư viện rất tốt!
Markus L,

6

Tất cả các câu trả lời được đăng cho đến nay đều có một số vấn đề: Hầu hết sử dụng X500Namephụ thuộc vào Lâu đài tiền thưởng bên trong hoặc bên ngoài. Phần sau được xây dựng dựa trên câu trả lời của @ Jakub và chỉ sử dụng API JDK công khai, nhưng cũng trích xuất CN theo yêu cầu của OP. Nó cũng sử dụng Java 8, mà bạn thực sự nên làm vào giữa năm 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(", "))

Trong trường hợp của tôi, CN nằm trong một RDN đa thuộc tính. Tôi nghĩ rằng bạn sẽ cần phải nâng cao giải pháp này để đối với mỗi RDN, bạn sẽ lặp lại các thuộc tính RDN, thay vì chỉ xem thuộc tính đầu tiên của RDN, mà tôi nghĩ đó là điều bạn đang ngầm làm ở đây.
peterh

4

Đây là cách thực hiện bằng cách sử dụng regex over cert.getSubjectX500Principal().getName(), trong trường hợp bạn không muốn phụ thuộc vào BouncyCastle.

Regex này sẽ phân tích cú pháp một tên phân biệt, cung cấp namevalnhóm bắt cho mỗi trận đấu.

Khi chuỗi DN chứa dấu phẩy, chúng có nghĩa là được trích dẫn - regex này xử lý chính xác cả chuỗi được trích dẫn và bỏ dấu ngoặc kép, đồng thời xử lý các dấu ngoặc kép trong chuỗi được trích dẫn:

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

Đây là định dạng độc đáo:

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

Đây là liên kết để bạn có thể thấy nó hoạt động: https://regex101.com/r/zfZX3f/2

Nếu bạn muốn regex chỉ có CN, thì phiên bản điều chỉnh này sẽ làm được điều đó:

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


Câu trả lời chắc chắn nhất xung quanh. Ngoài ra, nếu bạn muốn hỗ trợ ngay cả OID được chỉ định bởi số của nó (ví dụ: OID.2.5.4.97), các ký tự được phép phải được mở rộng từ [AZ] thành [AZ, 0-9,.]
yurislav

3

Tôi có BouncyCastle 1.49 và lớp nó có bây giờ là org.bouncycastle.asn1.x509.Certificate. Tôi đã xem xét mã của IETFUtils.valueToString()- nó đang thực hiện một số thoát lạ mắt với dấu gạch chéo ngược. Đối với một tên miền, nó sẽ không làm bất cứ điều gì xấu, nhưng tôi cảm thấy chúng tôi có thể làm tốt hơn. Trong các trường hợp tôi đã xem xét cn.getFirst().getValue()trả về các loại chuỗi khác nhau mà tất cả đều triển khai giao diện ASN1String, giao diện này cung cấp phương thức getString (). Vì vậy, những gì có vẻ hiệu quả với tôi là

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

Tôi đã gặp phải sự cố dấu gạch chéo ngược, vì vậy điều này đã khắc phục sự cố của tôi.
Amber

3

CẬP NHẬT: Lớp này nằm trong gói "sun" và bạn nên sử dụng nó một cách thận trọng. Cảm ơn Emil đã nhận xét :)

Chỉ muốn chia sẻ, để có được CN, tôi làm:

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

Về nhận xét của Emil Lundberg, hãy xem: Tại sao các nhà phát triển không nên viết chương trình gọi là gói 'mặt trời'


Đây là câu trả lời yêu thích của tôi trong số các câu trả lời hiện tại vì nó đơn giản, dễ đọc và chỉ sử dụng những gì có trong JDK.
Emil Lundberg

Đồng ý với những gì bạn đã nói về việc sử dụng các lớp JDK :)
Rad

3
Tuy nhiên, cần lưu ý rằng javac cảnh báo về việc X500Namelà một API độc quyền nội bộ có thể bị xóa trong các bản phát hành trong tương lai.
Emil Lundberg

Vâng, sau khi đọc Câu hỏi thường gặp được liên kết, tôi cần thu hồi nhận xét đầu tiên của mình. Lấy làm tiếc.
Emil Lundberg

1
Không có vấn đề gì cả. Những gì bạn đã chỉ ra thực sự quan trọng. Cảm ơn :) Trong thực tế, tôi không sử dụng lớp đó nữa: P
Rad

2

Thật vậy, nhờ gtraknó có vẻ như để lấy chứng chỉ máy khách và trích xuất CN, điều này rất có thể hoạt động.

    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;

Kiểm tra câu hỏi có liên quan này stackoverflow.com/a/28295134/2413303
EpicPandaForce

1

Có thể sử dụng cryptacular là một thư viện mật mã Java được xây dựng trên bouncycastle để dễ sử dụng.

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

Tốt hơn nên sử dụng đề xuất @Erdem Memisyazici.
Ghetolay


1

Tìm nạp CN từ chứng chỉ không đơn giản. Đoạn mã dưới đây chắc chắn sẽ giúp ích cho bạn.

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

Một cách nữa để làm với Java thuần túy:

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

Biểu thức regex, khá tốn kém để sử dụng. Đối với một nhiệm vụ đơn giản như vậy, nó có thể sẽ là một giết quá nhiều. Thay vào đó, bạn có thể sử dụng phân tách chuỗi đơn giản:

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

Tôi thực sự thích nó! Nền tảng và thư viện độc lập. Điều này thực sự tuyệt vời!
user2007447

2
Bỏ phiếu từ tôi. Nếu bạn đọc RFC 2253 , bạn sẽ thấy có những trường hợp cạnh mà bạn phải xem xét, ví dụ như dấu phẩy thoát \,hoặc giá trị được trích dẫn.
Duncan Jones

0

X500Name là triển khai nội bộ của JDK, tuy nhiên bạn có thể sử dụng phản chiếu.

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 đã làm cho việc khai thác dễ dàng hơn nhiều:

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

Tôi không thể tìm thấy bất kỳ .getCommonName()phương pháp nào trong X500Name .
lapo

(@lapo) bạn có chắc là bạn không thực sự sử dụng sun.security.x509.X500Name- mà như các câu trả lời khác đã lưu ý vài năm trước đó là không có tài liệu và không thể dựa vào?
dave_thompson_085

Chà, tôi đã liên kết lớp JavaDoc org.bouncycastle.asn1.x500.X500Name, lớp này không hiển thị phương thức đó…
lapo

0

Đối với các thuộc tính đa giá trị - sử dụng 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;
                        }
                    }
                }
            }
        }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.