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?

Martin C.
fonte
Possível duplicata de Análise do CN de um DN de certificado
Ahmad Abdelghany
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());
gtrak
fonte
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());
}
Jakub
fonte
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.

preguiça
fonte
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 :-(

Ivin
fonte
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
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(", "))
Abhijit Sarkar
fonte
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>"(?:[^"]|"")+"|[^,]+))

Cocowalla
fonte
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();
GL
fonte
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'

Rad
fonte
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;
EpicPandaForce
fonte
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);
Ghetolay
fonte
É melhor usar a sugestão do @Erdem Memisyazici.
Ghetolay
1

Você pode tentar usar getName (X500Principal.RFC2253, oidMap) ou getName(X500Principal.CANONICAL, oidMap)ver qual formata melhor a string DN. Talvez um dos oidMapvalores do mapa seja a string que você deseja.

Gilbert Le Blanc
fonte
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();
vinayaka cn
fonte
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);
}
barth
fonte
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 "";
}
AivarsDa
fonte
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);
}
bro.xian
fonte
0

BC tornou a extração muito mais fácil:

X500Principal principal = x509Certificate.getSubjectX500Principal();
X500Name x500name = new X500Name(principal.getName());
String cn = x500name.getCommonName();
s1m0nw1
fonte
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;
                        }
                    }
                }
            }
        }
TodayGuessWhat
fonte
0

Com Spring Security é possível usar SubjectDnX509PrincipalExtractor:

X509Certificate certificate = ...;
new SubjectDnX509PrincipalExtractor().extractPrincipal(certificate).toString();
MastaP
fonte