Distribution des plugins
Vous avez créé un plugin ; maintenant, il doit quitter votre machine sans demander aux personnes qui l’installent de vous faire aveuglément confiance. La distribution de ZeroClaw repose sur deux couches indépendantes : les signatures de manifeste Ed25519 (qui a publié ceci) et l’installation via le registre (comment il y arrive). Cette page couvre les deux, vérifiées par rapport à crates/zeroclaw-plugins/src/signature.rs, src/plugin_registry.rs, et le chemin d’installation dans host.rs.
Signature
Qu’est-ce qui est signé
La signature couvre les octets canoniques du manifeste. L’hôte analyse le TOML, supprime uniquement les entrées racine exactes signature et publisher_key, préserve le reste du document et supprime les lignes vides finales (canonical_manifest_bytes dans signature.rs). Cela rend la signature auto-intégrable : signez le manifeste sans ces champs racine, puis ajoutez-les ; la vérification les supprime avant le contrôle.
Conséquences à connaître :
- Renseignez le SHA-256 du composant dans le champ racine
wasm_sha256avant la signature. La signature lie ensuite ce condensat au manifeste. L’hôte le vérifie sur les octets exacts qu’il admet et compile ; le mode strict rejette les manifestes exécutables qui en sont dépourvus. Lesha256du registre ci-dessous protège indépendamment l’archive de distribution pendant le transfert. - Les champs imbriqués portant des noms tels que
config_schema.properties.signature, ainsi que les champs racine préfixés de manière similaire tels quesignature_algorithm, restent signés. Le reformatage ou la réorganisation du contenu conservé invalide la signature. Signez en dernier, une fois le manifeste finalisé. - Les marqueurs d’exposition de la configuration tels que
config_schema.properties.api_token.x-secretrelèvent donc de la politique couverte par la signature. Modifier une propriété d’outil ou de canal entre une exposition dans la configuration publique et un accèssecrets.getà portée restreinte nécessite une reconstruction et une nouvelle signature. - Les paquets signés par l’ancien canonicaliseur basé sur des préfixes ne nécessitent une nouvelle signature que s’ils reposaient sur l’un de ces cas limites ou sur une décoration TOML attachée à un champ supprimé. Les manifestes ordinaires conservent le même contenu signé.
Clés et processus
La signature utilise Ed25519 via les mêmes primitives ring que celles avec lesquelles l’hôte vérifie. La signature est en base64url (sans remplissage) et la clé publique est encodée en hexadécimal. La crate expose la chaîne d’outils complète (signature.rs) : generate_signing_key produit une paire de clés PKCS#8 et sa clé publique hexadécimale, sign_manifest produit la signature base64url sur les octets canoniques, et public_key_hex récupère la clé publique à partir d’une clé privée stockée. sign_manifest_document effectue les deux étapes et renvoie le manifeste signé avec les deux champs racines déjà en place. Il n’existe pas de commande zeroclaw plugin sign : les éditeurs appellent ces fonctions depuis leur pipeline de publication. La crate fournit une aide prête à l’emploi, crates/zeroclaw-plugins/examples/sign_manifest.rs. Exécutez-la depuis une copie de travail de ZeroClaw en pointant les chemins vers le répertoire de votre plugin, et conservez la clé privée en dehors des deux arborescences. Utilisez un nouveau répertoire de paquet distinct et un nouveau chemin d’archive pour chaque version ; l’aide le crée lorsqu’elle écrit le manifeste signé :
cargo run -p zeroclaw-plugins --example sign_manifest -- keygen ~/.keys/my-plugin.pk8
cargo run -p zeroclaw-plugins --example sign_manifest -- \
sign path/to/my-plugin/manifest.toml path/to/my-plugin-package/manifest.toml \
--key ~/.keys/my-plugin.pk8 --payload path/to/my-plugin/my-plugin.wasm
cp path/to/my-plugin/my-plugin.wasm path/to/my-plugin-package/my-plugin.wasm
(cd path/to/my-plugin-package && zip -r ../my-plugin.zip .)
Cette recette suppose wasm_path = "my-plugin.wasm". --payload calcule le hachage du composant mais ne le copie pas. Copiez ces octets exacts, ainsi que tous les autres actifs requis, vers les chemins que le manifeste déclare relativement au répertoire du paquet avant de créer l’archive zip. N’archivez que ce répertoire, contenant le manifest.toml signé ; n’incluez pas le manifeste source non signé et ne modifiez pas le manifeste signé après la signature.
keygen écrit une nouvelle clé privée en créant son répertoire si nécessaire (lisible uniquement par le propriétaire sous Unix), refuse d’écraser un fichier existant et affiche la clé publique hexadécimale. sign enregistre le SHA-256 du composant dans wasm_sha256 lorsque --payload est fourni, signe, vérifie la signature avec le vérificateur de l’hôte, écrit le manifeste signé et affiche la même clé publique. Pour un manifeste qui déclare wasm_path, il avertit lorsqu’aucun wasm_sha256 n’est enregistré, car le mode strict refuse un tel paquet. Un manifeste sans wasm_path, comme un bundle de skill, n’accepte pas --payload : l’hôte rejette une empreinte à cet endroit.
Sous Windows, le fichier de clé hérite de l’ACL de son répertoire. Conservez la clé privée dans un répertoire que seul l’éditeur peut lire.
Le manifeste signé contient alors deux champs racine supplémentaires : signature (la valeur base64url) et publisher_key (votre clé publique hexadécimale). Placez-les tous les deux avant le premier en-tête de table, y compris [config_schema] ; les ajouter après un en-tête de table en fait des membres de cette table selon les règles TOML, et l’hôte verra un manifeste non signé.
Si vous intégrez les champs à la main après la signature, placez-les directement après un autre champ racine. L’éditeur TOML de l’hôte rattache une ligne vide ou un commentaire à l’entrée ou à l’en-tête de table situé juste en dessous. Si les champs se retrouvent directement sous une ligne vide ou un commentaire déjà présent dans le manifeste que vous avez signé, la vérification supprime cette ligne avec les champs, les octets restants ne correspondent plus à ce que vous avez signé, et le mode strict rejette le paquet. Une ligne vide ou un commentaire que vous ajoutez directement au-dessus des champs est supprimé avec eux et est sans effet. Tout ce que vous ajoutez en dessous se rattache à l’entrée ou à l’en-tête de table suivant et modifie les octets signés ; n’ajoutez donc rien à cet endroit. sign_manifest_document et l’assistant placent les champs avant de signer, la question ne se pose donc pas.
name = "my-plugin"
version = "0.1.0"
wasm_path = "my-plugin.wasm"
wasm_sha256 = "<64-hex-character-component-sha256>"
signature = "<base64url-signature>"
publisher_key = "<hex-public-key>"
[config_schema]
type = "object"
properties = {}
additionalProperties = false
Les opérateurs qui souhaitent vous faire confiance ajoutent cette clé hexadécimale à leur liste plugins.security.trusted_publisher_keys :
zeroclaw config set plugins.security.signature_mode strict
zeroclaw config set plugins.security.trusted_publisher_keys ['["<your-key-hex>"]']
Comportement de la vérification
La vérification s’exécute à la fois lors de la découverte et de l’installation (enforce_signature_policy appelée depuis host.rs) ; la découverte ignore un plugin défaillant et journalise, l’installation renvoie l’erreur. La matrice de modes, du côté de l’opérateur :
| Mode | Non signé | Signé, clé non de confiance | Signé, signature invalide | Signé et de confiance |
|---|---|---|---|---|
disabled | loads | chargements, non vérifiés | chargements, non vérifiés | chargements, non vérifiés |
permissive | se charge avec un avertissement | se charge avec un avertissement | se charge avec un avertissement | chargements, vérifiés |
strict | rejeté | rejeté | rejeté | loads |
Notez ce que strict signifie pour vous en tant qu’éditeur : un opérateur en mode strict charge votre plugin uniquement si votre clé exacte figure dans son ensemble de confiance et que les octets du manifeste se vérifient. Toute modification du manifeste après signature, par vous ou par quiconque dans le chemin de distribution, brique l’installation. C’est le but.
Publication dans le registre
Le chemin d’installation est le répertoire local des plugins ; un registre n’est qu’un index JSON consulté au moment de la commande (zeroclaw plugin search / install). Ces deux commandes n’existent que dans les binaires où l’hôte de plugins est compilé (voir fonctionnalités de build) : les archives de publication pour les cibles desktop et serveur 64 bits l’incluent, et chaque publication exécute un plugin à partir de chacune d’elles avant d’être publiée. Les archives ARM 32 bits, l’archive Android expérimentale et les builds source par défaut ne l’incluent pas (le tableau par artefact répertorie chaque artefact). La récupération d’un index via l’une de ces deux commandes le met en cache localement. zeroclaw plugin list combine ensuite les paquets installés avec le cache, sans requête réseau ; il conserve séparément les versions installées et celles du registre, au lieu de deviner si une chaîne de version arbitraire est plus récente. L’index par défaut est le fichier registry.json du dépôt zeroclaw-labs/zeroclaw-plugins ; les registres privés ne sont qu’une URL plus loin (--registry <url> pour chaque commande, ou la variable d’environnement ZEROCLAW_PLUGIN_REGISTRY_URL, résolues dans cet ordre selon registry_url dans src/plugin_registry.rs).
Une entrée de registre (PluginRegistryEntry dans crates/zeroclaw-plugins/src/registry.rs) contient : name, version, description et author optionnels, capabilities, l’url de l’archive, et un condensat sha256 optionnel de l’archive zip. Le condensat n’est facultatif que pour zeroclaw plugin install, qui le vérifie lorsqu’il est présent : l’étape Plugins de zeroclaw quickstart refuse une entrée qui en est dépourvue, il faut donc publier un condensat pour chaque entrée.
Le contrat d’archive
zeroclaw plugin install <name> résout l’entrée, télécharge l’archive zip, vérifie l’empreinte lorsqu’elle est présente, procède à une extraction sécurisée, puis transmet le répertoire extrait au même chemin d’admission et d’installation que celui utilisé pour une installation locale : la signature du manifeste est vérifiée et celui-ci est validé, un nom déjà installé est refusé, le composant est lu une seule fois (confiné au paquet, sans liens symboliques, limité à 64 MiB), et la vérification de chargement effectuée au moment de l’installation instancie ces octets admis par rapport au monde WIT de cet hôte avant toute installation. Un composant qui ne se charge pas fait échouer l’installation avec le diagnostic d’instanciation ; --no-verify l’installe malgré tout, en ignorant la vérification, et le démon ignorera alors le plugin au démarrage. L’extraction est défensive par construction (src/plugin_registry.rs), et votre archive doit lui résister :
- Le zip doit contenir soit un
manifest.tomlau niveau racine, soit exactement un répertoire de plugin imbriqué en contenant un. Zéro manifeste ou plus d’un est une archive rejetée. - Les noms d’entrée comportant une traversée de chemin, des chemins absolus ou des préfixes de lecteur Windows sont rejetés.
- Le téléchargement est limité lors du streaming (50 MiB) afin qu’un serveur qui ne fournit pas
Content-Lengthne puisse pas forcer une mise en mémoire tampon sans limite ; l’extraction est plafonnée à la même limite pour qu’une bombe zip ne puisse pas s’étendre sans limite.
Résolution des versions : lorsque l’installateur reçoit un nom sans version, il choisit la dernière entrée correspondante dans l’index ; un name@version épinglé sélectionne exactement cette version. Ordonnez les noms répétés dans votre registre intentionnellement, du plus ancien au plus récent. zeroclaw plugin update sélectionne de la même manière et ne compare les chaînes de version que pour l’égalité : un opérateur dont la version installée correspond à celle que la règle sélectionne est à jour et ne télécharge rien. Publiez chaque paquet modifié sous une nouvelle version, car une entrée republiée sous la version déjà installée n’est jamais proposée lors d’une mise à jour.
La recherche n’est pas une frontière de confiance
zeroclaw plugin search est une découverte non authentifiée sur l’index ; elle n’installe, n’active ni n’exécute jamais rien. C’est à l’installation que la sécurité intervient : vérification du digest, extraction sécurisée, validation du manifeste et politique de signature de l’opérateur, identique à une installation depuis un chemin local. Publiez en conséquence : supposez que tout ce qui précède l’installation est un transport non fiable.
La liste de contrôle de l’éditeur
[!IMPORTANT] Les fichiers
.wasmet.cwasmcompilés sont des artefacts binaires, souvent de plusieurs mégaoctets chacun. Ne les versionnez pas dans un arbre source git sans Git LFS : chaque reconstruction validée en tant que blob simple gonfle l’historique du dépôt de manière permanente, et les outils degit diff/revue peinent avec eux. Traitez-les comme n’importe quelle autre sortie de compilation : ajouteztarget/et*.wasm/*.cwasmà.gitignore, et distribuez-les plutôt via un artefact de release ou une archive de registre de plugins. Si un artefact doit vraiment vivre dans l’arbre, suivez le motif avec LFS (git lfs track "*.wasm") avant le premier commit.
- Finalisez le manifest : nom, version, capacités et l’ensemble de permissions le plus restrictif que le code utilise.
- Construire le composant ; pour les bundles de compétences, valider le frontmatter sur chaque
SKILL.md(la découverte imposenameetdescription). - Signer : générer ou charger votre clé Ed25519, signer les octets du manifeste canonique, intégrer
signatureetpublisher_key. - Compressez le répertoire du paquet assemblé (un manifeste signé, son composant référencé et les ressources requises, sans astuces de chemin, de moins de 50 Mio).
- Calculer le SHA-256 du zip et publier l’entrée du registre avec le digest.
- Publiez l’hex de votre clé publique quelque part où les opérateurs peuvent le vérifier indépendamment du registre (votre dépôt, votre site). La clé, et non le registre, est ce en quoi les opérateurs du mode
strictont confiance. - À chaque publication : incrémenter
version, re-signer (la ligne de version est dans les octets canoniques), re-digérer, ajouter la nouvelle entrée après l’ancienne.