key-cape/docs/current-token-client.md

138 lines
8.5 KiB
Markdown
Raw Permalink Normal View History

# Shared current-token client and private CLI observations
Source preparation under proposed KEY-WP-0042 and the existing CUST-WP-0080 /
NK48 / Apps operation and application obligations. This is a client of the
issuer's optional online profile, not another issuer, PDP, approval service or
application-local token registry. Native issuer/profile/consumer admission is
still required. Actual operation/effect permission remains with existing owners.
## One producer, two consumption surfaces
`src/internal/authclient/online.go` is the single implementation. The existing
KeyCape executable exposes it as `check-current-token`, `revoke-token` and
`check-inactive-token` for owner processes. Go consumers use the generated
`adapters/go/tokenstatus` module and import `keycape/pkg/tokenstatus` through an
exact pinned source/dependency lock. `make tokenstatus-export` generates that
five-file module; `make tokenstatus-check`, ordinary `make test` and the root Go
suite refuse drift. The sole client transformation is its first package
declaration; all remaining bytes and the shared JOSE primitive are exact.
No policy evaluator, signing key, credential, daemon or deployment is exported.
The client uses the existing producer `internal/jose` verifier. Its additive
`VerifyTyped` path requires the actual RS256 / JWT producer header; legacy
`Verify` callers retain their existing type policy. Keys are independently
admitted public JWKS, never fetched from the status response or discovered by
the token. The minimal generated module uses Go 1.23 and standard libraries.
Its file boundary is the current **Linux owner profile**: POSIX mode checks do
not establish Windows ACL behavior or a broader platform admission.
## Current status is an authenticated fact
Online human PKCE ID tokens additionally carry the exact access-token `at_hash`
binding described in `online-token-status.md`. This client verifies access-token
facts; a consumer that also uses an ID token separately authenticates that token,
checks its purpose, and joins its signed hash, identity, issuance, assurance and
tenant provenance to the exact access grant. Neither token substitutes for the
other. The online profile's hash is additive; disabled legacy issuance is unchanged.
`OnlineClient.Check(ctx, token, ExpectedIdentity{...})` validates the signature,
issuer, exact single resource audience, subject, tenant, human/service type,
optional authorized-party binding, access-token purpose, unique jti and integer
issued/expiry/not-before bounds before reading the private caller credential.
It bounds identity by expiry and an independently admitted token age (at most
one hour), then performs a new authenticated introspection. It joins every
returned identity/client/audience/jti/scope/time field to those signed facts.
After network I/O it checks time again and reloads JWT trust so a delayed answer
cannot retain expired facts or a withdrawn signing key. No positive is cached.
The explicit HTTPS issuer origin supplies the exact `/introspect` and `/revoke`
paths; redirects, query/fragment/ambiguous issuer paths, environment proxies and
ambient transports are not used. CA, JWKS and private caller credential files
are reread for each operation with regular-file/mode/size guards. File opens do
not block on FIFO replacement. Private credentials can be atomically rotated
without retaining a cached value. Requests have a five-second bound and responses
a 64 KiB bound. Duplicate/null/missing/extra fields, mismatched facts, non-JSON or
cacheable status responses and unavailable/unauthenticated transport refuse with
one fixed error. Secret, token and response values are never part of errors.
`CurrentToken.Claims()` returns a separate copy of verified signed facts. Those
facts remain snapshots: original authentication time is not the current lookup
time. Minimum assurance/authentication age, tenant provenance, current groups,
resource/action policy, exact approval and effect authority remain the existing
consumer profile's obligations. A fresh status response is not a new MFA act or
permission. A Forgejo sign-in token does not grant release-write or inverse
authority, and no operator client/route is selected by this source package.
## Negative scope and cleanup
`ObserveInactive` performs an independent **read-only** status lookup. It never
infers inactivity from transport failure, malformed output or an unsuccessful
positive check. `Revoke` sends the exact token, requires an empty successful
response and then obtains that separate negative readback. Both return only
`InactiveForClient`: the exact digest is currently inactive for the configured
client. Unknown and other-client tokens have the same negative response.
That result alone cannot prove original grant ownership, global revocation or a
completed operation's cleanup. The owner retains its independently authenticated
original issuance/current-positive evidence, exact token digest/jti and operation
correspondence before mapping a negative observation to a parent-session receipt.
Cleanup can continue after business-token freshness/key withdrawal; it creates
no business authority. Unavailable/ambiguous cleanup remains unverified and
requires the existing durable owner retry/recovery path. It is not success.
## Existing CLI
The public admission file contains only references and independently admitted
bindings; for example (paths/client are placeholders, not operational defaults):
```json
{
"issuer": "https://owner-admitted-issuer.example",
"audience": "owner-admitted-resource",
"client_id": "owner-admitted-confidential-client",
"ca_file": "/run/owner-identity/issuer-ca.pem",
"jwks_file": "/run/owner-identity/admitted-jwks.json",
"secret_file": "/run/owner-identity/current-client-credential",
"max_token_age": "5m"
}
```
Use an existing owner-acquired private KeyCape token JSON file, and the access
token's exact `sha256:` digest from the owner-bound session/transaction record.
No token or client credential goes in argv. Follow the existing Warden routing /
owner-native acquisition and custody path; this source adds no credential lane.
```text
keycape check-current-token --admission-file /ABS/profile.json --token-file /ABS/private-token.json --token-sha256 sha256:OWNER_BOUND_DIGEST --subject OWNER_EXPECTED_SUBJECT --tenant OWNER_EXPECTED_TENANT --principal-type human --out /ABS/PRIVATE-DIR/new-observation.json
keycape revoke-token --admission-file /ABS/profile.json --token-file /ABS/private-token.json --token-sha256 sha256:OWNER_BOUND_DIGEST --out /ABS/PRIVATE-DIR/new-cleanup-observation.json
keycape check-inactive-token --admission-file /ABS/profile.json --token-file /ABS/private-token.json --token-sha256 sha256:OWNER_BOUND_DIGEST --out /ABS/PRIVATE-DIR/new-independent-negative.json
```
The placeholders deliberately do not form an operational command. Current checks
require explicit expected subject/tenant/type. Negative/cleanup commands refuse
those business-identity flags because they report only the bounded client status.
Duplicate flags and duplicate/unknown/null input fields refuse. The token file
must be private, the digest must match the exact parsed access token, and the new
observation must have a 0700 parent outside Git. Outputs are exclusive 0600 files;
failures remove only their own inode and cannot delete another attempt's replaced
file. Standard output/errors expose fixed status, not claims or credential values.
`keycape.current-token-observation.v1` is a **private, unsigned producer command
result**, not a portable execution receipt. Current output includes bounded
identity/token references and signed assurance/tenant-provenance facts; it omits
bearers, credentials and group/role snapshots. Negative output states its client
scope. The invoking owner authenticates exact binary/config/custody and binds its
own case, acceptance, descriptor, source/target, principal, transaction and effect.
Local file presence, digests or a successful command do not supply those grants.
## Scope of verification
Source checks use actual token/status handlers over isolated HTTPS, actual human
PKCE claim construction and the same CLI implementation. The separate cross-owner
fixture runs a built executable, generated public Go consumer and actual Warden
Executor/SQLite in distinct Python processes. Synthetic case/catalog/owner
attestations and a disposable file target do not establish Forgejo/browser
acceptance, real-factor possession, production custody or approved native effects.
The online issuer profile, owner mappings and source/package/type admission remain
under review; all corresponding native gates stay open.