<!-- HushSpec 1.0.0; guide; stable
releaseCommit: e771ec647b7f26a0a09ff852886eb8a91093bc58
docsCommit: 5a252dbf7eb46b507ac9493dff8a695abf5b5e9a
sourcePath: docs/src/signing-spec.md
canonical: https://www.hushspec.org/docs/signing/
Relative links in this unmodified source body are relative to sourcePath.
-->

# Policy Signing

The full normative specification is at [`spec/hushspec-signing.md`](https://github.com/backbay-labs/hush/blob/main/spec/hushspec-signing.md). The 0.2 envelope and keyring schemas are [`schemas/hushspec-signature.v1.schema.json`](https://github.com/backbay-labs/hush/blob/main/schemas/hushspec-signature.v1.schema.json) and [`schemas/hushspec-keyring.v1.schema.json`](https://github.com/backbay-labs/hush/blob/main/schemas/hushspec-keyring.v1.schema.json).

A policy signature is a detached JSON envelope (`policy.yaml.sig`) carrying an Ed25519 signature over the **canonical content hash of the resolved policy**, not over the file bytes. Reformatting the YAML keeps the signature valid; changing a base policy reached through `extends` invalidates it.

The envelope's claims (`format_version`, `algorithm`, `key_id`, `signed_at`, optional `expires_at`, `policy_version`, `policy_name`, `signer`, and `content_hash`) are all inside the signed input, which is the RFC 8785 canonical form of the envelope without `signature`.

Keys are standard PEM (PKCS#8 private, SubjectPublicKeyInfo public); `key_id` is the SHA-256 of the SPKI DER, which verifiers recompute. Trust is a keyring file listing public keys with optional retirement (`not_after`) and revocation.

Verification is an ordered list of ten checks, each with a fixed reason code, ending in `valid` or the first failure. Enforcement points configured to require signatures verify on load and refuse to evaluate against an unverified policy.

Vectors: `fixtures/signing/vectors.yaml` lists eighteen cases signed with a published, test-only key.

## Run the evidence lab

Download [verify.py](https://hushspec.org/docs-examples/evidence/verify.py) and the
[quickstart policy](https://hushspec.org/docs-examples/quickstart/policy.yaml) into one directory.
With Python 3.10+ and the v1 CLI installed:

```sh
python3 verify.py --policy policy.yaml
```

The lab generates fresh disposable keys inside a private temporary directory.
It signs and verifies a policy, checks a denied receipt, verifies a signed log,
and creates and verifies a bundle. It then asserts these refusals:

| Alteration | Expected result |
|---|---|
| Change the policy name | `content_hash_mismatch` |
| Verify using an unrelated public key | `unknown_key_id` |
| Advance the synthetic verifier clock beyond expiry | `expired` |
| Flip one signature byte, preserving base64url shape | `signature_mismatch` |
| Break the second log entry's predecessor | Line 2 diagnostic naming `prev_hash` |
| Change a bundle's signed payload | `dsse_signature_mismatch` |
| Remove a log's final receipt | Remaining prefix verifies; completeness is not established |

The fixed/advanced clocks in examples test expiry. Production verification
uses a trustworthy current clock, not a conveniently chosen historical time.

## Load-time verification

Configure a guard/resolver with signature requirements and an independently
provisioned keyring. Verify every inheritance hop under that contract, not just
the leaf after deployment. Built-in hops and matching explicit digest pins
have their specified trust treatment; see the
[SDK resolution contract](reference/sdk-api.md#merge-resolve-verify-on-load-digest-pins).

A missing or invalid required signature cannot become an unrestricted policy.
Ordinary guards can represent verification refusal as a deny-all state; an
ordinary failed reload retains the last good policy. The experimental
invocation coordinator instead refuses subsequent calls after a rejected
installation. Do not transfer one lifecycle's recovery rules to the other.

## Rotation, expiry and rollback

1. Distribute a new trusted public key through an authenticated operator channel.
2. Sign an approved policy with the new private key; verify with the receiving
   system's actual trust configuration before activating it.
3. Retain old public keys for historical verification, with explicit retirement
   or revocation according to your incident and retention policy.
4. Persist the last accepted integer `policy_version` per policy name and pass
   that lower bound to verification. A valid old signature is not rollback
   protection unless the relying party retains and checks that state.

`expires_at`, key retirement and revocation are different checks. An expired
envelope is not repaired by allowing more clock skew; a revoked key is not
restored by a new signature using that same key. The full reason-code table is
in the [registry reference](reference/registries.md).

## Private-key handling

Production private keys are operator inputs. Keep them out of policies,
repositories, logs, screenshots and support tickets; restrict filesystem
permissions and use your secret-management process. Do not adopt the published
fixture key or a key generated by the evidence lab as a trust root.

For an operator-managed PEM key pair, these commands require a private working
directory and restrictive umask:

```bash
umask 077
openssl genpkey -algorithm ed25519 -out signing.key.pem
openssl pkey -in signing.key.pem -pubout -out signing.pub.pem
```
