Como extrair o CN do X509Certificate em Java?


93

Estou usando um SslServerSockete certificados de cliente e desejo extrair o CN do SubjectDN do cliente X509Certificate.

No momento eu ligo, cert.getSubjectX500Principal().getName()mas é claro que isso me dá o DN formatado total do cliente. Por alguma razão, estou interessado apenas na CN=theclientparte do DN. Existe uma maneira de extrair essa parte do DN sem analisar a String sozinho?



2
@AhmadAbdelghany Você percebeu que minha pergunta é cerca de 1,5 anos mais velha do que a linkada? Então, se alguma coisa, o outro é uma duplicata minha :-)
Martin C.

Ponto justo. Vou sinalizar o outro.
Ahmad Abdelghany

a solução Stream Abhijit Sarkar insira a descrição do link aqui funciona bem!
Christian M.

Respostas:


90

Aqui está um código para a nova API BouncyCastle não obsoleta. Você precisará das distribuições bcmail e bcprov.

X509Certificate cert = ...;

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

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

10
@grak, estou interessado em como você descobriu essa solução. Certamente, apenas olhando a documentação da API, eu nunca seria capaz de descobrir isso.
Elliot Vargas

5
sim, eu compartilho esse sentimento ... Eu tive que perguntar na lista de mala direta.
gtrak

7
Observe que este código no BouncyCastle (1.47) atual (23 de outubro de 2012) também requer a distribuição bcpkix.
EwyynTomato

Um certificado pode ter vários CNs. Em vez de apenas retornar cn.getFirst (), você deve iterar por todos e retornar uma lista de CNs.
varrunr

5
O IETFUtils.valueToStringnão parece produzir um resultado correto. Eu tenho um CN que inclui alguns sinais de igual por causa da codificação de base 64 (por exemplo AAECAwQFBgcICQoLDA0ODw==). O valueToStringmétodo adiciona barras invertidas ao resultado. Em vez disso, o uso toStringparece estar funcionando. É difícil determinar se esse é de fato um uso correto da API.
Chris

95

aqui está outra maneira. a ideia é que o DN que você obtém está no formato rfc2253, que é o mesmo usado para o DN do LDAP. Então, por que não reutilizar a 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
Um atalho útil se você estiver usando spring: LdapUtils.getStringValue (ldapDN, "cn");
Berthier Lemieux

por favor, dê uma
olhada

Pelo menos no caso em que estou trabalhando no CN está dentro de um RDN multi-atributo. Em outras palavras: a solução proposta não itera sobre os atributos do RDN. Deveria!
peterh

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

Observação: embora pareça uma boa solução, ela apresenta alguns problemas. Eu usei este por alguns anos até que descobri problemas de decodificação com campos "fora do padrão". Para campos com tipos como tipos bem conhecidos, como CN(aka 2.5.4.3) Rdn#getValue()contém a String. No entanto, para tipos personalizados, o resultado é byte[](talvez baseado em uma representação codificada interna começando com #). Ofc, byte[]-> Stringé possível, mas contém caracteres adicionais (imprevisíveis). Eu resolvi isso com soluções @laz baseadas em BC, porque ele lida e decodifica isso corretamente no String.
Knalli

12

Se adicionar dependências não for um problema, você pode fazer isso com a API do Bouncy Castle para trabalhar com certificados 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);

Atualizar

No momento desta postagem, essa era a maneira de fazer isso. Como o gtrak menciona nos comentários, no entanto, essa abordagem agora está obsoleta. Veja o código atualizado do gtrak que usa a nova API Bouncy Castle.


parece que o X509Name está obsoleto no Bouncycastle 1.46 e eles pretendem usar o x500Name. Sabe alguma coisa sobre isso ou a alternativa pretendida para fazer a mesma coisa?
gtrak

Uau, olhando para a nova API, estou tendo dificuldade em descobrir como realizar o mesmo objetivo do código acima. Talvez os arquivos da lista de discussão do Bouncycastle tenham uma resposta. Vou atualizar esta resposta se descobrir.
laz

Estou tendo o mesmo problema. Por favor, deixe-me saber se você encontrar algo. Isso é tudo que eu obtive: x500name = X500Name.getInstance (PrincipalUtil.getIssuerX509Principal (cert)); RDN cn = x500name.getRDNs (BCStyle.CN) [0];
gtrak de

Descobri como fazer isso por meio de uma discussão de lista de e-mails, criei uma resposta que mostra como.
gtrak

Bom encontrar gtrak. Passei 10 minutos tentando descobrir isso em um ponto e nunca mais voltei a fazer isso.
laz

9

Como alternativa ao código do gtrak que não precisa de '' 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: Usei sua solução até que meu SW teve que ser executado no Android. E o Android não implementa javax.naming.ldap :-(


Essa é exatamente a mesma razão pela qual eu criei esta solução: portando para Android ...
Ivin

8
Não tenho certeza de quando isso mudou, mas agora funciona: X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(usando java 8)
trichner

por favor, dê uma
olhada

O IETFUtils.valueToStringretorna o valor em formato de escape . Descobri que simplesmente invocar .toString()funciona para mim.
holmis83

7

Uma linha com http://www.cryptacular.org

CertUtil.subjectCN(certificate);

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

Dependência de Maven:

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

Observe que a série Cryptacular 1.1.x é para Java 7 e 1.2.x para Java 8. Muito boa biblioteca, entretanto!
Markus L de

6

Todas as respostas postadas até agora têm algum problema: a maioria usa a X500Namedependência interna ou externa do Bounty Castle. O seguinte baseia-se na resposta de @Jakub e usa apenas a API JDK pública, mas também extrai o CN conforme solicitado pelo OP. Ele também usa o Java 8, que em meados de 2017, você realmente deveria.

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

No meu caso, o CN está dentro de um RDN de vários atributos. Acho que você precisará aprimorar essa solução para que, para cada RDN, itere sobre os atributos do RDN, em vez de apenas olhar para o primeiro atributo do RDN, que acho que é o que você está fazendo implicitamente aqui.
peterh

4

Veja como fazer isso usando um regex cert.getSubjectX500Principal().getName(), caso você não queira depender do BouncyCastle.

Esta regex analisará um nome distinto, dando namee valum grupo de captura para cada correspondência.

Quando as strings de DN contêm vírgulas, elas devem ser colocadas entre aspas - este regex lida corretamente com strings entre aspas e sem aspas, e também lida com aspas escapadas em strings entre aspas:

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

Aqui está bem formatado:

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

Aqui está um link para que você possa vê-lo em ação: https://regex101.com/r/zfZX3f/2

Se você quiser que um regex obtenha apenas o CN, esta versão adaptada fará isso:

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


A resposta mais robusta que existe. Além disso, se você quiser oferecer suporte até mesmo aos OIDs especificados por seu número (por exemplo, OID.2.5.4.97), os caracteres permitidos devem ser estendidos de [AZ] para [AZ, 0-9 ,.]
yurislav

3

Tenho o BouncyCastle 1.49 e a classe que ele tem agora é org.bouncycastle.asn1.x509.Certificate. Eu olhei para o código de IETFUtils.valueToString()- está escapando com barras invertidas. Para um nome de domínio, não faria nada de mal, mas sinto que podemos fazer melhor. Nos casos que observei, cn.getFirst().getValue()retornos diferentes tipos de strings que implementam a interface ASN1String, que existe para fornecer um método getString (). Então, o que parece funcionar para mim é

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

Corri para o problema da barra invertida, então isso resolveu meu problema.
Amber de

3

ATUALIZAÇÃO: Esta classe está no pacote "sun" e você deve usá-la com cautela. Obrigado Emil pelo comentário :)

Só queria compartilhar, para conseguir o CN, eu:

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

Sobre o comentário de Emil Lundberg, veja: Por que os desenvolvedores não devem escrever programas que chamam pacotes 'sun'


Este é o meu favorito entre as respostas atuais, pois é simples, legível e usa apenas o que está incluído no JDK.
Emil Lundberg

Concorde com o que você disse sobre o uso de classes JDK :)
Rad

3
Deve-se notar, entretanto, que javac avisa sobre X500Nameser uma API proprietária interna que pode ser removida em versões futuras.
Emil Lundberg,

Sim, depois de ler o FAQ vinculado , preciso revogar meu primeiro comentário. Desculpa.
Emil Lundberg,

1
Não tem problema nenhum. O que você apontou é muito importante. Obrigado :) Na verdade, eu não uso mais essa classe: P
Rad

2

Na verdade, graças a gtrakele parece que, para obter o certificado do cliente e extrair o CN, isso provavelmente funcionará.

    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;

Verifique esta questão relevante stackoverflow.com/a/28295134/2413303
EpicPandaForce

1

Poderia usar cryptacular que é uma biblioteca criptográfica Java construída em cima do bouncycastle para fácil uso.

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

É melhor usar a sugestão do @Erdem Memisyazici.
Ghetolay


1

Buscar o CN do certificado não é tão simples. O código abaixo definitivamente irá ajudá-lo.

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

Mais uma maneira de fazer com Java simples:

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

Expressões Regex são bastante caras para usar. Para uma tarefa tão simples, provavelmente será uma matança exagerada. Em vez disso, você pode usar uma divisão de string simples:

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

Eu realmente gosto! Plataforma e biblioteca independente. Isso é muito legal!
user2007447

2
Vote contra mim. Se você ler o RFC 2253 , verá que há casos extremos que deve considerar, por exemplo, vírgulas de escape \,ou valores entre aspas.
Duncan Jones

0

X500Name é uma implementação interna do JDK, no entanto, você pode usar reflexão.

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 tornou a extração muito mais fácil:

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

Não consigo encontrar nenhum .getCommonName()método em X500Name .
lapo

(@lapo) tem certeza de que não está usando sun.security.x509.X500Name- o que, como outras respostas observadas vários anos antes, não é documentado e não é confiável?
dave_thompson_085

Bem, eu vinculei o JavaDoc da org.bouncycastle.asn1.x500.X500Nameclasse, que não mostra esse método ...
lapo

0

Para atributos de vários valores - usando a API LDAP ...

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

0

Com Spring Security é possível usar SubjectDnX509PrincipalExtractor:

X509Certificate certificate = ...;
new SubjectDnX509PrincipalExtractor().extractPrincipal(certificate).toString();
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.