Skip to content

Signing & trust

Every plugin package has a deterministic identity — a SHA-256 digest computed over its manifest and code. Sign that digest with ed25519 and the app can auto-trust the package on sight, no per-install prompt required. This page covers how the digest is built, how signing and verification work, and the exact steps to get a publisher key pinned.

package_digest(pkg_dir) computes a single SHA-256 hash over, in order:

  1. plugin.json — with its "sig" field blanked, keys sorted, and compact JSON separators (,/:, no whitespace).
  2. The manifest’s entry file — the plugin’s main .py script.
  3. Every allow-listed file under widgets/, screen/, and icons/ — extensions .py, .json, .png, .txt only, sorted by relative path.

Hidden files (dotfiles) and __pycache__ directories are excluded everywhere. Each file’s relative path (forward slashes) and raw bytes are fed into the hash, so renaming, moving, or editing any covered file changes the digest.

Because "sig" is blanked before hashing, the digest is stable across the sign step itself — signing doesn’t change what it signs.

plugin.json’s "publisher" and "sig" fields carry an ed25519 signature of the digest’s hex string. signing.verify_package() (and the app’s _verify_plugin_signature) return one of three values:

Return value Meaning
None Unsigned — "sig" is empty. Falls through to the hash/marketplace trust flow: the digest is checked against the local trust cache and the Mixlar marketplace API, and an unknown package prompts the user before any plugin code runs.
True Signed, and the signature verifies against a pinned publisher key. Auto-trusted.
False Signed but invalid — an unknown "publisher" id, malformed base64, or a tampered/mismatched package. Never auto-trusted.

Only publishers pinned in both the app’s _PUBLISHER_KEYS and the SDK’s mixlar.signing.PUBLISHER_KEYS are auto-trusted. An unpinned key always verifies False — even if the signature is cryptographically correct against that key. This is intentional: possessing a keypair proves nothing about identity until a maintainer has pinned the public half on both sides.

PUBLISHER_KEYS = {
"mixlar-official-1": "AjxJeR5Ns4h1GdUYEzP3biW6vWoStOG1/ydMccDFWZU=",
}
Terminal window
mixlar-sdk keygen --key-id my-studio

This writes a base64 ed25519 private key to %APPDATA%\Mixlar\keys\my-studio.ed25519.key (or wherever --out points) and prints the matching public key to stdout. The private key never leaves your machine — only the public key gets shared for pinning.

The same thing programmatically:

from mixlar import signing
priv_b64, pub_b64 = signing.generate_keypair()
signing.save_private_key_file("my.key", priv_b64)

priv_b64 is the base64 of the 32 raw private bytes — the format load_private_key_file expects. pub_b64 is what eventually goes into PUBLISHER_KEYS.

Terminal window
mixlar-sdk sign --key-id my-studio
mixlar-sdk sign --key-id my-studio --key-file ./keys/my-studio.ed25519.key

Without --key-file, the private key resolves to the conventional path signing.default_key_file(key_id)%APPDATA%\Mixlar\keys\<key-id>.ed25519.key.

Under the hood this is signing.sign_package(pkg_dir, key_id, priv_b64), which:

  1. Writes "publisher" to key_id and blanks "sig" in plugin.json, then persists the manifest (both fields must already be in their final pre-digest state before hashing).
  2. Computes package_digest(pkg_dir).
  3. Signs the digest’s hex string with the ed25519 private key.
  4. Writes the base64 signature back into plugin.json’s "sig" field.
Terminal window
mixlar-sdk verify
[ok] VALID — signature verifies against a pinned publisher key

Otherwise you’ll see [warn] UNSIGNED or [error] INVALID, matching the tri-state table above. verify exits 0 for valid-or-unsigned and 1 for invalid — a broken signature is the only failure state the CLI treats as an error.

Terminal window
mixlar-sdk pack # unsigned .mixplugin next to the package
mixlar-sdk pack --sign my-studio # sign, then pack
mixlar-sdk pack --out dist/my_plugin.mixplugin

A .mixplugin is a plain zip of the package folder, with the folder itself as the top-level entry — extracting it next to a destination reproduces the package unchanged. No compression trickery, no embedded code execution. See Packing & the .mixplugin format for the full layout and mixlar.packaging API (pack, unpack, install, manifest_of, contents).

pack() validates the manifest first (raising on failure), signs in place when sign_with=(key_id, priv_b64) is given, then zips deterministically — sorted entry order, fixed 1980-01-01 timestamps — so identical inputs always produce a byte-identical .mixplugin.

Getting a third-party publisher key pinned

Section titled “Getting a third-party publisher key pinned”

Until a self-serve key-issuance flow exists (tracked on the roadmap), pinning a key is a manual, two-sided edit performed by a Mixlar Labs maintainer:

  1. Run mixlar-sdk keygen --key-id <your-id> and note the printed public key (base64, 32 raw bytes).
  2. Send <your-id> and the public key to a Mixlar Labs maintainer.
  3. The maintainer adds one entry in two places, so the app and SDK-based tooling agree on what’s trusted:
    • mixlar.signing.PUBLISHER_KEYS in src/mixlar/signing.py — the SDK’s copy, used by mixlar-sdk verify and any headless tooling.
    • the app’s _PUBLISHER_KEYS dict in mixlar_mini.py — what the running app itself trusts.
  4. A new SDK/app release ships with the entry (or, for internal testing, both files are edited locally).
Function Returns Notes
package_digest(pkg_dir) hex str | None None if pkg_dir has no plugin.json
generate_keypair() (priv_b64, pub_b64) fresh ed25519 keypair
public_from_private(priv_b64) pub_b64 derive the public half from a private key
default_key_file(key_id) path str %APPDATA%\Mixlar\keys\<key-id>.ed25519.key
load_private_key_file(path) / save_private_key_file(path, priv_b64) read/write the base64 private key file
sign_package(pkg_dir, key_id, priv_b64) signature str (base64) writes publisher/sig into the manifest in place
verify_signature(manifest, digest, publisher_keys=None) True | False | None pure function, tri-state per the table above
verify_package(pkg_dir, publisher_keys=None) True | False | None convenience: reads the manifest, digests the folder, verifies