Issuance governance
Control who may mint authority, how much they may issue, and whether an affected lineage may receive a successor grant.
On this page
Bound the authority an issuer can mint before any of it reaches an agent. A local trust-control service signs a five-use, five-minute review grant, refuses any request one step past that ceiling, and leaves you holding a verified grant. During an incident, the kernel refuses every successor grant for a frozen lineage.
An agent can obey the scope of every capability and still gather too much authority if its issuer hands out oversized or replacement grants. Two entrypoints govern issuance: the trust-control service authenticates an issuance role and applies configured policy ceilings, and an embedded kernel can also require an authenticated tenant-and-lineage context and consult its installed freeze authority.
StartA request inside the tier is signed, checked, and returned
The workload bearer authorizes the request and the body names the subject. The review tier allows invoke, at most five invocations, no delegation, a 300-second lifetime, and constrained tool grants. The authority signs, the service validates the returned grant against the request, and answers HTTP 200. Least privilege.
1. Set the ceiling before the issuer starts
Enforcement at use assumes the grant was legitimately issued. An issuer that accepts whatever scope a requester proposes turns a narrow worker task into a broad credential, and a compromised lineage could replace withdrawn authority through it. Write the ceiling as policy; this one permits invoke only, at most five invocations, a 300-second lifetime, and constrained tool grants, with no delegation:
hushspec: "0.1.0"
name: "bounded-review-issuance"
# One ceiling across the score range makes the issuance comparison independent
# of a fresh subject's reputation history. Tool admission is configured separately.
extensions:
reputation:
tiers:
review:
score_range: [0.0, 1.0]
max_scope:
operations: [invoke]
max_invocations: 5
max_delegation_depth: 0
ttl_seconds: 300
constraints_required: trueOne tier covers the whole score range, so a fresh subject's reputation history leaves the result unchanged; a deployment can use several reputation and runtime-assurance tiers where they change the permitted work. The ceiling limits operations, counts, costs when configured, and lifetime. The requesting host still chooses the repository server, tool name, and path restriction, the serving tool kernel enforces the resulting grant at use, and whether a supplied constraint expresses your intended policy stays your decision.
2. Start the issuer with separate operator and workload credentials
Before you begin
Build the Execution application and install the Chio CLI with curl -fsSL https://chio.computer/install.sh | sh, plus OpenSSL, curl, and jq. The archive includes the policy above and the direct-grant verifier. Run every command from the application directory.
Create the operator's secrets and a subject key, then build the verifier:
umask 077
mkdir .issuance
openssl rand -hex 32 > .issuance/issuer.seed
openssl rand -hex 32 > .issuance/admin.token
openssl rand -hex 32 > .issuance/workload.token
printf 'Authorization: Bearer %s\n' "$(cat .issuance/workload.token)" > .issuance/workload.header
openssl genpkey -algorithm ED25519 -out .issuance/subject.pem
openssl pkey -in .issuance/subject.pem -pubout -outform DER \
| tail -c 32 | xxd -p -c 32 > .issuance/subject.pub
cargo build --locked --bin verify-grantThe subject's private key belongs to the prospective capability holder; the issuer seed and both service credentials belong to the operator. They share one private directory only for this local exercise. Start the service in this terminal and leave it running:
CHIO_TRUST_SERVICE_TOKEN="$(cat .issuance/admin.token)" \
CHIO_TRUST_AUTHORITY_WORKLOAD_TOKEN="$(cat .issuance/workload.token)" \
chio --authority-seed-file .issuance/issuer.seed \
--receipt-db .issuance/receipts.db \
trust serve --listen 127.0.0.1:18940 --policy policies/issuance.yamlBoth bearers reach POST /v1/capabilities/issue. The workload credential permits authority status and issuance, and general authority administration such as key rotation stays with the administrative bearer. Requests with cluster-peer headers use the authenticated cluster-mutation protocol instead. Keep the original state if you restart the service.
3. Request exactly the review grant
In a second terminal in the same directory, read the authority's public key and retain it, then ask for the review grant. The bearer authorizes asking, and the body names the subject that receives the grant:
curl --fail-with-body --silent --show-error \
--header @.issuance/workload.header \
http://127.0.0.1:18940/v1/authority > .issuance/authority.json
jq -er '.publicKey | select(type == "string" and length == 64)' \
.issuance/authority.json > .issuance/issuer.pub
jq -n --rawfile subject .issuance/subject.pub '{
subjectPublicKey: ($subject | rtrimstr("\n")),
ttlSeconds: 300,
scope: {grants: [{
server_id: "workspace", tool_name: "read_file",
operations: ["invoke"], max_invocations: 5,
constraints: [{type: "path_prefix", value: "src/"}]
}]}
}' > .issuance/request.json
curl --fail-with-body --silent --show-error \
--header @.issuance/workload.header --header 'Content-Type: application/json' \
--data-binary @.issuance/request.json \
http://127.0.0.1:18940/v1/capabilities/issue > .issuance/issued.json
jq '.capability' .issuance/issued.json > .issuance/capability.jsonYou should see HTTP 200 and a signed grant for the selected subject: workspace/read_file, five invocations, and the src/ path restriction. Issuance creates authority and invokes no tool; the receiving host recognizes this issuer and admits each call under its own current checks.
The workload token proves permission to request, never that an arbitrary subject key belongs to the requester. A host serving untrusted workers selects the subject from its authenticated enrollment or session and builds the request under its own issuance policy. This loopback exercise trusts its own service, while a remote consumer needs an independently provisioned issuer pin and an authenticated connection, since the issuer named inside an incoming capability establishes no trust.
4. Push one value past the ceiling
Keep the subject, tool, and path fixed. Request a lifetime of 301 seconds, then six invocations; both requests are otherwise identical to the successful one:
jq '.ttlSeconds = 301' .issuance/request.json > .issuance/too-long.request.json
jq '.scope.grants[0].max_invocations = 6' .issuance/request.json > .issuance/too-many.request.json
for case_name in too-long too-many; do
status=$(curl --silent --show-error --output ".issuance/$case_name.response.json" \
--write-out '%{http_code}' --header @.issuance/workload.header \
--header 'Content-Type: application/json' \
--data-binary "@.issuance/$case_name.request.json" \
http://127.0.0.1:18940/v1/capabilities/issue)
test "$status" = 403 || exit 1
jq -r '.error' ".issuance/$case_name.response.json"
donerequested ttl_seconds 301 exceeds policy tier 'review' ceiling 300 requested tool grant 'workspace'/'read_file' exceeds policy tier 'review' scope ceiling
You should see HTTP 403 for both, each with the tier's reason, and no capability returned. The same policy also refuses an unconstrained tool grant and a grant that adds delegation. Every applicable ceiling must hold, so a smaller TTL cannot pay for a disallowed operation. The workload credential also stays outside authority administration:
status=$(curl --silent --show-error --output .issuance/rotation.response.json \
--write-out '%{http_code}' --request POST \
--header @.issuance/workload.header http://127.0.0.1:18940/v1/authority)
test "$status" = 401 || exit 1
jq -r '.error' .issuance/rotation.response.jsonmissing or invalid control bearer token
That is the control-authentication refusal, returned while the same credential stays valid for issuance and status. Read every issuance response by its status:
| Response | Read it as | Next action |
|---|---|---|
200 with a capability | The selected authority returned a grant | Verify it and compare the expected request before use |
401 control- | This route rejected the presented role | Correct the role or its credential |
400 subject or workload- | A parsed request failed field validation | Repair the original request or attestation binding |
403 issuance denied | Policy or verified- | Inspect the returned reason and governing tier |
500 authority error | Issuance or returned-grant validation failed | Retain the original request and operator trace |
A 500 alone leaves a signing failure, an invalid signature, an untrusted issuer, and a response mismatch indistinguishable. After a remote-authority error, keep the original operation and investigate the authority's result before concluding that nothing was signed.
5. Verify the signature and compare the request
While the five-minute grant is current, check its signature against the retained issuer key, then compare subject, scope, and lifetime with your request. The verifier checks a direct grant's trust and validity window; the jq assertion compares the expected request, which this policy leaves unchanged:
verify-grant .issuance/capability.json .issuance/issuer.pub
jq -e --slurpfile request .issuance/request.json '
.subject == $request[0].subjectPublicKey and
.scope == $request[0].scope and
(.expires_at - .issued_at) == $request[0].ttlSeconds
' .issuance/capability.jsonYou should see the verifier accept the grant and the assertion exit successfully. The service has already validated its wrapped authority's response before returning it. Where a runtime-assurance tier adds a required constraint, derive the expected result from the operator policy, since the returned scope then differs from the request. Stop the service with Ctrl-C; its seed and database remain for a later restart.
A frozen lineage is refused before signing and again before return
Ceilings answer how much authority may be issued. During an incident the question is whether a tenant and lineage may receive any successor authority at all. The response side sets the freeze: a response plan with any lineage-scoped effect (SuspendCapabilitySet or FreezeIssuance) is valid only when FreezeIssuance is its first effect, so the lineage cannot acquire a successor while the response narrows or suspends its existing grants.
The plan's tenant and the effect's lineage form the pair that issuance admission queries. The embedded kernel's issue_capability_with_security_context runs that query through an installed issuance admission authority, in four steps:
- Accept the context. Require an installed authority, a nonzero context generation, and a principal matching the requested subject.
- Ask before signing. Query the authority about the exact tenant, lineage, and issuance operation.
- Validate the response. Check issuer, signature, subject, scope, TTL, and any expected workload security binding.
- Recheck before returning. Rebuild the query and check current issuance admission again.
The host derives the context from its authenticated session authority; an incoming tenant string or requested subject is no substitute. IssuanceFreezeAdmission connects the kernel's admission interface to the selected freeze store, validates its decision binding, and refuses a frozen query:
pub trait CapabilityIssuanceAdmissionAuthority: Send + Sync {
fn ensure_ready(&self) -> chio_security_types::ports::PortResult<()>;
fn authorize(
&self,
query: &chio_security_types::ports::IssuanceFreezeAdmissionQuery,
) -> chio_security_types::ports::PortResult<()>;
}The second check covers a freeze that lands while signing is in progress: the kernel refuses to publish the returned capability under the changed admission state. With governance installed, the context-free issue_capability refuses for lack of authoritative context, and the context-bound method refuses a substituted principal and an active freeze however narrow the proposed scope.
Where the configured authority supplies a workload binding, a BindSecurityContext caveat seals the tenant, lineage, session, principal, isolation epoch, generation, workload, server, and workload signer under the capability signature. Validate the exact expected binding on return, because a valid signature over another context is insufficient.
Signed security-binding fields
pub struct CapabilitySecurityBinding {
pub schema: String,
pub tenant_id: String,
pub lineage_id: String,
pub session_id: String,
pub principal_id: String,
pub isolation_epoch_id: String,
pub context_generation: u64,
pub workload_id: String,
pub server_id: String,
pub workload_signer_public_key: String,
}Test the installed path with a valid subject, a substituted principal, a freeze before signing, and a freeze before publication, and keep those results apart from the HTTP ceiling checks in steps 1 to 5, which install no kernel admission authority. A freeze stops new issuance through the governed path; Revocation withdraws authority already issued.
Read more in Response authorization, which covers how a plan becomes authorized to apply.