Skip to content

Latest commit

 

History

History
117 lines (84 loc) · 6.8 KB

File metadata and controls

117 lines (84 loc) · 6.8 KB

Trust-list Format, Signing, Updates, and Recovery

The trust list associates publisher identities with APK signing-certificate fingerprints. The JSON payload is trusted only after its exact bytes pass Ed25519 verification with the root public key compiled into VPL.

Format

VPL currently accepts only format_version: 1:

{
  "format_version": 1,
  "list_version": 4,
  "generated_at": "2026-07-26T00:00:00Z",
  "expires_at": "2027-07-26T00:00:00Z",
  "keys": [
    {
      "uuid": "11111111-1111-1111-1111-111111111111",
      "name": "Example Publisher",
      "type": "org",
      "confidence": 1,
      "description": "Example entry for the schema.",
      "web": "https://example.invalid",
      "hashes": [
        {
          "value": "sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA|sha1:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
          "state": "active",
          "description": "Current signing certificate."
        }
      ]
    }
  ]
}

Root fields:

Field Rule
format_version Required positive integer; currently exactly 1.
list_version Required positive integer used for active-state rollback checks. Must strictly increase on every release; see Publishing an update.
generated_at Required ISO-8601 instant.
expires_at Required ISO-8601 instant that must not precede generated_at.
keys Required array of publisher records.

Publisher fields:

Field Rule
uuid Canonical lowercase UUID, globally unique in the list.
name Required nonblank string, at most 256 characters.
type person or org.
confidence Integer 0, 1, or 2; see the trust-model document for runtime meaning.
description Nullable string, at most 4096 characters.
web Nullable HTTPS URL, at most 2048 characters.
hashes One to 128 certificate records.

Certificate fields:

Field Rule
value Canonical `sha256:<64 uppercase hex>
state active or banned.
description Nullable string, at most 4096 characters.
banned_at Nullable ISO-8601 instant; forbidden for an active certificate.

The parser accepts at most 2048 publishers, requires every certificate fingerprint to be globally unique, rejects duplicate or unknown JSON fields, rejects trailing data, and decodes UTF-8 strictly. The configured payload limit cannot exceed 4 MiB.

Signing

Sign the exact JSON byte sequence. Reformatting, changing line endings, or adding whitespace after signing changes the payload and invalidates the signature.

The detached Ed25519 signature may be published as raw 64-byte data or as base64 text accepted by VPL's signature decoder. The corresponding root public key is compiled into the library. The root private key must never be committed, bundled in an application, exposed in launcher settings, or printed in logs.

Publishing an update

  1. Modify the trust-list data and increase list_version for the new release. This is mandatory, not a convention: re-publishing any changed content under a list_version a client already holds is permanently uninstallable on that client, so a ban added that way never reaches anyone. Bump it even to extend expires_at alone.
  2. Set valid generated_at and expires_at instants.
  3. Validate the payload against the strict schema and size bounds.
  4. Sign the final, unchanged JSON bytes.
  5. Publish the identical JSON and detached signature at every configured mirror.
  6. Verify that a clean VPL installation can download and install the published pair.

Mirror behavior

VerifiedPluginLoadConfig builds a JSON URL and signature URL from each HTTPS prefix and the two configured relative suffixes. VPL supports at most eight unique prefixes.

VPL downloads mirrors concurrently. A candidate wins only after both resources are obtained and the pair passes signature, schema, and rollback validation. A failed or invalid fast mirror does not prevent a slower valid mirror from succeeding. Remaining downloads are cancelled after acceptance.

Redirects are disabled. Connection and read timeouts both use networkTimeoutMillis. JSON is bounded by maxTrustListBytes; signature responses are bounded to 1024 bytes.

With one mirror, VPL may send the stored ETag and accept HTTP 304. With multiple mirrors, it downloads content from every candidate without an ETag so one origin's 304 cannot hide newer content from another origin. Every mirror should serve the same release pair.

Rollback boundary

Before installation, VPL compares the candidate list_version with the currently selected valid list. A lower version is rejected. An equal version is also rejected once a valid current snapshot exists, so that a lagging mirror answering first cannot end the mirror race and discard a newer list served by a slower mirror. This protects normal operation while a current, previous, or bundled baseline remains available.

Because an equal version is rejected, re-issuing changed content without incrementing list_version cannot be installed by any client that already holds that version. VPL reports this case as REJECTED_VERSION_REUSE rather than NOT_MODIFIED, and logs a warning, so the publishing fault is visible instead of being mistaken for a steady state.

The version is not a tamper-resistant device counter. If application data is completely cleared, VPL reinitializes from the list bundled in the installed library. It does not promise to remember the highest version ever seen across data deletion or full-data rollback. Shipping a newer bundled list in a host update raises the clean-install baseline.

Atomic installation and recovery

Downloaded JSON and signature bytes are written to temporary files and read back through the normal signature and schema path. Before replacing the active pair, VPL preserves the previous valid pair. Metadata records the installed list version, last successful update time, and an optional ETag.

At load time VPL tries:

  1. trusted-authors.json and its signature.
  2. The previous JSON/signature pair.
  3. The JSON/signature pair bundled in library assets.

An invalid pair is never used merely because it exists. If the previous pair is selected, VPL attempts to repair the current pair. If storage is unavailable, it can continue with the verified bundled pair in memory.

Expiry and update failure

When expires_at is before the current device time, VPL reports TRUST_LIST_EXPIRED. The selected signed list still participates in decisions; expiry is currently diagnostic rather than an automatic global block. A host should retry updates and make the stale state observable without weakening certificate rules.

Network, signature, schema, rollback, and storage failures are reported through TrustListRefreshResult. They do not replace or erase the last valid signed list.