Rust Canister 開発セキュリティ・ベストプラクティス

スマートコントラクトの Canister コントロール

SNS のような分散型ガバナンスシステムを使い、Canister に分散型コントローラーを持たせる

セキュリティ上の懸念事項

Canister のコントローラーは、好きなときに Canister を変更・更新することができます。Canister が ICP などのアセットを保存している場合、コントローラーが Canister を更新することでアセットを盗み、Cycle を自分のアカウントに転送できることを意味します。

推奨

  • SNS コミュニティが投票によって集団的に承認した場合のみ Canister への変更が実行されるように、Canister のコントロールを Internet Computer の Service Nervous System(SNS)のような分散型ガバナンスシステムに渡すことを検討しましょう。SNS を使用する場合、SNS サブネット上の SNS を使用することで、その SNS が NNS の恩恵にあずかり、IC の一部として維持されていることが保証されます。これらの SNS は近日中に公開される予定です。ロードマップ設計提案 を参照してください。

  • もう一つの選択肢は、Canister コントローラーを完全に削除して、イミュータブル Canister スマートコントラクトを作成することです。しかし、これは Canister のアップグレードができないことを意味し、例えばバグが見つかった場合に、深刻な影響を与える可能性があることに注意してください。分散型ガバナンスシステムを使用し、スマートコントラクトをアップグレードできるオプションは、他のブロックチェーンと比較して、Internet Computer のエコシステムの大きな利点です。

    • 他のブロックチェーンとは異なり、(IC の)イミュータブルなスマートコントラクトは実行に Cycle を必要とし、また Cycle を受け取ることができることに留意してください。

  • IC 上に DAO(分散型自律組織 )をゼロから実装することも可能です。これを行う場合(例えば基本的なDAOの例 のような形で)、セキュリティ的に危険性があり、慎重にセキュリティレビューする必要があることに注意してください。さらに、ユーザーは DAO が DAO 自身によって制御されていることを確認する必要があります。

依存するスマートコントラクトの所有権を検証する

セキュリティ上の懸念事項

Canister が他の Canister のスマートコントラクトに依存する(つまり、Canister 間コールを行う)場合、依存する Canister のスマートコントラクトが分散型ガバナンスシステムによって所有されていることが不可欠です。そうでなければ、つまりコントローラーがいれば、他の人に気づかれることなくスマートコントラクトを変更し、例えば Canister が保有するアセットを盗むことができます。

推奨

分散型であることを求める Canister とやり取りする場合は、NNS、Service Nervous System(SNS)、分散型ガバナンスシステムによってコントロールされていることを確認し、どのような条件で、誰によってスマートコントラクトが変更できるかを確認しましょう。

認証

特定のユーザーにとって認証が必要であるアクションを確認する

セキュリティ上の懸念事項

アクションに関する認証を行わない場合、攻撃者はユーザーの代わりに機密性の高い操作を行うことができ、ユーザーのアカウントを危険にさらす可能性があります。

推奨

  • デザイン上、すべての Canister のコールに対して、コール元を特定することができます。コール元の Principal には、システム API のメソッド ic0.msg_caller_sizeic0.msg_caller_copyここ を参照)を使ってアクセスすることができます。Internet Identity などを使用する場合、Principal はこの特定のオリジン、ユーザー Identity になります。ここ を参照してください。一部のアクション(ユーザーのアカウントデータへのアクセスやアカウント固有の操作など)を Principal または Principal のセットに制限する必要がある場合、Rust で以下のように、Canister のコールで明示的に確認する必要があります。

    // この操作の実行を許可された Principal の公開鍵を pk とします。
    // この pk は Canister のステートに格納される可能性があります。
    if caller() != Principal::self_authenticating(pk) {  ic_cdk::trap(...) }

    // あるいは、Canister が異なる Principal のデータを BTreeMap<Principal, UserData>
    // のようなマップに保持する場合、Canister は各呼び出し元が自分のデータにのみアクセスし操作を
    // 実行できるようにする必要があります:
    if let Some(user_data) = user_data_store.get_mut(&caller()) {
    	// ユーザーのデータに対して操作を行う
    }
  • Rust では、ic_cdk クレートを使用して、ic_cdk::api::caller による呼び出し元の認証が可能です。返される Principal が Principal::self_authenticating 型であることを確認し、その Principal の公開鍵を用いてユーザのアカウントを特定します (上記のサンプルコードを参照してください)。

  • 認証されないアクションや認証前の潜在的に高価であろう操作を避けるために、できるだけ呼び出しの早い段階で認証を行います。匿名ユーザーを拒否する のもよいアイデアです。

認証されたコールで匿名 Principal を許可しない

セキュリティ上の懸念事項

ic0::api::callerPrincipal::anonymous() も返すかもしれません。認証されたコールにおいて、これはおそらく望ましくないでしょう(そしてセキュリティに影響を与える可能性があります)。なぜなら、認証されていないコールをする人にとって、これは共有アカウントのように振る舞うからです。

推奨

認証された呼び出しでは、コール元が匿名でないことを確認し、匿名である場合はエラーまたはトラップを返します。これは、例えば以下のようなヘルパーメソッドを使用することで一元的に行うことができます:

fn caller() -> Result<Principal, String> {
    let caller = ic0::api::caller();
    // 匿名 Principal は Canister と対話することを許可されていません。
    if caller == Principal::anonymous() {
        Err(String::from(
            "Anonymous principal not allowed to make calls.",
        ))
    } else {
        Ok(caller)
    }
}

アセット認証

HTTP アセット認証を使用し、raw.ic0.app を通して Dapp を提供しないようにします

セキュリティ上の懸念事項

IC 上の Dapps は、asset certification を使って、ブラウザに配信される HTTP アセットが本物である(つまり、サブネットによって閾値署名されている)ことを確認することができます。アプリがアセット認証を行わない場合、アセット認証がチェックされない raw.ic0.app を通して安全ではない方法で提供されます。これは、単一の悪意のあるノードまたはバウンダリノードが、ブラウザに配信されるアセットを自由に変更できるため、安全ではありません。

アプリが ic0.app に加えて raw.ic0.app を通して提供される場合、攻撃者は安全でない raw.ic0.app を使用させて(フィッシングのような)ユーザーを騙す攻撃の可能性があります。

推奨

  • サービスワーカーがアセット認証を確認する <canister-id>.ic0.app を通してのみ、アセットを提供します。<canister-id>.raw.ic0.app を通してアセットを提供しないでください。

  • アセット Canister を使用してアセットを配信するか(アセット証明書を自動的に作成する)、または例えば、 NNS DappInternet Identity で行われているように、アセット証明書を含む ic-certificate ヘッダを追加してください。

  • Canister の http_request メソッドで、リクエストが raw で送られてきたかどうかを確認します。もしそうなら、エラーを返して、アセットを提供しないようにします。

Canister ストレージ

ステート変数のために Cell/RefCellthread_local! を使用し、すべてのグローバルを1つのバスケットに入れる

セキュリティ上の懸念事項

Canister には、グローバルなミュータブルステートが必要です。Rust では、これを実現するためにいくつかの方法があります。しかし、いくつかのオプションでは、例えばメモリ破壊を引き起こすような危険性を伴います。

ユーザーごとに Canister に保存できるデータ量を制限する

セキュリティ上の懸念事項

ユーザーが大量のデータを Canister に保存できる場合、これを悪用し Canister のストレージを満杯にし使用できなくする可能性があります。

推奨

ユーザー毎に Canister に保存できるデータ量を制限します。アップデートコールでユーザーのデータが保存されるたびに、この制限を確認する必要があります。

ステーブルメモリの使用を考慮し、バージョンアップし、テストする

セキュリティ上の懸念事項

Canister メモリは、アップグレードをまたいで保持されることはありません。アップグレードをまたいでデータを保持する必要がある場合、pre_upgrade で Canister メモリをシリアライズし、post_upgrade でそれをデシリアライズするのが自然な方法でしょう。しかし、これらの方法で利用できる命令数には限りがあります。メモリが大きくなりすぎると、Canister を更新することができなくなります。

推奨

Canister の機密データ暗号化を検討する

セキュリティ上の懸念事項

デフォルトでは、Canister は整合性を提供しますが、機密性は提供しません。Canister に保存されたデータはノード/レプリカが読み取ることができます。

推奨

  • Canister 上のあらゆる個人情報(ユーザーの個人情報やプライベートな情報など)をエンドツーエンドで暗号化することを検討しましょう。

  • 例として Dapp Encrypted Notes(暗号化ノート) ではエンドツーエンドの暗号化が可能であることを説明しています。

バックアップを作成する

セキュリティ上の懸念事項

以下の理由により、Canister が使用不能になり二度とアップグレードできなくなる可能性があります:

  • アップグレードプロセスに不具合がある(アプリ開発者のバグによる)。

  • データを永続化するコードのバグにより、状態が不整合、若しくは破損する。

推奨

  • アップグレードに使用される方法がテストされていることを確認、または Canister をイミュータブルにしましょう。

  • Canister を再インストールできるように、ディザスタリカバリ戦略を立てておくとよいでしょう。

  • How to audit an Internet Computer canister(Internet Computer Canister の監査方法) の "Backup and recovery" のセクションを参照してください。

Canister 間コールとロールバック

await の後にパニックを起こさない、await のバウンダリを越えて共有リソースをロックしない

セキュリティ上の懸念事項

パニックやトラップは、Canister のステートをロールバックします。そのため、トラップやパニックが発生した後のステートの変化には注意が必要です。これは Canister 間のコールが行われる場合にも重要な懸念事項となります。Canister 間コールの await の後にパニックやトラップが発生すると、Canister 間コールのコールバック呼び出しの前のスナップショットにステートが戻されます (コール全体の前ではありません!)。

これは例えば、次のような問題を引き起こす可能性があります:

  • Canister 間コール前のステート変更でステートが一貫せず、Canister 間コール後にパニックが発生すると、Canister のステートが一貫しないことになります。

  • 特に、Canister 間コール前に割り当てられたリソース (ロックやメモリなど) が解放されないと、Canister が永遠にロックされるなどの問題が発生する可能性があります。

  • 一般に、開発者が期待したときにデータが永続化されないとバグが発生することがあります。

推奨

Canister 間コール中にステートが変化する可能性に注意する

セキュリティ上の懸念事項

メッセージはアトミックに処理されます(コール全体はそうではありません)。これは、以下のようなセキュリティ問題を引き起こす可能性があります:

  • Time-of-check time-of-use:Canister 間コールの前にグローバルなステートに関するある条件をチェックし、コールが戻ったときにそれがまだ保持されていると誤って仮定してしまうこと。

推奨

信頼できる Canister に対してのみ、Canister 間コールを行う

セキュリティ上の懸念事項

  • 悪意のある可能性のある Canister に Canister 間コールが行われた場合、DoS 問題につながる可能性があり、また、Candid のデコードに関連する問題がある可能性があります。また、Canister コールから返されたデータが信頼できないにもかかわらず、信頼できると見なされる可能性があります。

  • Canister がコールバックで呼び出された場合、Peer がレスポンスしないとレシーバーが無制限にストールし DoS が発生する可能性があります。Canister がそのようなステートになると、もはやアップグレードすることはできません。回復には、再インストール、Canister のステート消去が必要です。

  • 要約すると、Canister の動作が Canister 間コールレスポンスに依存する場合、Canister の DoS、過剰なリソースの消費、またはロジックバグの原因となる可能性があるということです。

推奨

コールグラフにループがないことを確認する

セキュリティ上の懸念事項

コールグラフのループ(例:Canister A が B を呼び出し、B が C を呼び出し、C が A を呼び出す)により、Canister のデッドロックが発生する場合があります。

推奨

Canister アップグレード

アップグレード中のパニックに注意する

セキュリティ上の懸念事項

もし Canister が pre_upgrade でトラップやパニックを起こすと、Canister を恒久的にブロックすることになり、結果としてアップグレードが失敗したり、まったくできなくなったりすることがあります。

推奨

  • 本当に回復不可能な場合を除き、pre_upgrade フックでのパニックやトラップは避け、無効な状態をアップグレードで修正できるようにしましょう。pre-upgrade フックでのパニックはアップグレードを妨げますし、pre-upgrade フックは古いコードによって制御されているので、アップグレードを永久にブロックすることができます。

  • post_upgrade フックでは、ステートが無効な場合にパニックを発生させ、アップグレードを再試行して無効な状態の修正を試みることができるようにします。post_upgrade フックでのパニックはアップグレードを中断させますが、新しいコードで再試行することができます。

  • Test the upgrade hooks.Effective Rust Canisters から)

  • How to audit an Internet Computer canister(Internet Computer Canister の監査方法) の upgrade のセクションも参照してください(ただし、Mokoto が対象)。

  • Current limitations of the Internet Computer(インターネットコンピュータの現在の制限) のセクション "Bugs in pre_upgrade hooks" を参照してください。

その他

システム API コールが存在する場合でも、Canister コードをテストする

セキュリティ上の懸念事項

Canister はシステム API と相互作用するため、ユニットテストではシステム API を呼び出すことができないためコードのテストが難しくなります。このため、ユニットテストが不足する可能性があります。

推奨

  • システム API に依存しない疎結合のモジュールを作成し、それらをユニットテストする。この recommendationEffective Rust Canisters から)を参照してください。

  • システム API とまだ相互作用する部分については、システム API の薄い抽象化を作成し、ユニットテストでフェイクを行います。recommendationEffective Rust Canisters から)を参照してください。例えば、以下のように ”Runtime” を実装し、テストでは ”MockRuntime” を使用することができます(コード:Dimitris Sarlis)。

    use ic_cdk::api::{
        call::call, caller, data_certificate, id, print, time, trap,
    };

    #[async_trait]
    pub trait Runtime {
        fn caller(&self) -> Result<Principal, String>;
        fn id(&self) -> Principal;
        fn time(&self) -> u64;
        fn trap(&self, message: &str) -> !;
        fn print(&self, message: &str);
        fn data_certificate(&self) -> Option<Vec<u8>>;
        (...)
    }

    #[async_trait]
    impl Runtime for RuntimeImpl {
        fn caller(&self) -> Result<Principal, String> {
            let caller = caller();
            // 匿名 Principal は Canister と対話することはできません
            if caller == Principal::anonymous() {
                Err(String::from(
                    "Anonymous principal not allowed to make calls.",
                ))
            } else {
                Ok(caller)
            }
        }

        fn id(&self) -> Principal {
            id()
        }

        fn time(&self) -> u64 {
            time()
        }

        (...)

    }

    pub struct MockRuntime {
        pub caller: Principal,
        pub canister_id: Principal,
        pub time: u64,
        (...)
    }

    #[async_trait]
    impl Runtime for MockRuntime {
        fn caller(&self) -> Result<Principal, String> {
            Ok(self.caller)
        }

        fn id(&self) -> Principal {
            self.canister_id
        }

        fn time(&self) -> u64 {
            self.time
        }

        (...)

    }

Canister ビルドの再現性を高める

セキュリティ上の懸念事項

Canister が主張することを実行するかどうかを検証することができるはずです。IC はデプロイされた WASM モジュールの SHA 256 ハッシュを提供します。これが有用であるためには Canister のビルドが再現可能である必要があります。

推奨

Canister のビルドを再現できるようにする。この recommendation を見てください(Effective Rust Canisters から)。Developer docs on this も参照してください。

Canister からメトリクスを公開する

セキュリティ上の懸念事項

攻撃された場合、アカウント数、内部データ構造のサイズ、ステーブルメモリなど、Canister から関連するメトリクスを取得できるのは素晴らしいことです。

時間が厳密に単調であるとしてはいけません

セキュリティ上の懸念事項

System API から読み込まれる時刻は単調ですが、厳密には単調ではありません。そのため後続の2回の呼び出しで同じ時刻を返すことがあり、time API を使用した場合にセキュリティバグが発生する可能性があります。

推奨

How to audit an Internet Computer canister(Internet Computer Canister の監査方法) の セクション "Time is not strictly monotonic” を参照してください。

Cycle バランスのドレインを防ぎます

セキュリティ上の懸念事項

Canister は Cycle を消費するため、Cycle を消費する攻撃には本質的に脆弱です。

推奨

これを軽減するために、Canister レベルでの監視、早期認証、レート制限を検討してください。また、攻撃者は最も多くの Cycle を消費しているコールを狙っていることに注意してください。How to audit an Internet Computer canister(Internet Computer Canister の監査方法) の "Cycle balance drain attacks section” を参照してください。

Internet Computer に特有でなく一般的なベストプラクティス

このセクションのベストプラクティスは非常に一般的なものであり、Internet Computer に特化したものではありません。このリストは決して完全なものではなく、過去に問題になった非常に具体的な懸念事項をいくつか挙げているに過ぎません。

インプットの検証

セキュリティ上の懸念事項

query and update calls で送信されるデータは一般的に信頼できません。メッセージサイズの上限は数 MB です。これは、例えば以下のような問題を引き起こす可能性があります。

  • 検証されていないデータが Web UI でレンダリングされたり、他のシステムで表示された場合、インジェクション攻撃(XSS など)につながる可能性があります。

  • 大きなサイズのメッセージが送信され、Canister に保存される可能性があり、ストレージを過剰に消費します。

  • 大きなインプット(大きなリストや文字列など)は過剰な計算を引き起こし、DoS の原因となり、多くの Cycle を消費する可能性があります。Protect against draining the cycles balance も参照してください。

推奨

  • 入力チェックを行いましょう。例えば、OWASP cheat sheet を参照してください。

  • How to audit an Internet Computer canister(Internet Computer Canister の監査方法) のセクション "Large data attacks" (Candid space bombs に注意してください)を御覧ください。

  • ASVS の 5.1.4:構造化されたデータに対して強く型付けされており、許容される文字、長さ、パターンを含む定義されたスキーマに照らして検証されること (例:クレジットカード番号、電話、または郵便番号が一致するかどうかなど、関連する二つのフィールドが妥当かどうか検証すること)。

Rust:安全でない Rust コードを使ってはいけません

セキュリティ上の懸念事項

安全でない Rust コードはメモリ破壊の問題を引き起こす可能性があるため危険です

推奨

Rust: integerのオーバーフローを回避します

セキュリティ上の懸念事項

Rust の integer はオーバーフローすることがあります。このようなオーバーフローはデバッグ環境ではパニックになりますが、リリースコンパイルでは値はただ黙ってラップされるだけです。これは、例えば integer をインデックスやユニーク ID として使用する場合や、Cycle や ICP 数量を計算する場合などに、セキュリティ上の大きな問題を引き起こす可能性があります。

推奨

  • ラップする可能性のある整数演算がないか、コードを注意深く見直してください。

  • これらの演算には、 saturated_addsaturated_subchecked_addchecked_sub などの saturatedchecked のバリアントを使用する。例えば、 u32 については Rust docs を参照してください。

  • Rust security guidelines on integer overflows も参照してください。

高額なコール料については、Captcha や Proof-of-work の利用を検討する

セキュリティ上の懸念事項

使用するメモリや消費する Cycle などの点でアップデートやクエリのコールが高額な場合、ボットが Canister を使用不能にすることが容易になります(例えば、ストレージを一杯にすることなどによって)。

推奨

Dapp がそのような操作を提供する場合、Captcha や proof of work を追加するなどのボット対策テクニックを検討します。例えば、internet identity に Captcha の実装があります。