web-dev-qa-db-fra.com

Créer par programme un certificat X509 à l'aide d'OpenSSL

J'ai une application C/C++ et j'ai besoin de créer un certificat pem X509 contenant à la fois une clé publique et privée. Le certificat peut être auto-signé ou non signé, peu importe.

Je veux le faire dans une application, pas depuis la ligne de commande.

Quelles fonctions OpenSSL feront cela pour moi? Tout exemple de code est un bonus!

68
Tim

Vous devrez d'abord vous familiariser avec la terminologie et les mécanismes.

Un certificat X.509 , par définition, n'inclut pas de clé privée. Au lieu de cela, il s'agit d'une version signée par l'autorité de certification de la clé publique (avec tous les attributs que l'autorité de certification met dans la signature). Le format PEM ne prend en charge que le stockage séparé de la clé et du certificat, bien que vous puissiez ensuite concaténer les deux.

Dans tous les cas, vous devrez invoquer plus de 20 fonctions différentes de l'API OpenSSL pour créer une clé et un certificat auto-signé. Un exemple est dans la source OpenSSL elle-même, dans demos/x509/mkcert.c

Pour une réponse plus détaillée, veuillez consulter explication de Nathan Osman ci-dessous.

43
Martin v. Löwis

Je me rends compte que c'est une réponse très tardive (et longue). Mais compte tenu de la façon dont cette question semble se classer dans les résultats des moteurs de recherche, j'ai pensé qu'il pourrait être utile d'écrire une réponse décente pour.

Une grande partie de ce que vous lirez ci-dessous est empruntée à cette démo et aux documents OpenSSL. Le code ci-dessous s'applique à la fois à C et C++.


Avant de pouvoir réellement créer un certificat, nous devons créer une clé privée. OpenSSL fournit le EVP_PKEY structure pour stocker en mémoire une clé privée indépendante de l'algorithme. Cette structure est déclarée dans openssl/evp.h mais est inclus par openssl/x509.h (dont nous aurons besoin plus tard) afin que vous n'ayez pas vraiment besoin d'inclure explicitement l'en-tête.

Afin d'allouer un EVP_PKEY structure, nous utilisons EVP_PKEY_new :

EVP_PKEY * pkey;
pkey = EVP_PKEY_new();

Il existe également une fonction correspondante pour libérer la structure - EVP_PKEY_free - qui accepte un seul argument: le EVP_PKEY structure initialisée ci-dessus.

Maintenant, nous devons générer une clé. Pour notre exemple, nous allons générer une clé RSA. Cela se fait avec le RSA_generate_key fonction déclarée dans openssl/rsa.h. Cette fonction renvoie un pointeur sur une structure RSA.

Une simple invocation de la fonction pourrait ressembler à ceci:

RSA * rsa;
rsa = RSA_generate_key(
    2048,   /* number of bits for the key - 2048 is a sensible value */
    RSA_F4, /* exponent - RSA_F4 is defined as 0x10001L */
    NULL,   /* callback - can be NULL if we aren't displaying progress */
    NULL    /* callback argument - not needed in this case */
);

Si la valeur de retour de RSA_generate_key est NULL, puis quelque chose s'est mal passé. Sinon, nous avons maintenant une clé RSA, et nous pouvons l'attribuer à notre EVP_PKEY structure antérieure:

EVP_PKEY_assign_RSA(pkey, rsa);

La structure RSA sera automatiquement libérée lorsque EVP_PKEY la structure est libérée.


Maintenant, pour le certificat lui-même.

OpenSSL utilise le X509 structure pour représenter un certificat x509 en mémoire. La définition de cette structure est dans openssl/x509.h. La première fonction dont nous allons avoir besoin est X509_new . Son utilisation est relativement simple:

X509 * x509;
x509 = X509_new();

Comme ce fut le cas avec EVP_PKEY, il existe une fonction correspondante pour libérer la structure - X509_free.

Maintenant, nous devons définir quelques propriétés du certificat en utilisant X509_* les fonctions:

ASN1_INTEGER_set(X509_get_serialNumber(x509), 1);

Cela définit le numéro de série de notre certificat sur "1". Certains serveurs HTTP open source refusent d'accepter un certificat avec un numéro de série de "0", qui est la valeur par défaut. L'étape suivante consiste à spécifier la durée pendant laquelle le certificat est réellement valide. Nous le faisons avec les deux appels de fonction suivants:

X509_gmtime_adj(X509_get_notBefore(x509), 0);
X509_gmtime_adj(X509_get_notAfter(x509), 31536000L);

La première ligne définit la propriété notBefore du certificat sur l'heure actuelle. (Le X509_gmtime_adj la fonction ajoute le nombre de secondes spécifié à l'heure actuelle - dans ce cas aucune.) La deuxième ligne définit la propriété notAfter du certificat sur 365 jours (60 secondes * 60 minutes * 24 heures * 365 jours) ).

Maintenant, nous devons définir la clé publique de notre certificat à l'aide de la clé que nous avons générée précédemment:

X509_set_pubkey(x509, pkey);

Puisqu'il s'agit d'un certificat auto-signé, nous définissons le nom de l'émetteur sur le nom du sujet. La première étape de ce processus consiste à obtenir le nom du sujet:

X509_NAME * name;
name = X509_get_subject_name(x509);

Si vous avez déjà créé un certificat auto-signé sur la ligne de commande auparavant, vous vous souvenez probablement qu'on vous a demandé un code de pays. Voici où nous le fournissons avec l'organisation ("O") et le nom commun ("CN"):

X509_NAME_add_entry_by_txt(name, "C",  MBSTRING_ASC,
                           (unsigned char *)"CA", -1, -1, 0);
X509_NAME_add_entry_by_txt(name, "O",  MBSTRING_ASC,
                           (unsigned char *)"MyCompany Inc.", -1, -1, 0);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
                           (unsigned char *)"localhost", -1, -1, 0);

(J'utilise la valeur "CA" ici parce que je suis canadien et que c'est notre code de pays. Notez également que le paramètre # 4 doit être explicitement converti en unsigned char *.)

Maintenant, nous pouvons réellement définir le nom de l'émetteur:

X509_set_issuer_name(x509, name);

Et enfin, nous sommes prêts à effectuer le processus de signature. Nous appelons X509_sign avec la clé que nous avons générée précédemment. Le code pour cela est douloureusement simple:

X509_sign(x509, pkey, EVP_sha1());

Notez que nous utilisons l'algorithme de hachage SHA-1 pour signer la clé. Cela diffère du mkcert.c démo que j'ai mentionné au début de cette réponse, qui utilise MD5.


Nous avons maintenant un certificat auto-signé! Mais nous n'avons pas encore terminé - nous devons écrire ces fichiers sur le disque. Heureusement, OpenSSL nous a également couverts avec le PEM_* fonctions déclarées dans openssl/pem.h. Le premier dont nous aurons besoin est PEM_write_PrivateKey pour avoir enregistré notre clé privée.

FILE * f;
f = fopen("key.pem", "wb");
PEM_write_PrivateKey(
    f,                  /* write the key to the file we've opened */
    pkey,               /* our key from earlier */
    EVP_des_ede3_cbc(), /* default cipher for encrypting the key on disk */
    "replace_me",       /* passphrase required for decrypting the key on disk */
    10,                 /* length of the passphrase string */
    NULL,               /* callback for requesting a password */
    NULL                /* data to pass to the callback */
);

Si vous ne voulez pas crypter la clé privée, passez simplement NULL pour les troisième et quatrième paramètres ci-dessus. Quoi qu'il en soit, vous voudrez certainement vous assurer que le fichier n'est pas lisible dans le monde entier. (Pour les utilisateurs Unix, cela signifie chmod 600 key.pem.)

Ouf! Nous en sommes maintenant à une seule fonction - nous devons écrire le certificat sur le disque. La fonction dont nous avons besoin pour cela est PEM_write_X509:

FILE * f;
f = fopen("cert.pem", "wb");
PEM_write_X509(
    f,   /* write the certificate to the file we've opened */
    x509 /* our certificate */
);

Et c'est fini! J'espère que les informations contenues dans cette réponse sont suffisantes pour vous donner une idée approximative de la façon dont tout fonctionne, bien que nous ayons à peine effleuré la surface d'OpenSSL.

Pour ceux qui souhaitent voir à quoi ressemble tout le code ci-dessus dans une application réelle, j'ai créé un Gist (écrit en C++) que vous pouvez voir ici .

175
Nathan Osman

Y a-t-il une chance de le faire via un appel system depuis votre application? Plusieurs bonnes raisons pour cela:

  • Licence: L'appel de l'exécutable openssl le sépare sans doute de votre application et peut offrir certains avantages. Avertissement: consultez un avocat à ce sujet.

  • Documentation: OpenSSL est livré avec une documentation en ligne de commande phénoménale qui simplifie considérablement un outil potentiellement compliqué.

  • Testabilité: vous pouvez exercer OpenSSL à partir de la ligne de commande jusqu'à ce que vous compreniez exactement comment créer vos certificats. Il y a beaucoup d'options; attendez-vous à y passer environ une journée jusqu'à ce que vous obteniez tous les détails. Après cela, il est trivial d'incorporer la commande dans votre application.

Si vous choisissez d'utiliser l'API, vérifiez le openssl-dev liste des développeurs sur www.openssl.org.

Bonne chance!

2
Adam Liss

Tutoriel très simple pour créer des certificats numériques http://publib.boulder.ibm.com/infocenter/rsthelp/v8r0m0/index.jsp?topic=/com.ibm.rational.test.lt.doc/topics/ tcreatecertopenssl.html

À propos de l'exécution de ces commandes à partir de votre code, je ne suis pas sûr.

0
Jaime Hablutzel

Nathan Osman l'a expliqué de manière approfondie et complète, avait le même problème à résoudre en C++ alors voici mon petit ajout, concept réécrit de style cpp avec quelques mises en garde prises en compte:

bool generateX509(const std::string& certFileName, const std::string& keyFileName, long daysValid)
{
    bool result = false;

    std::unique_ptr<BIO, void (*)(BIO *)> certFile  { BIO_new_file(certFileName.data(), "wb"), BIO_free_all  };
    std::unique_ptr<BIO, void (*)(BIO *)> keyFile { BIO_new_file(keyFileName.data(), "wb"), BIO_free_all };

    if (certFile && keyFile)
    {
        std::unique_ptr<RSA, void (*)(RSA *)> rsa { RSA_new(), RSA_free };
        std::unique_ptr<BIGNUM, void (*)(BIGNUM *)> bn { BN_new(), BN_free };

        BN_set_Word(bn.get(), RSA_F4);
        int rsa_ok = RSA_generate_key_ex(rsa.get(), RSA_KEY_LENGTH, bn.get(), nullptr);

        if (rsa_ok == 1)
        {
            // --- cert generation ---
            std::unique_ptr<X509, void (*)(X509 *)> cert { X509_new(), X509_free };
            std::unique_ptr<EVP_PKEY, void (*)(EVP_PKEY *)> pkey { EVP_PKEY_new(), EVP_PKEY_free};

            // The RSA structure will be automatically freed when the EVP_PKEY structure is freed.
            EVP_PKEY_assign(pkey.get(), EVP_PKEY_RSA, reinterpret_cast<char*>(rsa.release()));
            ASN1_INTEGER_set(X509_get_serialNumber(cert.get()), 1); // serial number

            X509_gmtime_adj(X509_get_notBefore(cert), 0); // now
            X509_gmtime_adj(X509_get_notAfter(cert), daysValid * 24 * 3600); // accepts secs

            X509_set_pubkey(cert.get(), pkey.get());

            // 1 -- X509_NAME may disambig with wincrypt.h
            // 2 -- DO NO FREE the name internal pointer
            X509_name_st* name = X509_get_subject_name(cert.get());

            const uchar country[] = "RU";
            const uchar company[] = "MyCompany, PLC";
            const uchar common_name[] = "localhost";

            X509_NAME_add_entry_by_txt(name, "C",  MBSTRING_ASC, country, -1, -1, 0);
            X509_NAME_add_entry_by_txt(name, "O",  MBSTRING_ASC, company, -1, -1, 0);
            X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, common_name, -1, -1, 0);

            X509_set_issuer_name(cert.get(), name);
            X509_sign(cert.get(), pkey.get(), EVP_sha256()); // some hash type here


            int ret  = PEM_write_bio_PrivateKey(keyFile.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr);
            int ret2 = PEM_write_bio_X509(certFile.get(), cert.get());

            result = (ret == 1) && (ret2 == 1); // OpenSSL return codes
        }
    }

    return result;
}

Bien sûr, il devrait y avoir plus vérifications des valeurs de retour de la fonction, en fait toutes d'entre eux devraient être vérifiés mais cela rendrait un échantillon trop "branchu" et est assez facile à améliorer de toute façon.

0
MasterAler