Files
v/docs/registry-protocol.md
Mahdi ramezaan zaade 182ebde118 vpm: refuse world-readable signing keys, never assert SBOM license conclusions (#30626)
* vpm: validate registry TLS, check returned identity, skip client setup for serve

Three review findings on the registry transport, each with a test.

Registry requests over HTTPS now validate certificates, whether or not
they carry a token; tokenless metadata was previously fetched with
validation off, so a MITM could replace it. Plain HTTP and non-registry
traffic are unchanged.

decode_registry_module now checks the body against the requested route: a
body naming another module, or another version when one was requested, is
refused instead of decoded. A cross-wired registry previously flowed into
resolution output.

v registry serve no longer requires a writable VModules home. Dispatch runs
before ensure_vmodules_dir_exist, and init_settings skips the log-cache
setup for the registry command, so a service account can serve a registry
from its working directory.

* vpm: refuse world-readable signing keys, never assert SBOM license conclusions

Two Codex review findings on the registry, each with a test.

A signing key file readable by group or others lets any local user forge
signatures, so it is no longer loaded. The check is Unix-only by design:
Windows has no equivalent mode check and accepts the file there, while a
missing file is refused everywhere. A refused key serves unsigned, exactly
like a missing one, which verification already fails closed on.

`licenseConcluded` is a derived result and nothing here derives one, so it
is now always NOASSERTION. The publisher's declaration stays in
`licenseDeclared`. Consumers previously read an unexamined claim as a
compliance result.
2026-10-18 13:03:15 +03:00

8.9 KiB

VPM Registry Protocol Design

Overview

The VPM registry protocol defines how registries communicate with VPM. It enables third-party registries and private registries. The reference implementation is not connected to v install; the client integration remains separate work.

Running the Reference Server

Run the server from the repository root:

./v registry serve --port 9090

The standalone tool also accepts vpm registry serve [--port <port>], with -p as a port alias. Ports use decimal digits and must be from 1 to 65535; the default is 9090. The server loads .vpm-registry/index.json and artifacts from the working directory and listens on all interfaces. Publishing and yanking remain in-process Registry methods.

Unknown routes return HTTP 404, including conditional requests for missing metadata. Successful GET metadata supports ETag revalidation with case-insensitive HTTP header names. Archive transports verify the exact bytes sent against recorded metadata. Paths retain percent-encoded spelling; the server removes trailing slashes before routing.

Design Goals

  • CDN-compatible: No query parameters, just GET on predictable paths (like Go's proxy protocol)
  • Content-addressed artifacts: Immutable once published, like Zig's hash-as-version or Go's immutable zips
  • Scalable: Per-file fetch, not full clone (like Cargo's sparse protocol)
  • Signed registry: Mirrors don't need to be trusted (like Hex's signed protobuf registry)
  • Checksum transparency log: Append-only Merkle tree for all published hashes, like Go's sum.golang.org
  • Private registries: Token-based authentication (like Cargo or Packagist)
  • SBOM/SPDX output: Supply chain security

API Endpoints

Core Endpoints (Go-style, CDN-compatible)

Endpoint Purpose
GET /<module>/@v/list List all known versions of a module
GET /<module>/@v/<version>.info JSON metadata for a specific version
GET /<module>/@v/<version>.mod The v.mod file for that version
GET /<module>/@v/<version>.zip The full module source as a zip file
GET /<module>/@latest Latest version info

Registry Configuration (Cargo sparse-style)

Endpoint Purpose
GET /config.json Registry URLs, the auth_required flag, and signing public key

Authentication Endpoints

There are none. No route in this repository accepts or issues credentials, so there is nothing to log in to and no token to obtain from one. What the client sends, and what the server does with it, is described under Authentication below.

Publishing

Endpoint Purpose
PUT /api/publish Publish a new module version
PUT /api/yank/<module>/<version> Yank a version
PUT /api/unyank/<module>/<version> Unyank a version

Search and Discovery

Endpoint Purpose
GET /api/search?q=<query> Search modules
GET /api/modules/<module> Module metadata
GET /api/modules/<module>/versions List versions

Metadata Format

Per-version entry (JSON):

{
  "name": "mymodule",
  "version": "1.2.3",
  "description": "A sample module",
  "license": "MIT",
  "dependencies": {
    "othermodule": "^1.0.0"
  },
  "checksum": "sha256:abc123...",
  "yanked": false,
  "published_at": "2024-01-01T00:00:00Z",
  "features": {
    "extra": ["othermodule/extra"]
  }
}

Content Addressing

  • Each version's source is stored as a zip file
  • Published module names and versions must each be one nonempty path component, without separators, drive prefixes, . or .. components, trailing dots/spaces, or NUL/line-break characters
  • The zip file's SHA-256 checksum is recorded in the metadata
  • Accepted raw, sha256:, SHA256:, and omitted checksums are stored as sha256:<verified digest>
  • The checksum is used for integrity verification on download
  • Artifact downloads require a recorded version and a matching, nonempty checksum; orphan files are never served
  • A transparency log records all published checksums

Registry Signing

  • The registry signs its metadata with a private key
  • config.json includes the registry's signing public key
  • Mirrors serve the signed metadata without needing to be trusted
  • Clients verify the signature before accepting metadata
  • Signing sorts module, dependency, and feature map keys; map insertion order does not affect the signed bytes. Array order remains significant, and the change log is excluded.

Yank/Retire Semantics

  • Yanked versions are not selected by new resolutions
  • Existing lockfiles that reference a yanked version continue to work
  • Yanking is reversible (unyank)
  • Retired versions are marked as deprecated with a reason

Caching Strategy

  • ETag / Last-Modified headers for conditional requests
  • CDN-friendly: all endpoints are GET with predictable paths
  • Immutable artifacts cached indefinitely
  • Metadata cached with ETag validation

SBOM/SPDX Output

  • v sbom command outputs SPDX JSON format
  • Includes all transitive dependencies
  • Includes license information
  • Includes checksums for all packages
  • JSON uses the SPDX 2.3 field names, with filesAnalyzed: false for registry metadata. Package IDs encode both module name and version, so each package version has a distinct identifier.
  • Field names and package identifiers follow the official SPDX JSON schema.

Change Feed

  • GET /api/changes?since=<timestamp> returns changes since timestamp
  • Enables incremental syncing for mirrors
  • Each change is a JSON object with module, version, and change type

Authentication

Authentication here is entirely a client-side concern. The VPM client attaches a bearer token to its own registry requests. The registry server in this repository reads no credential and authenticates nothing.

  • Authorization: Bearer <token> is set by the VPM client only.
  • Registry requests over HTTPS validate TLS certificates, whether or not they carry a token.
  • The token comes from VPM_TOKEN_<HOST>, where <HOST> is the registry's hostname with . and - folded to _ and uppercased, so https://vpm.example.com/ reads VPM_TOKEN_VPM_EXAMPLE_COM. VPM_TOKEN is the fallback when no scoped variable is set, and covers a machine with a single private registry. registry_token in cmd/tools/vpm/common.v:149 builds the name; vpm_http_request at common.v:98 attaches the header when it is non-empty, so a registry that needs none receives no such header at all.
  • There is no login endpoint, and nothing obtains or issues a token. No route in this repository accepts credentials. A token is provisioned in the environment by whoever configures it, which is why it is a VPM client variable and not part of the protocol.
  • The registry server authenticates nothing. auth_required is a field of RegistryConfig (registry.v:26) that is hardcoded to false (registry.v:478) and that no code reads, so no endpoint can require a credential. It is present in /config.json so that a client can parse the document, not because anything checks it.
  • A registry answering 401 is reported by require_registry_token (common.v:166) as an error naming the variable to set, rather than as an opaque transport failure. This repository's registry answers only 200, 304 and 404, so that path is reached against a third-party registry.
  • Authenticated HTTP redirects keep the registry's scheme, host and effective port (vpm_registry_redirect, common.v:133), so http:// and https:// are different origins, as are a default port and an explicit one. A request that would cross an origin fails rather than leaking the token to the new target. Unauthenticated requests are unaffected: the check is skipped when the request carries no Authorization header.

Migration Path

  • Existing VPM registry continues to work
  • New protocol is opt-in via registry configuration
  • VPM falls back to direct VCS for modules not in any registry
  • Gradual migration: registry serves metadata, VCS serves source

Research Summary

Pattern Go Cargo Zig npm Hex Packagist
SemVer Yes Yes No (hash) Yes Yes Yes
Content-addressed zip .crate hash tarball tarball No (VCS)
Lockfile go.sum Cargo.lock build.zig.zon package-lock.json mix.lock composer.lock
Checksum DB sum.golang.org Index cksum N/A dist.integrity Signed registry N/A
Yank/Retire No Yes N/A Yes (revoke) Yes (retire) No
CDN-compatible Yes Yes (sparse) N/A Yes Yes Yes
Signed registry No No No No Yes No
Auth for reads No Optional No No Optional No
Auth for writes N/A Token N/A Token OAuth2/Key Token
Search No Yes N/A Yes Yes Yes
Change feed No No No No No Yes