Chio/Docs

Issuance governance

Control who may mint authority, how much they may issue, and whether an affected lineage may receive a successor grant.

Guide chio@5f63e6e

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.

Start
Issuance gatesChoose a request.
askchecksignvalidaterecheckreturntrust-control HTTPcontext-bound kernelbefore signingbefore returnfreeze storerrole bearerHTTP 403policy ceilingσauthorityreturned grantκgrantrsession contextrefused before signingKkernelrefused before returnκgrantaskchecksignvalidaterecheckreturnHTTPkernelfreezer403σκrrefusedKrefusedκ

A 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.

The two entrypoints that mint authority, on one axis of issuance stages: trust-control HTTP issuance with its policy ceiling, and context-bound kernel issuance with its freeze checks. The response plan that sets a freeze, workload security bindings, and runtime-assurance tiers are left out.

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:

policies/issuance.yamlyaml
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: true

One 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:

Terminalbash
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-grant

The 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:

Terminalbash
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.yaml

Both 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:

Terminalbash
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.json

You 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:

Terminalbash
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"
done
Output
requested 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:

Terminalbash
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.json
Output
missing 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:

ResponseRead it asNext action
200 with a capability
The selected authority returned a grant
Verify it and compare the expected request before use
401 control-authentication refusal
This route rejected the presented role
Correct the role or its credential
400 subject or workload-binding error
A parsed request failed field validation
Repair the original request or attestation binding
403 issuance denied
Policy or verified-assurance requirements rejected the request
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:

Terminalbash
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.json

You 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:

  1. Accept the context. Require an installed authority, a nonzero context generation, and a principal matching the requested subject.
  2. Ask before signing. Query the authority about the exact tenant, lineage, and issuance operation.
  3. Validate the response. Check issuer, signature, subject, scope, TTL, and any expected workload security binding.
  4. 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:

crates/kernel/chio-kernel/src/kernel/mod.rs207–214rust
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
crates/core/chio-core-types/src/capability/caveat.rs15–26rust
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.

Next: choose which issuers a kernel accepts