>100 Views
September 12, 26
スライド概要
未踏ITの有志合宿で話した内容です
公開 「miseが勝手に検証してくれる」の裏側 GitHub Artifact AttestationsとSigstore Kattyan
公開 きっかけ: 自作ツールのリリースに4行足した sou1118/typdiff1 のrelease workflow2 permissions: contents: read id-token: write # 追加 attestations: write # 追加 # ...ビルド後... - uses: actions/[email protected] with: subject-path: typdiff-*.tar.gz するとmise installが勝手にこうなる [2/3] checksum typdiff-aarch64-... [2/3] verify GitHub artifact attestations [2/3] ✓ GitHub artifact attestations verified • こちらはmise側に何も設定していない • この✓は一体何を検証したのか? 1https://github.com/sou1118/typdiff/pull/21 2 actions/attest: https://github.com/actions/attest 2
公開 そもそも何を解決したいのか • リリースバイナリを落とすとき、 普通は「誰がどこで何からビルドしたか」が 分からない • checksumは「配布物が途中で壊れていない」ことしか言わない • しかもchecksumも同じリリースページから降ってくる(同じ鍵で守られた金庫と鍵) • 欲しいのは来歴(provenance)3 — 次の主張を後から暗号学的に検証できる形で 残したい 成果物 typdiff-…-apple-darwin.tar.gz sha256: 1fb5f0db…fab2f 「repo Xのworkflow Yがtag Zから作った」 ← → 出所 + 発行者の署名 ソース/ビルド repo X / workflow Y / tag Z 3SLSA Provenance v1: https://slsa.dev/spec/v1.0/provenance 3
公開 全体像: 署名する側と検証する側 GitHub公式の解説4 署名(GitHub Actions側) Runner → OIDC ID Token → Fulcio (CA) → 10分だけ有効な証明書 → in-toto/SLSA文書に署名 → Rekor (透明性ログ) → Attestations APIに保存 検証(mise側) 成果物のSHA-256 → Attestations APIからbundle取得 → TUFで信頼の起点を取得 → 証明書チェーン/ログを検証 → subjectのdigestと照合 4https://docs.github.com/en/actions/concepts/security/artifact-attestations 4
公開 Sigstore: 鍵を持たない署名(keyless signing) • 従来の署名の一番の問題は鍵の管理 (保管・ローテーション・失効・漏洩)5 • Sigstoreの発想: 長期鍵を持たない。ビルドのたびに作って捨てる ← ② IDトークンを要求(id-token: write) → Actionsの OIDCプロバイダ → Fulcio (CA)6 → Rekor7 (追記専用の透明性ログ) ID Token (repo, workflow, ref …) ワークフロー(Runner) ③ ID Token + 公開鍵を提示 ① 鍵ペアをその場で生成 ← 有効期間10分のX.509証明書(claim入り) ⑤ 秘密鍵は捨てる ④ 署名 + 証明書を記録 • 鍵を使い捨てにするという解決法 5Sigstoreの全体像: https://docs.sigstore.dev/about/overview/ 6https://docs.sigstore.dev/certificate_authority/overview/ 7https://docs.sigstore.dev/logging/overview/ 5
公開
実際に発行された証明書(typdiff v0.1.3)
Issuer: O=sigstore.dev, CN=sigstore-intermediate
Validity
Not Before: Aug 20 16:44:03 2026 GMT
Not After : Aug 20 16:54:03 2026 GMT
← 10分
Subject:
← 空 (人間の識別子がない)
X509v3 Subject Alternative Name: critical
URI:https://github.com/sou1118/typdiff/.github/workflows/release.yml@refs/tags/v0.1.3
1.3.6.1.4.1.57264.1.8 (Issuer)
https://token.actions.githubusercontent.com
1.3.6.1.4.1.57264.1.11 (Runner Env)
github-hosted
1.3.6.1.4.1.57264.1.12 (Source Repo URI) https://github.com/sou1118/typdiff
1.3.6.1.4.1.57264.1.21 (Run Invocation) .../actions/runs/32393569126/attempts/1
CT Precertificate SCTs: ...
• 主体は人間ではなく「ワークフローのref」89
• OIDCのclaimがそのままX.509拡張になっている
8OIDの一覧: https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md
9再現: gh api /repos/{owner}/{repo}/attestations/sha256:<digest> → 証明書をopenssl x509 -inform der -text
6
公開
署名されている中身: in-toto + SLSA Provenance
{ "_type": "https://in-toto.io/Statement/v1",
"subject": [{ "name": "typdiff-aarch64-apple-darwin.tar.gz",
"digest": { "sha256": "1fb5f0db...fab2f" } }],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": { "buildDefinition": {
"buildType": "https://actions.github.io/buildtypes/workflow/v1",
"externalParameters": { "workflow": { "ref": "refs/tags/v0.1.3",
"repository": "https://github.com/sou1118/typdiff",
"path": ".github/workflows/release.yml" } },
"resolvedDependencies": [{ "uri": "git+https://github.com/sou1118/typdiff@refs/tags/v0.1.3",
"digest": { "gitCommit": "8f7dedf1..." } }] },
"runDetails": { "builder": { "id": ".../release.yml@refs/tags/v0.1.3" } } } }
• subject (何を) × predicate (どう作ったか)という汎用の形10
• actions/attestはpredicateを渡さないとSLSA build provenanceを自動生成する
11
10in-toto Statement v1: https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md
11Attestation Modes: https://github.com/actions/attest
7
公開 コンテナ: Sigstore BundleとDSSE • GitHubが返すのはapplication/vnd.dev.sigstore.bundle.v0.3+json12 • dsseEnvelope: payload (さっきのJSON) / payloadType / signatures • verificationMaterial: certificate / tlogEntries / timestampVerificationData • DSSE (Dead Simple Signing Envelope)13 • payloadそのものではなくpayloadTypeを含めてエンコードした列に署名する •「JSONとして同じだが別の型として解釈させる」類の混同攻撃を防ぐ • tlogEntriesにはRekorのlogIndex / integratedTimeに加えて inclusion proof (ハッシュツリーの経路)とSET (ログの署名)が入る • → ログサーバに問い合わせずに「確かにログに載っている」ことを検証できる • 証明書は10分で失効するが、 この時刻が「署名時点では有効だった」ことを示す 12Bundleの定義: https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto 13https://github.com/secure-systems-lab/dsse/blob/master/protocol.md 8
公開 検証側(mise)は何をしているか • miseはmise-sigstoreという自前クレートで検証している14 mise ① 成果物のSHA-256を計算 ④ 証明書チェーン・SCT・ inclusion proof / SETを検証 ⑤ DSSE署名を検証し subject[].digestと ① を照合 ← ② GET /repos/{owner}/{repo}/attestations/sha256:{digest}15 → GitHub Attestations API → Sigstoreの TUF16リポジトリ Sigstore bundle ③ trusted rootを取得・更新 ← Fulcio / Rekorの公開鍵 • ⑤ が抜けると「どこかの正しい成果物の署名」で任意のファイルが通ってしまう • デフォルト有効(github_attestations)17。mise.lockにもprovenanceとして 記録される 14https://github.com/jdx/mise crates/mise-sigstore, src/github/sigstore.rs 15https://docs.github.com/en/rest/repos/attestations 16https://theupdateframework.io/ 17https://mise.jdx.dev/dev-tools/backends/github.html 9
公開 ハマりどころ: Sigstoreインスタンスが2つある bundleの署名証明書のissuerで分岐する1819 ↓ public repo / O=sigstore.dev private / internal repo / O=GitHub, Inc. Sigstore public good instance GitHub内製インスタンス ・公開の透明性ログあり ・inclusion proof / SCTが付く ・標準ポリシーで完全に検証 ・公開の透明性ログがない ・inclusion proofもSCTも無い ・trusted rootを差し替え + skip_sct / skip_tlog • 素直に「Sigstoreの標準ポリシー」で検証すると右が必ず落ちる •「同じ仕組みに見えて信頼の根っこが別物」というのが実装を読むと分かる 18miseのis_github_internal_certificate / verify_bundle (crates/mise-sigstore/src/lib.rs) 19https://github.com/actions/attestのREADME、およびhttps://docs.github.com/en/actions/concepts/security/artifact-attestations 10
公開 これは何を保証しないのか • GitHub公式ドキュメント20いわく artifact attestations are not a guarantee that an artifact is secure • 保証するのは「このソースとこのビルド手順から出てきた」ことだけ • attestationは誰でも自分のリポジトリで作れる •「署名がある」≠「信頼できる」。どのrepo / workflowを信じるかは人間が決める • 厳密にやるならgh attestation verify --signer-workflow ...のようにidentityを固定する • ビルドスクリプト自体が汚染されていれば、 検証は普通に通る • ここから先はSLSAのlevel (ビルド環境の隔離、改ざん耐性)の話になる • public repoでは出所が公開の透明性ログに載る (隠したいものは載せない) 20https://docs.github.com/en/actions/concepts/security/artifact-attestations 11
公開 まとめ • actions/attestは4行で入るが、中では OIDC → Fulcioの10分証明書 → in-toto/ SLSA文書にDSSE署名 → Rekorが動いている • 鍵を長期保管しないための設計(keyless)が全体の形を決めている • 検証側はTUFで信頼の起点を取り直し、証明書・ログ・そして subject digestの 一致を確認している • public / privateでインスタンスが分かれており、検証ポリシーの分岐が必要 • 同じ仕組みはnpm provenance21 / PyPI (PEP 740)22 / Homebrewのbottle検 証23などにも広がっている •「署名されている」ことより「何が署名されているか」を読むのが大事 21https://docs.npmjs.com/generating-provenance-statements 22https://peps.python.org/pep-0740/ 23 HOMEBREW_VERIFY_ATTESTATIONS: https://docs.brew.sh/Manpage 12