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

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

Я хочу сделать это внутри приложения, а не из командной строки.

Какие функции OpenSSL сделают это за меня? Любой пример кода - это бонус!


person Community    schedule 02.11.2008    source источник


Ответы (4)


Сначала вам необходимо ознакомиться с терминологией и механизмами.

Сертификат X.509 по определению не включает закрытый ключ. Вместо этого это версия открытого ключа, подписанная CA (вместе с любыми атрибутами, которые CA помещает в подпись). Формат PEM действительно поддерживает только отдельное хранение ключа и сертификата, хотя затем вы можете объединить их.

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

Для получения более подробного ответа см. объяснение Натана Османа ниже.

person Martin v. Löwis    schedule 02.11.2008
comment
Да, мне нужно больше познакомиться с концепциями SSL. Я проверю этот пример, спасибо за ссылку (хотя у ссылки есть проблема, но я разберусь). Я также использовал Crypto ++ для некоторых вещей, в этом случае он может быть проще, чем OpenSSL. - person ; 02.11.2008
comment
Спасибо! Выбрал этот ответ из-за предоставленной ссылки. - person ; 02.11.2008

Я понимаю, что это очень поздний (и длинный) ответ. Но, учитывая, насколько хорошо этот вопрос ранжируется в результатах поисковых систем, я подумал, что, возможно, стоит написать достойный ответ.

Многое из того, что вы прочтете ниже, заимствовано из эту демонстрацию и документы 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 сертификата устанавливается текущее время. (Функция 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 ++), который вы можете просмотреть здесь.

person Nathan Osman    schedule 26.02.2013
comment
Спасибо за отличный ответ и объяснение! Только небольшое сомнение: это предложение Now we need to set the public key for our certificate using the key we generated earlier: опечатка? Разве public key не должно быть private key? - person Kelvin Hu; 22.10.2013
comment
Мне пришлось добавить fclose (f) в конце. В противном случае записываемый файл был 0B - person Qamar Suleiman; 22.10.2013
comment
хороший и исчерпывающий четкий ответ. И еще одно: как добавить в сертификат дополнительные параметры, содержащиеся в файле openssl.cnf. например добавить расширение subjectAltName? - person karim; 11.06.2015
comment
Этот ответ был огромной помощью. Для людей, которые хотят добавить расширение к своему сертификату, см .: stackoverflow.com/questions/35616853/ - person Bryan; 26.02.2016
comment
Что, если мы не хотим иметь только самоподписанный сертификат? Как создать приватный ключ и CSR? Кто подписывает и возвращает подписанный сертификат? - person Lonko; 08.05.2016
comment
@Lonko Я бы предложил задать новый вопрос. - person Nathan Osman; 08.05.2016
comment
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, "localhost", -1, -1, 0), вероятно, ошибается. Имена хостов всегда входят в SAN. Если он присутствует в CN, то он также должен присутствовать в SAN (в этом случае вы должны указать его дважды). Дополнительные правила и причины см. В разделах Как подписать запрос на подпись сертификата в своем центре сертификации и Как создать самоподписанный сертификат с помощью openssl? - person jww; 08.06.2017
comment
Из документации: RSA_generate_key () устарел в OpenSSL 0.9.8; используйте вместо этого RSA_generate_key_ex (). И оба будут устаревшими в OpenSSL 3.0. - person Philipp Claßen; 07.05.2020

Натан Осман подробно и подробно объяснил это, у него была та же проблема, которую нужно было решить на 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;
}

Конечно, должно быть больше проверок возвращаемых значений функций, на самом деле все из них должны быть проверены, но это сделало бы образец слишком "ветвистым" и его довольно легко улучшить. в любом случае.

person MasterAler    schedule 13.08.2019
comment
Похоже, мы забыли отправить указатель позади уникального указателя при установке срока действия .... Я бы вставил код, но, видимо, я не могу заставить его выглядеть как код. - person AcidTonic; 17.09.2019

Есть ли шанс сделать это с помощью system вызова из вашего приложения? Несколько веских причин для этого:

  • Лицензирование: вызов исполняемого файла openssl, возможно, отделяет его от вашего приложения и может дать определенные преимущества. Заявление об ограничении ответственности: проконсультируйтесь по этому поводу у юриста.

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

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

Если вы решите использовать API, проверьте список openssl-dev разработчиков на сайте www.openssl.org.

Удачи!

person Adam Liss    schedule 02.11.2008
comment
OpenSSL - это лицензия в стиле apache, его можно использовать в коммерческих приложениях, как и любую другую лицензию без авторского лева. Люди по-прежнему могут захотеть проконсультироваться с юристом, чтобы убедиться, что все, что они делают, в порядке, но у него нет проблем, связанных с GPL. - person Louis Gerbarg; 02.11.2008
comment
Отметил и обновил - спасибо. Отделение кода с открытым исходным кодом от кода с закрытым исходным кодом, как правило, является хорошей идеей, и, если эффективность не является критически важной, другие причины являются хорошим аргументом в пользу использования автономной утилиты openssl. - person Adam Liss; 02.11.2008
comment
Я бы предпочел не использовать для этого системный вызов. Ваша точка зрения о документации очень верна - документы для SSL-стороны OpenSSL не очень помогают. - person ; 02.11.2008
comment
На самом деле есть проблемы, связанные с GPL: лицензия Apache 1.0 и лицензия BSD с 4 пунктами, по которой распространяется OpenSSL, несовместимы с программным обеспечением GPL. В GPL есть исключение для библиотек, предоставляемых ОС, поэтому, если вы подключаетесь к OpenSSL, предоставленному вашим дистрибутивом, вам может сойти с рук. См. также - person Mathias Brossard; 09.06.2011