Comment extraire CN de X509Certificate en Java?

91

J'utilise SslServerSocketdes certificats client et et je souhaite extraire le CN du SubjectDN du client X509Certificate.

Pour le moment, j'appelle cert.getSubjectX500Principal().getName()mais cela me donne bien sûr le DN formaté total du client. Pour une raison quelconque, je suis simplement intéressé par la CN=theclientpartie du DN. Existe-t-il un moyen d'extraire cette partie du DN sans analyser moi-même la chaîne?

Martin C.
la source
Reproduction possible de l' analyse du CN à partir d'un certificat DN
Ahmad Abdelghany
2
@AhmadAbdelghany Vous vous êtes rendu compte que ma question a environ 1,5 an de plus que celle liée? Donc si quelque chose, l'autre est un double du mien :-)
Martin C.
Bon point. Je vais signaler l'autre.
Ahmad Abdelghany
la solution Stream Abhijit Sarkar entrez la description du lien ici fonctionne très bien!
Christian M.

Réponses:

90

Voici du code pour la nouvelle API BouncyCastle non obsolète. Vous aurez besoin des distributions bcmail et bcprov.

X509Certificate cert = ...;

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

return IETFUtils.valueToString(cn.getFirst().getValue());
gtrak
la source
9
@grak, je suis intéressé par la façon dont vous avez trouvé cette solution. Il est certain qu'en regardant la documentation de l'API, je n'aurais jamais pu comprendre cela.
Elliot Vargas
5
oui, je partage ce sentiment ... j'ai dû demander sur la liste de diffusion.
gtrak
7
Notez que ce code sur le BouncyCastle actuel (23 octobre 2012) (1.47) nécessite également la distribution bcpkix.
EwyynTomato
Un certificat peut avoir plusieurs CN. Au lieu de simplement renvoyer cn.getFirst (), vous devriez parcourir tout et retourner une liste de CN.
varrunr
5
Le IETFUtils.valueToStringne semble pas produire un résultat correct. J'ai un CN qui inclut des signes égaux en raison du codage en base 64 (par exemple AAECAwQFBgcICQoLDA0ODw==). La valueToStringméthode ajoute des barres obliques inverses au résultat. Au lieu de cela, l'utilisation toStringsemble fonctionner. Il est difficile de déterminer qu'il s'agit en fait d'une utilisation correcte de l'API.
Chris
94

voici une autre manière. l'idée est que le DN que vous obtenez est au format rfc2253, qui est le même que celui utilisé pour LDAP DN. Alors pourquoi ne pas réutiliser l'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
la source
1
Un raccourci utile si vous utilisez spring: LdapUtils.getStringValue (ldapDN, "cn");
Berthier Lemieux
veuillez
jeter
Au moins pour le cas où je travaille sur le CN est dans un RDN multi-attribut. En d'autres termes: la solution proposée n'itère pas sur les attributs du RDN. Cela devrait!
peterh le
String commonName = new LdapName(certificate.getSubjectX500Principal().getName()).getRdns().stream() .filter(i -> i.getType().equalsIgnoreCase("CN")).findFirst().get().getValue().toString();
Reto Höhener
Remarque: bien que cela ressemble à une bonne solution, elle présente quelques problèmes. J'utilisais celui-ci pendant quelques années jusqu'à ce que je découvre des problèmes de décodage avec des champs "non standard". Pour les champs avec des types comme les types bien connus comme CN(aka 2.5.4.3) Rdn#getValue()contient un String. Cependant, pour les types personnalisés, le résultat est byte[](peut-être basé sur une représentation codée interne commençant par #). Ofc, byte[]-> Stringest possible, mais contient des caractères supplémentaires (imprévisibles). J'ai résolu cela avec les solutions @laz basées sur BC, car il gère et décode cela correctement dans String.
knalli
12

Si l'ajout de dépendances n'est pas un problème, vous pouvez le faire avec l' API de Bouncy Castle pour travailler avec les certificats 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);

Mise à jour

Au moment de cette publication, c'était la manière de procéder. Cependant, comme le mentionne gtrak dans les commentaires, cette approche est désormais obsolète. Consultez le code mis à jour de gtrak qui utilise la nouvelle API Bouncy Castle.

laz
la source
il semble que X509Name soit obsolète dans Bouncycastle 1.46, et ils ont l'intention d'utiliser x500Name. Vous savez quelque chose à ce sujet ou sur l'alternative envisagée pour faire la même chose?
gtrak
Wow, en regardant la nouvelle API, j'ai du mal à comprendre comment atteindre le même objectif que le code ci-dessus. Peut-être que les archives de la liste de diffusion Bouncycastle pourraient avoir une réponse. Je mettrai à jour cette réponse si je la comprends.
laz
J'ai le même problème. S'il vous plaît laissez-moi savoir si vous proposez quelque chose. C'est ce que j'ai obtenu: x500name = X500Name.getInstance (PrincipalUtil.getIssuerX509Principal (cert)); RDN cn = x500name.getRDNs (BCStyle.CN) [0];
gtrak le
J'ai trouvé comment le faire via une discussion de liste de diffusion, j'ai créé une réponse qui montre comment.
gtrak
Bonne trouvaille gtrak. J'ai passé 10 minutes à essayer de le comprendre à un moment donné et je n'y suis jamais revenu.
laz
9

Comme alternative au code de gtrak qui n'a pas besoin 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: J'ai utilisé votre solution jusqu'à ce que mon logiciel soit exécuté sur Android. Et Android n'implémente pas javax.naming.ldap :-(

Ivin
la source
C'est exactement la même raison pour laquelle je suis venu avec cette solution: le portage sur Android ...
Ivin
8
Je ne sais pas quand cela a changé, mais cela fonctionne maintenant: X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName()); String cn = x500Name.getCommonName();(en utilisant java 8)
trichner
veuillez
jeter
Le IETFUtils.valueToStringrenvoie la valeur sous forme d' échappement . J'ai trouvé que l'invocation .toString()fonctionnait simplement pour moi.
holmis83
6

Toutes les réponses publiées jusqu'à présent ont un problème: la plupart utilisent la X500Namedépendance interne ou externe de Bounty Castle. Ce qui suit s'appuie sur la réponse de @ Jakub et utilise uniquement l'API JDK publique, mais extrait également le CN comme demandé par l'OP. Il utilise également Java 8, qui se tenait à la mi-2017, vous devriez vraiment.

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
la source
Dans mon cas, le CN est dans un RDN multi-attribut. Je pense que vous devrez améliorer cette solution afin que pour chaque RDN vous itérez sur les attributs RDN, plutôt que de simplement regarder le premier attribut du RDN, ce que je pense que c'est ce que vous faites implicitement ici.
peterh
4

Voici comment le faire en utilisant une regex over cert.getSubjectX500Principal().getName(), au cas où vous ne voudriez pas prendre une dépendance sur BouncyCastle.

Cette regex analysera un nom distinctif, donnant nameet valun groupe de capture pour chaque match.

Lorsque les chaînes DN contiennent des virgules, elles sont censées être entre guillemets - cette expression régulière gère correctement les chaînes entre guillemets et non, et gère également les guillemets échappés entre guillemets:

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

Voici bien formaté:

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

Voici un lien pour que vous puissiez le voir en action: https://regex101.com/r/zfZX3f/2

Si vous voulez qu'une regex n'obtienne que le CN, alors cette version adaptée le fera:

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

Cocowalla
la source
La réponse la plus robuste qui soit. De plus, si vous souhaitez prendre en charge même les OID spécifiés par son numéro (par exemple, OID.2.5.4.97), les caractères autorisés doivent être étendus de [AZ] à [AZ, 0-9 ,.]
yurislav
3

J'ai BouncyCastle 1.49, et la classe qu'il a maintenant est org.bouncycastle.asn1.x509.Certificate. J'ai regardé dans le code de IETFUtils.valueToString()- il fait un peu de fantaisie échapper avec des barres obliques inverses. Pour un nom de domaine, cela ne ferait rien de mal, mais je pense que nous pouvons faire mieux. Dans les cas que j'ai examinés, cn.getFirst().getValue()renvoie différents types de chaînes qui implémentent toutes l'interface ASN1String, qui est là pour fournir une méthode getString (). Donc, ce qui semble fonctionner pour moi est

Certificate c = ...;
RDN cn = c.getSubject().getRDNs(BCStyle.CN)[0];
return ((ASN1String)cn.getFirst().getValue()).getString();
GL
la source
J'ai rencontré le problème de la barre oblique inverse, donc cela a résolu mon problème.
Ambre
3

MISE À JOUR: Cette classe est dans le package "sun" et vous devez l'utiliser avec prudence. Merci Emil pour le commentaire :)

Je voulais juste partager, pour obtenir le CN, je fais:

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

Concernant le commentaire d'Emil Lundberg, voir: Pourquoi les développeurs ne devraient pas écrire des programmes qui appellent des packages `` sun ''

Rad
la source
C'est mon préféré parmi les réponses actuelles car il est simple, lisible et n'utilise que ce qui est inclus dans le JDK.
Emil Lundberg
D'accord avec ce que vous avez dit sur l'utilisation des classes JDK :)
Rad
3
Il faut cependant noter que javac met en garde contre le fait X500Namequ'il s'agit d'une API propriétaire interne qui pourrait être supprimée dans les versions futures.
Emil Lundberg
Oui, après avoir lu la FAQ liée, je dois révoquer mon premier commentaire. Pardon.
Emil Lundberg
1
Aucun problème du tout. Ce que vous avez souligné est vraiment important. Merci :) En fait, je n'utilise plus cette classe: P
Rad
2

En effet, grâce à gtrakil semble que pour obtenir le certificat client et extraire le CN, cela fonctionne très probablement.

    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
la source
Vérifiez cette question pertinente stackoverflow.com/a/28295134/2413303
EpicPandaForce le
1

Pourrait utiliser cryptacular qui est une bibliothèque cryptographique Java construite au-dessus de bouncycastle pour une utilisation facile.

RDNSequence dn = new NameReader(cert).readSubject();
return dn.getValue(StandardAttributeType.CommonName);
Ghetolay
la source
Il vaut mieux utiliser la suggestion @Erdem Memisyazici.
Ghetolay
1

Vous pouvez essayer d'utiliser getName (X500Principal.RFC2253, oidMap) ou getName(X500Principal.CANONICAL, oidMap)pour voir lequel formate le mieux la chaîne DN. Peut-être que l'une des oidMapvaleurs de la carte sera la chaîne souhaitée.

Gilbert Le Blanc
la source
1

Récupérer CN à partir d'un certificat n'est pas aussi simple. Le code ci-dessous vous aidera certainement.

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
la source
1

Une autre façon de faire avec Java brut:

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
la source
0

Les expressions Regex sont plutôt coûteuses à utiliser. Pour une tâche aussi simple, ce sera probablement un sur-kill. Au lieu de cela, vous pouvez utiliser un simple fractionnement de chaîne:

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
la source
J'aime vraiment ça! Indépendant de la plateforme et de la bibliothèque. C'est vraiment cool!
user2007447
2
Votez contre moi. Si vous lisez la RFC 2253 , vous verrez qu'il y a des cas extrêmes que vous devez considérer, par exemple des virgules échappées \,ou des valeurs entre guillemets.
Duncan Jones
0

X500Name est une implémentation interne de JDK, mais vous pouvez utiliser la réflexion.

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
la source
0

BC a rendu l'extraction beaucoup plus facile:

X500Principal principal = x509Certificate.getSubjectX500Principal();
X500Name x500name = new X500Name(principal.getName());
String cn = x500name.getCommonName();
s1m0nw1
la source
Je ne trouve aucune .getCommonName()méthode dans X500Name .
lapo
(@lapo) Êtes-vous sûr de ne pas utiliser réellement sun.security.x509.X500Name- ce qui, comme d'autres réponses notées plusieurs années plus tôt, n'est pas documenté et ne peut pas être invoqué?
dave_thompson_085
Eh bien, j'ai lié le JavaDoc de la org.bouncycastle.asn1.x500.X500Nameclasse, qui ne montre pas cette méthode…
lapo
0

Pour les attributs à valeurs multiples - à l'aide de l'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;
                        }
                    }
                }
            }
        }
Aujourd'hui
la source