Програмно створити сертифікат X509 за допомогою OpenSSL


77

У мене є програма C / C ++, і мені потрібно створити сертифікат X509 pem, що містить як відкритий, так і приватний ключ. Сертифікат може бути самопідписаним або без підпису, не має значення.

Я хочу зробити це всередині програми, а не з командного рядка.

Які функції OpenSSL зроблять це для мене? Будь-який зразок коду - бонус!

Відповіді:


49

Спочатку вам потрібно ознайомитися з термінологією та механізмами.

X.509 сертифікат , за визначенням, не включає в себе закритий ключ. Натомість це версія відкритого ключа, підписана ЦС (разом із будь-якими атрибутами, які ЦС вносить у підпис). Формат PEM насправді підтримує лише окреме зберігання ключа та сертифіката, хоча потім ви можете об’єднати ці два.

У будь-якому випадку вам потрібно буде викликати 20+ різних функцій API OpenSSL, щоб створити ключ і самопідписаний сертифікат. Приклад - у самому джерелі OpenSSL, у демо / x509 / mkcert.c

Для більш детальної відповіді, будь ласка, дивіться пояснення Натана Османа нижче.


Так - мені потрібно більше ознайомитись з поняттями ssl. Я перевірю приклад, дякую за посилання (хоч у посилання є проблема, але я розберусь.) Я також використовував Crypto ++ для деяких речей, у цьому випадку він може бути простішим, ніж OpenSSL.

Дякую! Вибрали цю відповідь через надане посилання.

202

Я розумію, що це дуже пізня (і довга) відповідь. Але, враховуючи, наскільки добре це питання, здається, займає місце в результатах пошукової системи, я зрозумів, що, можливо, варто написати гідну відповідь.

Багато з того, що ви прочитаєте нижче, запозичено з цієї демонстрації та документів OpenSSL. Наведений нижче код стосується як С, так і С ++.


Перш ніж ми зможемо створити сертифікат, нам потрібно створити приватний ключ. OpenSSL забезпечує EVP_PKEYструктуру для зберігання незалежного від алгоритму приватного ключа в пам'яті. Ця структура оголошена в, openssl/evp.hале включена вopenssl/x509.h (що нам знадобиться пізніше), тому вам не потрібно явно включати заголовок.

Для того, щоб виділити EVP_PKEYструктуру, ми використовуємо EVP_PKEY_new:

Існує також відповідна функція для звільнення структури - EVP_PKEY_free- яка приймає один аргумент: EVP_PKEYструктура, ініціалізована вище.

Тепер нам потрібно згенерувати ключ. Для нашого прикладу ми згенеруємо ключ RSA. Це робиться з RSA_generate_keyфункцією, яка оголошена в openssl/rsa.h. Ця функція повертає покажчик наRSA структуру.

Просте виклик функції може виглядати так:

Якщо повернене значення RSA_generate_keyє NULL, то щось пішло не так. Якщо ні, то тепер у нас є ключ RSA, і ми можемо призначити його нашій EVP_PKEYструктурі з попередніх:

RSAСтруктура буде автоматично звільняється , коли EVP_PKEYструктура звільняється.


Тепер щодо самого сертифіката.

OpenSSL використовує X509структуру для представлення сертифіката x509 в пам'яті. Визначення цієї структури наведено у openssl/x509.h. Перша функція, яка нам потрібна, - це X509_new. Його використання є відносно простим:

Як і у випадку EVP_PKEY, існує відповідна функція звільнення конструкції -X509_free .

Тепер нам потрібно встановити кілька властивостей сертифіката, використовуючи деякі X509_*функції:

Це встановлює серійний номер нашого сертифіката на "1". Деякі HTTP-сервери з відкритим кодом відмовляються приймати сертифікат із серійним номером "0", що є типовим. Наступним кроком є ​​вказати проміжок часу, протягом якого сертифікат дійсно діє. Ми робимо це за допомогою наступних двох викликів функцій:

Перший рядок встановлює notBeforeвластивість сертифіката на поточний час. ( X509_gmtime_adjФункція додає вказану кількість секунд до поточного часу - в даному випадку жодної.) Другий рядок встановлює сертифікатnotAfter властивість на 365 днів (60 секунд * 60 хвилин * 24 години * 365 днів).

Тепер нам потрібно встановити відкритий ключ для нашого сертифіката, використовуючи ключ, який ми створили раніше:

Оскільки це самопідписний сертифікат, ми встановлюємо ім’я видавця на ім’я суб’єкта. Першим кроком у цьому процесі є отримання теми:

Якщо ви коли-небудь раніше створювали самопідписаний сертифікат у командному рядку, ви, мабуть, пам’ятаєте, як вас запитували код країни. Ось де ми надаємо його разом із організацією ('O') та загальною назвою ('CN'):

(Я використовую тут значення "CA", оскільки я канадець, і це код нашої країни. Також зверніть увагу, що параметр # 4 повинен бути явно переданий до unsigned char * .)

Тепер ми можемо фактично встановити ім'я емітента:

І нарешті, ми готові виконати процес підписання. Ми дзвонимо X509_signза допомогою ключа, який ми створили раніше. Код для цього до болю простий:

Зверніть увагу, що ми використовуємо алгоритм хешування SHA-1 для підписання ключа. Це відрізняється від mkcert.cдемонстрації, яку я згадав на початку цієї відповіді, де використовується MD5.


Тепер у нас є самопідписаний сертифікат! Але ми ще не закінчили - нам потрібно записати ці файли на диск. На щастя, OpenSSL нас там теж охопив PEM_*функціями, декларованими в openssl/pem.h. Перше, що нам знадобиться, це PEM_write_PrivateKeyзбереження нашого приватного ключа.

Якщо ви не хочете шифрувати закритий ключ, просто перейдіть NULLдо третього та четвертого параметрів вище. У будь-якому випадку, ви точно захочете переконатись, що файл неможливо прочитати у всьому світі. (Для користувачів Unix це означає chmod 600 key.pem.)

Ух! Тепер ми перейшли до однієї функції - нам потрібно записати сертифікат на диск. Для цього нам потрібна така функція PEM_write_X509:


І ми закінчили! Сподіваємось, інформації в цій відповіді достатньо, щоб дати вам приблизне уявлення про те, як все працює, хоча ми ледве подряпали поверхню OpenSSL.

Для тих, хто зацікавлений побачити, як виглядає весь наведений вище код у реальному додатку, я зібрав Gist (написаний на C ++), який ви можете переглянути тут .


Дякуємо за чудову відповідь та пояснення! Лише невеликий сумнів: чи є це речення Now we need to set the public key for our certificate using the key we generated earlier:друкарською помилкою? Не повинно public keyбути private key?
Келвін Ху

3
Мені довелося додати fclose (f) наприкінці. В іншому випадку файл писався 0B
Камар Сулейман

приємна та вичерпна чітка відповідь. Ще одне: як додати більше параметрів до сертифіката, як це знайдено у файлі openssl.cnf. наприклад, додати розширення subjectAltName?
karim

Ця відповідь була гігантською допомогою. Для людей, які хочуть додати розширення до свого сертифіката, див .: stackoverflow.com/questions/35616853/…
Брайан

1
З документів: "RSA_generate_key () застаріло в OpenSSL 0.9.8; замість цього використовуйте RSA_generate_key_ex ()." І те, і інше буде застарілим у OpenSSL 3.0.
Філіп Классен,

2

Є шанс зробити це за допомогою systemдзвінка з вашої програми? Кілька вагомих причин для цього:

  • Ліцензування. Виклик opensslвиконуваного файлу, можливо, відокремлює його від вашої програми і може надати певні переваги. Застереження: проконсультуйтеся з цим у юриста.

  • Документація: OpenSSL постачається з феноменальною документацією до командного рядка, що значно спрощує потенційно складний інструмент.

  • Тестованість: ви можете здійснювати OpenSSL з командного рядка, поки не зрозумієте, як саме створювати сертифікати. Варіантів дуже багато ; сподівайтесь витратити на це близько доби, поки не зрозумієте всі деталі. Після цього просто включити команду до програми.

Якщо ви вирішите використовувати API, перегляньте список openssl-devрозробників на www.openssl.org.

Удачі!


5
OpenSSL - це ліцензія за ліцензією стилю apache, вона може використовуватися в комерційних програмах, як і будь-яка інша ліцензія, що не має копілефт. Люди все ще можуть проконсультуватися з адвокатом, щоб переконатися, що все, що вони роблять, добре, але у нього немає проблем, пов’язаних з GPL
Луї Гербарг, 02

Відзначено та оновлено - дякую. Відокремлення відкритого коду від закритого коду, як правило, є гарною ідеєю, і якщо ефективність не є критично важливою, інші причини служать вагомим аргументом для використання автономної утиліти openssl.
Адам Лісс

2
Я волів би не використовувати системний виклик для цього. Ваша думка щодо документації є дуже вагомою - документи для сторони SSL OpenSSL не надто допомагають.

1
Насправді є проблеми, пов’язані з GPL: ліцензія Apache 1.0 та ліцензія BSD з 4-ма положеннями, за якими розповсюджується OpenSSL, несумісні з програмним забезпеченням GPL. У GPL є виняток для бібліотек, що надаються ОС, тому, якщо ви зв’яжетеся з OpenSSL, що надається вашим дистрибутивом, ви можете з цим уникнути. Дивіться також
Матіас Броссард

2

Натан Осман пояснив це широко і повно, мав вирішити ту саму проблему на C ++, тому ось мій невеликий додаток, переписана концепція в стилі cpp з урахуванням декількох застережень:

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

Звичайно, повинно бути більше перевірок повернутих значень функції, насправді всі вони повинні бути перевірені, але це зробило б зразок занадто «розгалуженим» і його досить легко поліпшити в будь-якому випадку.


Схоже, ми забули надіслати вказівник за унікальним вказівником при встановленні терміну дії .... Я б вставив код, але, мабуть, я не можу змусити його виглядати як код.
AcidTonic
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.