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.
How the package digest is built
Section titled “How the package digest is built”package_digest(pkg_dir) computes a single SHA-256 hash over, in order:
plugin.json— with its"sig"field blanked, keys sorted, and compact JSON separators (,/:, no whitespace).- The manifest’s
entryfile — the plugin’s main.pyscript. - Every allow-listed file under
widgets/,screen/, andicons/— extensions.py,.json,.png,.txtonly, 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.
The three trust states
Section titled “The three trust states”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=",}Generating a key
Section titled “Generating a key”mixlar-sdk keygen --key-id my-studioThis 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.
Signing a package
Section titled “Signing a package”mixlar-sdk sign --key-id my-studiomixlar-sdk sign --key-id my-studio --key-file ./keys/my-studio.ed25519.keyWithout --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:
- Writes
"publisher"tokey_idand blanks"sig"inplugin.json, then persists the manifest (both fields must already be in their final pre-digest state before hashing). - Computes
package_digest(pkg_dir). - Signs the digest’s hex string with the ed25519 private key.
- Writes the base64 signature back into
plugin.json’s"sig"field.
Verifying
Section titled “Verifying”mixlar-sdk verify[ok] VALID — signature verifies against a pinned publisher keyOtherwise 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.
Packing into a .mixplugin
Section titled “Packing into a .mixplugin”mixlar-sdk pack # unsigned .mixplugin next to the packagemixlar-sdk pack --sign my-studio # sign, then packmixlar-sdk pack --out dist/my_plugin.mixpluginA .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:
- Run
mixlar-sdk keygen --key-id <your-id>and note the printed public key (base64, 32 raw bytes). - Send
<your-id>and the public key to a Mixlar Labs maintainer. - The maintainer adds one entry in two places, so the app and SDK-based tooling agree on what’s trusted:
mixlar.signing.PUBLISHER_KEYSinsrc/mixlar/signing.py— the SDK’s copy, used bymixlar-sdk verifyand any headless tooling.- the app’s
_PUBLISHER_KEYSdict inmixlar_mini.py— what the running app itself trusts.
- A new SDK/app release ships with the entry (or, for internal testing, both files are edited locally).
Reference: mixlar.signing API
Section titled “Reference: mixlar.signing API”| 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 |
Next steps
Section titled “Next steps”- Packing & the
.mixpluginformat — the zip layout signing feeds into. - Package layout — where
plugin.json,entry,widgets/,screen/, andicons/live. - CLI: pack, sign, publish — the
mixlar-sdkcommands used throughout this page. - Roadmap — process isolation and self-serve key issuance, both future work.