# Signing

Wirken uses Ed25519 across three independent signing surfaces: per-instance audit chain-head signing, per-skill bundle signing, and offline release signing of the binary distribution. Each surface has its own keypair, file location, and verification call site; no single key signs across surfaces.

## Chain-head signing

Per-instance signing of `ChainHead` records bracketing session start/end and cadence checkpoints. The signing key is generated lazily on first use, stored owner-only on disk, and never leaves the gateway process.

- **Key material.** Ed25519, raw 32-byte seed. Stored at `<data_dir>/audit/audit-signing.key` (mode 0o600 on Unix). Public counterpart at `<data_dir>/audit/audit-signing.pub`. Generated by `AuditSigningKey::load_or_create` at first call; subsequent calls re-read the existing files. See `crates/audit/src/signing.rs:78-110`.
- **Canonical message format.** Length-prefixed, schema-versioned. Domain separator `"wirken/audit-chain-head/v1\0"`, schema version `2`. Full layout and field-by-field byte budget are in [audit-events.md](audit-events.md#chain-head-signing).
- **Verification call site.** `wirken session verify` recomputes the message bytes from the row's `(seq_start, seq_end, prev_hash, hash, schema_version)` tuple and verifies the signature with `ed25519_dalek::verify_strict` against the recorded `signing_pubkey`. Source: `crates/audit/src/signing.rs:189-208` (message build) and the same module's verify helper.

## Skill signing

Per-skill bundle signing with a composite signature scope. The signature covers `SKILL.md` plus `skill.wasm` when present; sibling files outside that pair are not in scope.

- **Composite hash layout.** When `skill.wasm` exists: `sha256(SKILL.md_bytes || 0x00 || skill.wasm_bytes)`. When it does not: plain `sha256(SKILL.md_bytes)`. The null-byte separator prevents a trailing null in `SKILL.md` from hashing identically to a preceding empty wasm. Source: `crates/gateway/src/skill_registry.rs:109-140` (`hash_skill_bundle`).
- **Sign call.** `wirken skills sign <dir>` signs the composite hash with Ed25519 and writes the signature as hex to `<dir>/SKILL.sig`, plus the public key to `<dir>/SKILL.pub`. Source: `crates/gateway/src/skill_registry.rs:147-172` (`sign_skill`).
- **Key material.** Per-author signing key at `<data_dir>/signing-key.hex` (hex-encoded Ed25519 seed, mode 0o600 on Unix). Generated on first `wirken skills sign` invocation if absent. See `crates/cli/src/commands/skills.rs:205-237`.
- **Verification at install.** `wirken skills install` calls `verify_skill_with_expected_key_and_delegation` against the registry-supplied `signer_key`. When the binary embeds a non-empty `wirken-registry-pubkey.pub` at build time, the registry-supplied key must also carry a `signer_key_delegation` signature by the bundled root. Source: `crates/gateway/src/skill_registry.rs:261-353`.
- **Verification at load.** Every `SkillLoader::load_file` call re-runs `verify_skill_self_signed` against the on-disk `SKILL.sig` and `SKILL.pub`. A bundle with no `SKILL.sig` is refused at load unless `WIRKEN_ALLOW_UNSIGNED_SKILLS=1`, in which case the loader emits a warn on stderr and proceeds. A bundle with a signature that does not verify is always refused, regardless of the env var. Source: `crates/agent/src/skill.rs:218` (call site), `crates/agent/src/skill.rs:469-503` (`verify_skill_signature` wrapper), `crates/gateway/src/skill_registry.rs:174-238` (`verify_skill_self_signed`).
- **Bypass.** `WIRKEN_ALLOW_UNSIGNED_SKILLS=1` skips the signature requirement for bundles with no `SKILL.sig` / `SKILL.pub`. A bundle with an invalid signature is never accepted under this bypass. Listed in the [security-properties escape-hatch table](security-properties.md#documented-escape-hatches).

## Release signing

Offline maintainer signing of release artifacts. The maintainer's private key is held outside the repo tree; the public key is embedded in `install.sh` and pinned in `KEYS`.

- **What gets signed.** `checksums.sha256`, signed with `ssh-keygen -Y sign` to produce `checksums.sha256.sig`. The release-build CI produces both files alongside the platform binaries on every `v*` tag.
- **Verification at install.** `install.sh` fetches both files, verifies the signature with `ssh-keygen -Y verify` against the embedded public key, then verifies the binary's SHA-256 against the signed checksums file. Every failure path is fail-closed: missing signature, missing checksum, mismatched digest, missing local `sha256sum`/`shasum`, all abort the install. The documented opt-out is `WIRKEN_ALLOW_UNVERIFIED=1` at install time only.
- **Trust anchor.** The Ed25519 public key embedded in `install.sh` is also pinned in `KEYS` (top-level) and listed by fingerprint in `SECURITY.md`. Rotation is a three-step procedure that swaps the trust anchor in one commit and immediately cuts a new release signed with the new key.
- **Detail.** The full maintainer runbook lives in [release-process.md](release-process.md); the cryptographic rationale and rotation/recovery procedures in [release-signing.md](release-signing.md).

## Source references

- Chain-head signing: `crates/audit/src/signing.rs:38-208`.
- Skill bundle signing: `crates/gateway/src/skill_registry.rs:101-353`.
- Skill load-time verification: `crates/agent/src/skill.rs:218`, `:469-503`.
- Release procedure: [docs/release-process.md](release-process.md).
- Release cryptographic detail: [docs/release-signing.md](release-signing.md).
