Files
aios/docs/implementation-baseline-1.1/AUTHORIZATION_REPORT.md
T

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:

  1. an Owner signs aios.owner-delegation-grant.v1 with a supplied Ed25519 key;
  2. the local authorization service verifies and durably registers that grant;
  3. the service issues a short, signed aios.operational-epoch.v1;
  4. the service embeds that epoch in a signed aios.task-principal.v1 and binds the principal to it by digest;
  5. 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.