9.9 KiB
Baseline 1.1 signed authorization chain
Result
aios_baseline.authorization implements a new authorization chain without
changing the frozen historical owner-delegation wire formats:
- an Owner signs
aios.owner-delegation-grant.v1with a supplied Ed25519 key; - the local authorization service verifies and durably registers that grant;
- the service issues a short, signed
aios.operational-epoch.v1; - the service embeds that epoch in a signed
aios.task-principal.v1and binds the principal to it by digest; - every authorization and effect reservation rereads SQLite and checks the current root grant digest, generation, revocation state, expiry, signature, tenant, subject and narrowed scope.
The TaskPrincipal is a short authorization credential. It does not manufacture a Task claim or ClaimLease. The task authority and executor must independently enforce their authoritative claim lease, heartbeat and fencing token.
Public API
AuthorizationService(
db_path,
owner_public_keys, # key_id -> Ed25519PublicKey
machine_private_key,
machine_key_id,
*,
owner_key_scopes, # key_id -> exact tenant_ids/subject_ids lists
clock=time.time,
control_context_verifier=None,
trusted_machine_public_keys=None,
)
sign_owner_delegation_grant(
owner_private_key,
owner_key_id,
*,
grant_id,
generation,
tenant_id,
subject_id,
mission_ids,
task_ids,
operations,
component_ids,
environments,
issued_at,
validity_mode, # required: UNTIL_REVOKED or EXPIRES_AT
expires_at=None,
max_effects=None,
destructive_ceiling="NONE",
)
service.register_grant(signed_envelope) -> dict
service.issue_principal(grant_id, *, exact_scope, ttl_seconds) -> dict
service.authorize(principal, *, exact_scope) -> dict
service.assert_effect_bounds(principal, operation_definition, *, exact_scope) -> dict
service.reserve_effect(grant_id, effect_id, principal, *, exact_scope) -> bool
service.revoke_grant(grant_id, *, control_context) -> int
register_grant and authorize return JSON-safe copies of the signed grant.
reserve_effect returns True for the first reservation and False for an
idempotent retry of the same stable effect ID. It reauthorizes and reserves in
one BEGIN IMMEDIATE transaction. A configured max_effects applies per grant
generation. An omitted max_effects leaves mission-level budget accounting to
the calling service.
The grant signing helper requires an explicit validity mode. It never creates a
key and does not accept an unsigned grant payload as authority. EXPIRES_AT
requires an integer expiry after issuance. UNTIL_REVOKED forbids an expiry.
The signed destructive_ceiling defaults to NONE and is strictly one of
NONE, REVERSIBLE, or IRREVERSIBLE. assert_effect_bounds accepts only a
trusted OperationDefinition: NONE permits only READ_ONLY; REVERSIBLE
also requires rollback, required state/exact readback, and reconciliation; and
IRREVERSIBLE is an explicit maximum. A ceiling never replaces Job
authorization, durable intent, claim lease, receiver fencing, readback, or
reconciliation.
Owner public-key trust is separately constrained by the required constructor
argument owner_key_scopes. Its key set must exactly equal owner_public_keys;
each key has exact nonempty tenant_ids and subject_ids lists. A wildcard is
effective only when the configuration explicitly supplies the sole list item
"*". Registration and every later durable grant load check the signing key's
scope, so placing keys for several tenants in one process does not let one
tenant's Owner key sign another tenant's grant.
Exact wire objects
Every signed object uses an exact signing object:
{"algorithm":"Ed25519","key_id":"...","signature":"base64-64-byte-signature"}
The signature covers the schema, every payload field, the signing algorithm and
key ID, and, for a TaskPrincipal, the complete nested signed epoch. The message
starts with the AIOS-AUTHORIZATION domain, a NUL separator, the schema, and a
second NUL separator before canonical JSON.
An OwnerDelegationGrant has exact outer keys schema, payload, signing.
Its payload has:
{
"grant_id": "...",
"generation": 1,
"tenant_id": "...",
"subject_id": "...",
"scope": {
"mission_ids": ["..."],
"task_ids": ["..."],
"operations": ["..."],
"component_ids": ["..."],
"environments": ["..."]
},
"issued_at": 2000000000,
"validity": {"mode": "UNTIL_REVOKED"},
"limits": {},
"destructive_ceiling": "NONE"
}
The alternative validity is
{"mode":"EXPIRES_AT","expires_at":<integer>}. limits is either empty or
contains one positive integer max_effects. Each scope list is nonempty and
may contain explicit values or the sole wildcard "*".
An OperationalEpoch has exact outer keys schema, payload, signing. Its
payload has epoch_id, root_grant_id, root_grant_digest,
root_grant_generation, all seven exact binding fields (tenant_id,
subject_id, mission_id, task_id, operation, component_id,
environment), issued_at and expires_at.
A TaskPrincipal has exact outer keys schema, payload, operational_epoch,
signing. Its payload has credential_id, nonce, parent_epoch_digest, the
same three root fields, the same seven binding fields, issued_at and
expires_at. Epoch and principal validity is capped at 300 seconds and at an
explicit root-grant expiry.
Durable state and revocation
SQLite stores the canonical signed grant, its digest, its current durable generation and revocation flag. Revocation increments the durable generation in an immediate transaction, records a revocation row, and leaves old proofs invalid even when their short expiry has not elapsed. Reopening the service on the same database retains that decision. The same signed generation cannot clear revocation. Reactivation requires a newly Owner-signed generation greater than the durable revocation generation.
revoke_grant fails closed unless control_context_verifier returns the exact
boolean True for the caller's authenticated local control-service context.
The authorization module does not authenticate network requests; the hosting
service must construct that trusted context from its local authenticated API
boundary. Authorization never relies on the verifier or a cache for freshness;
it reads current durable state on every use.
The active machine signing key is always trusted for newly issued objects.
trusted_machine_public_keys can retain previous public keys for the remainder
of already-issued proof lifetimes during rotation. Owner trust keys and machine
trust keys remain separate.
Verification
The initial failure-first run stopped during collection because
aios_baseline.authorization did not yet exist. After implementation, a focused
run found and fixed strict nonce validation and clarified that substituting the
nested epoch invalidates the outer signature before the parent-digest check.
The final focused commands were:
PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=/workspace/scratch/63950cb15f5c/test_deps:$PWD/active-runtime/app \
python3 -m pytest -q \
tests/baseline_v1_1/test_authorization.py \
tests/baseline_v1_1/test_claim_heartbeat.py
Result before the ceiling extension: 25 passed in 0.80s. The post-extension
focused run reported 28 passed in 0.35s. After the Owner trust-root and claim
heartbeat hardening, the current focused result is 35 passed.
The tests cover exact-scope success, all three signature layers, unknown Owner keys, unsigned payload refusal, fake Mapping parents, nested-parent substitution, every cross-scope dimension, grant narrowing, live and persisted revocation, proof renewal without Owner reapproval, explicit root expiry, Owner-signed generation reactivation, strict types and signature encoding, explicit helper validity, durable idempotent effect budgets, unbounded budget delegation, machine-key rotation, refusal of untrusted retired keys, NaN and empty-identity rejection, and the revocation-generation boundary around durable effect budgets. Revocation cannot reset a budget or revive the immutable old signed generation; only a newer Owner-signed generation can reactivate it. The extension tests also cover signed ceiling tampering, conservative default denial, rollback/readback requirements, and explicit irreversible authority. They now also cover exact Owner-key scope configuration, cross-tenant and cross-subject Owner-key refusal, explicitly configured global trust, cancellation before heartbeat, and the exact lease-expiry boundary.
Security review disposition
| Severity | Status | Boundary | Disposition |
|---|---|---|---|
| High | Closed | Owner trust roots | Each trusted Owner key now requires explicit tenant and subject scope; no implicit cross-tenant authority remains. |
| High | Closed | Revocation and effect budget | Every use rereads root generation and revocation state in SQLite. Revocation cannot revive or reset the old generation's reservations. |
| High | Closed | Receipt and recovery integration | Generic Jobs require the exact transition graph and trusted, fully bound signed receipts; Task recovery preserves terminal Owner cancellation. |
| Medium | Closed | Claim heartbeat | Heartbeat now checks terminal Task state and performs the live-lease update with status='ACTIVE' and lease_until>now under one immediate transaction. |
| High | Pending M3 | Mutating receiver fence | The supplied handler remains read-only. Any future mutating receiver must atomically enforce the authoritative fence at the sink and recheck authorization, claim and deadline around receiver execution. |
| Medium | Accepted for initial single-host profile | Admission scope | The durable unknown-effect admission latch intentionally stops new effects across the shared Job store. This conservative availability policy is documented in SERVICE_LOOP_REPORT.md; later tenant/resource partitioning must retain a global storage-integrity gate. |