web-dev-qa-db-ja.com

OpenSSLを使用してプログラムでX509証明書を作成する

C/C++アプリケーションがあり、公開キーと秘密キーの両方を含むX509 pem証明書を作成する必要があります。証明書は自己署名されていても、署名されていなくてもかまいません。

コマンドラインからではなく、アプリ内でこれを行いたいです。

どのOpenSSL機能がこれを私のために行いますか?サンプルコードはボーナスです!

68
Tim

最初に用語とメカニズムを理解する必要があります。

X.509certificateは、定義により、秘密鍵を含みません。代わりに、公開鍵のCA署名バージョンです(CAが署名に追加する属性とともに)。 PEM形式は、実際にはキーと証明書の個別のストレージのみをサポートしますが、その後、2つを連結できます。

いずれにせよ、OpenSSL APIの20以上の異なる機能を呼び出して、キーと自己署名証明書を作成する必要があります。例は、OpenSSLソース自体の demos/x509/mkcert.c にあります

より詳細な回答については、下記の Nathan Osmanの説明 をご覧ください。

43

私はこれが非常に遅い(そして長い)答えであることを理解しています。しかし、この質問が検索エンジンの結果でどれだけうまくランク付けされているかを考えると、まともな答えを書く価値があると考えました。

以下で読むものの多くは、 このデモ および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構造という1つの引数を受け入れます。

次に、キーを生成する必要があります。この例では、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」の証明書の受け入れを拒否します。これはデフォルトです。次のステップでは、証明書が実際に有効な期間を指定します。それには、次の2つの関数呼び出しを使用します。

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

最初の行は、証明書のnotBeforeプロパティを現在の時刻に設定します。 (X509_gmtime_adj関数は、指定された秒数を現在の時間に加算します-この場合はなしです。)2行目は、証明書の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 ハッシュアルゴリズムを使用してキーに署名していることに注意してください。これは、MD5を使用するこの回答の冒頭で述べたmkcert.cデモとは異なります。


これで自己署名証明書ができました!ただし、まだ完了していません。これらのファイルをディスクに書き込む必要があります。ありがたいことに、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 */
);

秘密鍵を暗号化したくない場合は、上記の3番目と4番目のパラメーターにNULLを渡すだけです。いずれにせよ、あなたは間違いなくファイルが誰でも読めないようにしたいと思うでしょう。 (Unixユーザーの場合、これはchmod 600 key.pemを意味します。)

ふう!これで機能が1つになりました-証明書をディスクに書き込む必要があります。これに必要な関数は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の表面をかろうじて見てきましたが、この回答の情報がすべてがどのように機能するかの大まかなアイデアを提供するのに十分であることを願っています。

上記のすべてのコードが実際のアプリケーションでどのように見えるかに興味がある人のために、 here を表示できるGist(C++で記述)をまとめました。

175
Nathan Osman

アプリ内からsystem呼び出しを介してこれを行う可能性はありますか?これを行ういくつかの正当な理由:

  • ライセンス:openssl実行可能ファイルを呼び出すと、ほぼ間違いなくアプリケーションから分離され、特定の利点が得られる場合があります。 免責事項:これについて弁護士に相談してください。

  • ドキュメント:OpenSSLにはphenomenalコマンドラインドキュメントが付属しており、潜在的に複雑なツールを大幅に簡素化します。

  • テスト容易性:証明書の作成方法を正確に理解するまで、コマンドラインからOpenSSLを実行できます。 lotのオプションがあります。すべての詳細が正しくなるまで、これに約1日を費やすことを期待してください。その後、コマンドをアプリに組み込むのは簡単です。

APIを使用する場合は、openssl-dev www.openssl.orgの開発者リスト。

幸運を!

2
Adam Liss

デジタル証明書を作成する非常に簡単なチュートリアル http://publib.boulder.ibm.com/infocenter/rsthelp/v8r0m0/index.jsp?topic=/com.ibm.rational.test.lt.doc/topics/ tcreatecertopenssl.html

コードからこれらのコマンドを実行することについてはわかりません。

0
Jaime Hablutzel

Nathan Osman それを大きく完全に説明し、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;
}

もちろん、関数の戻り値のmoreチェックがあるはずです。実際にはallそれらのうちの1つをチェックする必要がありますが、サンプルが「分岐しすぎ」てしまい、とにかく改善するのはかなり簡単です。

0
MasterAler