LPM-cli

Swift Package Registry (SE-0292)

How LPM implements the SE-0292 Swift Package Registry spec — identity mapping, signing, certificates.

LPM implements the SE-0292 Swift Package Registry API on its registry side. Once you've run lpm swift-registry once, Swift Package Manager (SPM) on your machine can resolve LPM-hosted Swift packages natively — no manual fork-as-git-dep, no shim.

This page is the conceptual reference. For the user-facing setup walkthrough, see Using LPM with Swift.

What SE-0292 specifies

SE-0292 defines a REST API for Swift package registries that SPM can talk to in lieu of a git URL. It covers:

  • Listing versions for a package (GET /{scope}/{name})
  • Fetching a version's source archive (GET /{scope}/{name}/{version}.zip)
  • Fetching a version's Package.swift manifest (GET /{scope}/{name}/{version}/Package.swift)
  • Login (POST /login)
  • Optional package signing via CMS

LPM implements all of the above plus signature verification metadata. SPM clients see a standard SE-0292 server.

Identity mapping

LPM packages are named @lpm.dev/owner.pkg-name. SE-0292 scopes can't contain ., so LPM maps:

LPM nameSE-0292 identity
@lpm.dev/owner.pkg-namelpmdev.owner_pkg-name
@lpm.dev/myorg.swift-utilslpmdev.myorg_swift-utils

Rules:

  • The LPM scope @lpm.dev/ becomes the SE-0292 scope lpmdev (no dots, no @).
  • The _ between owner and package name is the unambiguous separator. LPM forbids _ in both owner and package name (enforced at the storage layer), so the boundary is always clear even when either half contains hyphens. @lpm.dev/swift-server.async-http-client and @lpm.dev/swift.server-async-http-client map to two distinct identifiers (lpmdev.swift-server_async-http-client vs lpmdev.swift_server-async-http-client) — no ambiguity.

Identity translation is automatic — lpm install @lpm.dev/owner.swift-pkg writes the SE-0292 identity into Package.swift. You don't deal with the mapping by hand.

Setup: lpm swift-registry

lpm swift-registry              # idempotent
lpm swift-registry --force      # re-download the signing certificate (cert rotation)

Four steps under the hood. Every step is fail-loud — if any one of them returns a non-success exit, the command aborts instead of completing with broken signing:

  1. Set the registry for the lpmdev scope.
    swift package-registry set --scope lpmdev https://lpm.dev/api/swift-registry
  2. Log in. Uses the LPM bearer token resolved through the standard session manager (LPM_TOKEN → keychain → OIDC → none).
  3. Install the signing certificate. Downloads the CMS signing cert (DER) from https://lpm.dev/api/swift-registry/certificate and writes it to SPM's trust store at ~/.swiftpm/security/trusted-root-certs/lpm.der. The DER body is size-checked before being written; an empty / truncated / HTML-error response aborts instead of leaving a broken cert on disk.
  4. Configure the SPM signing trust policy. Writes ~/.swiftpm/configuration/registries.json with a default signing.onUnsigned = "warn" + signing.onUntrustedCertificate = "warn" policy and a scope-specific override pinning lpmdev to signing.onUntrustedCertificate = "silentAllow" — see Trust model for the rationale.

--force re-downloads the cert even when a valid one is on disk. A failed re-download with --force is fatal even if a stale cert is still on disk — silently keeping the stale cert would defeat the explicit rotation the user asked for.

When lpm install first encounters a Swift package in a project that hasn't been set up, it runs the same four-step path (in ensure_configured mode) before resolving the dep. Same fail-loud contract applies, so a one-shot lpm install @lpm.dev/owner.swift-pkg cannot finish "successfully" with signing broken.

Trust model

What the signature buys you, and what it doesn't — be precise about both:

Current posture: detached package integrity + HTTPS transport authenticity.

  • Detached package integrity. SPM verifies the CMS signature against the source archive bytes on every install. A tampered tarball fails the integrity check before any code runs.
  • Transport authenticity via HTTPS. The lpm.dev origin is the trust anchor for "this came from LPM" — the system CA chain authenticates the connection that carried both the signing cert and the tarballs themselves.

Not current posture: trusted signer identity verification.

LPM's signing cert is self-signed, non-CA, and code-signing-only. SPM's root trust store rejects it (root certs must have basicConstraints CA=true). The silentAllow scope override in step 4 tells SPM to accept the signature without a chain to a system trust root for the lpmdev scope. The cryptographic CMS check still runs; what's bypassed is the "is this signer in my trust chain?" gate.

The signer today is the LPM registry, not the package author — the signature attests "this came from LPM," not "this came from author X." Per-author attribution (Sigstore, transparency log, whatever shape it takes) is a deliberately separate, future track. This page will be updated when that lands; until then, treat any "signed by author X" mental model as not implemented.

Installing a Swift package

lpm install @lpm.dev/owner.swift-pkg
lpm install @lpm.dev/owner.swift-pkg@1.2.0

Behind the scenes:

  1. LPM resolves the version against the SE-0292 endpoint.
  2. Updates Package.swift — appends the dep into dependencies:, wires it into the requested target's targets: array.
  3. Triggers swift package resolve so SPM downloads the source archive, verifies the signature, and writes Package.resolved.

Use lpm install for Swift, not lpm add. lpm add is source-delivery (copies bytes into your project); lpm install is the registry-resolved-dep path. lpm add still works for Swift today (with --target) but it's the legacy path for that ecosystem.

Publishing a Swift package

lpm publish

Detected as a Swift package when lpm.config.json declares ecosystem: "swift" and the repo has a Package.swift at the root. The publish pipeline does the standard pack + secret scan + quality + upload, plus three Swift-specific steps:

  1. Generate the SE-0292 source archive. Wraps the pack output into a {pkgName}-{version}/ top-level directory and creates the .zip SPM expects. The wrapper is a hard SE-0292 requirement (SPM uses stripFirstLevel semantics on the archive).
  2. Compute and record the SHA-256 checksum. SPM verifies this on download. The hex is stored in the release metadata; the base64 form is sent in the Digest HTTP response header on download.
  3. Sign the package. Generates a CMS-1.0.0 detached signature (ECDSA P-256 / SHA-256) using the LPM signing key. The signature is base64-encoded into release metadata and surfaced in three places per release:
    • The release metadata JSON (base64 CMS string)
    • The Digest HTTP response header on download
    • A comment block in the Package.swift manifest response

Verifying signatures

SPM verifies the CMS detached signature on every package archive at install time. With lpm swift-registry configured (or --force after a documented cert rotation), the verifier path runs cleanly without prompting.

Two failure shapes a user can run into:

  • onUnsigned: warn — a Swift package without a signature surfaces as a warning, not a hard error. LPM-published packages are always signed, so this typically only appears for non-LPM packages SPM resolves alongside.
  • Cert missing or out of date — SPM emits a "signer is not trusted" error. Re-run lpm swift-registry (or lpm swift-registry --force if you suspect a stale cert) to refresh both the cert file and the registries.json scope override.

If the cert rotates server-side, re-run lpm swift-registry --force. A failed --force re-download is fatal — lpm will not silently keep using the stale cert.

Wire format details

For the curious — the things SPM expects from the server side:

EndpointReturns
GET /{scope}/{name}List of versions (JSON)
GET /{scope}/{name}/{version}Release metadata (JSON) — signature, checksum, dependencies
GET /{scope}/{name}/{version}.zipSource archive with {name}-{version}/ top level, Digest: SHA-256={base64} header
GET /{scope}/{name}/{version}/Package.swiftManifest with signature in a comment block
POST /loginAuth flow (handled by lpm swift-registry automatically)
GET /certificatePublic cert in DER format

Implementation lives in the LPM origin server and registry worker. The Rust CLI is a client of this API; it doesn't implement the server side itself.

Limitations

  • Today lpm swift-registry configures the lpmdev scope only. Custom-scope publishing (e.g., acme for a private LPM tenant) isn't surfaced in the CLI yet.
  • Cert rotation is manual — no automatic refresh on schedule. The signing cert is long-lived; rotation events will be communicated via release notes.
  • The SE-0292 --allow-insecure-http flag is honored only for http:// registry URLs (i.e., local dev against http://localhost). The hosted lpm.dev endpoint is HTTPS-only.

See also