- Proposal: SE-0391
- Author: Yim Lee
- Review Manager: Tom Doron
- Status: Implemented (Swift 5.9)
- Implementation:
- apple/swift-package-manager#6101
- apple/swift-package-manager#6146
- apple/swift-package-manager#6159
- apple/swift-package-manager#6169
- apple/swift-package-manager#6188
- apple/swift-package-manager#6189
- apple/swift-package-manager#6215
- apple/swift-package-manager#6217
- apple/swift-package-manager#6220
- apple/swift-package-manager#6229
- apple/swift-package-manager#6237
- Review: (pitch), (review), (acceptance)
A package registry makes packages available to consumers. Starting with Swift 5.7, SwiftPM supports dependency resolution and package download using any registry that implements the service specification proposed alongside with SE-0292. SwiftPM does not yet provide any tooling for publishing packages, so package authors must manually prepare the contents (e.g., source archive) and interact with the registry on their own to publish a package release. This proposal aims to standardize package publishing such that SwiftPM can offer a complete and well-rounded experience for using package registries.
Publishing package release to a Swift package registry generally involves these steps:
- Gather package release metadata.
- Prepare package source archive by using the
swift package archive-source
subcommand. - Sign the metadata and archive (if needed).
- Authenticate (if required by the registry).
- Send the archive and metadata (and their signatures if any) by calling the "create a package release" API.
- Check registry server response to determine if publication has succeeded or failed (if the registry processes request synchronously), or is pending (if the registry processes request asynchronously).
SwiftPM can streamline the workflow by combining all of these steps into a single
publish
command.
We propose to introduce a new swift package-registry publish
subcommand to SwiftPM
as well as standardization on package release metadata and package signing to ensure a
consistent user experience for publishing packages.
Typically a package release has metadata associated with it, such as URL of the source code repository, license, etc. In general, metadata gets set when a package release is being published, but a registry service may allow modifications of the metadata afterwards.
The current registry service specification states that:
- A client (e.g., package author, publishing tool) may provide metadata for a package release by including it in the "create a package release" request. The registry server will store the metadata and include it in the "fetch information about a package release" response.
- If a client does not include metadata, the registry server may populate it unless the client specifies otherwise (i.e., by sending an empty JSON object
{}
in the "create a package release" request).
It does not, however, define any requirements or server-client API contract on the metadata contents. We would like to change that by proposing the following:
- Package release metadata will continue to be sent as a JSON object.
- Package release metadata must adhere to the schema.
- Package release metadata will continue to be included in the "create a package release" request as a multipart section named
metadata
in the request body. - Registry server may allow and/or populate additional metadata by expanding the schema, but it must not alter any of the predefined properties.
- Registry server may make any properties in the schema and additional metadata it defines required. Registry server may fail the "create a package release" request if any required metadata is missing.
- Client cannot change how registry server handles package release metadata. In other words, client will no longer be able to instruct registry server not to populate metadata by sending an empty JSON object
{}
. - Registry server will continue to include metadata in the "fetch information about a package release" response.
Package release metadata submitted to a registry must be a JSON object of type
PackageRelease
, the schema of which is defined below.
Expand to view JSON schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/apple/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md",
"title": "Package Release Metadata",
"description": "Metadata of a package release.",
"type": "object",
"properties": {
"author": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the author."
},
"email": {
"type": "string",
"description": "Email address of the author."
},
"description": {
"type": "string",
"description": "A description of the author."
},
"organization": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the organization."
},
"email": {
"type": "string",
"description": "Email address of the organization."
},
"description": {
"type": "string",
"description": "A description of the organization."
},
"url": {
"type": "string",
"description": "URL of the organization."
},
},
"required": ["name"]
},
"url": {
"type": "string",
"description": "URL of the author."
},
},
"required": ["name"]
},
"description": {
"type": "string",
"description": "A description of the package release."
},
"licenseURL": {
"type": "string",
"description": "URL of the package release's license document."
},
"readmeURL": {
"type": "string",
"description": "URL of the README specifically for the package release or broadly for the package."
},
"repositoryURLs": {
"type": "array",
"description": "Code repository URL(s) of the package release.",
"items": {
"type": "string",
"description": "Code repository URL."
}
}
}
}
Property | Type | Description | Required |
---|---|---|---|
author |
Author | Author of the package release. | |
description |
String | A description of the package release. | |
licenseURL |
String | URL of the package release's license document. | |
readmeURL |
String | URL of the README specifically for the package release or broadly for the package. | |
repositoryURLs |
Array | Code repository URL(s) of the package. It is recommended to include all URL variations (e.g., SSH, HTTPS) for the same repository. This can be an empty array if the package does not have source control representation. Setting this property is one way through which a registry can obtain repository URL to package identifier mappings for the "lookup package identifiers registered for a URL" API. A registry may choose other mechanism(s) for package authors to specify such mappings. |
Property | Type | Description | Required |
---|---|---|---|
name |
String | Name of the author. | ✓ |
email |
String | Email address of the author. | |
description |
String | A description of the author. | |
organization |
Organization | Organization that the author belongs to. | |
url |
String | URL of the author. |
Property | Type | Description | Required |
---|---|---|---|
name |
String | Name of the organization. | ✓ |
email |
String | Email address of the organization. | |
description |
String | A description of the organization. | |
url |
String | URL of the organization. |
A registry may require packages to be signed. In order for SwiftPM to be able to download and handle signed packages from a registry, we propose to standardize package signature format and establish server-client API contract on package signing.
Package signature format will be identified by the underlying standard/technology (e.g., Cryptographic Message Syntax (CMS), JSON Web Signature (JWS), etc.) and version number. In the initial release, all signatures will be in CMS.
Signature format ID | Description |
---|---|
cms-1.0.0 |
Version 1.0.0 of package signature in CMS |
Package signature format cms-1.0.0
uses CMS.
CMS Attribute | Details |
---|---|
Content type | Signed-Data |
Encapsulated data | The content being signed (EncapsulatedContentInfo.eContent ) is omitted since we are constructing an external signature. |
Message digest algorithm | SHA-256, computed on the package source archive. |
Signature algorithm | ECDSA P-256 |
Number of signatures | 1 |
Certificate | Certificate that contains the signing key. It is up to the registry to define the certificate policy (e.g., trusted root(s)). |
The signature, represented in CMS, will be included as part of the "create a package release" API request.
A registry receiving such signed package will:
- Check if the signature format (
cms-1.0.0
) is accepted. - Validate the signature is well-formed according to the signature format.
- Validate the certificate chain meets registry policy.
- Extract public key from the certificate and use it to verify the signature.
Then the registry will process the package and save it for client downloads if publishing is successful.
The registry must include signature information in the "fetch information about a package release" API
response to indicate the package is signed and the signature format (cms-1.0.0
).
After downloading a signed package SwiftPM will:
- Check if the signature format (
cms-1.0.0
) is supported. - Validate the signature is well-formed according to the signature format.
- Validate that the signed package complies with the locally-configured signing policy.
- Extract public key from the certificate and use it to verify the signature.
A registry that requires package signing should provide documentations on the signing requirements (e.g., any requirements for certificates used in signing).
A registry must also modify the "create package release" API to allow signature in the request, as well as the response for the "fetch package release metadata" and "download package source archive" API to include signature information.
Users will be able to configure how SwiftPM handles packages downloaded from a
registry. In the user-level registries.json
file, which by default is located at
~/.swiftpm/configuration/registries.json
, we will introduce a new security
key:
{
"security": {
"default": {
"signing": {
"onUnsigned": "prompt", // One of: "error", "prompt", "warn", "silentAllow"
"onUntrustedCertificate": "prompt", // One of: "error", "prompt", "warn", "silentAllow"
"trustedRootCertificatesPath": "~/.swiftpm/security/trusted-root-certs/",
"includeDefaultTrustedRootCertificates": true,
"validationChecks": {
"certificateExpiration": "disabled", // One of: "enabled", "disabled"
"certificateRevocation": "disabled" // One of: "strict", "allowSoftFail", "disabled"
}
}
},
"registryOverrides": {
// The example shows all configuration overridable at registry level
"packages.example.com": {
"signing": {
"onUnsigned": "warn",
"onUntrustedCertificate": "warn",
"trustedRootCertificatesPath": <STRING>,
"includeDefaultTrustedRootCertificates": <BOOL>,
"validationChecks": {
"certificateExpiration": "enabled",
"certificateRevocation": "allowSoftFail"
}
}
}
},
"scopeOverrides": {
// The example shows all configuration overridable at scope level
"mona": {
"signing": {
"trustedRootCertificatesPath": <STRING>,
"includeDefaultTrustedRootCertificates": <BOOL>
}
}
},
"packageOverrides": {
// The example shows all configuration overridable at package level
"mona.LinkedList": {
"signing": {
"trustedRootCertificatesPath": <STRING>,
"includeDefaultTrustedRootCertificates": <BOOL>
}
}
}
},
...
}
Security configuration for a package is computed using values from the following (in descending precedence):
packageOverrides
(if any)scopeOverrides
(if any)registryOverrides
(if any)default
The default
JSON object contains all configurable security options
and their default value when there is no override.
-
signing.onUnsigned
: Indicates how SwiftPM will handle an unsigned package.Option Description error
SwiftPM will reject the package and fail the build. prompt
SwiftPM will prompt user to see if the unsigned package should be allowed. - If no, SwiftPM will reject the package and fail the build.
- If yes and the package has never been downloaded, its checksum will be stored for local TOFU. Otherwise, if the package has been downloaded before, its checksum must match the previous value or else SwiftPM will reject the package and fail the build.
warn
SwiftPM will not prompt user but will emit a warning before proceeding. silentAllow
SwiftPM will allow the unsigned package without prompting user or emitting warning. -
signing.onUntrustedCertificate
: Indicates how SwiftPM will handle a package signed with an untrusted certificate.Option Description error
SwiftPM will reject the package and fail the build. prompt
SwiftPM will prompt user to see if the package signed with an untrusted certificate should be allowed. - If no, SwiftPM will reject the package and fail the build.
- If yes, SwiftPM will proceed with the package as if it were an unsigned package.
warn
SwiftPM will not prompt user but will emit a warning before proceeding. silentAllow
SwiftPM will allow the package signed with an untrusted certificate without prompting user or emitting warning. -
signing.trustedRootCertificatesPath
: Absolute path to the directory containing custom trusted roots. SwiftPM will include these roots in its trust store, and certificates used for package signing must chain to roots found in this store. This configuration allows override at the package, scope, and registry levels. -
signing.includeDefaultTrustedRootCertificates
: Indicates if SwiftPM should include default trusted roots in its trust store. This configuration allows override at the package, scope, and registry levels. -
signing.validationChecks
: Validation check settings for the package signature.Validation Description certificateExpiration
enabled
: SwiftPM will check that the current timestamp when downloading falls within the signing certificate's validity period. If it doesn't, SwiftPM will reject the package and fail the build.disabled
: SwiftPM will not perform this check.
certificateRevocation
With the exception of disabled
, SwiftPM will check revocation status of the signing certificate. SwiftPM will only support revocation check done through OCSP in the first feature release.strict
: Revocation check must complete successfully and the certificate must be in good status. SwiftPM will reject the package and fail the build if the revocation status is revoked or unknown (including revocation check not supported or failed).allowSoftFail
: SwiftPM will reject the package and fail the build iff the certificate has been revoked. SwiftPM will allow the certificate's revocation status to be unknown (including revocation check not supported or failed).disabled
: SwiftPM will not perform this check.
A certificate is trusted if it is chained to any roots in SwiftPM's trust store, which is a combination of:
- SwiftPM's default trust store, if
signing.includeDefaultTrustedRootCertificates
istrue
. - Custom root(s) in the configured trusted roots directory at
signing.trustedRootCertificatesPath
.
Otherwise, a certificate is untrusted and handled according to the signing.onUntrustedCertificate
setting.
Both signing.includeDefaultTrustedRootCertificates
and signing.trustedRootCertificatesPath
support multiple levels of overrides. SwiftPM will choose the configuration value that has the
highest specificity.
For example, when evaluating the value of signing.includeDefaultTrustedRootCertificates
or signing.trustedRootCertificatesPath
for package mona.LinkedList
:
- SwiftPM will use the package override from
packageOverrides
(i.e.,packageOverrides["mona.LinkedList"]
), if any. - Otherwise, SwiftPM will use the scope override from
scopeOverrides
(i.e.,scopeOverrides["mona"]
), if any. - Next, depending on the registry the package is downloaded from, SwiftPM will look for and use the registry override in
registryOverrides
, if any. - Finally, if no override is found, SwiftPM will use the value from
default
.
When SwiftPM downloads a package release from registry via the "download source archive" API, it will:
- Search local fingerprints storage, which by default is located at
~/.swiftpm/security/fingerprints/
, to see if the package release has been downloaded before and its recorded checksum. The checksum of the downloaded source archive must match the previous value or else trust on first use (TOFU) check would fail. - Fetch package release metadata from the registry to get:
- Checksum for TOFU if the package release is downloaded for the first time.
- Signature information if the package release is signed.
- Retrieve security settings from the user-level
registries.json
. - Check if the package is allowed based on security settings.
- Validate the signature according to the signature format if package is signed.
- Some certificates allow SwiftPM to extract additional information that can drive additional security features. For packages signed with these certificates, SwiftPM will apply additional, publisher-level TOFU by extracting signing identity from the certificate and enforcing the same signing identity across all signed versions of a package.
The new package-registry publish
subcommand will create a package
source archive, sign it if needed, and publish it to a registry.
> swift package-registry publish --help
OVERVIEW: Publish a package release to registry
USAGE: package-registry publish <id> <version>
ARGUMENTS:
<package-id> The package identifier.
<package-version> The package release version being created.
OPTIONS:
--url The registry URL.
--scratch-directory The path of the directory where working file(s) will be written.
--metadata-path The path to the package metadata JSON file if it's not 'package-metadata.json' in the package directory.
--signing-identity The label of the signing identity to be retrieved from the system's secrets store if supported.
--private-key-path The path to the certificate's PKCS#8 private key (DER-encoded).
--cert-chain-paths Path(s) to the signing certificate (DER-encoded) and optionally the rest of the certificate chain. The signing certificate must be listed first.
--dry-run Dry run only; prepare the archive and sign it but do not publish to the registry.
id
: The package identifier in the<scope>.<name>
notation as defined in SE-0292. It is the package author's responsibility to register the package identifier with the registry beforehand.version
: The package release version in SemVer 2.0 notation.url
: The URL of the registry to publish to. SwiftPM will try to determine the registry URL by searching for a scope-to-registry mapping or use the[default]
URL inregistries.json
. The command will fail if this value is missing.scratch-directory
: The path of the working directory. SwiftPM will write to the package directory by default.
The following may be required depending on registry support and/or requirements:
metadata-path
: The path to the JSON file containing package release metadata. By default, SwiftPM will look for a file namedpackage-metadata.json
in the package directory if this is not specified. SwiftPM will include the content of the metadata file in the request body if present. If the package source archive is being signed, the metadata will be signed as well.signing-identity
: The label that identifies the signing identity to use for package signing in the system's secrets store if supported.private-key-path
: Required for package signing unlesssigning-identity
is specified, this is the path to the private key used for signing.cert-chain-paths
: Required for package signing unlesssigning-identity
is specified, this is the signing certificate chain.
A signing identity encompasses a private key and a certificate. On
systems where it is supported, SwiftPM can look for a signing identity
using the query string given via the --signing-identity
option. This
feature will be available on macOS through Keychain in the initial
release, so a certificate and its private key can be located by the
certificate label alone.
Otherwise, both --private-key-path
and --cert-chain-paths
must be
provided to locate the signing key and certificate chain.
SwiftPM will sign the package source archive and package release metadata if signing-identity
or both private-key-path
and cert-chain-paths
are set.
All signatures in the initial release will be in the cms-1.0.0
format.
Using these inputs, SwiftPM will:
- Generate source archive for the package release.
- Sign the source archive and metadata if the required parameters are provided.
- Make HTTP request to the "create a package release" API.
- Check server response for any errors.
Prerequisites:
- Run
swift package-registry login
to authenticate registry user if needed. - The user has the necessary permissions to call the "create a package release" API for the package identifier.
A registry must update this existing endpoint to handle package release metadata as described in a previous section of this document.
If the package being published is signed, the client must identify the signature format
in the X-Swift-Package-Signature-Format
HTTP request header so that the
server can process the signature accordingly.
Signatures of the source-archive and metadata are sent as part of the request body
(source-archive-signature
and metadata-signature
, respectively):
PUT /mona/LinkedList?version=1.1.1 HTTP/1.1
Host: packages.example.com
Accept: application/vnd.swift.registry.v1+json
Content-Type: multipart/form-data;boundary="boundary"
Content-Length: 336
Expect: 100-continue
X-Swift-Package-Signature-Format: cms-1.0.0
--boundary
Content-Disposition: form-data; name="source-archive"
Content-Type: application/zip
Content-Length: 32
Content-Transfer-Encoding: base64
gHUFBgAAAAAAAAAAAAAAAAAAAAAAAA==
--boundary
Content-Disposition: form-data; name="source-archive-signature"
Content-Type: application/octet-stream
Content-Length: 88
Content-Transfer-Encoding: base64
l1TdTeIuGdNsO1FQ0ptD64F5nSSOsQ5WzhM6/7KsHRuLHfTsggnyIWr0DxMcBj5F40zfplwntXAgS0ynlqvlFw==
--boundary
Content-Disposition: form-data; name="metadata"
Content-Type: application/json
Content-Transfer-Encoding: quoted-printable
Content-Length: 25
{ "repositoryURLs": [] }
--boundary
Content-Disposition: form-data; name="metadata-signature"
Content-Type: application/octet-stream
Content-Length: 88
Content-Transfer-Encoding: base64
M6TdTeIuGdNsO1FQ0ptD64F5nSSOsQ5WzhM6/7KsHRuLHfTsggnyIWr0DxMcBj5F40zfplwntXAgS0ynlqvlFw==
A registry may update this existing endpoint for the metadata changes described in this document.
If the package release is signed, the registry must include a signing
JSON
object in the response:
{
"id": "mona.LinkedList",
"version": "1.1.1",
"resources": [
{
"name": "source-archive",
"type": "application/zip",
"checksum": "a2ac54cf25fbc1ad0028f03f0aa4b96833b83bb05a14e510892bb27dea4dc812",
"signing": {
"signatureBase64Encoded": "l1TdTeIuGdNsO1FQ0ptD64F5nSSOsQ5WzhM6/7KsHRuLHfTsggnyIWr0DxMcBj5F40zfplwntXAgS0ynlqvlFw==",
"signatureFormat": "cms-1.0.0"
}
}
],
"metadata": { ... }
}
If a registry supports signing, it must update this existing endpoint
to include the X-Swift-Package-Signature-Format
and X-Swift-Package-Signature
headers in
the HTTP response for a signed package source archive.
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: public, immutable
Content-Type: application/zip
Content-Disposition: attachment; filename="LinkedList-1.1.1.zip"
Content-Length: 2048
Content-Version: 1
Digest: sha-256=oqxUzyX7wa0AKPA/CqS5aDO4O7BaFOUQiSuyfepNyBI=
Link: <https://mirror-japanwest.example.com/mona-LinkedList-1.1.1.zip>; rel=duplicate; geo=jp; pri=10; type="application/zip"
X-Swift-Package-Signature-Format: cms-1.0.0
X-Swift-Package-Signature: l1TdTeIuGdNsO1FQ0ptD64F5nSSOsQ5WzhM6/7KsHRuLHfTsggnyIWr0DxMcBj5F40zfplwntXAgS0ynlqvlFw==
This proposal introduces the framework for package signing, allowing package authors the ability to provide additional authenticity guarantees by signing their source archives before publishing them to the registry. Package users will be able to control the kind(s) of packages they trust by specifying a local validation policy. This can include a trust on first use approach, or by validating against a pre-configured set of trusted roots.
While this proposal introduces the package signature format, it does not validate that a package is published by a specific entity. Instead, it validates that a package is published by an entity who can obtain a signing certificate that meets the requirements defined by the registry, which could be anybody. As such, it does not provide any protection against malware, and it would be wrong to assumed that signed packages can be trusted unconditionally.
In this proposal, package signing is primarily intended to provide additional security controls for package registries. By requiring packages be signed, and by the registry limiting what keys or identities are allowed to publish packages, a registry can provide additional security in the event the package author's registry credentials are compromised.
Although this proposal introduces policy controls for package users, they are limited in scope, and do not yet allow SwiftPM to validate that multiple package versions are from the same entity--recording signing identities for TOFU provides some protection against a compromised registry, but it is not for all packages and more work needs to be done before it can be so. As such, SwiftPM continues to trust the registry to provide authentic packages and accurate information about the signature status of the package.
Revocation checking via OCSP implicitly discloses to the certificate authority and anyone on the network the packages that a user may be downloading. If this is a concern, revocation check can be disabled in SwiftPM configuration.
Current packages won't be affected by changes in this proposal.
A package manifest is a reference list of files that are present in the source archive. We considered an approach where SwiftPM would produce such manifest, sign the manifest instead of the source archive, then create a new archive containing the source archive, manifest, and signature file. This way the archive and its signature can be distributed by the registry as a single file.
However, given the potential complications with extracting files from the archive and verifying manifest contents, moreover there is no restriction that would require single-file download (i.e., SwiftPM can download the source archive and signature separately), we have decided to take the approach covered in previous sections of this proposal.
We considered using the key in a certificate as signing identity for local publisher-level TOFU (i.e., different versions of a package must have the same signing identity). However, since key can change easily (e.g., lost key, key rotation, etc.), all users of the package must reset data used for local TOFU each time or else TOFU check would fail, which can introduce significant overhead and confusion.
Private keys are encrypted typically. SwiftPM commands that have private key
as input, such as package sign
and package-registry publish
, should support
reading encrypted private key. This could mean modifying the command to prompt
user for the passphrase if needed, and adding a --private-key-passphrase
option to the command for non-interactive/automation use-cases.
Parts of the package release metadata can be populated by SwiftPM using information found in the package directory. The auto-generated metadata can serve as a default or starting point which package authors may optionally edit, and ensure every package release to have metadata.
SwiftPM may support alternative mechanisms to check revocation besides OCSP.
In the current proposal, signing identity is left for the package registry to define, implement, and enforce at publication time. However, this requires SwiftPM to rely on the package registry to correctly implement these checks, and a compromise of the registry, or SwiftPM's connection to the registry, would potentially allow for unauthorized packages to be published. Performing additional checks in SwiftPM can mitigate this risk, but requires defining a consistent identity that can be extracted and relied upon, and determining how those identities are provisioned and authorized.
A future Swift evolution proposal can provide specification of a certificate from which signing identity can be extracted, such that more certificates can be used for local publisher-level TOFU, which provides an extra layer of trust on top of checksum TOFU done at the package release level.
In the proposed implementation, the signing certificate associated with the package may expire, and this can prevent SwiftPM from validating the information and revocation status of the certificate. Using approaches such as Time Stamping Authority or having the registry itself perform a countersignature, information about when a package was first published can be provided, even after the signing certificate has expired. This can avoid the need for package authors to re-sign packages when the signing certificate expires.
SwiftPM's TOFU mitigation could be further improved by
including checksum and signing identity in Package.resolved
(or another similar file), which then gets included in the package content.
Including such security metadata would allow distributing information about
direct and transitive dependencies across the ecosystem much faster than a
local-only TOFU without requiring a centralized database/service to vend
this information.
{
"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
}