Policy Signing#
The full normative specification is at spec/hushspec-signing.md. The 0.2 envelope and keyring schemas are schemas/hushspec-signature.v1.schema.json and 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 and the quickstart policy into one directory. With Python 3.10+ and the v1 CLI installed:
python3 verify.py --policy policy.yamlThe 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.
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#
- Distribute a new trusted public key through an authenticated operator channel.
- Sign an approved policy with the new private key; verify with the receiving system's actual trust configuration before activating it.
- Retain old public keys for historical verification, with explicit retirement or revocation according to your incident and retention policy.
- Persist the last accepted integer
policy_versionper 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.
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:
umask 077
openssl genpkey -algorithm ed25519 -out signing.key.pem
openssl pkey -in signing.key.pem -pubout -out signing.pub.pem