VC++, MFC(WinInet), OpenSSLで無料のタイムスタンプサービスに接続する

## タイムスタンプサービス

無料でタイムスタンプトークンを発行するサービス、[Free TSA Project](http://eswg.jnsa.org/sandbox/freetsa/) に接続します。タイムスタンプリクエスト、タイムスタンプレスポンス、タイムスタンプトークンの生成、解析には[OpenSSL](https://www.openssl.org/)を利用しました。HTTP通信には、[MFC WinInet](https://learn.microsoft.com/ja-jp/cpp/mfc/mfc-classes-for-creating-internet-client-applications?view=msvc-170)を利用しました。

## タイムスタンププロトコル

タイムスタンプに関わる仕様は、[RFC3161](https://datatracker.ietf.org/doc/html/rfc3161)、[RFC5816](https://datatracker.ietf.org/doc/html/rfc5816) で 定義されています。

HTTP通信で行う際の仕様が [RFC3161 の 3.4 Time-Stamp Protocol via HTTP](https://datatracker.ietf.org/doc/html/rfc3161#section-3.4) に記載されています。

## 開発に必要なもの

* OpenSSL 3.0系
* Visual Stuido 2022 Commnunity

## OpenSSLのインストール

[Win32/Win64 OpenSSL](https://slproweb.com/products/Win32OpenSSL.html)より Windows版 OpenSSLをダウンロードして、インストールします。64bit版のサンプルを作成するので、「Win64 OpenSSL v3.0.7」をインストールしました。

## Visual studio 2022 Commnunityのインストール

[Visual studio 2022 Commnunity](https://visualstudio.microsoft.com/ja/free-developer-offers/)よりVisual studio 2022 Commnunity版をダウンロードします。ダウンロードした VisualStudioSetup.exe を実行して、インストールします。

## MFC のインストール

「Visual Studio 2022 Community」をデフォルトインストールした場合、 MFC(Microsoft Foundation Class)は含まれていないので、追加でインストールします。

Visual Studio Instller を起動します。

![VisualStudioInstaller.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2639843/26d5413c-4927-ebe3-3063-45b40295f826.png)

Visual Studio Community 2022 の 変更ボタンを押下します。

![VisualStudioInstaller_MFC追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2639843/f9a1608a-9534-a567-0f83-8e53b8c2e088.png)

C++ によるデスクトップ開発を選択します。インストールの詳細で「最新のv143ビルドツール用 C++ MFC (x86およびx64)」をチェックします。
変更ボタンを押して、インストールします。


## プロジェクトの設定

プロジェクトを作成するとき、「Windows デスクトップウィザード」を選択し、次へボタンを押下します。

![NewProject.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2639843/ca79a34d-3168-7d86-a87b-b2757c3ee2cd.png)

作成ボタンを押下します。

![MakeProject.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2639843/87d735fe-2c7c-9851-96b0-b9cdaf51c6dd.png)

アプリケーションの種類で「コンソールアプリケーション(.exe)」を選択し、「MFC アプリケーション」をチェックし、OKボタンを押下し、プロジェクトを作成します。

![ConsoleApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2639843/c7346ca5-b9b5-d5a4-d996-42555bc86b9a.png)



サンプルのプロジェクトのプロパティの以下の項目に値を追加します。

| 項目 | 設定する値 |
|-------------------|----------------|
| 追加のインクルードディレクトリ | C:\Program Files\OpenSSL-Win64\include |
| 追加のライブラリディレクトリ | C:\Program Files\OpenSSL-Win64\lib\VC |
| 追加のライブラリファイル | libcrypto64MD.lib |


## サンプルプログラム

```c++
#include "pch.h"
#include "framework.h"
#include "Project1.h"
#include <afxinet.h>

#include <openssl/sha.h>
#include <openssl/ts.h>

#include <string>
#include <fstream>
#include <vector>


#ifdef _DEBUG
#define new DEBUG_NEW
#endif


// 唯一のアプリケーション オブジェクトです

CWinApp theApp;

//タイムスタンプリクエストの作成
int createTimestampReq(std::vector<unsigned char>& sha256_data, std::vector<unsigned char>& ts_req_encode) {

    TS_REQ* ts_req = NULL;
    TS_MSG_IMPRINT* msg_imprint = NULL;
    X509_ALGOR* algo = NULL;
    const EVP_MD* md = NULL;

    do {
        //タイムスタンプリクエストの作成
        if ((ts_req = TS_REQ_new()) == NULL)
            break;
        if (!TS_REQ_set_version(ts_req, 1))
            break;

        //MessageImprintの作成
        if ((msg_imprint = TS_MSG_IMPRINT_new()) == NULL)
            break;

        if ((algo = X509_ALGOR_new()) == NULL)
            break;

        if ((md = EVP_get_digestbyname("sha256")) == NULL)
            break;

        if ((algo->algorithm = OBJ_nid2obj(EVP_MD_get_type(md))) == NULL)
            break;

        if ((algo->parameter = ASN1_TYPE_new()) == NULL)
            break;
        algo->parameter->type = V_ASN1_NULL;

        if (!TS_MSG_IMPRINT_set_algo(msg_imprint, algo))
            break;

        if (!TS_MSG_IMPRINT_set_msg(msg_imprint, sha256_data.data(), sha256_data.size()))
            break;

        if (!TS_REQ_set_msg_imprint(ts_req, msg_imprint))
            break;

        if (!TS_REQ_set_cert_req(ts_req, 1))
            break;

    } while (0);

    int len = i2d_TS_REQ(ts_req, 0);

    std::vector<unsigned char> ts_req_data;
    ts_req_data.resize(len);

    unsigned char* p = ts_req_data.data();
    i2d_TS_REQ(ts_req, &p);

    ts_req_encode.resize(ts_req_data.size());
    copy(ts_req_data.begin(), ts_req_data.end(), ts_req_encode.begin());

    return 0;
}

int httpTimeStampClient(std::string url, std::vector<unsigned char>& request, std::vector<unsigned char>& response) {

    //URLの解析
    CString strServerName;
    INTERNET_PORT nPort;
    CString strObject;
    DWORD dwServiceType;
    DWORD dwAccessType = INTERNET_OPEN_TYPE_DIRECT;

    std::string appName("HelloWorld");

    if (!AfxParseURL(CString(url.data()), dwServiceType, strServerName, strObject, nPort) ||
        (dwServiceType != AFX_INET_SERVICE_HTTP && dwServiceType != AFX_INET_SERVICE_HTTPS)) {
        std::cerr << "AfxParseURL Error" << std::endl;
        return 1;
    }

    CInternetSession* m_session = new CInternetSession(CString(appName.data()), 0, dwAccessType, 0, NULL, INTERNET_FLAG_DONT_CACHE);
    if (m_session == NULL) {
        std::cerr << "CInternetSession Error" << std::endl;
        return 1;
    }

    BOOL brtn;
    HTTP_VERSION_INFO httpverinfo;
    httpverinfo.dwMajorVersion = 1;
    httpverinfo.dwMinorVersion = 1;
    brtn = m_session->SetOption(INTERNET_OPTION_HTTP_VERSION, &httpverinfo, sizeof(httpverinfo));
    if (brtn == FALSE) {
        std::cerr << "SetOption Error" << std::endl;
        return 1;
    }

    //HTTP接続の確立
    CHttpConnection* pServer = m_session->GetHttpConnection(strServerName, nPort);
    if (pServer == NULL) {
        std::cerr << "GetHttpConnection Error" << std::endl;
        return 1;
    }

    //HTTPリクエスト要求の作成
    DWORD dwFlag = INTERNET_FLAG_DONT_CACHE | INTERNET_FLAG_NO_AUTO_REDIRECT;

    if (dwServiceType == AFX_INET_SERVICE_HTTPS) {
        dwFlag |= INTERNET_FLAG_SECURE;
    }
    CHttpFile* pFile = pServer->OpenRequest(CHttpConnection::HTTP_VERB_POST, strObject, NULL, 1, NULL, CString("HTTP/1.1"), dwFlag);
    if (pFile == NULL) {
        std::cerr << "OpenRequest Error" << std::endl;
        return 1;
    }

    CString contentType("Content-Type: application/timestamp-query");
    brtn = pFile->SendRequest(contentType, (LPVOID)request.data(), (DWORD)request.size());
    if (brtn == FALSE) {
        std::cerr << "SendRequest Error" << std::endl;
        return 1;
    }

    //要求イベントの結果受信
    DWORD dwRet;
    brtn = pFile->QueryInfoStatusCode(dwRet);
    if (brtn == FALSE) {
        std::cerr << "QueryInfoStatusCode Error" << std::endl;
        return 1;
    }

    DWORD dwbodysize = 10000;
    DWORD dwbodysizesize = sizeof(dwbodysize);

    //エラーで返ってくる
    brtn = pFile->QueryInfo(HTTP_QUERY_FLAG_NUMBER | HTTP_QUERY_CONTENT_LENGTH, &dwbodysize, &dwbodysizesize);
    if (brtn == FALSE) {
        std::cerr << "QueryInfo Error" << std::endl;
    }

    UINT rsize = 0;
    std::vector<unsigned char> responsebody;

    unsigned char tmpbody[10000];
    memset(tmpbody, '\0', sizeof(tmpbody));

    while ((rsize = pFile->Read(tmpbody, 10000)) > 0) {
        for (UINT i = 0; i < rsize; i++) {
            responsebody.push_back(tmpbody[i]);
        }
    }

    response.resize(responsebody.size());
    std::copy(responsebody.begin(), responsebody.end(), response.begin());

    return 0;
}

int main()
{
    int nRetCode = 0;

    HMODULE hModule = ::GetModuleHandle(nullptr);
    if (hModule == NULL) {
        wprintf(L"致命的なエラー: GetModuleHandle が失敗しました\n");
        nRetCode = 1;
        return nRetCode;
    }


    // MFC を初期化して、エラーの場合は結果を出力する
    if (!AfxWinInit(hModule, nullptr, ::GetCommandLine(), 0)) {
        wprintf(L"致命的なエラー: MFC の初期化が失敗しました\n");
        nRetCode = 1;
        return nRetCode;
    }

    std::string str("Hello World");

    //ハッシュ値の作成
    unsigned char* p;
    std::vector<unsigned char> sha256_data;
    sha256_data.resize(32);
    p = sha256_data.data();

    SHA256((const unsigned char*)str.data(), str.size(), p);

    //タイムスタンプリクエストの作成
    std::vector<unsigned char> timestamp_req_data;
    createTimestampReq(sha256_data, timestamp_req_data);

    //タイムスタンプリクエストをファイルへ出力
    std::ofstream ofs("HelloWorld.txt.tsq", std::ios_base::out | std::ios_base::binary);
    if (ofs.fail()) {
        std::cerr << "File open error: " << "HelloWorld.txt.tsq" << "\n";
        std::exit(EXIT_FAILURE);
    }

    ofs.write((char*)timestamp_req_data.data(), timestamp_req_data.size());
    if (ofs.fail()) {
        std::cerr << "File write error: " << "HelloWorld.txt.tsq" << "\n";
        std::exit(EXIT_FAILURE);
    }

    ofs.close();
    if (ofs.fail()) {
        std::cerr << "File close error: " << "HelloWorld.txt.tsq" << "\n";
        std::exit(EXIT_FAILURE);
    }

    std::string url("http://eswg.jnsa.org/freetsa");
    std::vector<unsigned char> responseBody;
    httpTimeStampClient(url, timestamp_req_data, responseBody);

    TS_RESP* ts_resp = NULL;
    const unsigned char* p2 = NULL;
    p2 = responseBody.data();
    ts_resp = d2i_TS_RESP(NULL, &p2, responseBody.size());

    //タイムスタンプレスポンスからタイムスタンプトークンを取り出す
    PKCS7* ts_token = NULL;
    ts_token = TS_RESP_get_token(ts_resp);

    //タイムスタンプレスポンスをファイルへ出力
    std::ofstream ofs2("HelloWorld.txt.tsr", std::ios_base::out | std::ios_base::binary);
    if (ofs2.fail()) {
        std::cerr << "File open error: " << "HelloWorld.txt.tsr" << "\n";
        std::exit(EXIT_FAILURE);
    }

    ofs2.write((char*)responseBody.data(), responseBody.size());
    if (ofs2.fail()) {
        std::cerr << "File write error: " << "HelloWorld.txt.tsr" << "\n";
        std::exit(EXIT_FAILURE);
    }

    ofs2.close();
    if (ofs2.fail()) {
        std::cerr << "File close error: " << "HelloWorld.txt.tsr" << "\n";
        std::exit(EXIT_FAILURE);
    }

    //タイムスタンプトークンをファイルへ出力
    BIO* out = NULL;
    if ((out = BIO_new_file("HelloWorld.txt.tst", "wb")) == NULL) {
        std::cerr << "File close error: " << "HelloWorld.txt.tst" << "\n";
        std::exit(EXIT_FAILURE);
    }
    i2d_PKCS7_bio(out, ts_token);

    return nRetCode;
}

```