npm パッケージに認証バッジを付けてもらった話 (npm Provenance を試す)

2023/05/08に公開
1

この記事では、npmパッケージの透明性と検証性が向上する新機能であるnpm package provenance (プロベナンス) について解説します。実際に公開中のパッケージに認証バッジを付けてみて、さらに使用側としてプロベナンスを検証する手順を解説します。

npm パッケージのスクリーンショット。バージョンの隣に GitHub Actions からの認証バッジが付いている
npm パッケージのバージョンの隣に認証バッジが付いている

対象読者

  • npmでパッケージを公開しているのでプロベナンスを付けたい
  • インストールしている依存関係のプロベナンスを検証してみたい
  • npm provenanceの仕組みを簡単に知りたい

はじめに

最近、npmのパッケージに悪質なコードを含んで公開されたという問題が起こったのは記憶に新しいのではないでしょうか[1]。この例に限らず、オープンソースサプライチェーン攻撃は日々増加しており大きなリスクとなっています[2]

実は、npmでは認証情報さえあれば npm publish とタイプするだけで任意のコードを好きなように公開できます。パッケージを使用しようとしても、どのソースコードからビルドされたのか、どのビルドプロセスを用いられたのかを使用者側から検証できないといった課題があります。

さらには、npmにGitHubへのリンクが載っていますが、このリンクも package.json の情報を転記しているだけで、実際にそのコードがGitHubにあるとは限りません。また、GitHubにあるとしても、そのコードがnpmに公開されたコードと一致しているとも限りません。

OSSが信頼される1つの理由として、ソースコードや変更履歴が公開されているため透明性が高く、検証しやすいことが挙げられます。しかし、npmのパッケージは同等の透明性と検証性が確保されていません。

これらの背景から、 2023年4月より、npm package provenanceがパブリックベータ版で提供開始されました。この機能を用いるとnpmのパッケージにプロベナンスが発行されます。パッケージを使用するユーザーが、ビルド元のコミットやビルドプロセスなどを検証できるようにすることで、透明性と検証性が向上しています。

今回は、私がメンテナーであるnpmパッケージへプロベナンスを付け、さらに使用側として検証してみました。

プロベナンスを付けてみた

まずは、私がメンテナーである npm パッケージにプロベナンスを付けてみます。

ワークフローの変更

プロベナンスを付与する方法は npm 公式ドキュメント に記載されています。現状、ベータ版であるため対応しているCDサービスはGitHub Actionsのみです。

すでにGitHub Actionsでパブリッシュするワークフローを組んでいれば、必要な作業は id-token の生成権限付与と npm publish--provenance を追加するだけです。

実際の差分はこちらで確認できます。

workflow.yml
name: Publish (latest)
on:
  release:
    types: [created]
jobs:
  build:
    runs-on: ubuntu-latest
    environment: npm
+    permissions:
+      contents: read
+      id-token: write
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18.x'
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
-      - run: npm publish
+      - run: npm publish --provenance
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

パブリッシュ

その後、GitHubのリリースをすることでワークフローをトリガーします。

npmパッケージのページを開いてみると、新バージョンに認証バッジとプロベナンスが追加されていました。

npm パッケージのスクリーンショット。Provenance という項目が追加され、GitHub Actions によってこのバージョンが署名されていることを示している
npm パッケージに Provenance が追加され認証バッジが付いている

すでにGitHub Actionsのワークフローを組んでいた場合、鍵を生成するなどの複雑な作業は不要で数分で対応できることがわかりました。開発者体験がとても良いですね。

実際のnpmページは下記から確認できます。

https://www.npmjs.com/package/gatsby-plugin-fix-fouc/v/1.0.3-beta.0

プロベナンスを検証してみた

無事にプロべナンスを追加できたので、使用者側として検証してみます。

検証できる情報

まずは、npmページについたプロベナンスのカードを見ると主に次の情報がわかります。

  • ソースコードのコミットハッシュ
  • ビルドした際に使用されたワークフロー
  • ワークフローの実行結果
  • など

また、プロベナンスに対するSigstoreによる署名ログも付与されています。これらはnpmのサイトからリンクされています。

ソースコードのコミットハッシュ

ワークフローがトリガーされたコミットのハッシュにリンクされています。

ビルド元のソースコードやコミット履歴を検証できます。

https://github.com/bicstone/gatsby-plugin-fix-fouc/tree/5abb98d705367364fa88a9a4e6a4e0f13801bf2c

ビルドした際に使用されたワークフロー

使用されたワークフローのソースコードにリンクされています。

ビルド時のコマンドを確認することで、ソースコードからのビルド手順を検証できます。

https://github.com/bicstone/gatsby-plugin-fix-fouc/actions/runs/4871672535/workflow

ワークフローの実行結果

GitHub Actionsのワークフロー実行結果にリンクされています。

私のパッケージでは Deployment protection rules において承認を必須にしており、承認履歴も残っています。 (アカウントを乗っ取れば誰でも承認できるのであくまでも参考程度)

https://github.com/bicstone/gatsby-plugin-fix-fouc/actions/runs/4871672535/attempts/1

プロベナンスに対する Sigstore による署名ログ

プロベナンスは、 Sigstore によって署名されています。

署名のログはRekorで閲覧できます。この公開された署名により、プロベナンスや発行済みのパッケージが改ざんされた場合に検出ができます。

https://search.sigstore.dev/?logIndex=19589594

npm CLI から検証

さらに、依存関係で使用しているパッケージでプロベナンスやパッケージが改ざんされていないかどうかを、 npm CLI 9.5以上であれば検証できます。ちなみにyarnやpnpmなどでは執筆時点では検証できません[3]。最近npm CLI回帰の流れを耳にすることがありますが、また1つ理由が増えましたね。

$ npm audit signatures
audited 1302 packages in 14s
1302 packages have verified registry signatures
1 package has a verified attestation
  • x packages have verified registry signatures はnpmによる署名を用いて、パブリッシュされたものから改ざんされていないことを検証しています。 (以前からある機能)
  • x package has a verified attestation はプロベナンスの署名を用いて、プロべナンスの情報やパッケージが改ざんされていないことを検証しています。

CDプロセスに npm audit signatures を追加することで、改ざんされていないかどうかをデプロイのたびに検証できます。

プロベナンスの仕組み

仕組みについてはGitHub公式ブログで解説されています。Sigstoreの仕組みを書くだけで1記事書けるレベルなので、今回は概要だけ解説します。詳しくは公式ブログをご覧ください。

https://github.blog/2023-04-19-introducing-npm-package-provenance/

まず、GitHub Actions内で次のようなプロベナンスが作成されます。

プロベナンスの例

_type: https://in-toto.io/Statement/v0.1
subject:
  - name: pkg:npm/gatsby-plugin-fix-fouc@1.0.3-beta.0
    digest:
      sha512: >-
        a38cd31ada1fb30cf24b8ec672c152930ea660f5add6773a33953c2d4650a60fb42e1720a44ce0acbf02fc16b8314c8c353e27eccf518315366823d4ea04fb83
predicateType: https://slsa.dev/provenance/v0.2
predicate:
  buildType: https://github.com/npm/cli/gha@v1
  builder:
    id: https://github.com/npm/cli@9.5.1
  invocation:
    configSource:
      uri: >-
        git+https://github.com/bicstone/gatsby-plugin-fix-fouc@refs/tags/v1.0.3-beta.0
      digest:
        sha1: 5abb98d705367364fa88a9a4e6a4e0f13801bf2c
      entryPoint: >-
        bicstone/gatsby-plugin-fix-fouc/.github/workflows/publish-next.yml@refs/tags/v1.0.3-beta.0
    parameters: {}
    environment:
      GITHUB_ACTOR_ID: "47806818"
      GITHUB_EVENT_NAME: release
      GITHUB_REF: refs/tags/v1.0.3-beta.0
      GITHUB_REF_TYPE: tag
      GITHUB_REPOSITORY: bicstone/gatsby-plugin-fix-fouc
      GITHUB_REPOSITORY_ID: "528440344"
      GITHUB_REPOSITORY_OWNER_ID: "47806818"
      GITHUB_RUN_ATTEMPT: "1"
      GITHUB_RUN_ID: "4871672535"
      GITHUB_RUN_NUMBER: "10"
      GITHUB_SHA: 5abb98d705367364fa88a9a4e6a4e0f13801bf2c
      GITHUB_WORKFLOW_REF: >-
        bicstone/gatsby-plugin-fix-fouc/.github/workflows/publish-next.yml@refs/tags/v1.0.3-beta.0
      GITHUB_WORKFLOW_SHA: 5abb98d705367364fa88a9a4e6a4e0f13801bf2c
  metadata:
    buildInvocationId: 4871672535-1
    completeness:
      parameters: false
      environment: false
      materials: false
    reproducible: false
  materials:
    - uri: git+https://github.com/bicstone/gatsby-plugin-fix-fouc
      digest:
        sha1: 5abb98d705367364fa88a9a4e6a4e0f13801bf2c

このプロベナンスには、該当のコミットやワークフローの情報、これらのハッシュなどの情報を含んでいます。このプロべナンスは次のような手順で署名、検証、公開されます。

  1. GitHub Actions
    • 使い捨ての公開鍵と秘密鍵のキーペアを生成
    • OpenID Connect (OIDC) サービスを用いて、ID tokenを取得
    • プロべナンスを生成
    • Sigstoreの認証局 (Fulcio) にID token, 公開鍵を送信
  2. Fulcio (認証局)
    • ID tokenを検証
    • 検証が成功すれば、短命の署名証明書を発行してGitHub Actionsに返す
  3. GitHub Actions
    • 秘密鍵でプロべナンスに署名
    • 署名したプロベナンスと署名証明書をSigstoreの台帳 (Rekor) にアップロード
  4. Rekor (台帳)
    • 署名したプロベナンスと署名証明書を検証
    • 検証が成功すれば、台帳に記録して公開
  5. GitHub Actions
    • npmにパッケージ, 署名したプロべナンス, 署名証明書を送信
  6. npm (リポジトリ)
    • 送信されたパッケージ, 署名されたプロべナンス, 署名証明書を検証
      • 署名証明書に添付されているIdentityがGitHub Actionsになっているか
      • プロべナンスの署名が有効で、改ざんされていないか
      • パッケージのハッシュ値がプロべナンスと一致し、改ざんされていないか
      • など
    • 検証が成功すれば新バージョンを発行

下記はイメージです。

まとめ

オープンソースサプライチェーン攻撃が増加している背景から、npm package provenanceという新機能が導入されました。この機能で、npmパッケージの透明性や検証性を向上させることができます。

(認証情報が漏れる前提で) OSSのメンテナーを信じずに、ソースコードとビルドプロセスを直接検証できるようにするというのが素晴らしいと考えています。まさしくOSSの本質ですね。

内部では複雑なフローが動いていますが、とても簡単な手順でプロべナンスを付けられ、検証できるように工夫されていました。OSSのメンテナーが秘密鍵を管理する必要がない仕組みはよく考えられていて、業界はオープンソースサプライチェーン攻撃に対する取り組みに本気なのだと改めて感じさせられました。

他のプラットフォームにも導入されていき、将来的には、プロベナンス付きでnpmパッケージを公開することがスタンダードになるのではないかと考えています。

脚注
  1. Infinite loop causing Denial of Service in colors · GHSA-5rqg-jm4f-cqx7 · GitHub Advisory Database / Embedded malware in ua-parser-js · GHSA-pjwm-rvh2-c87w · GitHub Advisory Database など ↩︎

  2. 8th Annual State of the Software Supply Chain Report | Sonatype ↩︎

  3. Generating provenance statements | npm Docs ↩︎

Discussion

おおいし (bicstone)おおいし (bicstone)

記事の中で npm audit signatures を紹介していますが、 npm audit signature などタイプミスしても npm audit と入力されたとみなされてしまうので注意が必要です (私は1回ミスしました)