Programmgesteuertes Erstellen eines X509-Zertifikats mit OpenSSL

76

Ich habe eine C / C ++ - Anwendung und muss ein X509-PEM-Zertifikat erstellen, das sowohl einen öffentlichen als auch einen privaten Schlüssel enthält. Das Zertifikat kann selbstsigniert oder nicht signiert sein, spielt keine Rolle.

Ich möchte dies in einer App tun, nicht über die Befehlszeile.

Welche OpenSSL-Funktionen erledigen das für mich? Jeder Beispielcode ist ein Bonus!

jww
quelle

Antworten:

49

Sie müssen sich zuerst mit der Terminologie und den Mechanismen vertraut machen.

Ein X.509- Zertifikat enthält per Definition keinen privaten Schlüssel. Stattdessen handelt es sich um eine von der Zertifizierungsstelle signierte Version des öffentlichen Schlüssels (zusammen mit allen Attributen, die die Zertifizierungsstelle in die Signatur einfügt). Das PEM-Format unterstützt wirklich nur die getrennte Speicherung des Schlüssels und des Zertifikats - obwohl Sie die beiden dann verketten können.

In jedem Fall müssen Sie mehr als 20 verschiedene Funktionen der OpenSSL-API aufrufen, um einen Schlüssel und ein selbstsigniertes Zertifikat zu erstellen. Ein Beispiel befindet sich in der OpenSSL-Quelle selbst in demos / x509 / mkcert.c

Eine ausführlichere Antwort finden Sie in der folgenden Erklärung von Nathan Osman .

Martin v. Löwis
quelle
Ja - ich muss mich besser mit SSL-Konzepten vertraut machen. Ich werde das Beispiel überprüfen, danke für den Link (der Link hat zwar ein Problem, aber ich werde es herausfinden.) Ich habe auch Crypto ++ für einige Dinge verwendet, es könnte in diesem Fall einfacher sein als OpenSSL.
Vielen Dank! Diese Antwort wurde aufgrund des angegebenen Links ausgewählt.
200

Mir ist klar, dass dies eine sehr späte (und lange) Antwort ist. Aber wenn man bedenkt, wie gut diese Frage in den Suchmaschinenergebnissen zu rangieren scheint, dachte ich, dass es sich lohnen könnte, eine anständige Antwort zu schreiben.

Vieles, was Sie unten lesen werden, stammt aus dieser Demo und den OpenSSL-Dokumenten. Der folgende Code gilt sowohl für C als auch für C ++.


Bevor wir tatsächlich ein Zertifikat erstellen können, müssen wir einen privaten Schlüssel erstellen. OpenSSL bietet die EVP_PKEYStruktur zum Speichern eines algorithmisch unabhängigen privaten Schlüssels im Speicher. Diese Struktur ist in deklariert openssl/evp.h, wird aber von openssl/x509.h(was wir später benötigen werden) eingeschlossen, sodass Sie den Header nicht wirklich explizit einschließen müssen.

Um eine EVP_PKEYStruktur zuzuweisen , verwenden wir EVP_PKEY_new:

EVP_PKEY * pkey;
pkey = EVP_PKEY_new();

Es gibt auch eine entsprechende Funktion zum Freigeben der Struktur - EVP_PKEY_free-, die ein einziges Argument akzeptiert: die EVP_PKEYoben initialisierte Struktur.

Jetzt müssen wir einen Schlüssel generieren. In unserem Beispiel generieren wir einen RSA-Schlüssel. Dies geschieht mit der RSA_generate_keyFunktion, die in deklariert ist openssl/rsa.h. Diese Funktion gibt einen Zeiger auf eine RSAStruktur zurück.

Ein einfacher Aufruf der Funktion könnte folgendermaßen aussehen:

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 */
);

Wenn der Rückgabewert von RSA_generate_keyist NULL, ist ein Fehler aufgetreten. Wenn nicht, haben wir jetzt einen RSA-Schlüssel, den wir unserer EVP_PKEYStruktur von früher zuweisen können :

EVP_PKEY_assign_RSA(pkey, rsa);

Die RSAStruktur wird automatisch freigegeben, wenn die EVP_PKEYStruktur freigegeben wird.


Nun zum Zertifikat selbst.

OpenSSL verwendet die X509Struktur, um ein x509-Zertifikat im Speicher darzustellen. Die Definition für diese Struktur ist in openssl/x509.h. Die erste Funktion, die wir brauchen werden, ist X509_new. Die Verwendung ist relativ einfach:

X509 * x509;
x509 = X509_new();

Wie bei EVP_PKEYgibt es eine entsprechende Funktion zum Freigeben der Struktur - X509_free.

Jetzt müssen wir einige Eigenschaften des Zertifikats mit einigen X509_*Funktionen festlegen :

ASN1_INTEGER_set(X509_get_serialNumber(x509), 1);

Dies setzt die Seriennummer unseres Zertifikats auf '1'. Einige Open-Source-HTTP-Server lehnen es ab, ein Zertifikat mit der Seriennummer '0' zu akzeptieren. Dies ist die Standardeinstellung. Der nächste Schritt besteht darin, die Zeitspanne anzugeben, in der das Zertifikat tatsächlich gültig ist. Wir machen das mit den folgenden zwei Funktionsaufrufen:

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

In der ersten Zeile wird die notBeforeEigenschaft des Zertifikats auf die aktuelle Zeit gesetzt. (Die X509_gmtime_adjFunktion addiert die angegebene Anzahl von Sekunden zur aktuellen Zeit - in diesem Fall keine.) In der zweiten Zeile wird die notAfterEigenschaft des Zertifikats auf 365 Tage festgelegt (60 Sekunden * 60 Minuten * 24 Stunden * 365 Tage).

Jetzt müssen wir den öffentlichen Schlüssel für unser Zertifikat mit dem zuvor generierten Schlüssel festlegen:

X509_set_pubkey(x509, pkey);

Da es sich um ein selbstsigniertes Zertifikat handelt, setzen wir den Namen des Ausstellers auf den Namen des Betreffs. Der erste Schritt in diesem Prozess besteht darin, den Betreffnamen zu erhalten:

X509_NAME * name;
name = X509_get_subject_name(x509);

Wenn Sie zuvor ein selbstsigniertes Zertifikat in der Befehlszeile erstellt haben, werden Sie wahrscheinlich nach einem Ländercode gefragt. Hier stellen wir es zusammen mit der Organisation ('O') und dem allgemeinen Namen ('CN') zur Verfügung:

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

(Ich verwende hier den Wert 'CA', weil ich Kanadier bin und das ist unser Ländercode. Beachten Sie auch, dass Parameter 4 explizit in einen umgewandelt werden muss unsigned char *.)

Jetzt können wir den Namen des Ausstellers tatsächlich festlegen:

X509_set_issuer_name(x509, name);

Und schließlich sind wir bereit, den Signierprozess durchzuführen. Wir rufen X509_signmit dem Schlüssel an, den wir zuvor generiert haben. Der Code dafür ist schmerzlich einfach:

X509_sign(x509, pkey, EVP_sha1());

Beachten Sie, dass wir den SHA-1- Hashing-Algorithmus verwenden, um den Schlüssel zu signieren. Dies unterscheidet sich von der mkcert.cDemo, die ich zu Beginn dieser Antwort erwähnt habe und die MD5 verwendet.


Wir haben jetzt ein selbstsigniertes Zertifikat! Aber wir sind noch nicht fertig - wir müssen diese Dateien auf die Festplatte schreiben. Zum Glück hat OpenSSL uns auch dort mit den PEM_*Funktionen abgedeckt, die in deklariert sind openssl/pem.h. Der erste, den wir benötigen, ist das PEM_write_PrivateKeySpeichern unseres privaten Schlüssels.

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 */
);

Wenn Sie den privaten Schlüssel nicht verschlüsseln möchten, übergeben Sie einfach NULLden dritten und vierten Parameter oben. In jedem Fall sollten Sie unbedingt sicherstellen, dass die Datei nicht für die Welt lesbar ist. (Für Unix-Benutzer bedeutet dies chmod 600 key.pem.)

Wütend! Jetzt haben wir nur noch eine Funktion: Wir müssen das Zertifikat auf die Festplatte schreiben. Die Funktion, die wir dafür benötigen, ist 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 */
);

Und wir sind fertig! Hoffentlich reichen die Informationen in dieser Antwort aus, um Ihnen eine ungefähre Vorstellung davon zu geben, wie alles funktioniert, obwohl wir die Oberfläche von OpenSSL kaum zerkratzt haben.

Für diejenigen, die sehen möchten, wie der gesamte obige Code in einer realen Anwendung aussieht, habe ich einen Gist (in C ++ geschrieben) zusammengestellt, den Sie hier anzeigen können .

Nathan Osman
quelle
Vielen Dank für die hervorragende Antwort und Erklärung! Nur ein kleiner Zweifel: Now we need to set the public key for our certificate using the key we generated earlier:Ist dieser Satz ein Tippfehler? Sollte das nicht public keysein private key?
Kelvin Hu
3
Ich musste am Ende fclose (f) hinzufügen. Ansonsten war die Datei, die geschrieben wurde, 0B
Qamar Suleiman
nette und umfassende klare Antwort. Eine Sache mehr, wie man dem Zertifikat weitere Parameter hinzufügt, wie in der Datei openssl.cnf zu finden. zB um die Erweiterung subjectAltName hinzuzufügen?
Karim
Diese Antwort war eine gigantische Hilfe. Für Leute, die eine Erweiterung zu ihrem Zertifikat hinzufügen möchten, siehe: stackoverflow.com/questions/35616853/…
Bryan
1
Aus den Dokumenten: "RSA_generate_key () war in OpenSSL 0.9.8 veraltet; verwenden Sie stattdessen RSA_generate_key_ex ()." Und beide werden in OpenSSL 3.0 veraltet sein.
Philipp Claßen
2

Gibt es eine Möglichkeit, dies über einen systemAnruf in Ihrer App zu tun ? Mehrere gute Gründe dafür:

  • Lizenzierung: Wenn Sie die opensslausführbare Datei aufrufen, wird sie möglicherweise von Ihrer Anwendung getrennt und bietet möglicherweise bestimmte Vorteile. Haftungsausschluss: Wenden Sie sich hierzu an einen Anwalt.

  • Dokumentation: OpenSSL enthält eine phänomenale Befehlszeilendokumentation, die ein möglicherweise kompliziertes Tool erheblich vereinfacht.

  • Testbarkeit: Sie können OpenSSL über die Befehlszeile ausführen, bis Sie genau wissen, wie Sie Ihre Zertifikate erstellen. Es gibt viele Möglichkeiten; Erwarten Sie ungefähr einen Tag damit, bis Sie alle Details richtig verstanden haben. Danach ist es trivial, den Befehl in Ihre App zu integrieren.

Wenn Sie sich für die Verwendung der API entscheiden, überprüfen Sie die openssl-devEntwicklerliste auf www.openssl.org.

Viel Glück!

Adam Liss
quelle
5
OpenSSL ist eine Lizenz unter einer Apache-Lizenz. Sie kann in kommerziellen Apps wie jede andere Nicht-Copyleft-Lizenz verwendet werden. Die Leute möchten vielleicht immer noch einen Anwalt konsultieren, um sicherzustellen, dass alles, was sie tun, in Ordnung ist, aber es gibt keine Probleme mit der GPL
Louis Gerbarg
Notiert und aktualisiert - danke. Die Trennung von Open Source- und Closed Source-Code ist im Allgemeinen eine gute Idee. Wenn die Effizienz nicht von entscheidender Bedeutung ist, sind die anderen Gründe ein gutes Argument für die Verwendung des eigenständigen Dienstprogramms openssl.
Adam Liss
2
Ich würde dazu lieber keinen Systemaufruf verwenden. Ihr Hinweis zur Dokumentation ist sehr zutreffend - die Dokumente für die SSL-Seite von OpenSSL helfen nicht viel.
1
Tatsächlich gibt es Probleme mit der GPL: Die Apache 1.0-Lizenz und die BSD-Lizenz mit 4 Klauseln, unter denen OpenSSL vertrieben wird, sind beide nicht mit der GPL-Software kompatibel. In der GPL gibt es eine Ausnahme für vom Betriebssystem bereitgestellte Bibliotheken. Wenn Sie also eine Verknüpfung mit der von Ihrer Distribution bereitgestellten OpenSSL herstellen, können Sie damit davonkommen. Siehe auch
Mathias Brossard
2

Nathan Osman erklärte es sehr ausführlich und hatte das gleiche Problem in C ++ zu lösen. Hier ist mein kleines, umgeschriebenes Konzept im cpp-Stil mit ein paar Einschränkungen:

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

Natürlich sollte es mehr Kontrollen der Rückgabewerte der Funktion, tatsächlich alle von ihnen überprüft werden soll , aber das wäre eine Probe zu „branchy“ zu machen und ist recht einfach irgendwie zu verbessern.

MasterAler
quelle
Sieht so aus, als hätten wir vergessen, den Zeiger hinter dem eindeutigen Zeiger zu senden, als der Ablauf festgelegt wurde. Ich würde den Code einfügen, aber anscheinend kann ich ihn nicht wie Code aussehen lassen.
AcidTonic