Swift Digest
SE-0391 | Swift Evolution

Package Registry Publish

Proposal
SE-0391
Authors
Yim Lee
Review Manager
Tom Doron
Status
Implemented (Swift 5.9)

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

SE-0292 でSwiftPMがPackage Registryからパッケージをダウンロードしたり依存解決したりできるようになりましたが、パッケージを公開する側 のツールはSwiftPMに用意されていませんでした。そのため、パッケージ作者はRegistryにパッケージを公開するために、次のような手順を自力で踏む必要がありました。

  1. パッケージリリースのメタデータを用意する
  2. swift package archive-source でソースアーカイブを作る
  3. 必要に応じてメタデータとアーカイブに署名する
  4. 必要に応じて(SE-0378 の)認証を行う
  5. Registryの「create a package release」APIをHTTPで叩いてアーカイブとメタデータを送る
  6. レスポンスをチェックして成功・失敗・非同期処理中のいずれかを判定する

これらはRegistryごとに微妙に事情が異なり、パッケージ作者が毎回手作業で組み立てるには煩雑です。さらに、メタデータの形式やパッケージ署名の方式についてRegistry-クライアント間の取り決めがなかったため、Registryごとに受け付けるメタデータの形や署名の扱いがばらつくおそれがありました。

SwiftPMがPackage Registryの利用体験を完結させるには、公開側のワークフローを標準化し、単一のコマンドにまとめる必要がありました。

02 どのように解決されるのか

SwiftPMに新しいサブコマンド swift package-registry publish を追加し、アーカイブ作成・署名・Registryへの送信までを一括で行えるようにします。あわせて、パッケージリリースのメタデータのスキーマと、パッケージ署名のフォーマットを標準化し、Registryとクライアントの間のAPIコントラクトを明文化します。

swift package-registry publish サブコマンド

publish サブコマンドは、指定したパッケージ識別子(<scope>.<name> 形式)とバージョン(SemVer 2.0)について、ソースアーカイブの生成、必要なら署名、Registryへの送信、レスポンスのチェックまでを行います。

swift package-registry publish <package-id> <package-version>
  [--url <registry-url>]
  [--scratch-directory <path>]
  [--metadata-path <path>]
  [--signing-identity <label>]
  [--private-key-path <path>]
  [--cert-chain-paths <paths>]
  [--dry-run]

主なオプションは次のとおりです。

  • --url: 公開先のRegistry URL。指定されなければ、registries.json のscope-to-registryマッピングか [default] の値が使われます。どちらも見つからなければエラーになります。
  • --scratch-directory: 中間ファイルを置く作業ディレクトリ。デフォルトはパッケージディレクトリ。
  • --metadata-path: メタデータJSONのパス。未指定ならパッケージディレクトリ直下の package-metadata.json が使われます。見つかればリクエストボディに含められ、署名も行う場合はメタデータにも署名されます。
  • --signing-identity: システムの秘密情報ストア(初回リリースではmacOSのKeychain)に登録された署名用IDのラベル。指定された場合はこれだけで秘密鍵と証明書チェーンを特定できます。
  • --private-key-path / --cert-chain-paths: --signing-identity が使えない環境での代替。前者はPKCS#8のDERエンコード秘密鍵、後者は署名証明書(先頭)とそのチェーン。
  • --dry-run: アーカイブ作成と署名まで行い、Registryへの送信はしません。

--signing-identity もしくは --private-key-path + --cert-chain-paths が指定されている場合に、ソースアーカイブとメタデータの両方に署名が行われます。初回リリースで利用できる署名フォーマットは cms-1.0.0(後述)のみです。

前提条件として、Registryが認証を要求する場合はあらかじめ swift package-registry login で認証を済ませておく必要があります。また、対象のパッケージ識別子はRegistryに事前に登録されている必要があります。

パッケージリリースメタデータ

メタデータは引き続き「create a package release」リクエストの metadata マルチパートセクションにJSONオブジェクトとして送られますが、その中身は標準スキーマ PackageRelease に従うことが必須になります。スキーマのトップレベルは次のプロパティを持ちます(いずれも任意)。

プロパティ 内容
author Author パッケージリリースの著者情報。name 必須、email / description / organization / url は任意
description String パッケージリリースの説明
licenseURL String ライセンス文書のURL
readmeURL String READMEのURL
repositoryURLs Array コードリポジトリのURL(同一リポジトリのSSH/HTTPSなどすべてのバリエーションを含めることが推奨)

author の中の organization も同様にネストしたオブジェクトで、name が必須です。

Registryはスキーマを拡張して追加のプロパティを受け付けたり必須化したりできますが、標準スキーマで定義済みのプロパティを別の意味に変えたり書き換えたりしてはなりません。また、必須メタデータが欠けている場合は「create a package release」リクエストを失敗させてかまいません。

これまで可能だった「空の {} を送ってRegistry側の自動入力を抑制する」という挙動は、この提案で取り下げられます。メタデータをどう扱うかはRegistryの方針に委ねられます。

パッケージ署名

署名フォーマットは識別子 <方式>-<バージョン> で表現されます。初回リリースで定義されるのは CMS ベースの cms-1.0.0 のみで、パラメータは次のとおり固定されています。

  • Content type: Signed-Data
  • Encapsulated data: 省略(外部署名)
  • メッセージダイジェスト: SHA-256(ソースアーカイブに対して計算)
  • 署名アルゴリズム: ECDSA P-256
  • 署名数: 1
  • 証明書: 署名鍵を含む証明書。どのルート証明書を信頼するかなどのポリシーはRegistry側で定めます

署名は「create a package release」リクエストのボディに、source-archive-signaturemetadata-signature というパートとして同梱されます。リクエストには X-Swift-Package-Signature-Format: cms-1.0.0 のようなヘッダが付きます。

Registry側は、フォーマットが受け付けられるものか・署名がフォーマットに沿っているか・証明書チェーンがポリシーに合致するかを確認し、証明書の公開鍵で署名を検証します。さらに、「fetch package release metadata」APIのレスポンスでは署名情報を公開し、「download package source archive」APIのレスポンスにも X-Swift-Package-Signature-FormatX-Swift-Package-Signature の各ヘッダを含める必要があります。

SwiftPM側の署名ポリシー

ユーザ側は、ダウンロードしたパッケージの署名をどの程度厳格に扱うかを ~/.swiftpm/configuration/registries.jsonsecurity キーで設定します。設定は default を基準に、registryOverrides / scopeOverrides / packageOverrides の順により具体的な指定が優先 されます(パッケージ > スコープ > Registry > デフォルト)。

{
  "security": {
    "default": {
      "signing": {
        "onUnsigned": "prompt",
        "onUntrustedCertificate": "prompt",
        "trustedRootCertificatesPath": "~/.swiftpm/security/trusted-root-certs/",
        "includeDefaultTrustedRootCertificates": true,
        "validationChecks": {
          "certificateExpiration": "disabled",
          "certificateRevocation": "disabled"
        }
      }
    }
  }
}

主な設定項目は次のとおりです。

  • signing.onUnsigned: 署名されていないパッケージの扱い。error(拒否)/ prompt(ユーザに確認)/ warn(警告のみ)/ silentAllow(黙認)。
  • signing.onUntrustedCertificate: 信頼されていない証明書で署名されたパッケージの扱い。同じく4段階。prompt で「許可する」と答えた場合、以降は未署名パッケージと同様に扱われます。
  • signing.trustedRootCertificatesPath: カスタムの信頼済みルート証明書を置くディレクトリ。
  • signing.includeDefaultTrustedRootCertificates: SwiftPM同梱の既定ルートを信頼ストアに含めるかどうか。
  • signing.validationChecks.certificateExpiration: 証明書の有効期限チェックを行うか。enabled なら期限切れ証明書で署名されたパッケージは拒否されます。
  • signing.validationChecks.certificateRevocation: 失効チェック。strict は失効状態が取得できない場合も拒否、allowSoftFail は失効が確定した場合のみ拒否、disabled は行いません。初回リリースで対応するプロトコルはOCSPのみです。

信頼されている証明書とは、SwiftPMの信頼ストア(既定ルート+カスタムルート)にチェーンする証明書のことで、それ以外は onUntrustedCertificate の対象になります。

ローカルTOFU

ダウンロード時、SwiftPMは ~/.swiftpm/security/fingerprints/ にチェックサムを記録し、同じパッケージリリースを再度取得した際にチェックサムが一致しない場合は失敗させる TOFU(trust on first use) を行います。加えて、署名に利用された証明書から安定した署名IDを取り出せる場合は、同じパッケージの別バージョンでも同じ署名IDが使われていることを要求する、publisher-levelのTOFUも併用されます(対応可能な証明書は限定的です)。

セキュリティ上の位置付け

この提案で導入されるのはあくまで「Registryで署名を要求するための土台」であり、「特定の人物が公開したパッケージであること」を検証するものではありません。署名されているからといって無条件に信頼できるわけではなく、マルウェア対策にもなりません。主な意図は、Registryの認証情報が漏れたときに、鍵や署名IDによって公開可能なパッケージを制限できるようにすることです。

また、OCSPによる失効チェックは、ダウンロード中のパッケージを証明書機関やネットワーク経路上の第三者に暗黙に開示することになります。気になる場合は certificateRevocationdisabled にできます。

03 今後の見通し

今回のスコープ外として、次のような拡張が方向性として示されています。いずれも将来の構想であり、実現を約束するものではありません。

暗号化された秘密鍵への対応

--private-key-path で渡す秘密鍵は通常暗号化されていますが、初回リリースでは暗号化された鍵を直接扱えません。将来的には、必要に応じてパスフレーズを対話的にプロンプトしたり、自動化用途のために --private-key-passphrase オプションを追加したりして、package signpackage-registry publish で暗号化済み秘密鍵を読み込めるようにすることが検討されています。

メタデータの自動生成

パッケージリリースのメタデータの一部は、パッケージディレクトリの情報からSwiftPM側で自動的に埋められる余地があります。自動生成された値をデフォルトや叩き台として提示し、必要なら作者が編集する、という形にすれば、すべてのパッケージリリースが何らかのメタデータを持てるようになります。

OCSP以外の失効チェック

初回リリースで対応する失効チェックはOCSPのみですが、将来的には他の手段による失効チェックも追加される可能性があります。

SwiftPM側での署名ID検証

現状、署名IDの定義・実装・公開時の検査はRegistry側に委ねられています。これはSwiftPMがRegistryの実装を信頼することを前提にしており、Registry自体やSwiftPM-Registry間の通信が侵害されると、不正なパッケージが公開されてしまうリスクがあります。SwiftPM側でも署名IDを検証できれば、このリスクを軽減できますが、そのためには「どの証明書から、どのような形式の署名IDを取り出すか」を仕様として定める必要があります。将来のProposalでこうした証明書仕様が整備されれば、より多くの証明書が publisher-level TOFU の対象になり、チェックサムTOFUの上に追加の信頼層を築けるようになります。

署名時刻の保全(Timestamping / Countersignature)

現行の設計では、署名に使った証明書が期限切れになると、SwiftPMが署名情報や失効状態を検証できなくなります。これを避けるため、Time Stamping Authority や、Registry自身による countersignature を活用して「いつ署名・公開されたか」を保全することが検討されています。これが実現すれば、証明書が期限切れになってもパッケージ作者が再署名する必要がなくなります。

Transitive trust

ローカルTOFUは「自分が一度取得したパッケージリリース」に対してしか効きません。そこで、Package.resolved(あるいは類似のファイル)にチェックサムや署名IDを書き出し、それをパッケージ本体に同梱することで、直接・推移的な依存についてのセキュリティ情報をエコシステム全体に伝搬させる「transitive trust」が構想されています。中央集権的なデータベースに頼らずに、ローカルTOFUよりも速く情報を共有できることが狙いです。Package.resolved のイメージは次のとおりです。

{
  "pins": [
    {
      "identity": "mona.LinkedList",
      "kind": "registry",
      "location": "https://packages.example.com/mona/LinkedList",
      "state": {
        "checksum": "ed008d5af44c1d0ea0e3668033cae9b695235f18b1a99240b7cf0f3d9559a30d",
        "version": "0.12.0"
      },
      "signingBy": {
        "identityType": <STRING>,
        "name": <STRING>,
        ...
      }
    },
    {
      "identity": "Foo",
      "kind": "remoteSourceControl",
      "location": "https://github.com/something/Foo.git",
      "state": {
        "revision": "90a9574276f0fd17f02f58979423c3fd4d73b59e",
        "version": "1.0.2",
      }
    }
  ],
  "version": 2
}