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 のコールに対して、コール元を特定することができます。コール元の Principal には、システム API のメソッド
ic0.msg_caller_size
とic0.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::caller
は Principal::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 Dapp や Internet Identity で行われているように、アセット証明書を含む
ic-certificate
ヘッダを追加してください。 -
Canister の
http_request
メソッドで、リクエストが raw で送られてきたかどうかを確認します。もしそうなら、エラーを返して、アセットを提供しないようにします。
Canister ストレージ
ステート変数のために Cell/RefCell
で thread_local!
を使用し、すべてのグローバルを1つのバスケットに入れる
ステーブルメモリの使用を考慮し、バージョンアップし、テストする
セキュリティ上の懸念事項
Canister メモリは、アップグレードをまたいで保持されることはありません。アップグレードをまたいでデータを保持する必要がある場合、pre_upgrade
で Canister メモリをシリアライズし、post_upgrade
でそれをデシリアライズするのが自然な方法でしょう。しかし、これらの方法で利用できる命令数には限りがあります。メモリが大きくなりすぎると、Canister を更新することができなくなります。
推奨
-
ステーブルメモリは、アップグレードしても持続するので、この問題に対処するために使用することができます。
-
Consider using stable memory. (Effective Rust Canisters から) そこで語られているデメリットも参照してください。
-
How to audit an Internet Computer canister(Internet Computer Canister の監査方法) の upgrade のセクションも参照してください(ただし、Mokoto が対象)。
-
バグを回避するためにステーブルメモリのためのテストを書きましょう。
-
開発者が作業しているいくつかのライブラリ(ほとんどが作業中か一部未完成):
-
Current limitations of the Internet Computer(インターネット・コンピュータの現在の制限) のセクション "Long running upgrades" と "[de]serialiser requiring additional wasm memory" を御覧ください。
-
例えば、internet identity は、ユーザーデータを保存するためにステーブルメモリを直接使用します。
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 が永遠にロックされるなどの問題が発生する可能性があります。
-
一般に、開発者が期待したときにデータが永続化されないとバグが発生することがあります。
推奨
-
Don’t lock shared resources across await boundaries (Effective Rust Canisters から)
-
How to audit an Internet Computer canister(Internet Computer Canister の監査方法) の ”Inter-canister calls" のセクションを参照してください。
-
コンテキストについては、IC interface spec on message execution を参照してください。
Canister 間コール中にステートが変化する可能性に注意する
セキュリティ上の懸念事項
メッセージはアトミックに処理されます(コール全体はそうではありません)。これは、以下のようなセキュリティ問題を引き起こす可能性があります:
-
Time-of-check time-of-use:Canister 間コールの前にグローバルなステートに関するある条件をチェックし、コールが戻ったときにそれがまだ保持されていると誤って仮定してしまうこと。
推奨
-
Canister 間のコール中にステートが変化する可能性があることに注意してください。このようなバグが発生しないように、慎重にコードを見直してください。
-
How to audit an Internet Computer canister(Internet Computer Canister の監査方法) の "Inter-canister calls” セクションを参照してください。
信頼できる Canister に対してのみ、Canister 間コールを行う
セキュリティ上の懸念事項
-
悪意のある可能性のある Canister に Canister 間コールが行われた場合、DoS 問題につながる可能性があり、また、Candid のデコードに関連する問題がある可能性があります。また、Canister コールから返されたデータが信頼できないにもかかわらず、信頼できると見なされる可能性があります。
-
Canister がコールバックで呼び出された場合、Peer がレスポンスしないとレシーバーが無制限にストールし DoS が発生する可能性があります。Canister がそのようなステートになると、もはやアップグレードすることはできません。回復には、再インストール、Canister のステート消去が必要です。
-
要約すると、Canister の動作が Canister 間コールレスポンスに依存する場合、Canister の DoS、過剰なリソースの消費、またはロジックバグの原因となる可能性があるということです。
推奨
-
信頼できる Canister への Canister 間コールのみを行いましょう。
-
Canister 間コールから返されたデータを消去しましょう。
-
How to audit an Internet Computer canister(Internet Computer Canister の監査方法) の "Talking to malicious canisters” セクションを参照してください。
-
Current limitations of the Internet Computer(インターネット・コンピュータの現在の制限) のセクション "Calling potentially malicious or buggy canisters can prevent canisters from upgrading" を参照してください。
コールグラフにループがないことを確認する
セキュリティ上の懸念事項
コールグラフのループ(例:Canister A が B を呼び出し、B が C を呼び出し、C が A を呼び出す)により、Canister のデッドロックが発生する場合があります。
推奨
-
このようなループは避けましょう!
-
詳しくは、Current limitations of the Internet Computer(インターネットコンピュータの現在の制限) セクション "Loops in call graphs" を参照してください。
Canister アップグレード
アップグレード中のパニックに注意する
セキュリティ上の懸念事項
もし Canister が pre_upgrade
でトラップやパニックを起こすと、Canister を恒久的にブロックすることになり、結果としてアップグレードが失敗したり、まったくできなくなったりすることがあります。
推奨
-
本当に回復不可能な場合を除き、
pre_upgrade
フックでのパニックやトラップは避け、無効な状態をアップグレードで修正できるようにしましょう。pre-upgrade フックでのパニックはアップグレードを妨げますし、pre-upgrade フックは古いコードによって制御されているので、アップグレードを永久にブロックすることができます。 -
post_upgrade
フックでは、ステートが無効な場合にパニックを発生させ、アップグレードを再試行して無効な状態の修正を試みることができるようにします。post_upgrade フックでのパニックはアップグレードを中断させますが、新しいコードで再試行することができます。 -
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 に依存しない疎結合のモジュールを作成し、それらをユニットテストする。この recommendation (Effective Rust Canisters から)を参照してください。
-
システム API とまだ相互作用する部分については、システム API の薄い抽象化を作成し、ユニットテストでフェイクを行います。recommendation (Effective 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 も参照してください。
時間が厳密に単調であるとしてはいけません
セキュリティ上の懸念事項
System API から読み込まれる時刻は単調ですが、厳密には単調ではありません。そのため後続の2回の呼び出しで同じ時刻を返すことがあり、time API を使用した場合にセキュリティバグが発生する可能性があります。
推奨
How to audit an Internet Computer canister(Internet Computer Canister の監査方法) の セクション "Time is not strictly monotonic” を参照してください。
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 security guidelines を参照してください。
-
Dfinity Rust Guidelines を検討してください。
Rust: integerのオーバーフローを回避します
セキュリティ上の懸念事項
Rust の integer はオーバーフローすることがあります。このようなオーバーフローはデバッグ環境ではパニックになりますが、リリースコンパイルでは値はただ黙ってラップされるだけです。これは、例えば integer をインデックスやユニーク ID として使用する場合や、Cycle や ICP 数量を計算する場合などに、セキュリティ上の大きな問題を引き起こす可能性があります。
推奨
-
ラップする可能性のある整数演算がないか、コードを注意深く見直してください。
-
これらの演算には、
saturated_add
やsaturated_sub
、checked_add
、checked_sub
などのsaturated
やchecked
のバリアントを使用する。例えば、u32
については Rust docs を参照してください。
高額なコール料については、Captcha や Proof-of-work の利用を検討する
セキュリティ上の懸念事項
使用するメモリや消費する Cycle などの点でアップデートやクエリのコールが高額な場合、ボットが Canister を使用不能にすることが容易になります(例えば、ストレージを一杯にすることなどによって)。
推奨
Dapp がそのような操作を提供する場合、Captcha や proof of work を追加するなどのボット対策テクニックを検討します。例えば、internet identity に Captcha の実装があります。