¿Cómo extraer CN de X509Certificate en Java?

92

Estoy usando SslServerSocketcertificados de cliente y y quiero extraer el CN ​​del SubjectDN del cliente X509Certificate.

Por el momento llamo, cert.getSubjectX500Principal().getName()pero esto, por supuesto, me da el DN total formateado del cliente. Por alguna razón, solo me interesa la CN=theclientparte del DN. ¿Hay alguna forma de extraer esta parte del DN sin analizar la cadena yo mismo?

Martín C.
fuente
Posible duplicado de Analizar el CN ​​de un certificado DN
Ahmad Abdelghany
2
@AhmadAbdelghany ¿Te diste cuenta de que mi pregunta es aproximadamente 1,5 años mayor que la vinculada? Entonces, en todo caso, el otro es un duplicado del mío :-)
Martin C.
Punto justo. Marcaré el otro.
Ahmad Abdelghany
la solución Stream Abhijit Sarkar ingrese la descripción del enlace aquí ¡ funciona bien!
Christian M.

Respuestas:

90

Aquí hay algo de código para la nueva API BouncyCastle no obsoleta. Necesitará distribuciones bcmail y bcprov.

X509Certificate cert = ...;

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

return IETFUtils.valueToString(cn.getFirst().getValue());
gtrak
fuente
10
@grak, me interesa cómo descubrió esta solución. Ciertamente, con solo mirar la documentación de la API, nunca podría resolver esto.
Elliot Vargas
5
sí, comparto ese sentimiento ... tuve que preguntar en la lista de correo.
gtrak
7
Tenga en cuenta que este código en BouncyCastle (1.47) actual (23 de octubre de 2012) también requiere la distribución bcpkix.
EwyynTomato
Un certificado puede tener varios CN. En lugar de simplemente devolver cn.getFirst (), debe recorrer todos y devolver una lista de CN.
varrunr
5
El IETFUtils.valueToStringno parece producir un resultado correcto. Tengo un CN que incluye algunos signos iguales debido a la codificación de base 64 (por ejemplo AAECAwQFBgcICQoLDA0ODw==). El valueToStringmétodo agrega barras diagonales inversas al resultado. En lugar de eso, el uso toStringparece estar funcionando. Es difícil determinar si este es de hecho un uso correcto de la API.
Chris
94

aquí hay otra forma. la idea es que el DN que obtenga esté en formato rfc2253, que es el mismo que se usa para LDAP DN. Entonces, ¿por qué no reutilizar la 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
fuente
1
Un atajo útil si está usando Spring: LdapUtils.getStringValue (ldapDN, "cn");
Berthier Lemieux
por favor,
eche
Al menos en el caso de que esté trabajando en el CN ​​está dentro de un RDN de atributos múltiples. En otras palabras: la solución propuesta no itera sobre los atributos del RDN. ¡Debería!
peterh
String commonName = new LdapName(certificate.getSubjectX500Principal().getName()).getRdns().stream() .filter(i -> i.getType().equalsIgnoreCase("CN")).findFirst().get().getValue().toString();
Reto Höhener
Nota: aunque parece una buena solución, tiene algunos problemas. Estuve usando este durante algunos años hasta que descubrí problemas de decodificación con campos "no estándar". Para campos con tipos como tipos conocidos como CN(aka 2.5.4.3) Rdn#getValue()contiene un String. Sin embargo, para los tipos personalizados, el resultado es byte[](quizás basado en una representación codificada interna que comienza con #). Ofc, byte[]-> Stringes posible, pero contiene caracteres adicionales (impredecibles). Resolví esto con las soluciones @laz basadas en BC, porque maneja y decodifica esto correctamente en String.
Knalli
12

Si agregar dependencias no es un problema, puede hacerlo con la API de Bouncy Castle para trabajar con 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);

Actualizar

En el momento de esta publicación, esta era la forma de hacerlo. Sin embargo, como menciona gtrak en los comentarios, este enfoque ahora está en desuso. Vea el código actualizado de gtrak que usa la nueva API de Bouncy Castle.

laz
fuente
parece que X509Name está obsoleto en Bouncycastle 1.46, y tienen la intención de usar x500Name. ¿Sabe algo sobre eso o la alternativa prevista para hacer lo mismo?
gtrak
Vaya, mirando la nueva API, me está costando descubrir cómo lograr el mismo objetivo que el código anterior. Quizás los archivos de la lista de correo de Bouncycastle puedan tener una respuesta. Actualizaré esta respuesta si lo averiguo.
laz
Estoy teniendo el mismo problema. Por favor, avíseme si se le ocurre algo. Hasta aquí he llegado: x500name = X500Name.getInstance (PrincipalUtil.getIssuerX509Principal (cert)); RDN cn = x500name.getRDNs (BCStyle.CN) [0];
gtrak
Encontré cómo hacerlo a través de una discusión de lista de correo, creé una respuesta que muestra cómo.
gtrak
Buen hallazgo gtrak. Pasé 10 minutos tratando de resolverlo en un momento y nunca volví a hacerlo.
laz
9

Como alternativa al código de gtrak que no necesita '' 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: He usado su solución hasta que mi SW tuvo que ejecutarse en Android. Y Android no implementa javax.naming.ldap :-(

Ivin
fuente
Esa es exactamente la misma razón por la que me encontré con esta solución: migrar a Android ...
Ivin
8
No estoy seguro de cuándo cambió esto, pero ahora funciona: X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(usando java 8)
trichner
por favor,
eche
El IETFUtils.valueToStringdevuelve el valor en escapado formulario. Encontré que simplemente invocar .toString()funcionaba para mí.
holmis83
7

Una línea con http://www.cryptacular.org

CertUtil.subjectCN(certificate);

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

Dependencia de Maven:

<dependency>
    <groupId>org.cryptacular</groupId>
    <artifactId>cryptacular</artifactId>
    <version>1.1.0</version>
</dependency>
Erdem Memisyazici
fuente
Tenga en cuenta que la serie Cryptacular 1.1.x es para Java 7 y 1.2.x para Java 8. ¡Sin embargo, muy buena biblioteca!
Markus L
6

Todas las respuestas publicadas hasta ahora tienen algún problema: la mayoría usa la X500Namedependencia interna o externa de Bounty Castle. Lo siguiente se basa en la respuesta de @ Jakub y usa solo la API pública de JDK, pero también extrae el CN ​​como lo solicita el OP. También usa Java 8, que estaba a mediados de 2017, realmente debería hacerlo.

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
fuente
En mi caso, el CN ​​está dentro de un RDN de atributos múltiples. Creo que necesitará mejorar esta solución para que para cada RDN iteraría sobre los atributos de RDN, en lugar de simplemente mirar el primer atributo del RDN, que creo que es lo que está haciendo implícitamente aquí.
peterh
4

A continuación, le mostramos cómo hacerlo usando una expresión regular cert.getSubjectX500Principal().getName(), en caso de que no desee tomar una dependencia en BouncyCastle.

Esta expresión regular analizará un nombre distinguido, dando namey valcapturando grupos para cada coincidencia.

Cuando las cadenas DN contienen comas, deben estar entrecomilladas: esta expresión regular maneja correctamente cadenas entre comillas y sin comillas, y también maneja comillas de escape en cadenas entre comillas:

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

Aquí está muy bien formateado:

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

Aquí tienes un enlace para que puedas verlo en acción: https://regex101.com/r/zfZX3f/2

Si desea que una expresión regular obtenga solo el CN, esta versión adaptada lo hará:

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

Cocowalla
fuente
La respuesta más robusta que existe. Además, si desea admitir incluso los OID especificados por su número (por ejemplo, OID.2.5.4.97), los caracteres permitidos deben extenderse de [AZ] a [AZ, 0-9 ,.]
yurislav
3

Tengo BouncyCastle 1.49, y la clase que tiene ahora es org.bouncycastle.asn1.x509.Certificate. Miré el código de IETFUtils.valueToString()- está haciendo un escape elegante con barras invertidas. Para un nombre de dominio no sería nada malo, pero creo que podemos hacerlo mejor. En los casos que he visto, cn.getFirst().getValue()devuelve diferentes tipos de cadenas que implementan la interfaz ASN1String, que está ahí para proporcionar un método getString (). Entonces, lo que parece funcionar para mí es

Certificate c = ...;
RDN cn = c.getSubject().getRDNs(BCStyle.CN)[0];
return ((ASN1String)cn.getFirst().getValue()).getString();
GL
fuente
Me encontré con el problema de la barra invertida, así que esto solucionó mi problema.
Ámbar
3

ACTUALIZACIÓN: Esta clase está en el paquete "sun" y debe usarla con precaución. Gracias Emil por el comentario :)

Solo quería compartir, para obtener el CN, lo hago:

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

Con respecto al comentario de Emil Lundberg, consulte: Por qué los desarrolladores no deberían escribir programas que llamen paquetes 'sun'

Rad
fuente
Esta es mi favorita entre las respuestas actuales, ya que es simple, legible y usa solo lo que se incluye en el JDK.
Emil Lundberg
De acuerdo con lo que dijiste sobre el uso de clases de JDK :)
Rad
3
Sin embargo, se debe tener en cuenta que javac advierte sobre X500Nameser una API patentada interna que puede eliminarse en versiones futuras.
Emil Lundberg
Sí, después de leer las preguntas frecuentes vinculadas , necesito revocar mi primer comentario. Lo siento.
Emil Lundberg
1
No hay problema. Lo que señaló es realmente importante. Gracias :) De hecho, ya no uso esa clase: P
Rad
2

De hecho, gracias a gtrakque parece que para obtener el certificado del cliente y extraer el CN, esto probablemente funcione.

    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
fuente
Consulte esta pregunta relevante stackoverflow.com/a/28295134/2413303
EpicPandaForce
1

Podría usar cryptacular, que es una biblioteca criptográfica de Java construida sobre bouncycastle para un uso fácil.

RDNSequence dn = new NameReader(cert).readSubject();
return dn.getValue(StandardAttributeType.CommonName);
Ghetolay
fuente
Bueno, mejor usa la sugerencia de @Erdem Memisyazici.
Ghetolay
1

Puede intentar usar getName (X500Principal.RFC2253, oidMap) o getName(X500Principal.CANONICAL, oidMap)ver cuál formatea mejor la cadena DN. Quizás uno de los oidMapvalores del mapa sea la cadena que desea.

Gilbert Le Blanc
fuente
1

Obtener CN del certificado no es tan simple. El siguiente código definitivamente te ayudará.

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
fuente
1

Una forma más de hacerlo con Java simple:

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
fuente
0

Las expresiones Regex son bastante caras de usar. Para una tarea tan simple, probablemente será una muerte excesiva. En su lugar, podría usar una división de cadena simple:

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
fuente
¡Me gusta mucho! Plataforma y biblioteca independientes. ¡Esto es realmente genial!
user2007447
2
Voto en contra de mí. Si lee RFC 2253 , verá que hay casos extremos que debe considerar, por ejemplo, comas de escape \,o valores entre comillas.
Duncan Jones
0

X500Name es una implementación interna de JDK, sin embargo, puede utilizar la reflexión.

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
fuente
0

BC facilitó mucho la extracción:

X500Principal principal = x509Certificate.getSubjectX500Principal();
X500Name x500name = new X500Name(principal.getName());
String cn = x500name.getCommonName();
s1m0nw1
fuente
No puedo encontrar ningún .getCommonName()método en X500Name .
lapo
(@lapo) ¿está seguro de que en realidad no está usando sun.security.x509.X500Name, que, como se señaló en otras respuestas varios años antes, no está documentado y no se puede confiar en él?
dave_thompson_085
Bueno, vinculé el JavaDoc de la org.bouncycastle.asn1.x500.X500Nameclase, que no muestra ese método…
lapo
0

Para atributos de varios valores, utilizando la 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;
                        }
                    }
                }
            }
        }
Hoy adivina que
fuente