Swift Digest
SE-0391 | Swift Evolution

Package Registry Publish

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

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も併用されます(対応可能な証明書は限定的で、拡張はFuture Directionsに委ねられています)。

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

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

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

Future Directions

今回のスコープ外として、次のような拡張が想定されています(speculativeなもので、実現を約束するものではありません)。

  • 暗号化された秘密鍵の対応(--private-key-passphrase や対話プロンプト)
  • パッケージディレクトリの情報からメタデータを自動生成する機能
  • OCSP以外の失効チェック手段の追加
  • SwiftPM側で署名IDを検証できる証明書仕様の整備(publisher-level TOFUの適用範囲を広げる)
  • 署名時刻の保全(Time Stamping AuthorityやRegistryによるcountersignatureの活用)。証明書が期限切れになっても再署名なしで検証を継続できるようにするため
  • Package.resolved にチェックサムや署名IDを記録して、直接・推移的な依存の情報をエコシステム全体で共有する「transitive trust」