68
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++Advent Calendar 2023

Day 19

C++ コンパイル時パスワード認証 〜コードを不正コンパイルから守ろう!〜

Last updated at Posted at 2023-12-18

ラクラムシ「今回は C++ に関する内容ですね!」

謎の女性「どういった内容ですか?」

ラクラムシ「 C++ でコンパイル時にパスワードを要求する方法について説明していきます!」

謎の女性「よろしくお願いします!」


この記事では、C++ でコンパイル時にパスワードを要求する方法について説明します( UNIX 系の環境に限ります)。

コンパイル時パスワード認証

みなさんは、プログラミングをするときにソースコードを公開していますか?
ソースコードを公開することで、ほかの人にバグを指摘してもらったり、新機能を提案してもらったりすることができます。

ソースコードを共有できるサイトはいろいろありますが、代表的なものに GitHub があります。
GitHub は無料で利用できるので、ソースコードを共有してみたいという人は使ってみましょう!

GitHub

しかし、「コードを見られるのはいいけど、勝手にコンパイルされたくない!1」というときはどうすればいいのでしょうか?

そんなときは、コンパイル時パスワード認証です!

ソースコードに以下のようなコードを挿入すると、正しいパスワードが入力されないとコンパイルできない(エラーが出る)ようになります。

main.cpp
// ====================
// ソースコードを不正コンパイルから守る!
// ====================

#include <string_view>

// プロンプト
#warning パスワードを入力してください。

// パスワード入力
constexpr std::string_view password =
#include "/dev/stdin"
;

// パスワードチェック
static_assert(password == "KW!yaQ)5j_esE$CH6$Q+RvVu*(XQmWIXh8D.", "パスワードが違います!!");



// ====================
// ソースコード
// ====================
#include <iostream>
int main() {
    std::cout << "Hello, World!\n";
}

このソースコードのパスワードは "KW!yaQ)5j_esE$CH6$Q+RvVu*(XQmWIXh8D." に設定されています。
これをコンパイルすると、次のようになります。

$ g++ -std=c++17 main.cpp
main.cpp:8:2: warning: #warning パスワードを入力してください。 [-Wcpp]
    8 | #warning パスワードを入力してください。
      |  ^~~~~~~
"KW!yaQ)5j_esE$CH6$Q+RvVu*(XQmWIXh8D."
(Ctrl+D)
$ ./a.out
Hello, World!

コンパイルすると「パスワードを入力してください。」とメッセージが表示され、入力待ちの状態になります。
ここでパスワードを入力して Ctrl + D で閉じると、実行可能ファイルが出力されました。

ではここで、間違ったパスワードを入力するとどうなるでしょうか?

$ g++ -std=c++17 main.cpp
main.cpp:8:2: warning: #warning パスワードを入力してください。 [-Wcpp]
    8 | #warning パスワードを入力してください。
      |  ^~~~~~~
"password"
(Ctrl+D)
main.cpp:16:24: error: static assertion failed: パスワードが違います!!
   16 | static_assert(password == "KW!yaQ)5j_esE$CH6$Q+RvVu*(XQmWIXh8D.", "パスワードが違います!!");
      |               ~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

「パスワードが違います!!」とエラーが出てコンパイルに失敗しました。
しっかりとソースコードが保護されていますね!

コンパイル時パスワード認証の仕組み

まずはコンパイル時にどうやって入力を受け取るかですが、これにはよく知られた[要出展]方法があって、 #include "/dev/stdin" で標準入力の内容がソースコードに埋め込まれることを利用します2
標準入力に入力された文字列リテラルを constexpr 変数で受け取ることで、パスワードを確認します。
std::string_view の比較演算子は constexpr であるので、 static_assert によってパスワードが正しくないときにエラーを出すことができます。

ハッシュ関数

これでパスワードを知らないとコンパイルできないようになったと思いましたが、なんとソースコードにパスワードが含まれています
これではコードを読めば誰でもパスワードがわかってコンパイルできますね!

そこで、パスワードそのものを使わずにパスワードをチェックするためにハッシュ関数を使います!

ハッシュ関数は任意のデータから別の値(ハッシュ値)を得る操作で、非可逆性変換であるためハッシュ値から元のデータを得ることが難しく、安全なパスワードチェックに使えます。

ハッシュ関数のアルゴリズムには様々なものがありますが、ここでは軽量な FNV-1a という方法を使って実装してみます。

main.cpp
// ====================
// ソースコードを不正コンパイルから守る!
// ====================

#include <string_view>
#include <cstdint>

// ハッシュ関数 (FNV-1a)
constexpr std::uint64_t constexpr_hash(std::string_view s) {
    std::uint64_t value = UINT64_C(14695981039346656037);
    for (auto&& c : s) {
        value ^= static_cast<std::uint64_t>(c);
        value *= UINT64_C(1099511628211);
    }
    return value;
}

// プロンプト
#warning パスワードを入力してください。

// パスワード入力
constexpr std::string_view password =
#include "/dev/stdin"
;

// パスワードチェック
static_assert(constexpr_hash(password) == UINT64_C(5032771906826054373), "パスワードが違います!!");



// ====================
// ソースコード
// ====================
#include <iostream>
int main() {
    std::cout << "Hello, World!\n";
}

これで同様にパスワードを要求することができますが、ソースコードからパスワードがバレません。

ソースコードを変更不可に

これで完璧になったかと思いましたが、まだ問題が残されています。
今の状態では、ソースコードを書き換えてパスワードチェックの部分を削除するだけで簡単に突破されてしまいます。

これではパスワードの意味がありません。
どうにかしてソースコードを変更できないようにする必要があります。

そこで役に立つのがライセンスです。

ライセンス(ソフトウェアライセンス)とは、ソフトウェアの使用、改変、再配布、商用利用などの可否や条件を定めたもので、利用者はライセンスに違反しない範囲で使用しなければなりません。

つまり、ソースコードに改変禁止のライセンスをつけてしまえばパスワード認証のコードが削除されることはありません

ライセンスは自分で作ることもできますが、ここでは広く使われているクリエイティブ・コモンズ・ライセンスによって改変禁止を示してみましょう。
CC BY-ND (Creative Commons Attribution-NoDerivs) を表示すると、利用者にクレジットの表示および改変不可を要求することができます。

main.cpp
// Copyright (c) 2023 Raclamusi
// This source code is licensed under CC BY-ND, see https://creativecommons.org/licenses/by-nd/4.0/ .

// ====================
// ソースコードを不正コンパイルから守る!
// ====================

#include <string_view>
#include <cstdint>

// ハッシュ関数 (FNV-1a)
constexpr std::uint64_t constexpr_hash(std::string_view s) {
    std::uint64_t value = UINT64_C(14695981039346656037);
    for (auto&& c : s) {
        value ^= static_cast<std::uint64_t>(c);
        value *= UINT64_C(1099511628211);
    }
    return value;
}

// プロンプト
#warning パスワードを入力してください。

// パスワード入力
constexpr std::string_view password =
#include "/dev/stdin"
;

// パスワードチェック
static_assert(constexpr_hash(password) == UINT64_C(5032771906826054373), "パスワードが違います!!");



// ====================
// ソースコード
// ====================
#include <iostream>
int main() {
    std::cout << "Hello, World!\n";
}

これでこのソースコードは自分以外変更することができません。

脆弱性

実はこのパスワード認証には致命的な脆弱性があります。

それは、 C++ のコードを入力でき、それがそのままコンパイルされることです。

#include を使って入力を得ているため、文字列リテラルに限らずあらゆる C++ のコードが入力可能です3
これを利用すると、パスワードを知らずとも、ソースコードを改変することなくパスワードチェックを突破して、コンパイルすることができてしまいます。

$ g++ -std=c++17 main.cpp
main.cpp:22:2: warning: #warning パスワードを入力してください。 [-Wcpp]
   22 | #warning パスワードを入力してください。
      |  ^~~~~~~
""
#define static_assert(...)
(Ctrl+D)
$ ./a.out
Hello, World!

おわりに

いかがでしたか?

この記事にあるソースコードについて

この記事に記載したソースコードには CC BY-ND が表示されているものがありますが、これは改変禁止にする方法を紹介するためのものであり、この記事のソースコードは改変も含めて自由に使用していただいて構いません。

  1. おまえは何を言っているんだ

  2. 正確には入力はコンパイルより前のプリプロセス時に読み込まれます。

  3. C23 で追加された #embed を使うと安全な入力が可能です。 C++ にも早く来い。

68
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?