戦術的信頼: 開発者のためのプラットフォーム暗号技術(1/2)
私たちの社会が依存しているデジタルシステムには、どれも何らかの 信頼 の概念があります。 通信する当事者は、自分が「話している」相手が 誰 なのかを確信を持って識別できます(認証)。 そして、その「会話」が プライベート であることに安心できます(機密性)。 ネットワークに接続されていないシステムでさえ、フラッシュしたり実行したりするコードが 変更 されたり 破損 したりしていないことを検証します(完全性)。
暗号ライブラリは、認証、機密性、完全性といった性質を支える技術的な仕組みです。 これらの不完全なソフトウェアコンポーネントは、社会的な信頼が構築され維持される基盤です。 したがって、暗号ライブラリに存在する悪用可能な欠陥は、深刻かつ広範な影響を及ぼす傾向があります1。
この2部構成のセクションは、厳密な学術的意味での応用暗号についてのものではありません。暗号プリミティブやプロトコル設計を基礎から説明することはしません。 そうしたより形式的な概念は、象牙の塔に存在すると仮定しましょう2。 私たちは、中世の農民として、長く生き残っている本番ソフトウェアという泥の中で戦っています。出荷し、パッチを当て、リファクタリングするのです。
これらのセクションは、[理論的には]健全な設計をデプロイする際の過酷な現実を扱います。実世界のソフトウェアに内在する特定のリスクを低減することを目指します。 これは、信頼を大規模に出荷する際に プラットフォームセキュリティエンジニアリング が何を意味するかについての1つの解釈です。 ここで扱う概念は言語に依存しません。おそらく、あなたの問題領域や選択した技術スタックにも適用できます。
ここでいう「プラットフォームセキュリティエンジニアリング」はどのように定義していますか?
機能チームがセキュアかつ迅速に出荷できるようにするライブラリ、フレームワーク、ツールを構築することです。 本質的には、高速なソフトウェア開発のために「強固なセキュリティ基盤」を提供することです。 コードレベルの一貫性という観点で。
では、この2部構成のセクションの予定は何でしょうか?
Part 1では コード に焦点を当てます(完全で実行可能なソースは、本書のリポジトリ内の code_snippets/chp14/tactical_trust にあります)。
私たちの概念実証プログラムは、たとえ控えめであっても、「シフトレフト」自動化の水準を引き上げることを目指しています(攻撃者に1インチを与えれば、彼らは1マイルを奪うでしょう)。
スタックの異なるレベルにおける、2つの暗号プラットフォームセキュリティ問題へのソリューションを試します。
-
API: 任意の規模のコードベースで、nonce再利用の脆弱性を体系的に防止できるでしょうか?
-
サプライチェーン: CIは暗号依存関係に固有のポリシーをどのように強制すべきでしょうか?
Part 2では 概念 に焦点を当てますが、それでも多くのコードを含みます。重点は、{問題,解決策} 空間をより高いレベルで探求することです。 スコープを 情報漏えい の問題に絞り、2つの一般的な脅威モデルというレンズを通して、脆弱性と最先端の緩和策を深掘りします。
-
Man-in-the-Middle (MITM): 攻撃者が2つ以上のエンドポイント間のネットワーク通信を傍受します。
-
Man-at-the-End (MATE): 攻撃者が1つ以上の通信エンドポイントを直接侵害します。
ソフトウェアエンジニアリングよりも暗号設計に関心がある場合はどうでしょうか?
朗報です、皆さん!3 私たちはコードレベルの戦術に焦点を当てていますが、あなたが現在どの段階にいるかに応じて、正しい設計を構築する助けとなる、高品質で戦略に焦点を当てたリソースがいくつかあります。例を示します。
暗号は初心者だが経験豊富な開発者ですか? → Dave Wong著『Real-world Cryptography』4
応用暗号を専門として仕事をしていますか? → Soatok’s Cryptography Blog5
近未来の暗号技術の最先端にいますか? → Real World Crypto Symposium6
API: より強力な型でNonce再利用を防ぐ
「Nonce」は「number used only once」を組み合わせたかばん語です。 その名前が示すように、同じnonceを誤って複数回使用すること、別名 nonce再利用 は、広く使われている多くの暗号アルゴリズムにとって壊滅的な落とし穴です。 一般的な操作では、重要なセキュリティ特性を維持するために、入力としてランダムなnonceに依存しています。
-
暗号化 - 一意なnonceはしばしば「Initialization Vector」(IV)と呼ばれます。これは 平文および/または鍵の復元 ならびに リプレイ攻撃(以前の通信を悪意を持って繰り返すこと)を防ぎます。
- WPA2は、2006年から2020年までWi-Fiネットワークにおける暗号化の事実上の標準でした。その寿命の終盤に、研究者たちはすべての実装に対する実用的な攻撃を実証しました7。Wi-Fiエンドポイントとネットワークに参加するクライアントとの間の4ウェイハンドシェイクにおける再送信ロジックを悪用することで、攻撃者はプロトコルがサポートするすべてのストリーム暗号についてnonce/IVの リセット/再利用 を強制できました(例: 「キーストリーム再利用」)。つまり、攻撃者はネットワークパケットを復号、再生し、[場合によっては]偽造できます。トランスポート層の完全な侵害です(例: TCP。ただしHTTPSは含まれません)。
-
署名 - 一意なnonceは 署名の偽造(攻撃者が作成したデータに対して検証に通る署名を生成すること)と 署名の複製(以前に署名されたデータのリプレイ)を防ぎます。
- Sony PlayStation 3は、製造開始から4年経っても真の脱獄が存在しない、史上最もセキュアなゲーム機になる態勢を整えていました。PS3はECDSAを使用して、初期ブートからユーザー空間アプリの起動までの信頼の連鎖を作成し、ソフトウェアライセンスチェックを暗号学的に強制していました。ECDSA署名は、nonceと署名対象データのハッシュを入力として受け取ります。ハッカーたちは、Sonyの実装がハードコードされたnonceを使用していることを発見しました8。この欠陥により、ECDSAの 秘密 署名鍵を容易に再計算できるようになり、その結果、攻撃者は任意の未ライセンスソフトウェアを実行できるようになりました。
- Sony PlayStation 3は、製造開始から4年経っても真の脱獄が存在しない、史上最もセキュアなゲーム機になる態勢を整えていました。PS3はECDSAを使用して、初期ブートからユーザー空間アプリの起動までの信頼の連鎖を作成し、ソフトウェアライセンスチェックを暗号学的に強制していました。ECDSA署名は、nonceと署名対象データのハッシュを入力として受け取ります。ハッカーたちは、Sonyの実装がハードコードされたnonceを使用していることを発見しました8。この欠陥により、ECDSAの 秘密 署名鍵を容易に再計算できるようになり、その結果、攻撃者は任意の未ライセンスソフトウェアを実行できるようになりました。
では次に、任意の大規模なコードベースにおいて、すべてのノンスがランダムかつ単回使用であることを、どのように証明すればよいでしょうか? 安全性の不変条件を言語の型システムにエンコードするのです。 誤用がほぼ不可能な API を作成でき、安全な API だけを使用するプログラムをコンパイルするだけで、その正しさの静的検証を自動的に得られます!
大胆な主張ですが、実装は比較的単純です。
use aead::{
Aead, AeadCore, Nonce, Payload,
rand_core::{CryptoRng, RngCore},
};
use core::error::Error;
/// Can be used in arbitrarily many decryption operations.
/// Its counterpart, [`EncryptionNonce`], can only be used for one encryption operation.
pub type DecryptionNonce<A> = Nonce<A>;
/// A safer nonce type for AEAD. See trait [`NonceSafeAead`].
//
// SECURITY: Intentionally opaque and unique. Do not derive/implement any of:
// `Default`, `Copy`, `Clone`, `Ord`, `Eq`, `Debug`, etc.
pub struct EncryptionNonce<A: AeadCore>(Nonce<A>);
impl<A: AeadCore> EncryptionNonce<A> {
/// Generate a new random nonce for AEAD-specific encryption.
pub fn generate_nonce(rng: impl CryptoRng + RngCore) -> Self {
EncryptionNonce(<A as AeadCore>::generate_nonce(rng))
}
/// Crate-private conversion into [`aead::Nonce`].
//
// SECURITY: Do not make `pub`, risks reuse with `aead::Aead` APIs.
fn less_safe_to_raw_nonce(self) -> Nonce<A> {
self.0
}
}
/// Nonce-safe AEAD. Guarantees the following properties:
///
/// 1. Nonce is random.
/// * Opaque type with rand-only constructor.
/// 2. Nonce is used in exactly one encryption operation.
/// * Pass-by-value consumption.
///
/// See also: [`EncryptionNonce`] and [`DecryptionNonce`].
pub trait NonceSafeAead {
/// Encrypt plaintext payload with a random, single-use nonce.
/// Returns ciphertext bytes and decryption-only nonce.
fn nonce_safe_encrypt<'msg, 'aad>(
&self,
enc_nonce: EncryptionNonce<Self>,
plaintext: impl Into<Payload<'msg, 'aad>>,
) -> Result<(Vec<u8>, DecryptionNonce<Self>), impl Error>
where
Self: AeadCore + Aead + Sized,
{
let nonce = enc_nonce.less_safe_to_raw_nonce();
self.encrypt(&nonce, plaintext)
.map(|ciphertext| (ciphertext, nonce))
}
/// Decrypt ciphertext.
/// Identical to [`aead::Aead::decrypt`], defined so that [`aead::Aead`]
/// doesn't have to be brought in-scope when using [`NonceSafeAead`].
//
// SECURITY: ban import of less safe `aead::Aead` trait.
fn decrypt<'msg, 'aad>(
&self,
dec_nonce: &DecryptionNonce<Self>,
ciphertext: impl Into<Payload<'msg, 'aad>>,
) -> Result<Vec<u8>, impl Error>
where
Self: AeadCore + Aead + Sized,
{
<Self as Aead>::decrypt(self, dec_nonce, ciphertext)
}
}
// Use above default impl for below algorithms
impl NonceSafeAead for chacha20poly1305::XChaCha20Poly1305 {}
impl NonceSafeAead for aes_gcm::Aes256Gcm {}
impl NonceSafeAead for aes_siv::Aes256SivAead {}
Aead9 は Rust 暗号エコシステムで広く使われているトレイトです。
これは、AES-256-GCM や XChaCha20Poly1305 のような Associated Data 付き認証暗号(AEAD)アルゴリズムの encrypt および decrypt 操作に対する共通インターフェイスを定義します。
この種のアルゴリズムは機密性と完全性の両方を提供し、さらに任意で、暗号化されていない「関連」メタデータ(ネットワークヘッダー、UUID、コンテキスト情報などを想像してください)を結び付けられます。
基本的に、AEAD は日常的な暗号化の問題のほとんどに対して、推奨されるオールインワンの解決策であるべきです。
さて、Aead の暗号化/復号 API10 はどちらも、単一のノンス型を参照として受け取ります: &Nonce<A: AeadCore>。つまりプログラマーは、以前に復号に使用したものと同じノンスを使って新しいデータを自由に暗号化できてしまいます(上の図 1 を参照)。
- ノンスが
AeadCoreトレイトに対してジェネリックである点に注目してください。これにより、アルゴリズム固有の配列サイズ(例: AES-256-GCM では[u8; 12](96 ビット)、XChaCha20Poly1305 では[u8; 24](192 ビット))を、すべての呼び出し箇所でコンパイル時に検証できます。
上記の再利用に対する解決策の要点は次のとおりです。encrypt には EncryptionNonce<A: AeadCore>、decrypt には DecryptionNonce<A: AeadCore> という、2つの異なるノンス型を使用します。
この二分化により、ノンス再利用の脆弱性を再びコンパイル時に(出荷前に、かつコードベース全体にわたって体系的に)防げます。その理由は次のとおりです。
-
EncryptionNonceは、ランダムに生成されること(rand のみのコンストラクタを持つ不透明型)と、単回使用であること(値渡しパラメータのセマンティクス)が保証されます。単回使用という性質は、Rust の [linear] 型システムと特に相性がよいです。復号側の対応物であるエイリアスtype DecryptionNonce<A> = Nonce<A>;は、通常どおり動作し続けます。 -
fn generate_nonce(rng: impl CryptoRng + RngCore)におけるマーカートレイトCryptoRng11 は重要です。偏りのある(一様ランダムではない、という意味の)ノンスは、再利用されたノンスと同じくらい破滅的になり得ます。別の ECDSA の大失敗では、偏りのあるノンスによって Bitcoin の秘密鍵が抽出可能になりました12。
「ノンス誤用耐性」のあるアルゴリズムはどうでしょうか? またサイズ制限は?
強い型付けは、ノンス再利用に対する唯一の解決策ではありません。 防御策はアルゴリズム自体の設計にも実装できます。AES-GCM-SIV13 を参照してください。 「Synthetic Initialization Vector」(SIV)は、平文を含む入力を使って最終的な IV/ノンスを導出します。実質的に、2つの異なる平文に2つの異なるノンスを使わせることになります。
しかし、同じメッセージが同じノンスで2回、同じ鍵の下で暗号化された場合、攻撃者はその2つのメッセージが等価であることを知ることになります(ただし、その内容はわかりません)。 この等価性の漏洩は、より大きな脅威モデルの文脈では重大な影響を持ち得るため、強い型付けで再利用を防ぐことは依然として、より高い保証を持つ選択肢です。
しかし、まだ安心はできません。 AES-256-GCM は、ランダムノンスを使用する場合、同じ鍵の下で安全に暗号化できるメッセージは 232(約43億)個までです14。それを超えるとノンス衝突(偶然の再利用)のリスクがあります。XChaCha20Poly1305 はその安全な上限を 280(実質的に無限です!)15 まで引き上げ、AES のハードウェアサポートがないデバイスではより高速です。
以下のユニットテストにより、NonceSafeAead トレイトが期待どおりに暗号化/復号することを検証できます。
use aead::{KeyInit, OsRng};
use nonce_typing::{EncryptionNonce, NonceSafeAead};
const PLAINTEXT_MSG: &[u8; 86] = b"Two cryptographers walk into a bar. \
Nobody else has a clue what they're talking about.";
#[test]
fn nonce_safe_xchacha20poly1305() {
use chacha20poly1305::XChaCha20Poly1305;
let key = XChaCha20Poly1305::generate_key(&mut OsRng);
let cipher = XChaCha20Poly1305::new(&key);
let enc_nonce = EncryptionNonce::<XChaCha20Poly1305>::generate_nonce(&mut OsRng);
let (ciphertext, dec_nonce) = cipher
.nonce_safe_encrypt(enc_nonce, PLAINTEXT_MSG.as_ref())
.unwrap();
let plaintext = cipher.decrypt(&dec_nonce, ciphertext.as_ref()).unwrap();
assert_eq!(&plaintext, PLAINTEXT_MSG);
}
しかし、実際に再利用を防げるのでしょうか?
同じ enc_nonce を2つの異なる nonce_safe_encrypt 呼び出しに渡してみてもかまいません。コンパイラエラーは見覚えのあるものになるはずです!
「形式検証済み」の暗号はどこから始めればよいですか?
任意の入力に対して、プログラムが特定の性質を満たすことを証明することが、形式検証の目的です。 データが「共有 XOR 可変」であることを保証する Rust の型システムは、特定の形式的手法と特に相性がよいです。メモリの状態について推論する必要が少なくなるためです。 暗号は検証コストも低めです。詳細な仕様が存在し、データ構造は静的に割り当てられ、入力サイズは有界です。
検証手法は多岐にわたり(定理証明、モデル検査、抽象解釈、シンボリック実行など)、対応するツールを活用するには通常かなりの専門知識が必要です。 しかし、
怠惰な多忙な開発者である私たちは、すでに形式検証済みのライブラリを容易に統合し、その恩恵を受けられます。 ネイティブ暗号の候補は2つあります。
aws-lc-rs(Amazon)16 - ソースコードのシンボリック実行を使用して、プログラムが、アルゴリズムの人間可読な仕様から手作業でエンコードされた機械可読な仕様と一致することを証明します。symcrypt(Microsoft)17 - ソースは、対話型(つまり半手動)の定理証明器向けのモデルへ変換されます。さらに、ファジングとモデルベーステストの組み合わせを使用して、タイミングサイドチャネルを検出します。形式検証は万能薬ではないことに留意してください。仕様が不完全な場合もあり、実装がモデルから逸脱する場合もあります。 前述の WPA2 4-way handshake は形式検証されていましたが、それでも悪用可能でした! その証明では、ネゴシエートされた鍵をいつインストールすべきかを指定できておらず、暗黙的に複数回のインストールを許してしまい、その結果、次回のインストール時にノンスがリセットされました 7。
サプライチェーン: 暗号パブリッシャーを許可リスト化し、重複を禁止する
公式パッケージレジストリを持つプログラミング言語は、使っていて楽しいものです。サードパーティライブラリを簡単に見つけて統合できるため、より速く提供でき、自分の問題領域やビジネスドメインにより集中できます。 しかし、あらゆる利便性にはコストがあります。 ここでは:
-
攻撃対象領域の拡大 - 巨大な依存関係グラフのどれほど深い位置にあっても、悪意のあるクレートが1つあるだけで、アプリケーション全体が侵害される可能性があります。また、タイポスクワッティング攻撃は、エコシステム全体の一定割合を無差別に被害者にします。
-
メモリ安全性の統計的な弱体化 - 依存関係の数は、
unsafeRustコード(公開クレートの19%がunsafeを使用しています18)や他言語のCFFIコードの量、ひいてはunsoundなコード全体の量(現実的にはunsafeの一部)とある程度相関している可能性があります。unsoundなコードは実行時にメモリ安全性エラーを引き起こす可能性があり、それらは本番環境で検出されないことがよくあります。 -
ソフトウェアの肥大化 - 推移的依存関係は数が広がりがちで19、「単純な」アプリの客観的なサイズと複雑性を爆発的に増大させます。プログラムが大きくなると、一般にアプリの起動は遅くなり、ダウンロード時間も長くなります。さらに、通常時(例: APIアップグレード)と緊急時(例: 脆弱な依存関係のアラート)の両方でメンテナンス負荷がかかります。
サプライチェーン保証は、暗号依存関係にとって特に重要です。暗号依存関係は、システム全体のセキュリティ特性に大きすぎるほどの影響を与える可能性が高いためです。スタックの上位にあるアプリケーションロジックは、暗黙的または明示的に暗号ライブラリに依存しがちです。
厳格な命令を受けたと想像してください。以下の2つの要件は、100万行超のモノレポ全体で必ず満たされなければなりません。
-
信頼済み公開元 - すべての直接的な(例: 非推移的な)暗号依存関係は、信頼済み公開元の小さな許可リストから取得されなければなりません。初期状態では
RustCrypto組織のみです20。-
根拠:
RUSTSEC21アラート量と、バックドア混入リスクの両方を最小化するため。 -
スコープ: 直接依存関係のみ。明示的に信頼する公開元は、引き続き自身の依存関係を選択できます。
-
-
重複なし - すべての直接および間接の暗号依存関係は、常にツリー内に正確に1つのバージョンだけを持たなければなりません。
-
根拠: 肥大化とプログラマーエラー(例: APIバージョン間の挙動の違いが不明瞭)の両方を最小化するため。
-
スコープ: すべての依存関係。重複による肥大化はおそらく回避可能です。いずれかのクレート所有者が最新への更新を検討すべきです。
-
このポリシー(以前のNonceSafeAead APIをうまく補完するもの)をどのように強制適用すればよいのでしょうか?
残念ながら、本稿執筆時点(v0.18)では、これらの具体的な要件を、人気があり成熟した依存関係グラフリンターであるcargo deny22でエンコードすることはできません。
cargo_metadata23の上に、カスタムの道具立てを用意する必要があります!
まずは、ビルダーパターン24の定型コード(公開API)から始めましょう。
use cargo_metadata::{CargoOpt, Metadata, MetadataCommand, Package, semver::Version};
use std::{
cell::OnceCell,
collections::{BTreeMap, BTreeSet, HashMap},
fs,
path::{Path, PathBuf},
};
/// A [`Policy`] violation.
/// Note: error variants do expose/re-export error enums from 3rd-party crates.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[allow(missing_docs)]
pub enum PolicyViolationError {
DuplicateCrateVersions(Vec<String>),
DisallowedCategoryPublisher(String, String),
MetadataReadError(String),
}
/// A builder for supply-chain policies.
#[derive(Default)]
pub struct Policy {
// Path to `Cargo.toml` we're analyzing
manifest_path: PathBuf,
// Workaround for `OnceCell::get_or_try_init` being nightly-only in Rust 1.88
cargo_metadata_result: OnceCell<Result<Metadata, PolicyViolationError>>,
// {category}
// `String`s lower-cased at construction time
no_dup_cats: Option<BTreeSet<String>>,
// category: {publisher}
// `String`s lower-cased at construction time
cat_pubs: Option<BTreeMap<String, BTreeSet<String>>>,
}
impl Policy {
/// Create a new policy, construct with path to workspace or crate-specific `Cargo.toml`.
pub fn new<P>(manifest_path: P) -> Result<Policy, std::io::Error>
where
P: AsRef<Path>,
{
let manifest_path = fs::canonicalize(manifest_path)?;
Ok(Self {
manifest_path,
..Default::default()
})
}
/// Rule 1 (Category-specific Trusted Publishers):
/// Ensure that a given category only contains crates from a fixed set of trusted publishers.
/// Assumes input iterator format `(category_1, publisher_1)...(category_n, publisher_n)`.
/// More then one publisher per category is supported.
pub fn allowed_category_publishers<I, S>(mut self, cat_pubs: I) -> Policy
where
I: Iterator<Item = (S, S)>,
S: Into<String>,
{
let mut cat_pubs = cat_pubs.peekable();
if cat_pubs.peek().is_some() {
let mut cat_map = BTreeMap::new();
for (c, p) in cat_pubs {
cat_map
.entry(c.into().to_ascii_lowercase())
.or_insert(BTreeSet::new())
.insert(p.into().to_ascii_lowercase());
}
self.cat_pubs = Some(cat_map);
} else {
self.cat_pubs = None;
}
self
}
/// ...省略: ルール2(カテゴリ固有の重複なし)...
/// Evaluate a built policy against a given workspace/crate.
pub fn run(&self) -> Result<(), PolicyViolationError> {
self.run_allowed_category_publishers()?;
self.run_no_duplicate_crate_categories()?;
Ok(())
}
このセクションの長さを抑えるため、2つ目のポリシー要件(暗号依存関係の重複なし)に関する足場の実装は省きます。
ただし、そのロジックは最初の要件と機械的に似ており、両方のルールに対応した完全で実行可能なおよそ300行のソースは、code_snippets/chp14/tactical_trust/supplychain_policyにあります。
上記のビルダーは、暗号クレートに固有のものを何もエンコードしていないことに注意してください。このインターフェイスは任意のカテゴリと公開元をサポートします。
実際の使用例を見る前に、ユーザーがallowed_category_publishersの呼び出しでcat_pubsを初期化するときに指定した信頼済み公開元に対する強制ロジックを掘り下げましょう(以下はプライベートAPIです)。
/// Collect dependency metadata for the entire workspace with all features enabled.
fn metadata(&self) -> Result<&Metadata, PolicyViolationError> {
let meta_result = self.cargo_metadata_result.get_or_init(|| {
MetadataCommand::new()
.manifest_path(&self.manifest_path)
.features(CargoOpt::AllFeatures)
.exec()
.map_err(|e| PolicyViolationError::MetadataReadError(e.to_string()))
});
meta_result.as_ref().map_err(|e| e.to_owned())
}
/// Get repo's publisher by parsing its URL.
// SECURITY: `dep.authors` isn't reliable - anyone can set any value in their crate's `Cargo.toml`.
fn get_repo_publisher(dep: &Package) -> Result<String, PolicyViolationError> {
let Some(repo_url) = dep
.repository
.as_ref()
.and_then(|url| url::Url::parse(url).ok())
else {
return Err(PolicyViolationError::MetadataReadError(format!(
"Missing or invalid repo URL for crate '{}'",
dep.name
)));
};
// If `repo_url` == "https://github.com/RustCrypto/AEADs/tree/master/aes-gcm"
// Then `repo_publisher` == "RustCrypto"
let Some(repo_publisher) = repo_url.path_segments().and_then(|mut path| path.next()) else {
return Err(PolicyViolationError::MetadataReadError(format!(
"Missing publisher name for repo URL '{repo_url}'"
)));
};
Ok(repo_publisher.to_string())
}
/// Run category-specific trusted publishers check.
fn run_allowed_category_publishers(&self) -> Result<(), PolicyViolationError> {
let Some(ref cat_pubs) = self.cat_pubs else {
return Ok(());
};
let metadata = self.metadata()?;
// ID direct dependencies
let direct_deps = metadata
.packages
.iter()
.filter(|pkg| pkg.manifest_path.as_path() == self.manifest_path)
.map(|pkg| &pkg.dependencies)
.flatten()
.collect::<Vec<_>>();
// Get full crate info for each ID-ed direct dependency
let direct_dep_crates = metadata
.packages
.iter()
.filter(|pkg| direct_deps.iter().any(|dep| dep.name == *pkg.name));
// Find disallowed category-specific publishers, if any
for dep_crate in direct_dep_crates {
for cat in &dep_crate.categories {
if let Some(expected_pubs) = cat_pubs.get(&cat.to_ascii_lowercase()) {
let actual_publisher = Self::get_repo_publisher(dep_crate)?.to_lowercase();
if !expected_pubs.contains(&actual_publisher) {
return Err(PolicyViolationError::DisallowedCategoryPublisher(
cat.clone(),
actual_publisher,
));
}
}
}
}
Ok(())
}
-
fn metadataは、すべての機能を有効にした状態で、ワークスペース全体の依存関係メタデータをメモ化して収集します。ユーザーが10種類の異なるクレートカテゴリに対して10個の要件を指定したとしても、収集は正確に1回だけ実行されます(Policyフィールドcargo_metadata_resultがOnceCell25であることを思い出してください)。 -
fn get_repo_publisherは、リポジトリの所有者をそのURLから解析します。このロジックは、GitHubとGitLabの両方のURLについて公開ユーザーまたは組織を抽出しますが、注意してください。このセクションのサプライチェーン側に含まれるコードはいずれも、本番利用に十分な堅牢性があるとは主張していません!cargo_metdataのPackage構造体にあるauthorsフィールド26には依存できません。これは、公開元になりすますために悪意を持って設定される可能性があるためです。代わりに、公開元識別の信頼できる情報源として、[おそらく有効な]URLを使用します。PKIは長期的にはより優れた解決策になりますが、これについては後述します。
-
fn run_allowed_category_publishersは、信頼済み公開元(要件1)ロジックの大部分です。対象プロジェクト(Policy::newにCargo.tomlパスを渡す対象)の直接依存関係を識別し、そのリストを反復して、ユーザー指定カテゴリに属しているが、そのカテゴリについてユーザーが許可した公開元から取得されていないクレートを探します。- クレートカテゴリラベルは任意ですが、ビルダーを拡張して「任意カテゴリまたはカテゴリ欠落に対する許可済み公開元」をサポートできます。これにより、想定外の公開元が紛れ込まないようにできます。また、私たちのポリシー評価ロジックは、ユーザー入力のカテゴリ名を検証していません。タイプミスがあるとチェックに合格してしまいます!カテゴリは固定されているため27、検証の追加は簡単でしょう。
では、高度なポリシー要件(カテゴリ固有の信頼済み公開元と重複排除)の強制適用をどのように展開すればよいのでしょうか?
強硬な選択肢は、build.rs(Rustビルドスクリプト28)を活用することです。
use supplychain_policy::Policy;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=Cargo.toml");
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR var not set");
let manifest_path = std::path::PathBuf::from(manifest_dir).join("Cargo.toml");
Policy::new(&manifest_path)
.expect("Invalid manifest path")
.allowed_category_publishers([("cryptography", "rustcrypto")].into_iter())
.no_duplicate_crate_categories(["cryptography"].into_iter())
.run()
.unwrap()
}
とはいえ、サプライチェーンポリシー違反でビルドを失敗させることは、強い規制上またはビジネス上の必要性がない限り、小規模な組織であっても、他の開発チームと良好な関係を築くための最善策ではないでしょう。
幸い、上記のPolicyビルダーはCLIツールで簡単にラップでき、ワークスペースごとに、ブロッキングまたは非ブロッキングなCIパイプラインへデプロイできます。
非ブロッキングの失敗は一元的に追跡し、自動的にトリアージできます。
上の概念実証では例外(例: 「この特定の名前付き重複は許可するが、カテゴリの残りには引き続き適用する」)に対応していませんでしたが、[バージョン管理され、CODEOWNERS で保護された] 設定ファイルから個々のクレート名/パブリッシャー名を読み取るようにすれば、すぐに拡張できます。
正当な例外を、文書化された根拠とともにサポートするのは現実的です――「完璧は善の敵」です。
Rustにおけるサプライチェーンセキュリティには、他にどのような選択肢がありますか?
幸いなことに、Rustのサプライチェーンセキュリティツールの状況は進化しています。 知っておくべきサンプルプロジェクト:
- シグネチャベースの脆弱性アラート: 既知の脆弱性を持つ21クレートが依存関係ツリーに含まれていないかスキャンする無料ツールである
cargo audit29 は、本番CIに必須です。ただし、「到達可能性分析」(コードが脆弱な関数を直接または間接的に呼び出すかを判断するためのコールグラフ走査)がないため、誤検知が発生します。- ヒューリスティックベースのマルウェア検出: Linux Foundationは、Goの
capslock30 ツールに対応するRust版の開発31に資金提供しています。ほかのユースケースに加えて、capslockは特定の依存関係についてケイパビリティ32(ファイルI/O、ネットワーク接続、コマンド実行など)を列挙し、それらが新しいバージョンで突然変化した場合に警告します。- 信頼できるパブリッシャー: 将来のPKIイニシアチブ33により、パブリッシャーを暗号学的に識別できるようになる可能性があります。これは、上のURL解析からの大きな改善です。関連するRFC34では、PyPI35の足跡をたどり、信頼できるインフラストラクチャからクレートを公開するためのサポートが概説されています。PKIは対応能力の向上も意味しますが、ビルドマシンが証明書失効リスト(CRL)を取得する頃には、実際の攻撃はすでに成功している可能性がある点に注意してください。
Rustの意図的に最小限な
stdライブラリは組み込み開発にとって恵みですが、日常的なタスクに対してサードパーティ製クレートに過度に依存することを促してしまいます。 対照的に、Goの標準ライブラリは、ビルドフラグを切り替えるだけでFIPS 140-3準拠の暗号36を提供し、Goツールチェーンを更新するだけで既存のプログラムにセキュアなRNG37をバックポートしました!
要点
「信頼は滴で得られ、バケツで失われる」。 おそらく格言でしょうが、商用ソフトウェアの文脈では特に真実味があります――少数の独占企業を除けば、どんな勝者もいつでも王座から引きずり下ろされ得るグローバルな競争です。
さて、信頼のための技術的メカニズムは暗号です。 有用な暗号の大半は、小さなマイクロコントローラー上であれ強力なサーバー上であれ、コードという形で実装され、実行されています。 そしてコードを正しく書くことは、特に大量に出荷している場合には、悪名高いほど困難です。
ソフトウェア品質は、実用的に測定するのと同じくらい、あるいはそれ以上に、確実に再現することが難しいものです。 私たちの最大の希望は、再現性を自動化することです。 品質基準がセキュリティである場合、自動化はプラットフォームセキュリティエンジニアリング機能の目標の1つです。 それは少なくとも、より広範なエンジニアリング組織に歩調を合わせる必要があり、理想的にはすべての機能チームを加速するべきです。
この最初のセクションでは、API(ノンスの再利用)とサプライチェーン(依存関係ポリシー)のレベルにおける、プラットフォーム暗号の問題に対する小規模なソリューションを探りました。 意図しているのは、人間のエラーに対するガードレールを自動化することですが、現在ではLLMによる自動補完が脆弱性率を高めています38 39。 良い知らせは、上記の技術がどちらの発生源からのリスクも軽減するはずだということです。 コンパイル時チェックは、コードがどのように生成されたかを気にしません。
2番目で最後のセクションでは、範囲はより狭くなりますが、より深く掘り下げます。 信頼における古典的なトピック、情報漏えい脆弱性を探ります。 パート2では、より長く、最先端の技術的概念に取り組みます。 今回はコーヒーが欲しくなるでしょう。
それでも、きっと楽しいはずです。 信じてください。
-
The Matter of Heartbleed. Zakir Durumeric, Frank Li, James Kasten, Nicolas Weaver, Johanna Amann, Jethro Beekman, Mathias Payer, David Adrian, Michael Bailey, Vern Paxson, J. Alex Halderman (2014). ↩
-
Ivory Tower. Wikipedia (2025年閲覧). ↩
-
Good new, everyone!. Futurama Wiki (2025年閲覧). ↩
-
[個人的なお気に入り] Dave Wong著 Real-world Cryptography. David Wong (2021). ↩
-
Soatok’s Cryptography Blog. Soatok (2025年閲覧). ↩
-
Real World Crypto Symposium. IACR (2025年閲覧). ↩
-
Key Reinstallation Attacks: Forcing Nonce Reuse in WPA2. Mathy Vanhoef, Frank Piessens (2017). ↩ ↩2
-
PS3 Epic Fail. FailOverflow (2010). ↩
-
トレイト
aead::Aead. RustCrypto organization (2025年閲覧). ↩ -
API
aead::Aead::encrypt. RustCrypto organization (2025年閲覧). ↩ -
トレイト
CryptoRng. RustCrypto organization (2025年閲覧). ↩ -
Biased Nonce Sense: Lattice Attacks against Weak ECDSA Signatures in Cryptocurrencies. Joachim Breitner, Nadia Heninger (2019). ↩
-
AES-GCM-SIV: Nonce Misuse-Resistant Authenticated Encryption. S. Gueron, A. Langley, Y. Lindell (2019). ↩
-
AEADs: getting better at symmetric cryptography. Adam Langley (2015). ↩
-
XChaCha: eXtended-nonce ChaCha and AEAD_XChaCha20_Poly1305. S. Arciszewski (2020). ↩
-
Unsafe Rust in the Wild: Notes on the Current State of Unsafe Rust. Rust Foundation (2024). ↩
-
Why Bloat Is Still Software’s Biggest Vulnerability: A 2024 plea for lean software. Bert Hubert (2024). ↩
-
Rust セキュリティアドバイザリデータベース. Rust Secure Code Working Group(2025年参照)。 ↩ ↩2
-
cargo_deny. Embark Studios(2025年参照)。 ↩ -
cargo_metadata. Oliver Schneider(2025年参照)。 ↩ -
Rust デザインパターン: Builder. Rust Unofficial(2025年参照)。 ↩
-
cargo_metadata::Package. Oliver Schneider(2025年参照)。 ↩ -
Rust ビルドスクリプト. Cargo チーム(2025年参照)。 ↩
-
cargo_audit. Alex Gaynor, Tony Arcieri, Sergey Davidoff (2025年閲覧). ↩ -
CRustabilities: ケイパビリティ、Rust、Capslock. Alpha-Omega(2025年参照)。 ↩
-
アーティファクト署名による Rust のサプライチェーンセキュリティの向上. Adam Harvey(2023年)。 ↩
-
crates.io への CI 公開におけるセキュリティ改善. The Rust Project(2025年参照)。 ↩
-
「Trusted Publishers」の紹介. Dustin Ingram(2023年)。 ↩
-
FIPS 140-3 準拠. Go Project(2025年参照)。 ↩
-
Go 1.22 におけるセキュアな乱数. Russ Cox, Filippo Valsorda(2024年)。 ↩
-
ユーザーは AI アシスタントを使うと、より安全でないコードを書くのか?. Neil Perry, Megha Srivastava, Deepak Kumar, Dan Boneh(2023年)。 ↩
-
キーボードの前で眠っているのか? GitHub Copilot のコード貢献のセキュリティ評価. Hammond Pearce, Baleegh Ahmad, Benjamin Tan, Brendan Dolan-Gavitt, Ramesh Karri(2025年)。 ↩