Not released yet. PeltyCMS is under development and not released yet; these docs describe the upcoming release and may change.
PeltyCMS Developer docs
User docsDevelopers
v0.1.0

Developer docs/Guides

Signed addon releases

Addon signatures authenticate a release to an operator-configured Ed25519 key. A signature never grants permissions, makes an addon safe to execute, or prevents rollback to an older valid release. Version/upgrade policy and permission checks remain separate server responsibilities.

#Release workflow

Store private keys outside addon source/package directories and outside Git. Use a private directory with restrictive permissions. Key generation creates files exclusively (no overwrite), sets mode 0600 on Unix, and never prints private key material. On Windows files inherit the parent directory ACL: restrict that ACL to the signing account before generating keys. Private keys are binary PKCS#8, not PEM, and are not encrypted; protect them with OS access controls or offline storage.

Shell
pelty-cli addon keygen /secure/release.pk8 --key-id release-2026 --public-out /secure/trusted-keys.json
pelty-cli addon hash ./my-addon
pelty-cli addon validate ./my-addon
pelty-cli addon sign ./my-addon --key /secure/release.pk8 --key-id release-2026
pelty-cli addon pack ./my-addon -o my-addon.zip
pelty-cli addon verify my-addon.zip --trusted-keys /secure/trusted-keys.json --require-signed

sign operates on an addon directory after hashes are finalized. It creates addon.sig exclusively, refusing an existing signature; explicitly remove an old signature before re-signing. pack includes it, and extraction preserves it. hash excludes addon.sig from the manifest files map, but does not remove an old signature or silently bless changed content. Rehashing or editing addon.json (including whitespace) invalidates the old signature. Re-sign after any change. Keep public trust files outside the addon too; they are operator inputs, not package contents. validate/pack check structure and integrity, not trust.

#Operator trust and revocation

The public output from keygen is a JSON map suitable for --trusted-keys:

JSON
{
  "release-2026": {
    "public_key": "<standard padded base64 of the 32-byte Ed25519 public key>",
    "revoked": false
  }
}

Distribute public keys through an authenticated administrative channel. Never populate trust from uploaded packages, manifests, registry metadata, or a key included alongside a downloaded ZIP. Merge approved keys into the operator trust map during rotation. Set revoked: true to reject an old key explicitly; removing it also rejects it as unknown. Revocation affects future verifications; already installed/running addons require separate operator action.

Without --require-signed, unsigned packages remain backward compatible, and CLI output explicitly identifies them as unauthenticated. An existing signature is always checked: malformed, unknown-key, revoked-key, and invalid signatures fail even in permissive mode. An unsigned package is not distinguishable from a signed package whose signature was removed, so require signatures for production trust enforcement.

#Server enforcement

Set PELTY_ADDON_TRUSTED_KEYS to the path of the operator-owned JSON trust map and PELTY_ADDON_REQUIRE_SIGNATURES=true to refuse unsigned disk addons. The server checks the trust file during initialization, rejects malformed configuration, and reloads it for each upload, installation, enable, startup activation and cluster resynchronization. Trust maps are limited to 64 KiB and 256 keys. The built-in host implementations are trusted application code, not downloaded packages.

Docker Compose forwards both variables; mount the public-key JSON read-only and use its container path. Never put private signing keys in the server container. With enforcement enabled, first-party disk/bundled WASM packages also need trusted signatures. There is no filename-based or registry-based trust exemption. Without enforcement, legacy unsigned packages remain usable, but any signature that is present must verify against an explicitly trusted, non-revoked key.

Revocation does not interrupt an invocation already in progress and the server does not watch trust-file changes. Disable affected addons immediately and restart or resynchronize every instance after updating trust. Failed verification during resynchronization unloads the affected WASM engine instead of retaining a previously loaded copy. This does not undo database changes or provider actions already performed by an addon.

The API returns a generic localized signature error; server diagnostics identify the addon and verification category without exposing package contents or secrets. Existing admin upload/install/enable actions remain permission-protected and audited. Signatures do not approve capabilities or replace the security review and upgrade checks.

#Library integration contract

rust,ignore
use pelty_extensions::{addon_pkg, addon_signing};

// Parse once and verify BEFORE extraction, migration, upgrade, or activation.
let pkg = addon_pkg::inspect(&zip_bytes, addon_pkg::Limits::default())?;
let keys: addon_signing::TrustedKeys = serde_json::from_str(&operator_keys_json)?;
let signer: Option<String> = addon_signing::verify(&pkg, &keys, require_signed)?;
// None: unsigned and allowed; Some(id): authenticated against operator trust.
let installed_path = addon_pkg::extract(&pkg, &extensions_root, false)?;

Exact public API (module pelty_extensions::addon_signing):

Package.signature: Option<ReleaseSignature> contains parsed metadata, not a trust verdict. Package.files excludes both addon.json and addon.sig. inspect() rejects malformed signatures as PkgError::Signature(String). verify() distinguishes Required, UnknownKey(String), RevokedKey(String), InvalidPublicKey(String), InvalidSignature, MalformedSignature, and InvalidContent. Private-key operations also expose InvalidPrivateKey, InvalidKeyId, and Random errors. No API/config changes are implicit in this library: all installation and upgrade entry points must invoke verification.

#Signature format v1

addon.sig is a reserved root file (also supported under the archive's single common wrapper directory). It must not be declared in addon.json.files or used as a migration. It is a strict JSON object, capped at 4096 decompressed bytes:

JSON
{
  "version": 1,
  "algorithm": "Ed25519",
  "key_id": "release-2026",
  "signature": "<standard padded base64 of the 64-byte signature>"
}

Unknown fields (including embedded public keys), duplicate fields, unsupported versions/algorithms, invalid key IDs, and invalid base64/lengths are rejected. Key IDs are 1–128 ASCII letters, digits, hyphens, underscores, or periods.

Ed25519 signs this unambiguous deterministic binary payload, not ZIP metadata or re-serialized JSON:

1. Domain bytes PeltyCMS addon release\0v1\0 (the \0 denotes a zero byte). 2. Length-prefixed UTF-8 key ID. 3. Length-prefixed entire original byte sequence of addon.json. 4. File count as an unsigned 64-bit big-endian integer. 5. For every declared content file in ascending Rust BTreeMap<String, _> path order: length-prefixed UTF-8 relative path, then length-prefixed lowercase ASCII SHA-256 hex digest of the actual file bytes.

Every length prefix is an unsigned 64-bit big-endian byte count. Actual hashes must match the manifest's declarations (case-insensitive hex comparison), with no missing or undeclared files. Signing/verifying rechecks that the public Package fields have not diverged from its raw manifest. The detached signature is excluded to avoid a circular hash. Repacking/compression or changing a common ZIP wrapper directory does not change the authenticated content.