Skip to content

Latest commit

 

History

History
539 lines (418 loc) · 22.5 KB

File metadata and controls

539 lines (418 loc) · 22.5 KB

Signed App Distribution

This document describes Cryptad's local signed staged app bundle workflow for first-party AppHost apps.

Scope

This page covers the local signed bundle sidecars and Gradle tasks used to stage, sign, and verify first-party AppHost apps. PR-194 treats staging, signing, and verification as Phase 3 release gates; see phase-3-platform-primacy-closeout.md and cryptad-release-workflow-and-runbook.md.

Related but documented elsewhere:

Catalog release channels

Signed bundles remain channel-neutral artifacts. Release jobs choose the catalog channel when they write the signed cryptad-app-catalog.properties entry. Schema v3 catalog entries can declare channel=stable|beta|nightly|deprecated, support.status, deprecation.status, optional replacementAppId, optional security advisory references, and minimumCryptaVersion / maximumCryptaVersion. These fields are signed catalog metadata: they do not weaken bundle digest or bundle signature verification.

The production-safe default is stable. Beta and nightly catalog entries are preview channels that operators must explicitly select in Web Shell browsing or app-update policy before automation can process them. Deprecated entries should carry replacement/deprecation metadata and are not ordinary automatic update candidates.

Bundle Files

stageApp still produces an unsigned staged bundle by default. Signing adds two UTF-8 sidecars at the bundle root:

cryptad-app.properties
bin/<launcher>.sh
static/...
static/crypta-ui/...
cryptad-app.digests
cryptad-app.signature

The sidecars follow the PR-192 local distribution format:

  • cryptad-app.digests
    • digest.version=1
    • digest.algorithm=SHA-256
    • file.<N>.path=<normalized relative path>
    • file.<N>.sha256=<lowercase hex sha256>
  • cryptad-app.signature
    • signature.version=1
    • signature.algorithm=Ed25519
    • signature.key.id=<stable key id>
    • signature.payload=cryptad-app.digests
    • signature.value.base64=<base64 Ed25519 signature over the exact digest sidecar bytes>

The digest includes cryptad-app.properties and regular bundle files. It excludes reserved distribution sidecars such as cryptad-app.digests, cryptad-app.signature, cryptad-app.catalog, and cryptad-app.catalog.signature.

The packager rejects archive metadata that can appear in local working trees, including AppleDouble ._* files, __MACOSX/ directories, and .DS_Store. Remove those files before signing or packaging a bundle.

UI Manifest Fields

App bundles can declare browser UI ownership with app.ui.mode and app.ui.entry.

app.ui.mode=none|shell-panel|static
app.ui.entry=static/index.html

The mode is optional for compatibility. Missing app.ui.entry means none; an absolute local entry such as /app/node/#queue infers shell-panel; a relative entry such as static/index.html infers static. Static entries are normalized relative paths inside the signed bundle and are validated during structure checks. They must not point at reserved distribution sidecars, absolute paths, traversal segments, Windows drive prefixes, empty segments, colons, or control characters. Existing shell-panel entries remain valid.

The repo-owned Queue Manager, legacy Publisher, Site Publisher, Profile Publisher, Social Inbox RC, Feed Reader, and Trust Graph Local RC bundles use app.ui.mode=static and app.ui.entry=static/index.html, so installed copies open through isolated app origins when available. Queue Manager and legacy Publisher remain compatibility fallbacks for the current retirement map. Site Publisher is the content reference app for local publishing workflows, Profile Publisher is the identity-profile reference app for vault-backed profile-document publishing, Social Inbox RC is the threaded social inbox reference app for local threads, read state, subscriptions, channel/search filters, safe author links, and advisory Trust Graph annotations. Feed Reader is the bounded content-subscription reference app for durable USK feed follow behavior, feed reading, and generated feed publication, and Trust Graph Local RC is the local trust-service reference app for trust statements, anchors, preview scoring, and the trust.score app-service provider.

See app-owned-ui.md for the /apps/{appId}/ route contract, first-party bootstrap JSON, static asset security boundary, and API summary fields. See platform-sdk-js.md for the staged browser SDK used by first-party static UI bundles. Static bundles should also include the canonical design-system assets under static/crypta-ui/, load crypta-ui-tokens.css and crypta-ui.css before app-specific CSS, and pass crypta-app ui lint before signing or cataloging. See app-ui-design-system.md.

Sandbox Manifest Fields

App bundles can declare the requested AppHost sandbox mode:

sandbox.mode=none|restricted-process|wasm-preview
sandbox.required=false

Both fields are optional. Missing sandbox.mode defaults to none, and missing sandbox.required defaults to false. Unknown modes and malformed booleans fail manifest validation before a bundle is installed or launched.

The restricted-process mode maps to AppHost's restricted-process provider selection. On Linux hosts with bubblewrap available, AppHost can launch through the bubblewrap provider and report supportLevel=enforced. On unsupported hosts, optional restricted-process apps fall back to best-effort launch hygiene with sanitized environment, explicit working directory, and app-scoped mutable directories. sandbox.required=true now requires an enforced restricted-process provider; best-effort support is not enough. The wasm-preview mode is reserved for a future provider and is unsupported by the default runtime.

Quota Manifest Fields

App bundles can declare data and cache quota metadata:

quota.data.bytes=0
quota.cache.bytes=0

Both fields are optional non-negative byte counts. AppHost enforces a data or cache quota only when the value is positive. A missing field and an explicit 0 both mean unlimited or no explicit app quota. This preserves compatibility with the first-party Queue Manager, Publisher, and Site Publisher staged manifests, which currently declare quota.data.bytes=0 and quota.cache.bytes=0.

Quota enforcement is scoped to AppHost-managed app data and cache directories. It does not apply to the immutable installed bundle, catalog scratch space, daemon datastore files, or arbitrary host paths. AppHost measures regular files without following symlinks and reports path-free warnings when a scan is incomplete. If a positive quota is active for that area, incomplete measurement blocks launch and automatic restart because AppHost cannot enforce the quota from a partial byte count. Runtime status and app summaries include measured data/cache usage, effective limits, over-limit flags, process-log size/limit metadata, and warning text that is safe for operator display.

Process-log size is controlled by host policy rather than signed manifest metadata. AppHost keeps process.log bounded on a best-effort basis at lifecycle and status checkpoints while preserving the tail of the file for diagnostics. AppHost may retain a small additional redaction overlap beyond the displayed process-log limit so bounded log reads can still redact tokens and known AppHost paths when the display tail begins inside a sensitive value.

App-data Schema Manifest Fields

Schema-changing app updates can declare a signed app-data migration contract:

app.data.schema.current=2
app.data.schema.namespaces=ui-state
app.data.schema.namespace.ui-state.current=2

app.data.migrations=ui-state-v1-v2
app.data.migration.ui-state-v1-v2.namespace=ui-state
app.data.migration.ui-state-v1-v2.from=1
app.data.migration.ui-state-v1-v2.to=2
app.data.migration.ui-state-v1-v2.command=bin/migrate-feed-data.sh
app.data.migration.ui-state-v1-v2.rollbackCompatible=false
app.data.migration.ui-state-v1-v2.requiresStopped=true
app.data.migration.ui-state-v1-v2.description=Upgrade UI state to schema v2.

These fields are normalized and validated with the manifest before the bundle is signed or installed. Migration command values are relative bundle paths, not shell strings. Absolute paths, traversal segments, Windows drive prefixes, empty segments, control characters, and unsafe namespace or step identifiers are rejected. Bundle validation checks that the resolved command is a regular safe file inside the signed bundle, but it does not depend on host executable-bit behavior. When a migration is actually required, the migration runner enforces direct launchability and fails closed before bundle replacement if the local host cannot execute the entrypoint without a shell.

During update staging/apply the Platform API exposes only path-free migration summaries. It does not expose the command path, staged bundle path, raw command output, tokens, private insert URIs, or raw app-data values. See app-upgrade-data-migrations.md for the lifecycle, rollback snapshot, and PR-250 non-goals.

Runtime Manifest Fields

App bundles can declare a minimal restart policy:

app.restart.policy=never|on-failure
app.restart.maxAttempts=0
app.restart.backoff.ms=0

All fields are optional. The default policy is never, with no automatic restart attempts. When a bundle opts in to on-failure, AppHost restarts only after a non-zero process exit, only within the current daemon session, and only up to app.restart.maxAttempts. Explicit operator stop suppresses automatic restart. Each restart receives a fresh CRYPTAD_APP_TOKEN.

app.restart.maxAttempts and app.restart.backoff.ms must be non-negative integers. AppHost does not persist restart state across daemon restarts and does not run app-provided health checks.

See apphost-runtime-hardening.md for the process boundary, runtime status, process-log tailing, token redaction, and remaining sandbox limitations.

Developer CLI

crypta-app is the developer-facing CLI for standalone AppHost bundles. It is delivered by the :platform-devtools application plugin and can be installed locally with:

./gradlew :platform-devtools:installDist

Use it when an app should be scaffolded, signed, packed, or cataloged outside the first-party apps/* Gradle projects:

crypta-app init \
  --dir build/dev-apps/hello-queue \
  --app-id hello-queue \
  --name "Hello Queue" \
  --version 0.1.0 \
  --ui-mode static \
  --permission queue.read

crypta-app ui lint --bundle-dir build/dev-apps/hello-queue --strict
crypta-app validate --bundle-dir build/dev-apps/hello-queue --strict
crypta-app sign \
  --bundle-dir build/dev-apps/hello-queue \
  --key-id dev-local \
  --private-key-file dev/app-signing-private.pem
crypta-app pack \
  --bundle-dir build/dev-apps/hello-queue \
  --output dist/apps/hello-queue-0.1.0.zip \
  --overwrite
crypta-app verify \
  --bundle-dir build/dev-apps/hello-queue \
  --trusted-key-id dev-local \
  --trusted-public-key-file dev/app-signing-public.pem

crypta-app init creates a standalone staged bundle directory, not a new Gradle subproject. The static template copies or vendors the browser SDK as static/crypta-platform.js when that resource is available, and copies canonical design-system assets into static/crypta-ui/. See app-dev-cli.md for the full scaffold, lint, pack, and catalog flow.

The CLI does not replace the first-party Gradle workflow. Queue Manager, legacy Publisher, Site Publisher, Profile Publisher, Social Inbox RC, Feed Reader, and Trust Graph Local RC can keep using their stageApp, signApp, and verifyApp tasks.

Catalog Store Metadata

Signed bundle manifests remain the source for installed app id, version, launch settings, UI entry, sandbox mode, quota metadata, and declared permissions. Signed catalogs can add optional store metadata around those bundles so the Web Shell can show a richer install/update review surface.

Catalog entry descriptors accepted by crypta-app catalog create can include homepage, source, license, categories, minimumCryptaVersion, review.status, review.note, permissions.rationale.<permission>, screenshot.N, changelog.summary, changelog.uri, and api.* compatibility metadata. The generated catalog writes those fields under app.<id>.* and uses catalog.version=2. Descriptor/artifact pairs without store metadata or API compatibility metadata continue to generate minimal catalog.version=1 catalogs.

These fields are display and review metadata. The catalog signature still authenticates the exact catalog bytes, and the app bundle signature still authenticates the bundle payload. Review notes, permission rationales, screenshot URLs, changelog URLs, and compatibility hints do not grant trust or bypass AppHost verification.

Gradle Tasks

Per app:

  • :apps:queue-manager:stageApp
  • :apps:queue-manager:signApp
  • :apps:queue-manager:verifyApp
  • :apps:publisher:stageApp
  • :apps:publisher:signApp
  • :apps:publisher:verifyApp
  • :apps:site-publisher:stageApp
  • :apps:site-publisher:signApp
  • :apps:site-publisher:verifyApp
  • :apps:profile-publisher:stageApp
  • :apps:profile-publisher:signApp
  • :apps:profile-publisher:verifyApp
  • :apps:social-inbox:stageApp
  • :apps:social-inbox:signApp
  • :apps:social-inbox:verifyApp
  • :apps:feed-reader:stageApp
  • :apps:feed-reader:signApp
  • :apps:feed-reader:verifyApp
  • :apps:trust-graph:stageApp
  • :apps:trust-graph:signApp
  • :apps:trust-graph:verifyApp

Root convenience tasks:

  • stageFirstPartyApps
  • signFirstPartyApps
  • verifyFirstPartyApps

stageApp remains unsigned on purpose. Use signApp when you need a signed local bundle, and verifyApp to re-digest and verify a signed staged bundle with the configured public key.

Signing Inputs

Use Gradle properties or environment variables. The same inputs work for both first-party app projects and the root convenience tasks.

Purpose Gradle property Environment variable Notes
Stable signing key id cryptadAppSigningKeyId CRYPTAD_APP_SIGNING_KEY_ID Required for signApp and verifyApp.
Private key bytes cryptadAppSigningPrivateKeyBase64 CRYPTAD_APP_SIGNING_PRIVATE_KEY_BASE64 Required for signApp. Base64-encoded PKCS#8 Ed25519 private key bytes.
Private key file cryptadAppSigningPrivateKeyFile CRYPTAD_APP_SIGNING_PRIVATE_KEY_FILE Alternative to *PrivateKeyBase64. File may contain PEM, Base64 text, or raw DER bytes.
Public key bytes cryptadAppSigningPublicKeyBase64 CRYPTAD_APP_SIGNING_PUBLIC_KEY_BASE64 Required for verifyApp. Base64-encoded X.509 Ed25519 public key bytes.
Public key file cryptadAppSigningPublicKeyFile CRYPTAD_APP_SIGNING_PUBLIC_KEY_FILE Alternative to *PublicKeyBase64. File may contain PEM, Base64 text, or raw DER bytes.

For private keys, prefer CRYPTAD_APP_SIGNING_PRIVATE_KEY_BASE64 or cryptadAppSigningPrivateKeyFile. Avoid putting base64 private key material directly on the shell command line with -PcryptadAppSigningPrivateKeyBase64=....

Runtime Trust Inputs

Production-facing AppHost installs and updates reject unsigned bundles by default. To install a signed staged bundle through the live node, start the node with either direct trusted-key inputs or a trusted-keys properties file.

Purpose System property Environment variable Notes
Trusted key id cryptad.apphost.trustedKeyId CRYPTAD_APPHOST_TRUSTED_KEY_ID Pairs with a direct trusted public key.
Trusted public key bytes cryptad.apphost.trustedPublicKeyBase64 CRYPTAD_APPHOST_TRUSTED_PUBLIC_KEY_BASE64 Base64-encoded X.509 Ed25519 public key bytes.
Trusted public key file cryptad.apphost.trustedPublicKeyFile CRYPTAD_APPHOST_TRUSTED_PUBLIC_KEY_FILE File may contain PEM, Base64 text, or raw DER bytes.
Trusted keys file cryptad.apphost.trustedKeysFile CRYPTAD_APPHOST_TRUSTED_KEYS_FILE Properties file with trusted.keys.version=1 plus key.<n>.id, key.<n>.algorithm, and key.<n>.public.key.base64.
Development-only unsigned installs cryptad.apphost.allowUnsigned=true CRYPTAD_APPHOST_ALLOW_UNSIGNED=true Explicit escape hatch for local development and tests. Do not enable in production.

Example trusted-keys file:

trusted.keys.version=1
key.0.id=dev-local
key.0.algorithm=Ed25519
key.0.public.key.base64=<base64-x509-public-key>

Local Workflow

Stage an unsigned bundle:

./gradlew :apps:queue-manager:stageApp

Stage the Site Publisher reference app:

./gradlew :apps:site-publisher:stageApp

Stage the Profile Publisher reference app:

./gradlew :apps:profile-publisher:stageApp

Stage the Feed Reader reference app:

./gradlew :apps:feed-reader:stageApp

Stage the Social Inbox RC reference app:

./gradlew :apps:social-inbox:stageApp

Stage the Trust Graph Local RC reference app:

./gradlew :apps:trust-graph:stageApp

Sign it with a local development key pair:

./gradlew :apps:queue-manager:signApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPrivateKeyFile=dev/app-signing-private.pem

Sign Site Publisher with the same local development key pair:

./gradlew :apps:site-publisher:signApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPrivateKeyFile=dev/app-signing-private.pem

Sign Profile Publisher with the same local development key pair:

./gradlew :apps:profile-publisher:signApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPrivateKeyFile=dev/app-signing-private.pem

Sign Feed Reader with the same local development key pair:

./gradlew :apps:feed-reader:signApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPrivateKeyFile=dev/app-signing-private.pem

Sign Social Inbox RC with the same local development key pair:

./gradlew :apps:social-inbox:signApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPrivateKeyFile=dev/app-signing-private.pem

Sign Trust Graph Local RC with the same local development key pair:

./gradlew :apps:trust-graph:signApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPrivateKeyFile=dev/app-signing-private.pem

Verify it with the matching public key:

./gradlew :apps:queue-manager:verifyApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPublicKeyFile=dev/app-signing-public.pem

Verify Site Publisher with the matching public key:

./gradlew :apps:site-publisher:verifyApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPublicKeyFile=dev/app-signing-public.pem

Verify Profile Publisher with the matching public key:

./gradlew :apps:profile-publisher:verifyApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPublicKeyFile=dev/app-signing-public.pem

Verify Feed Reader with the matching public key:

./gradlew :apps:feed-reader:verifyApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPublicKeyFile=dev/app-signing-public.pem

Verify Social Inbox RC with the matching public key:

./gradlew :apps:social-inbox:verifyApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPublicKeyFile=dev/app-signing-public.pem

Verify Trust Graph Local RC with the matching public key:

./gradlew :apps:trust-graph:verifyApp \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPublicKeyFile=dev/app-signing-public.pem

Stage, sign, and verify all first-party apps:

The root first-party tasks include Queue Manager, Publisher, Site Publisher, Profile Publisher, Social Inbox RC, Feed Reader, and Trust Graph Local RC.

./gradlew \
  stageFirstPartyApps \
  signFirstPartyApps \
  verifyFirstPartyApps \
  -PcryptadAppSigningKeyId=dev-local \
  -PcryptadAppSigningPrivateKeyFile=dev/app-signing-private.pem \
  -PcryptadAppSigningPublicKeyFile=dev/app-signing-public.pem

To install the signed bundle through the running node, export the matching trusted public key before starting Cryptad:

export CRYPTAD_APPHOST_TRUSTED_KEY_ID=dev-local
export CRYPTAD_APPHOST_TRUSTED_PUBLIC_KEY_FILE=dev/app-signing-public.pem

If you deliberately want to test unsigned local bundles against a live node, opt in explicitly:

export CRYPTAD_APPHOST_ALLOW_UNSIGNED=true

Key Handling

Do not commit production private keys, keystores, exported PKCS#8 private keys, or long-lived test secrets to this repository.

Preferred local patterns:

  • Keep signing keys outside the repo and pass file paths with -P...File=....
  • Keep trusted public keys outside the repo and pass them through environment variables or a local trusted-keys file outside the checkout.
  • Use environment variables for short-lived automation when file-based secrets are not practical.
  • Label local development keys clearly as non-production.

Future Work

Public catalog governance, silent third-party auto-update policy, Crypta app-artifact fetching, remote screenshot proxying, browser-side bundle uploads, and sandbox controls beyond the current Linux bubblewrap provider remain later platform work. Signed bundles now carry the manifest metadata, API compatibility hints, sandbox/quota declarations, and staged browser assets needed by catalog review, app-owned static UI routes, and the manual app-update lifecycle.