Программно создать сертификат X509 с помощью OpenSSL
У меня есть приложение C/C++, и мне нужно создать сертификат pem X509, содержащий как открытый, так и закрытый ключ. Сертификат может быть самоподписанным или неподписанным, не имеет значения.
Я хочу сделать это внутри приложения, а не из командной строки.
Какие функции OpenSSL сделают это для меня? Любой пример кода является бонусом!
4 ответа
Сначала вам нужно ознакомиться с терминологией и механизмами.
Сертификат X.509 по определению не включает в себя закрытый ключ. Вместо этого это версия открытого ключа, подписанная СА (вместе с любыми атрибутами, которые СА помещает в подпись). Формат PEM на самом деле поддерживает только раздельное хранение ключа и сертификата - хотя вы можете затем объединить их.
В любом случае вам нужно будет вызвать более 20 различных функций API OpenSSL для создания ключа и самозаверяющего сертификата. Пример - в самом источнике OpenSSL, в demos / x509 / mkcert.c
Для более подробного ответа, пожалуйста, смотрите объяснение Натана Османа ниже.
Я понимаю, что это очень поздний (и длинный) ответ. Но, учитывая, насколько хорошо этот вопрос оценивается в результатах поисковых систем, я решил, что стоит написать достойный ответ.
Многое из того, что вы прочтете ниже, позаимствовано из этой демонстрации и документации OpenSSL. Код ниже относится как к C, так и к C++.
Прежде чем мы сможем создать сертификат, нам нужно создать закрытый ключ. OpenSSL предоставляет EVP_PKEY
структура для хранения независимого от алгоритма закрытого ключа в памяти. Эта структура объявлена в openssl/evp.h
но включен openssl/x509.h
(что нам понадобится позже), поэтому вам не нужно явно включать заголовок.
Для того, чтобы выделить EVP_PKEY
структура, которую мы используем EVP_PKEY_new
:
EVP_PKEY * pkey;
pkey = EVP_PKEY_new();
Существует также соответствующая функция для освобождения структуры - EVP_PKEY_free
- который принимает один аргумент: EVP_PKEY
структура инициализирована выше.
Теперь нам нужно сгенерировать ключ. Для нашего примера мы сгенерируем ключ RSA. Это сделано с RSA_generate_key
функция, которая объявлена в openssl/rsa.h
, Эта функция возвращает указатель на RSA
состав.
Простой вызов функции может выглядеть так:
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 */
);
Если возвращаемое значение RSA_generate_key
является NULL
тогда что-то пошло не так. Если нет, то теперь у нас есть ключ RSA, и мы можем присвоить его нашему EVP_PKEY
структура из ранее:
EVP_PKEY_assign_RSA(pkey, rsa);
RSA
структура будет автоматически освобождена, когда EVP_PKEY
структура освобождена.
Теперь о самом сертификате.
OpenSSL использует X509
структура для представления сертификата x509 в памяти. Определение этой структуры находится в openssl/x509.h
, Первая функция, которая нам понадобится X509_new
, Его использование относительно просто:
X509 * x509;
x509 = X509_new();
Как было в случае с EVP_PKEY
есть соответствующая функция для освобождения структуры - X509_free
,
Теперь нам нужно установить несколько свойств сертификата, используя некоторые X509_*
функции:
ASN1_INTEGER_set(X509_get_serialNumber(x509), 1);
Это устанавливает серийный номер нашего сертификата на "1". Некоторые HTTP-серверы с открытым исходным кодом отказываются принимать сертификат с серийным номером "0", который используется по умолчанию. Следующим шагом является указание промежутка времени, в течение которого сертификат действительно действителен. Мы делаем это с помощью следующих двух вызовов функций:
X509_gmtime_adj(X509_get_notBefore(x509), 0);
X509_gmtime_adj(X509_get_notAfter(x509), 31536000L);
Первая строка устанавливает сертификат notBefore
собственность на текущее время. (The X509_gmtime_adj
Функция добавляет указанное количество секунд к текущему времени - в данном случае ни одного.) Вторая строка устанавливает сертификат notAfter
собственность до 365 дней с этого момента (60 секунд * 60 минут * 24 часа * 365 дней).
Теперь нам нужно установить открытый ключ для нашего сертификата, используя ключ, который мы сгенерировали ранее:
X509_set_pubkey(x509, pkey);
Так как это самозаверяющий сертификат, мы устанавливаем имя эмитента на имя субъекта. Первый шаг в этом процессе - получить имя субъекта:
X509_NAME * name;
name = X509_get_subject_name(x509);
Если вы когда-либо ранее создавали самозаверяющий сертификат в командной строке, вы, вероятно, помните, что вас спрашивали о коде страны. Вот где мы предоставляем его вместе с организацией ("O") и общим названием ("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);
(Я использую здесь значение "CA", потому что я канадец, и это код нашей страны. Также обратите внимание, что параметр #4 должен быть явно приведен к unsigned char *
.)
Теперь мы можем установить имя эмитента:
X509_set_issuer_name(x509, name);
И, наконец, мы готовы выполнить процесс подписания. Мы называем X509_sign
с ключом, который мы сгенерировали ранее. Код для этого до боли прост:
X509_sign(x509, pkey, EVP_sha1());
Обратите внимание, что мы используем алгоритм хеширования SHA-1 для подписи ключа. Это отличается от mkcert.c
Демо я упомянул в начале этого ответа, который использует MD5.
Теперь у нас есть самоподписанный сертификат! Но мы еще не закончили - нам нужно записать эти файлы на диск. К счастью, OpenSSL также предоставил нам PEM_*
функции, которые объявлены в openssl/pem.h
, Первое, что нам нужно, это PEM_write_PrivateKey
за сохранение нашего закрытого ключа.
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 */
);
Если вы не хотите шифровать закрытый ключ, просто передайте NULL
для третьего и четвертого параметра выше. В любом случае, вы обязательно захотите убедиться, что файл не доступен для чтения. (Для пользователей Unix это означает chmod 600 key.pem
.)
Уф! Теперь мы перешли к одной функции - нам нужно записать сертификат на диск. Для этого нам нужна функция 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 */
);
И мы сделали! Надеемся, что информации в этом ответе достаточно, чтобы дать вам общее представление о том, как все работает, хотя мы едва касались поверхности OpenSSL.
Для тех, кому интересно посмотреть, как весь приведенный выше код выглядит в реальном приложении, я собрал Gist (написанный на C++), который вы можете посмотреть здесь.
Натан Осман подробно и подробно объяснил это, у него была та же проблема, которую нужно было решить на 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;
}
Конечно, должно быть больше проверок возвращаемых значений функций, на самом деле все они должны быть проверены, но это сделало бы образец слишком "ветвистым", и его в любом случае довольно легко улучшить.
Любой шанс сделать это через system
звонить из своего приложения? Несколько веских причин для этого:
Лицензирование: вызов
openssl
Исполняемый файл, вероятно, отделяет его от вашего приложения и может обеспечить определенные преимущества. Отказ от ответственности: проконсультируйтесь с адвокатом по этому вопросу.Документация: OpenSSL поставляется с феноменальной документацией командной строки, которая значительно упрощает потенциально сложный инструмент.
Тестируемость: вы можете использовать OpenSSL из командной строки, пока не поймете, как именно создавать свои сертификаты. Есть много вариантов; рассчитывайте потратить на это около суток, пока не укажете все детали правильно. После этого легко включить команду в ваше приложение.
Если вы решили использовать API, проверьте openssl-dev
список разработчиков на www.openssl.org.
Удачи!
Очень простое руководство для создания цифровых сертификатов http://publib.boulder.ibm.com/infocenter/rsthelp/v8r0m0/index.jsp?topic=/com.ibm.rational.test.lt.doc/topics/tcreatecertopenssl.html
В выполнении этих команд из вашего кода я не уверен.