Plugin Signing Specification
Plugin Signing Specification
Section titled “Plugin Signing Specification”nSelf uses Ed25519 signatures to guarantee the integrity of every plugin distributed through ping.nself.org. This document specifies the exact canonical object that is signed and verified so that independent implementations (the TypeScript ping_api service and the Go CLI) produce and accept the same signature bytes.
Canonical Plugin Object
Section titled “Canonical Plugin Object”Before signing or verifying, both sides construct the same canonical JSON string from the plugin’s manifest fields.
Field set
Section titled “Field set”Exactly 6 fields are included, in this alphabetical order:
| Field | JSON type | Description |
|---|---|---|
bundle | string | The bundle this plugin belongs to (e.g. nclaw, nchat) |
name | string | Plugin identifier as registered in the plugin registry |
sha256 | string | Lowercase hex-encoded SHA-256 digest of the plugin tarball |
size | number (integer) | Byte size of the plugin tarball |
visibility | string | "public" or "private" |
version | string | Semver string (e.g. 1.2.3) |
Serialization rules
Section titled “Serialization rules”- Keys in strict alphabetical order:
bundle,name,sha256,size,visibility,version - No whitespace — compact JSON serialization (
JSON.stringifydefault) - UTF-8 encoding
- No trailing newline
- All string values are included verbatim — no normalization is applied
Why sorted keys?
Section titled “Why sorted keys?”JSON object key order is not guaranteed by the spec, but both Go (encoding/json with alphabetically-declared struct fields) and JavaScript (JSON.stringify with a replacer array) can produce stable, deterministic output. Alphabetical order is the most auditable choice and matches the convention used in other certificate-style formats.
Worked Example
Section titled “Worked Example”Given this plugin manifest:
bundle: nclawname: clawsha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855size: 102400visibility: publicversion: 1.2.3The canonical object is:
{"bundle":"nclaw","name":"claw","sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","size":102400,"visibility":"public","version":"1.2.3"}This exact byte string is passed to Ed25519 sign/verify — no hashing is applied on top of it. Ed25519 internally hashes the message before signing.
Signing (ping_api — TypeScript)
Section titled “Signing (ping_api — TypeScript)”import { canonicalPluginObject, type PluginCanonicalFields } from './plugin-sign';
const fields: PluginCanonicalFields = { bundle: 'nclaw', name: 'claw', sha256: 'e3b0c44...', size: 102400, visibility: 'public', version: '1.2.3',};
const canonical = canonicalPluginObject(fields);// canonical === '{"bundle":"nclaw","name":"claw",...}'// Pass canonical to ed25519.sign(privateKey, Buffer.from(canonical, 'utf8'))Implementation: web/backend/services/ping_api/src/plugin-sign.ts
Verification (CLI — Go)
Section titled “Verification (CLI — Go)”import "github.com/nself-org/nself/internal/plugin"
err := plugin.VerifyCanonicalSignature( bundle, name, sha256, visibility, version, size, authorPublicKeyHex, signatureHex,)if err != nil { // plugin rejected — signature invalid or fields tampered}Implementation: cli/internal/plugin/verify.go
Algorithm
Section titled “Algorithm”- Signing algorithm: Ed25519 (RFC 8032)
- Key format: Raw 32-byte public key / 64-byte private key (seed || public key), hex-encoded
- Message: The canonical UTF-8 JSON string (no additional hashing — Ed25519 is its own digest)
- Signature format: 64 bytes, hex-encoded
Test Vectors
Section titled “Test Vectors”The shared golden fixture at cli/internal/plugin/testdata/canonical_golden.json contains the expected canonical string for the reference inputs in the worked example above. Both the Go test suite (verify_test.go) and the TypeScript test suite (tests/plugin-sign.test.ts) read this file and assert byte-identical output.
To run the cross-language compatibility checks:
# Gocd cli && go test ./internal/plugin/ -run TestCanonicalPluginObject_GoldenFile -v
# TypeScriptcd web/backend/services/ping_api && pnpm test tests/plugin-sign.test.tsSecurity Notes
Section titled “Security Notes”- The canonical object covers the tarball digest (
sha256) and size, so any modification to the tarball content invalidates the signature. - The
bundleandvisibilityfields are included so an attacker cannot re-sign a plugin as belonging to a different bundle or change its access tier without a new valid signature. - Signatures are generated once by the nSelf signing key at publish time and stored in the plugin registry. The CLI fetches and verifies the signature on every install.
- The signing private key lives only in
~/.claude/vault.envon the nSelf build infrastructure and is never committed to any repository.