Java의 X509Certificate에서 CN을 추출하는 방법은 무엇입니까?


91

SslServerSocket및 클라이언트 인증서를 사용하고 있으며 클라이언트의 .NET Framework에서 SubjectDN에서 CN을 추출하려고합니다 X509Certificate.

지금 전화를 걸 cert.getSubjectX500Principal().getName()었지만 이것은 물론 클라이언트의 전체 형식 DN을 제공합니다. 어떤 이유로 나는 CN=theclientDN 의 일부 에만 관심이 있습니다. 문자열을 직접 구문 분석하지 않고 DN의이 부분을 추출하는 방법이 있습니까?



2
@AhmadAbdelghany 제 질문이 연결된 질문보다 약 1.5 년 더 오래되었다는 것을 깨달았습니까? 어떤 경우에 따라서, 다른 :-) 내 중복입니다
마틴 C.

공정한 포인트. 다른 하나에 플래그를 지정하겠습니다.
아마드 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());

9
@grak,이 솔루션을 어떻게 알아 냈는지에 관심이 있습니다. 확실히 API 문서를 보는 것만으로도 이것을 알아낼 수 없었습니다.
Elliot Vargas

5
네, 그 감정을 공유합니다 ... 메일 링리스트에 물어봐야했습니다.
gtrak

7
현재 (2012 년 10 월 23 일) BouncyCastle (1.47)의이 코드에는 bcpkix 배포도 필요합니다.
EwyynTomato

인증서에는 여러 CN이있을 수 있습니다. cn.getFirst ()를 반환하는 대신 모두를 반복하고 CN 목록을 반환해야합니다.
varrunr 2014 년

5
IETFUtils.valueToString올바른 결과를 나타나지 않습니다. 기본 64 인코딩 (예 :)으로 인해 등호를 포함하는 CN이 있습니다 AAECAwQFBgcICQoLDA0ODw==. 이 valueToString메서드는 결과에 백 슬래시를 추가합니다. 그 대신 사용 toString하는 것이 작동 하는 것 같습니다. 이것이 실제로 api의 올바른 사용법인지 결정하는 것은 어렵습니다.
Chris

94

여기에 또 다른 방법이 있습니다. 아이디어는 얻은 DN이 LDAP DN에 사용되는 것과 동일한 rfc2253 형식이라는 것입니다. 그렇다면 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
Spring을 사용하는 경우 유용한 단축키 하나 : LdapUtils.getStringValue (ldapDN, "cn");
Berthier Lemieux

내 질문을
살펴

적어도 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(일명 2.5.4.3) 과 같이 잘 알려진 유형과 같은 유형을 가진 필드의 Rdn#getValue()경우 String. 그러나 사용자 정의 유형의 경우 결과는 byte[](로 시작하는 내부 인코딩 표현을 기반으로 할 수 있음 #)입니다. Ofc, byte[]-> String가능하지만 추가 (예측 불가능한) 문자를 포함합니다. BC 기반 @laz 솔루션으로이 문제를 해결했습니다 String..
knalli

12

종속성 추가가 문제가되지 않는 경우 X.509 인증서 작업을 위해 Bouncy Castle의 API를 사용하여 이를 수행 할 수 있습니다 .

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이 주석에서 언급했듯이이 접근 방식은 이제 더 이상 사용되지 않습니다. 새로운 Bouncy Castle API를 사용하는 gtrak의 업데이트 된 코드 를 참조하십시오 .


Bouncycastle 1.46에서 X509Name이 더 이상 사용되지 않는 것처럼 보이며 x500Name을 사용하려고합니다. 그것에 대해 아는 것이 있거나 동일한 작업을 수행하기위한 대안이 있습니까?
gtrak

와, 새 API를 보면 위 코드와 동일한 목표를 달성하는 방법을 찾기가 어렵습니다. 아마도 Bouncycastle 메일 링리스트 아카이브에 답이있을 수 있습니다. 내가 알아 내면이 답변을 업데이트하겠습니다.
laz

나는 같은 문제가 있습니다. 어떤 일이 생기면 알려주세요. 이것은 내가 얻은 것입니다. x500name = X500Name.getInstance (PrincipalUtil.getIssuerX509Principal (cert)); RDN cn = x500name.getRDNs (BCStyle.CN) [0];
gtrak

나는 메일 링리스트 토론을 통해 그것을하는 방법을 찾았고 방법을 보여주는 답변을 만들었습니다.
gtrak 2011

좋은 찾기 gtrak. 나는 한 지점에서 그것을 알아 내려고 10 분을 보냈지 만 다시는 돌아 오지 않았다.
laz

9

``bcmail ''이 필요하지 않은 gtrak 코드의 대안 :

    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을 구현하지 않습니다 :-(


... 안드로이드로 포팅 : 그건 내가이 해결책 캠 정확히 같은 이유입니다
Ivin

8
언제 변경되었는지 확실하지 않지만 이제 작동합니다. X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(Java 8 사용)
trichner

내 질문에 대해
좀 봐주세요

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 시리즈는 Java 7 용이고 1.2.x는 Java 8 용입니다. 그래도 아주 좋은 라이브러리입니다!
Markus L

6

지금까지 게시 된 모든 답변에는 몇 가지 문제가 있습니다. 대부분은 내부 X500Name또는 외부 Bounty Castle 종속성을 사용합니다. 다음은 @Jakub의 답변을 기반으로하며 공용 JDK API 만 사용하지만 OP에서 요청한대로 CN을 추출합니다. 또한 2017 년 중반에 출시 된 Java 8을 사용합니다.

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

cert.getSubjectX500Principal().getName()BouncyCastle에 대한 종속성을 원하지 않는 경우을 통해 정규식을 사용하여 수행하는 방법은 다음과 같습니다 .

이 정규식은 고유 이름을 구문 분석하고 각 일치에 대한 그룹을 제공 name하고 val캡처합니다.

DN 문자열에 쉼표가 포함 된 경우 따옴표로 묶어야합니다.이 정규식은 인용 및 인용 해제 문자열을 모두 올바르게 처리하고 인용 문자열에서 이스케이프 된 따옴표도 처리합니다.

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

다음은 멋진 형식입니다.

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

여기에 링크가있어 실제 동작을 볼 수 있습니다 : https://regex101.com/r/zfZX3f/2

정규식 이 CN 가져 오도록 하려면이 조정 된 버전이 수행합니다.

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


가장 확실한 답변입니다. 또한 번호 (예 : OID.2.5.4.97)로 지정된 OID도 지원하려면 허용되는 문자를 [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();

백 슬래시 문제가 발생하여 문제가 해결되었습니다.
Amber

3

업데이트 :이 클래스는 "sun"패키지에 있으므로주의해서 사용해야합니다. 댓글에 대한 Emil 감사합니다 :)

CN을 얻기 위해 공유하고 싶었습니다.

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

Emil Lundberg의 의견에 대해서는 개발자가 'sun'패키지라고 부르는 프로그램을 작성해서는 안되는 이유를 참조하십시오.


이것은 간단하고 읽기 쉽고 JDK에 번들로 제공되는 것만 사용하기 때문에 현재 답변 중 가장 좋아하는 것입니다.
에밀 룬드 버그

JDK 클래스 사용에 대해 말씀하신 내용에 동의합니다. :)
Rad

3
그러나 javac는 X500Name향후 릴리스에서 제거 될 수있는 내부 독점 API 에 대해 경고 합니다.
에밀 룬드 버그

예, 링크 된 FAQ를 읽은 후 첫 번째 댓글을 취소해야합니다. 죄송합니다.
에밀 룬드 버그

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

쉬운 사용을 위해 bouncycastle 위에 빌드 된 Java 암호화 라이브러리 인 cryptacular를 사용할 수 있습니다.

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

정규 표현식은 사용하는 데 다소 비쌉니다. 이러한 간단한 작업의 경우 아마도 오버 킬일 것입니다. 대신 간단한 문자열 분할을 사용할 수 있습니다.

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 2014

2
나에게 반대 투표하십시오. RFC 2253 을 읽으면 이스케이프 된 쉼표 \,또는 인용 된 값과 같이 고려해야 할 예외적 인 경우가 있음을 알 수 있습니다 .
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();

X500Name.getCommonName() 에서 메서드를 찾을 수 없습니다 .
lapo

(@lapo) 실제로 사용 sun.security.x509.X500Name하고 있지 않습니까? 몇 년 전에 언급 한 다른 답변은 문서화되지 않았으며 신뢰할 수 없습니까?
dave_thompson_085

글쎄, 나는 org.bouncycastle.asn1.x500.X500Name그 방법을 보여주지 않는 클래스 의 JavaDoc을 링크했다 …
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.