Share current-token verification and scoped cleanup through one producer client
Some checks failed
Authentication acceptance / acceptance (push) Failing after 59s
Authentication acceptance / provider-contract (push) Successful in 13s
Build and Publish Container Image / build-and-push (push) Successful in 31s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a14bfc-9643-7872-aa12-98326dcf25e7
This commit is contained in:
tegwick 2026-10-18 11:36:26 +02:00
parent 67a0869c1e
commit 08c083bae9
19 changed files with 2228 additions and 3 deletions

View file

@ -3,7 +3,7 @@ IMAGE_REPO ?= coulomb/key-cape
IMAGE_TAG ?= latest
IMAGE := $(IMAGE_REGISTRY)/$(IMAGE_REPO):$(IMAGE_TAG)
.PHONY: dev seed build test lint contract-test image push image-tag
.PHONY: dev seed build test lint contract-test image push image-tag tokenstatus-export tokenstatus-check
PLAYBOOK_VALIDATOR ?= ../net-kingdom/tools/playbook-capability-contract/playbook_contract_validator.py
@ -16,9 +16,15 @@ seed:
build:
cd src && go build ./...
test:
test: tokenstatus-check
cd src && go test ./...
tokenstatus-export:
python3 tools/export_tokenstatus_client.py
tokenstatus-check:
python3 tools/export_tokenstatus_client.py --check
lint:
cd src && go vet ./...

View file

@ -0,0 +1,151 @@
# Target Revenue Source License
**Version 1.0, Candidate 1 (V1C1)**
---
> **PRELIMINARY CANDIDATE — SUBJECT TO CHANGE — NOT FINAL**
>
> This repository is governed by the Target Revenue Source License
> (TRSL), Version 1, Candidate 1. This is the framework's first working
> candidate, adopted as the org's preliminary operating license across
> all repos (maintainer decision, 2026-07-29) during the build/alpha
> stage — see `SCOPE.md` §1 and Appendix A of the canonical text (link
> below) for the alpha/beta risk-acceptance decision this reflects and
> what it does and does not mean. Full specialist legal review is
> explicitly deferred until the framework moves out of beta.
>
> **Canonical source, full candidate-status banner, and Appendix A
> (non-normative candidate notes tracking every open item):** the
> `coulomb` org's `target-revenue` repository,
> `specs/TargetRevenueSourceLicense-V1C1.md` — this file is the operative
> legal text (Preamble through Section 11) only; the canonical document
> is authoritative if this copy and that document ever diverge.
---
## Preamble
This Target Revenue Source License ("**License**") governs the Software identified in the applicable Phase Manifest. It implements the Target Revenue Framework: a defined development Phase accumulates Development Credit and Remission Credit against an immutable Initial Target until the Milestone Release automatically and irrevocably converts to a declared permissive Future License.
Commercial beneficiaries fund the creation and early availability of a software improvement; once the declared target is satisfied, the governed release becomes permissively open source.
## 1. Definitions
Capitalized terms used in this License have the meanings given below. Where a term is also defined in the Phase Manifest or Target Ledger for a specific Phase, the Phase Manifest and Target Ledger govern the *values* (amounts, dates, identifiers) and this License governs the *legal effect* of those values — the two must not be read as conflicting definitions of the same concept.
**"Commercial Entitlement"** means a right, purchased or otherwise granted under a Commercial Use Agreement, to make Commercial Use of the Software during a Phase.
**"Commercial Use"** means billing, invoicing, or otherwise charging any customer a fee, subscription, license fee, or other consideration for or in connection with use of the Software, at any time before the Conversion Event for the applicable Phase, regardless of whether the person or organization billed would otherwise qualify for Noncommercial Use. Commercial Use occurs by virtue of such billing alone, whether or not the resulting payment is registered with the Trust Service; in particular, billing a customer for pre-conversion use of the Software without recording the corresponding payment in the applicable Phase's Target Ledger is Commercial Use without a valid Commercial Entitlement — a violation of Section 3, addressed under Section 7 and, where applicable, the Enforcement Network described in the canonical repository's `specs/EnforcementNetworkConcept.md`.
**"Commercial Use Agreement"** means the separate agreement, referenced by the applicable Phase Manifest, under which a Commercial Entitlement is purchased or granted. This License does not itself set pricing, metering, or payment terms — those are governed by the Commercial Use Agreement.
**"Conversion Event"** means the moment the Outstanding Target for a Phase reaches zero, as computed from the Phase Manifest and Target Ledger per the Target Ledger Specification. The Conversion Event occurs automatically and is not conditioned on any declaration, attestation, or other act by the Licensor or any Trust Service.
**"Development Credit"** means the portion of a collected and settled payment explicitly allocated toward satisfying the Initial Target of a specific Phase, as recorded in that Phase's Target Ledger.
**"Future License"** means the permissive license identified in the applicable Phase Manifest, being either the MIT License or the Apache License, Version 2.0, which applies to the Milestone Release upon the Conversion Event.
**"Initial Target"** means the immutable monetary target declared for a Phase in its Phase Manifest.
**"Licensor"** means **Binky Hedgehog GmbH**, the party that publishes the Phase Manifest and holds the rights necessary to grant this License and the Future License for the Milestone Release.
**"Milestone Release"** means the precisely identified software release designated in the applicable Phase Manifest, identified by an immutable source revision, release artifact, or cryptographic digest.
**"Noncommercial Use"** means use of the Software for personal purposes, private study, hobby or amateur projects; use by any charitable organization, educational institution, public research organization, or government institution acting in a non-revenue-generating capacity; or other use of a materially similar character.
**"Outstanding Target"** means, at any time, `max(0, Initial Target − cumulative Development Credit − cumulative Remission Credit)` for a Phase, as computed from that Phase's Target Ledger.
**"Phase"** means a bounded development undertaking governed by one Initial Target, one Milestone Release, one degeneration policy, and one Future License declaration, as declared in a Phase Manifest.
**"Phase Manifest"** means the published, immutable declaration identifying a Phase, its Milestone Release, Initial Target, Future License, degeneration policy, and Target Ledger location, as specified in the Phase Manifest Specification.
**"Remission Credit"** means a transparent, non-revenue reduction of a Phase's Outstanding Target, generated under that Phase's published degeneration policy and recorded in the Target Ledger.
**"Settled Payment"** means a payment that has cleared through its payment processor and is no longer subject to reversal in the ordinary course (chargeback, dispute, or equivalent), as further specified by the applicable Commercial Use Agreement or monetization extension.
**"Software"** means the source code, object code, and associated documentation of the Milestone Release identified in the applicable Phase Manifest.
**"Target Ledger"** means the append-only record of Development Credit, Remission Credit, and correction entries for a Phase, as specified in the Target Ledger Specification.
**"You"** or **"Licensee"** means the individual or entity exercising rights under this License.
## 2. Grant of Rights for Noncommercial Use
Subject to the terms of this License, the Licensor grants You a worldwide, royalty-free, non-exclusive license, during the applicable Phase, to:
(a) use, reproduce, and study the Software for any Noncommercial Use;
(b) modify the Software and create derivative works of it for any Noncommercial Use; and
(c) redistribute the Software and Your modifications, in source or object form, for any Noncommercial Use, provided that You include this License, unmodified, with any such redistribution, and that You do not remove or alter any copyright, patent, trademark, or attribution notices contained in the Software.
This grant does not extend to Commercial Use. Commercial Use requires a Commercial Entitlement under Section 3.
## 3. Commercial Use
You may not make Commercial Use of the Software during the applicable Phase unless You hold a valid, current Commercial Entitlement under a Commercial Use Agreement with the Licensor covering the applicable Phase. A Commercial Entitlement granted under one Phase's Commercial Use Agreement does not extend to a later Phase's Milestone Release unless the Commercial Use Agreement expressly says so.
This Section 3 states the existence and boundary of the commercial-use restriction. It does not itself set pricing, invoicing, metering, audit rights, or payment terms — those are governed exclusively by the applicable Commercial Use Agreement.
## 4. Patent License
Subject to the terms of this License, each contributor to the Software grants You, during the applicable Phase and solely to the extent of rights granted under Sections 2 and 3, a perpetual (subject to the termination below), worldwide, non-exclusive, no-charge, royalty-free patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Software, limited to those patent claims licensable by that contributor that are necessarily infringed by their contribution(s) alone or by combination of their contribution(s) with the Software.
If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Software or a contribution incorporated within it constitutes direct or contributory patent infringement, then any patent licenses granted to You under this Section 4 for the Software shall terminate as of the date such litigation is filed.
## 5. Automatic Conversion to the Future License
**5.1 Automatic effect.** Upon the Conversion Event for a Phase, the rights and restrictions in Sections 3 (Commercial Use) of this License, as they apply to that Phase's Milestone Release, terminate automatically. In their place, the Milestone Release is licensed under the Future License identified in that Phase's Phase Manifest, effective as of the Conversion Event, without any further act, declaration, or attestation required by the Licensor, any Trust Service, or any other party.
**5.2 Irrevocability.** Once a valid Conversion Event has occurred for a Phase, no subsequent refund, chargeback, accounting correction, dispute, or termination of this License for an unrelated breach shall revoke, suspend, or otherwise impair the Future License grant for that Phase's Milestone Release. Any shortfall or dispute arising after a Conversion Event is a commercial or accounting matter between the relevant parties and does not reinstate a commercial-use restriction over already-converted Software.
**5.3 Prior freedom preserved.** A later Phase covering subsequent improvements to the Software does not restrict, withdraw, or otherwise affect the rights granted under the Future License for an earlier Phase's already-converted Milestone Release.
**5.4 Evidence, not cause.** A Trust Service may publish a Conversion Attestation documenting a Conversion Event. Such an attestation is evidence that the Conversion Event occurred; it is not a condition of, and its absence or delay does not postpone, the automatic effect described in Section 5.1. Any person may independently verify whether a Conversion Event has occurred directly from the Phase Manifest and Target Ledger.
## 6. Successive Phases
The Licensor may declare a new Phase covering subsequent improvements to the Software following a Milestone Release's Conversion Event. Each Phase is independently governed by its own Phase Manifest, Initial Target, degeneration policy, and Target Ledger. Nothing in a later Phase's Phase Manifest may be construed to reduce or withdraw rights already granted under Section 5 for an earlier Phase's Milestone Release.
## 7. Term and Termination
**7.1 Term.** This License applies to the Software for the duration of the applicable Phase, and, for the Milestone Release, indefinitely following that Phase's Conversion Event under the Future License.
**7.2 Termination for breach.** If You breach Section 3 (Commercial Use) or Section 2(c) (redistribution notice requirement), the Licensor may terminate this License as to You. Before such termination becomes effective, the Licensor shall provide You written notice of the breach; if You cure the breach within thirty (30) days of that notice, this License continues in effect. A second breach of the same provision within twelve (12) months may be terminated immediately without a further cure opportunity.
**7.3 Effect of termination.** Termination under this Section 7 affects only Your rights under Sections 2 and 3 for the Phase in which the breach occurred. It does not affect any rights already vested under Section 5 (Automatic Conversion) for a Milestone Release whose Conversion Event has already occurred, per Section 5.2.
**7.4 Public record of breach and resolution.** The Licensor shall cause the Trust Service to publish, as part of the public record for the affected Phase, notice of: (a) any breach notice issued under Section 7.2, stating the general nature of the breach and the date of notice; (b) whether the breach was cured within the applicable cure period, and the date of cure; and (c) any termination determination made under this Section 7, including its effective date and scope. This public record exists to give the ecosystem a transparent, verifiable conformity signal for the Phase, distinct from and in addition to the Development Credit and Remission Credit facts already published under Section 5.4 and the Target Ledger Specification.
A breach that You dispute, and that has not been finally determined, shall be recorded as **alleged**; it shall be recorded as **determined** only once the cure period has run without cure, or the dispute has been resolved against You under the applicable Commercial Use Agreement's dispute process, if any. The Trust Service shall update the record promptly upon resolution in either direction. Recording an alleged or determined breach under this Section 7.4 is a ministerial act of publishing the Licensor's determination (or a dispute process's outcome); it does not give the Trust Service discretionary authority to decide whether a breach occurred, consistent with Section 5.4's evidence-not-cause principle.
Whether, and under what conditions, the public record identifies a Commercial Entitlement holder by name is governed exclusively by the applicable Commercial Use Agreement, which the Licensor and that Commercial Entitlement holder negotiate and agree to directly. This License does not itself set a naming default. Where no Commercial Use Agreement addresses the question, or where the affected party has no Commercial Use Agreement at all (for example, a Section 2(c) breach by a Noncommercial Use licensee), the public record states the Phase and breach category only, without naming the party.
## 8. Disclaimer of Warranty
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. THE LICENSOR DOES NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR THAT ANY PHASE WILL REACH ITS CONVERSION EVENT.
## 9. Limitation of Liability
IN NO EVENT SHALL THE LICENSOR OR ANY CONTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE, EXCEPT TO THE EXTENT SUCH LIMITATION IS PROHIBITED BY APPLICABLE LAW.
## 10. Trademarks
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary attribution.
## 11. General Provisions
**11.1 Governing law and venue.** Adopted for alpha/beta 2026-07-29: disputes arising under this License shall be resolved by binding arbitration, seated at a neutral, arbitration-mature venue (Singapore or London are the two candidates concretely supported by current research), rather than by litigation in a national court. The specific arbitral institution and substantive governing law remain a per-deployment blank pending final selection; they are not fixed by this candidate. See the canonical repository's `history/260729-TRSL-Jurisdiction-Synthesis.md` §2.
**11.2 Severability.** If any provision of this License is held unenforceable, the remaining provisions remain in full force, and the unenforceable provision shall be reformed to the minimum extent necessary to make it enforceable.
**11.3 No waiver.** Failure to enforce any provision of this License is not a waiver of future enforcement of that or any other provision.
**11.4 Entire agreement (as to licensing).** This License, together with the applicable Phase Manifest and, where applicable, the Commercial Use Agreement, constitutes the entire agreement between You and the Licensor regarding the Software's licensing terms. Operations, service, and consulting arrangements are governed by separate agreements, if any, and are not part of this License.
**11.5 Definitions control.** Marketing materials, documentation, or other non-normative communications about the Software must not describe pre-Conversion-Event Software as "Open Source," "free software," or "open core." Pre-conversion Noncommercial Use is **source-available**; pre-conversion Commercial Use requires a **Commercial Entitlement**; only post-conversion Software may be described as Open Source, under the Future License.
---
**No Phase is currently declared for this repository under this License.** Until a Phase Manifest is published and registered with the Trust Service for a Milestone Release in this repository, Sections 2–7 above have no operative subject matter here — this License establishes the governing framework in advance of that declaration, consistent with the org-wide rollout decision recorded in `target-revenue`'s `workplans/TREV-WP-0008-governance-and-pilot-rollout.md`.

View file

@ -0,0 +1,3 @@
module keycape
go 1.23.0

View file

@ -0,0 +1,171 @@
// Package jose provides strict RS256 JWT signature verification against a JWK
// set. It is deliberately narrow: it establishes that a token was signed by a
// key in the given set and returns its claims. It applies no claim policy —
// issuer, audience, expiry and nonce rules differ per caller and belong with
// the caller that knows them.
//
// The strictness here is the point. A verifier that accepts an unexpected
// algorithm, an unbounded exponent or an undersized modulus is worse than no
// verifier, because callers stop checking what it appears to guarantee.
package jose
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"math/big"
"strings"
)
// ErrVerification is returned for every verification failure. The cause is
// deliberately not distinguished: a caller acting on why a token failed tends to
// leak that distinction to whoever supplied the token.
var ErrVerification = errors.New("jose: token verification failed")
// KeySet maps key IDs to RSA public keys.
type KeySet map[string]*rsa.PublicKey
// ParseJWKS parses a JWK set, keeping only RSA signing keys usable with RS256.
// Keys that are malformed, too small, or carry an even or trivial exponent are
// rejected rather than skipped, since a key set that is partly wrong is not
// evidence of anything.
func ParseJWKS(raw []byte) (KeySet, error) {
var document struct {
Keys []struct {
Kty string `json:"kty"`
Use string `json:"use"`
Alg string `json:"alg"`
Kid string `json:"kid"`
N string `json:"n"`
E string `json:"e"`
} `json:"keys"`
}
if err := json.Unmarshal(raw, &document); err != nil {
return nil, ErrVerification
}
keys := make(KeySet, len(document.Keys))
for _, key := range document.Keys {
// Ignore keys for other algorithms or purposes; they are not errors.
if key.Kty != "RSA" || (key.Use != "" && key.Use != "sig") || (key.Alg != "" && key.Alg != "RS256") {
continue
}
if key.Kid == "" {
return nil, ErrVerification
}
modulus, errN := decode(key.N)
exponent, errE := decode(key.E)
if errN != nil || errE != nil || len(exponent) == 0 || len(exponent) > 4 {
return nil, ErrVerification
}
public := &rsa.PublicKey{
N: new(big.Int).SetBytes(modulus),
E: int(new(big.Int).SetBytes(exponent).Int64()),
}
if public.N.BitLen() < 2048 || public.E < 3 || public.E%2 == 0 {
return nil, ErrVerification
}
if _, duplicate := keys[key.Kid]; duplicate {
return nil, ErrVerification
}
keys[key.Kid] = public
}
if len(keys) == 0 {
return nil, ErrVerification
}
return keys, nil
}
// KeyID returns the key ID from a token's JOSE header, after checking that the
// header names RS256 and carries no critical extensions. Callers use it to
// decide whether to refresh their key set before verifying.
func KeyID(token string) (string, error) {
header, _, _, err := split(token)
if err != nil {
return "", err
}
return header.Kid, nil
}
// Verify checks a token's RS256 signature against the key set and returns its
// claims. It does not inspect any claim.
func Verify(token string, keys KeySet) (map[string]interface{}, error) {
return verify(token, keys, "")
}
// VerifyTyped applies the same signature verifier with an explicit JOSE type.
// Existing Verify callers retain their claim/type policy; online access-token
// consumers opt into the producer's exact JWT header contract here.
func VerifyTyped(token string, keys KeySet, expectedType string) (map[string]interface{}, error) {
if expectedType == "" {
return nil, ErrVerification
}
return verify(token, keys, expectedType)
}
func verify(token string, keys KeySet, expectedType string) (map[string]interface{}, error) {
header, signingInput, signature, err := split(token)
if err != nil {
return nil, err
}
if expectedType != "" && header.Typ != expectedType {
return nil, ErrVerification
}
public, ok := keys[header.Kid]
if !ok {
return nil, ErrVerification
}
digest := sha256.Sum256([]byte(signingInput))
if rsa.VerifyPKCS1v15(public, crypto.SHA256, digest[:], signature) != nil {
return nil, ErrVerification
}
payload, err := decode(strings.Split(signingInput, ".")[1])
if err != nil {
return nil, ErrVerification
}
claims := map[string]interface{}{}
if json.Unmarshal(payload, &claims) != nil {
return nil, ErrVerification
}
return claims, nil
}
type joseHeader struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
Kid string `json:"kid"`
Crit []string `json:"crit"`
}
// split validates the token's shape and header and returns the header, the
// signing input, and the decoded signature.
func split(token string) (joseHeader, string, []byte, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return joseHeader{}, "", nil, ErrVerification
}
raw, err := decode(parts[0])
if err != nil {
return joseHeader{}, "", nil, ErrVerification
}
var header joseHeader
if json.Unmarshal(raw, &header) != nil {
return joseHeader{}, "", nil, ErrVerification
}
// An unexpected algorithm must fail before any key is selected, so "none"
// and symmetric algorithms can never reach the RSA path.
if header.Alg != "RS256" || header.Kid == "" || len(header.Crit) != 0 {
return joseHeader{}, "", nil, ErrVerification
}
signature, err := decode(parts[2])
if err != nil {
return joseHeader{}, "", nil, ErrVerification
}
return header, parts[0] + "." + parts[1], signature, nil
}
func decode(segment string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(segment)
}

View file

@ -0,0 +1,379 @@
package tokenstatus
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"io"
"math"
"mime"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"time"
"keycape/internal/jose"
)
var ErrOnlineToken = errors.New("current authenticated token refused")
// OnlineAdmission is independently admitted owner configuration. The public
// JWKS/CA and private client credential files are reread for every request.
// It selects the owning confidential client, not a resource-server delegation.
type OnlineAdmission struct {
Issuer string `json:"issuer"`
Audience string `json:"audience"`
ClientID string `json:"client_id"`
CAFile string `json:"ca_file"`
JWKSFile string `json:"jwks_file"`
SecretFile string `json:"secret_file"`
MaxTokenAge string `json:"max_token_age"`
}
type ExpectedIdentity struct {
Subject string
Tenant string
PrincipalType string
}
// CurrentToken contains signed identity facts joined to a current issuer
// observation. It is never a permission, approval or portable execution receipt.
type CurrentToken struct {
Issuer string `json:"issuer"`
Audience string `json:"audience"`
ClientID string `json:"client_id"`
Subject string `json:"subject"`
Tenant string `json:"tenant"`
PrincipalType string `json:"principal_type"`
TokenID string `json:"jti"`
TokenDigest string `json:"token_sha256"`
IssuedAt time.Time `json:"issued_at"`
NotAfter time.Time `json:"not_after"`
CheckedAt time.Time `json:"checked_at"`
claims map[string]interface{}
}
// Claims returns a separate copy of the verified signed facts. Authentication
// assurance/age, tenant provenance, current groups and resource/effect policy
// remain the consumer's existing profile obligations, not a new client policy.
func (t CurrentToken) Claims() map[string]interface{} {
if t.claims == nil {
return nil
}
raw, _ := json.Marshal(t.claims)
var out map[string]interface{}
_ = json.Unmarshal(raw, &out)
return out
}
// InactiveForClient proves only the issuer's current negative response for the
// named client/token digest. An unknown/other-client token gives the same result;
// original grant ownership is separately required before asserting cleanup.
type InactiveForClient struct {
Issuer string `json:"issuer"`
ClientID string `json:"client_id"`
TokenDigest string `json:"token_sha256"`
CheckedAt time.Time `json:"checked_at"`
}
type OnlineClient struct {
admission OnlineAdmission
age time.Duration
now func() time.Time
// Nil uses the normal resolver/dialer. Tests route a certified DNS name to
// isolated loopback; the TLS name/CA and JWT trust checks remain unchanged.
dialContext func(context.Context, string, string) (net.Conn, error)
}
func NewOnlineClient(a OnlineAdmission) (*OnlineClient, error) {
u, err := url.Parse(a.Issuer)
if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || strings.ContainsAny(a.Issuer, "?#") || u.RawPath != "" || (u.Path != "" && path.Clean(u.Path) != u.Path) || strings.HasSuffix(a.Issuer, "/") {
return nil, ErrOnlineToken
}
if a.Audience == "" || a.ClientID == "" || !filepath.IsAbs(a.CAFile) || !filepath.IsAbs(a.JWKSFile) || !filepath.IsAbs(a.SecretFile) {
return nil, ErrOnlineToken
}
age, err := time.ParseDuration(a.MaxTokenAge)
if err != nil || age <= 0 || age > time.Hour {
return nil, ErrOnlineToken
}
return &OnlineClient{admission: a, age: age, now: time.Now}, nil
}
func readOnlineFile(path string, private bool) ([]byte, error) {
limit := int64(1 << 20)
if private {
limit = 16384
}
return readOnlineBoundedFile(path, private, limit)
}
func readOnlineBoundedFile(path string, private bool, limit int64) ([]byte, error) {
if limit <= 0 || limit > 1<<20 {
return nil, ErrOnlineToken
}
if !filepath.IsAbs(path) {
return nil, ErrOnlineToken
}
f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NONBLOCK, 0)
if err != nil {
return nil, ErrOnlineToken
}
defer f.Close()
info, err := f.Stat()
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0022 != 0 || (private && info.Mode().Perm()&0007 != 0) {
return nil, ErrOnlineToken
}
raw, err := io.ReadAll(io.LimitReader(f, limit+1))
if err != nil || len(raw) > int(limit) || len(raw) == 0 {
return nil, ErrOnlineToken
}
return raw, nil
}
func tokenDigest(raw string) string {
digest := sha256.Sum256([]byte(raw))
return "sha256:" + hex.EncodeToString(digest[:])
}
func exactUnix(value interface{}) (time.Time, bool) {
n, ok := value.(float64)
if !ok || n <= 0 || n > 9007199254740991 || math.Trunc(n) != n {
return time.Time{}, false
}
return time.Unix(int64(n), 0).UTC(), true
}
func (c *OnlineClient) verify(raw string, expected ExpectedIdentity, now time.Time) (CurrentToken, error) {
var out CurrentToken
if c == nil || len(raw) == 0 || len(raw) > 32768 || expected.Subject == "" || expected.Tenant == "" || (expected.PrincipalType != "human" && expected.PrincipalType != "service") {
return out, ErrOnlineToken
}
keysRaw, err := readOnlineFile(c.admission.JWKSFile, false)
if err != nil {
return out, ErrOnlineToken
}
keys, err := jose.ParseJWKS(keysRaw)
if err != nil {
return out, ErrOnlineToken
}
claims, err := jose.VerifyTyped(raw, keys, "JWT")
if err != nil {
return out, ErrOnlineToken
}
if claims["iss"] != c.admission.Issuer || claims["aud"] != c.admission.Audience || claims["sub"] != expected.Subject || claims["tenant"] != expected.Tenant || claims["principal_type"] != expected.PrincipalType {
return out, ErrOnlineToken
}
if azp, present := claims["azp"]; present && azp != c.admission.ClientID {
return out, ErrOnlineToken
}
_, scopeOK := claims["scope"].(string)
jti, jtiOK := claims["jti"].(string)
decodedID, idErr := base64.RawURLEncoding.Strict().DecodeString(jti)
issued, issuedOK := exactUnix(claims["iat"])
expires, expiresOK := exactUnix(claims["exp"])
if !scopeOK || !jtiOK || idErr != nil || len(decodedID) != 32 || !issuedOK || !expiresOK || issued.After(now) || !expires.After(issued) || !now.Before(expires) {
return out, ErrOnlineToken
}
if nbf, present := claims["nbf"]; present {
start, valid := exactUnix(nbf)
if !valid || start.After(now) {
return out, ErrOnlineToken
}
}
end := issued.Add(c.age)
if expires.Before(end) {
end = expires
}
if !now.Before(end) {
return out, ErrOnlineToken
}
return CurrentToken{Issuer: c.admission.Issuer, Audience: c.admission.Audience, ClientID: c.admission.ClientID,
Subject: expected.Subject, Tenant: expected.Tenant, PrincipalType: expected.PrincipalType,
TokenID: jti, TokenDigest: tokenDigest(raw), IssuedAt: issued, NotAfter: end, CheckedAt: now, claims: claims}, nil
}
type statusWire struct {
Active *bool `json:"active"`
ClientID string `json:"client_id"`
TokenType string `json:"token_type"`
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience string `json:"aud"`
Scope string `json:"scope"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
TokenID string `json:"jti"`
}
func decodeOnlineObject(raw []byte, out interface{}) (map[string]bool, error) {
d := json.NewDecoder(bytes.NewReader(raw))
first, err := d.Token()
if err != nil || first != json.Delim('{') {
return nil, ErrOnlineToken
}
seen := map[string]bool{}
for d.More() {
key, err := d.Token()
name, ok := key.(string)
if err != nil || !ok || seen[name] {
return nil, ErrOnlineToken
}
seen[name] = true
var value json.RawMessage
if d.Decode(&value) != nil || bytes.Equal(bytes.TrimSpace(value), []byte("null")) {
return nil, ErrOnlineToken
}
}
if last, err := d.Token(); err != nil || last != json.Delim('}') {
return nil, ErrOnlineToken
}
if _, err := d.Token(); err != io.EOF {
return nil, ErrOnlineToken
}
d = json.NewDecoder(bytes.NewReader(raw))
d.DisallowUnknownFields()
if d.Decode(out) != nil {
return nil, ErrOnlineToken
}
return seen, nil
}
func decodeOnlineStatus(raw []byte) (statusWire, error) {
var out statusWire
seen, err := decodeOnlineObject(raw, &out)
if err != nil || out.Active == nil || (!*out.Active && len(seen) != 1) {
return out, ErrOnlineToken
}
if *out.Active {
if len(seen) != 10 {
return out, ErrOnlineToken
}
for _, field := range []string{"active", "client_id", "token_type", "iss", "sub", "aud", "scope", "iat", "exp", "jti"} {
if !seen[field] {
return out, ErrOnlineToken
}
}
}
return out, nil
}
func (c *OnlineClient) request(ctx context.Context, endpoint, raw string) (statusWire, error) {
var out statusWire
if c == nil || ctx == nil || ctx.Err() != nil || len(raw) == 0 || len(raw) > 32768 || (endpoint != "/introspect" && endpoint != "/revoke") {
return out, ErrOnlineToken
}
ca, err := readOnlineFile(c.admission.CAFile, false)
if err != nil {
return out, ErrOnlineToken
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(ca) {
return out, ErrOnlineToken
}
secretRaw, err := readOnlineFile(c.admission.SecretFile, true)
if err != nil {
return out, ErrOnlineToken
}
secret := strings.TrimSuffix(strings.TrimSuffix(string(secretRaw), "\n"), "\r")
if secret == "" || strings.ContainsAny(secret, "\r\n\x00") {
return out, ErrOnlineToken
}
form := url.Values{"token": {raw}, "token_type_hint": {"access_token"}}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.admission.Issuer+endpoint, strings.NewReader(form.Encode()))
if err != nil {
return out, ErrOnlineToken
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cache-Control", "no-cache")
req.SetBasicAuth(url.QueryEscape(c.admission.ClientID), url.QueryEscape(secret))
transport := &http.Transport{Proxy: nil, TLSClientConfig: &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12}, DisableKeepAlives: true, DialContext: c.dialContext}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport, Timeout: 5 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return ErrOnlineToken }}
response, err := client.Do(req)
if err != nil {
return out, ErrOnlineToken
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK || response.Header.Get("Cache-Control") != "no-store" {
return out, ErrOnlineToken
}
body, err := io.ReadAll(io.LimitReader(response.Body, 65537))
if err != nil || len(body) > 65536 {
return out, ErrOnlineToken
}
if endpoint == "/revoke" {
if len(body) != 0 {
return out, ErrOnlineToken
}
return out, nil
}
media, _, err := mime.ParseMediaType(response.Header.Get("Content-Type"))
if err != nil || media != "application/json" {
return out, ErrOnlineToken
}
return decodeOnlineStatus(body)
}
// Check verifies expected identity before accessing the private client credential,
// sends a new authenticated introspection and rechecks lifetime after the network.
func (c *OnlineClient) Check(ctx context.Context, raw string, expected ExpectedIdentity) (CurrentToken, error) {
if c == nil || ctx == nil || ctx.Err() != nil {
return CurrentToken{}, ErrOnlineToken
}
identity, err := c.verify(raw, expected, c.now().UTC())
if err != nil {
return CurrentToken{}, ErrOnlineToken
}
status, err := c.request(ctx, "/introspect", raw)
if err != nil || status.Active == nil || !*status.Active {
return CurrentToken{}, ErrOnlineToken
}
expires, _ := exactUnix(identity.claims["exp"])
if status.ClientID != c.admission.ClientID || status.TokenType != "Bearer" || status.Issuer != identity.Issuer || status.Subject != identity.Subject || status.Audience != identity.Audience || status.TokenID != identity.TokenID || status.Scope != identity.claims["scope"] || status.IssuedAt != identity.IssuedAt.Unix() || status.ExpiresAt != expires.Unix() {
return CurrentToken{}, ErrOnlineToken
}
end := c.now().UTC()
if end.Before(identity.CheckedAt) {
return CurrentToken{}, ErrOnlineToken
}
// A slow lookup cannot retain expired facts or a withdrawn JWT trust key.
return c.verify(raw, expected, end)
}
// Revoke performs cleanup only for the exact opaque token. An empty 200 is not
// proof: a separate authenticated introspection must confirm inactive-for-client.
// The caller retains original grant ownership and uncertainty/retry duties.
func (c *OnlineClient) Revoke(ctx context.Context, raw string) (InactiveForClient, error) {
if c == nil {
return InactiveForClient{}, ErrOnlineToken
}
if _, err := c.request(ctx, "/revoke", raw); err != nil {
return InactiveForClient{}, ErrOnlineToken
}
return c.ObserveInactive(ctx, raw)
}
// ObserveInactive is read-only. Unavailability or a malformed response is never
// interpreted as revocation; the negative result remains scoped to this client.
func (c *OnlineClient) ObserveInactive(ctx context.Context, raw string) (InactiveForClient, error) {
if c == nil {
return InactiveForClient{}, ErrOnlineToken
}
status, err := c.request(ctx, "/introspect", raw)
if err != nil || status.Active == nil || *status.Active {
return InactiveForClient{}, ErrOnlineToken
}
return InactiveForClient{Issuer: c.admission.Issuer, ClientID: c.admission.ClientID, TokenDigest: tokenDigest(raw), CheckedAt: c.now().UTC()}, nil
}

View file

@ -0,0 +1,18 @@
{
"source_of_truth": "key-cape source; generated by tools/export_tokenstatus_client.py",
"source_files": [
"src/internal/authclient/online.go",
"src/internal/jose/jose.go",
"LICENSE"
],
"transformation": "replace only the first package declaration authclient with tokenstatus; all remaining client bytes and shared JOSE bytes exact",
"module": "keycape",
"profile": "issuer online access-token status; no policy evaluator or deployment",
"supported_file_boundary": "Linux owner execution profile; POSIX mode checks; not a Windows ACL contract",
"sha256": {
"LICENSE": "sha256:ff413f361a3b3bf44cf50f110fbcc4837064d57eefbcef89df2b43997c6262b6",
"go.mod": "sha256:e8b028e1002e63a76dba78a4abbd67882307ad198e4783bad631652061aa0f54",
"internal/jose/jose.go": "sha256:a6de154cd81e34a521b8aac5dce6b45a3cfb015681ed16e2beb8f2a24c72c00d",
"pkg/tokenstatus/client.go": "sha256:f34626a85d67590b9c61474f19ce1021f8afd17093f8f522b3f98e9cef34e575"
}
}

View file

@ -0,0 +1,130 @@
# 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
`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.

View file

@ -109,6 +109,11 @@ operation/inverse token must be acquired and revoked on every terminal path;
renewed authentication cannot revive a consumed approval or replay a mutation.
This issuer capability alone implements none of those consumer bindings.
The producer-owned [current-token client](current-token-client.md) supplies the
shared Go/CLI transport and cryptographic/status join. Its private observations
retain this distinction: an inactive-for-client result is not an original
ownership or execution receipt. Native client/profile adoption remains open.
Native completion still requires independent issuer/profile review, exact
image/config/trust admission, bounded retained-token custody, real human/service
issuance, private transport, current-token/revocation integration in each owner,

View file

@ -49,7 +49,7 @@ func main() {
return
}
if len(os.Args) > 1 && (os.Args[1] == "login" || os.Args[1] == "service-token" || os.Args[1] == "verify-client") {
if len(os.Args) > 1 && (os.Args[1] == "login" || os.Args[1] == "service-token" || os.Args[1] == "verify-client" || os.Args[1] == "check-current-token" || os.Args[1] == "revoke-token" || os.Args[1] == "check-inactive-token") {
if err := authclient.Run(context.Background(), os.Args[1:], os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)

View file

@ -25,6 +25,9 @@ func Run(ctx context.Context, args []string, stderr io.Writer) error {
return errors.New("expected login, service-token or verify-client")
}
mode := args[0]
if mode == "check-current-token" || mode == "revoke-token" || mode == "check-inactive-token" {
return runOnline(ctx, mode, args[1:], stderr)
}
if mode == "verify-client" {
return verifyClient(ctx, args[1:], stderr)
}

View file

@ -0,0 +1,379 @@
package authclient
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"io"
"math"
"mime"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"time"
"keycape/internal/jose"
)
var ErrOnlineToken = errors.New("current authenticated token refused")
// OnlineAdmission is independently admitted owner configuration. The public
// JWKS/CA and private client credential files are reread for every request.
// It selects the owning confidential client, not a resource-server delegation.
type OnlineAdmission struct {
Issuer string `json:"issuer"`
Audience string `json:"audience"`
ClientID string `json:"client_id"`
CAFile string `json:"ca_file"`
JWKSFile string `json:"jwks_file"`
SecretFile string `json:"secret_file"`
MaxTokenAge string `json:"max_token_age"`
}
type ExpectedIdentity struct {
Subject string
Tenant string
PrincipalType string
}
// CurrentToken contains signed identity facts joined to a current issuer
// observation. It is never a permission, approval or portable execution receipt.
type CurrentToken struct {
Issuer string `json:"issuer"`
Audience string `json:"audience"`
ClientID string `json:"client_id"`
Subject string `json:"subject"`
Tenant string `json:"tenant"`
PrincipalType string `json:"principal_type"`
TokenID string `json:"jti"`
TokenDigest string `json:"token_sha256"`
IssuedAt time.Time `json:"issued_at"`
NotAfter time.Time `json:"not_after"`
CheckedAt time.Time `json:"checked_at"`
claims map[string]interface{}
}
// Claims returns a separate copy of the verified signed facts. Authentication
// assurance/age, tenant provenance, current groups and resource/effect policy
// remain the consumer's existing profile obligations, not a new client policy.
func (t CurrentToken) Claims() map[string]interface{} {
if t.claims == nil {
return nil
}
raw, _ := json.Marshal(t.claims)
var out map[string]interface{}
_ = json.Unmarshal(raw, &out)
return out
}
// InactiveForClient proves only the issuer's current negative response for the
// named client/token digest. An unknown/other-client token gives the same result;
// original grant ownership is separately required before asserting cleanup.
type InactiveForClient struct {
Issuer string `json:"issuer"`
ClientID string `json:"client_id"`
TokenDigest string `json:"token_sha256"`
CheckedAt time.Time `json:"checked_at"`
}
type OnlineClient struct {
admission OnlineAdmission
age time.Duration
now func() time.Time
// Nil uses the normal resolver/dialer. Tests route a certified DNS name to
// isolated loopback; the TLS name/CA and JWT trust checks remain unchanged.
dialContext func(context.Context, string, string) (net.Conn, error)
}
func NewOnlineClient(a OnlineAdmission) (*OnlineClient, error) {
u, err := url.Parse(a.Issuer)
if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || strings.ContainsAny(a.Issuer, "?#") || u.RawPath != "" || (u.Path != "" && path.Clean(u.Path) != u.Path) || strings.HasSuffix(a.Issuer, "/") {
return nil, ErrOnlineToken
}
if a.Audience == "" || a.ClientID == "" || !filepath.IsAbs(a.CAFile) || !filepath.IsAbs(a.JWKSFile) || !filepath.IsAbs(a.SecretFile) {
return nil, ErrOnlineToken
}
age, err := time.ParseDuration(a.MaxTokenAge)
if err != nil || age <= 0 || age > time.Hour {
return nil, ErrOnlineToken
}
return &OnlineClient{admission: a, age: age, now: time.Now}, nil
}
func readOnlineFile(path string, private bool) ([]byte, error) {
limit := int64(1 << 20)
if private {
limit = 16384
}
return readOnlineBoundedFile(path, private, limit)
}
func readOnlineBoundedFile(path string, private bool, limit int64) ([]byte, error) {
if limit <= 0 || limit > 1<<20 {
return nil, ErrOnlineToken
}
if !filepath.IsAbs(path) {
return nil, ErrOnlineToken
}
f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NONBLOCK, 0)
if err != nil {
return nil, ErrOnlineToken
}
defer f.Close()
info, err := f.Stat()
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0022 != 0 || (private && info.Mode().Perm()&0007 != 0) {
return nil, ErrOnlineToken
}
raw, err := io.ReadAll(io.LimitReader(f, limit+1))
if err != nil || len(raw) > int(limit) || len(raw) == 0 {
return nil, ErrOnlineToken
}
return raw, nil
}
func tokenDigest(raw string) string {
digest := sha256.Sum256([]byte(raw))
return "sha256:" + hex.EncodeToString(digest[:])
}
func exactUnix(value interface{}) (time.Time, bool) {
n, ok := value.(float64)
if !ok || n <= 0 || n > 9007199254740991 || math.Trunc(n) != n {
return time.Time{}, false
}
return time.Unix(int64(n), 0).UTC(), true
}
func (c *OnlineClient) verify(raw string, expected ExpectedIdentity, now time.Time) (CurrentToken, error) {
var out CurrentToken
if c == nil || len(raw) == 0 || len(raw) > 32768 || expected.Subject == "" || expected.Tenant == "" || (expected.PrincipalType != "human" && expected.PrincipalType != "service") {
return out, ErrOnlineToken
}
keysRaw, err := readOnlineFile(c.admission.JWKSFile, false)
if err != nil {
return out, ErrOnlineToken
}
keys, err := jose.ParseJWKS(keysRaw)
if err != nil {
return out, ErrOnlineToken
}
claims, err := jose.VerifyTyped(raw, keys, "JWT")
if err != nil {
return out, ErrOnlineToken
}
if claims["iss"] != c.admission.Issuer || claims["aud"] != c.admission.Audience || claims["sub"] != expected.Subject || claims["tenant"] != expected.Tenant || claims["principal_type"] != expected.PrincipalType {
return out, ErrOnlineToken
}
if azp, present := claims["azp"]; present && azp != c.admission.ClientID {
return out, ErrOnlineToken
}
_, scopeOK := claims["scope"].(string)
jti, jtiOK := claims["jti"].(string)
decodedID, idErr := base64.RawURLEncoding.Strict().DecodeString(jti)
issued, issuedOK := exactUnix(claims["iat"])
expires, expiresOK := exactUnix(claims["exp"])
if !scopeOK || !jtiOK || idErr != nil || len(decodedID) != 32 || !issuedOK || !expiresOK || issued.After(now) || !expires.After(issued) || !now.Before(expires) {
return out, ErrOnlineToken
}
if nbf, present := claims["nbf"]; present {
start, valid := exactUnix(nbf)
if !valid || start.After(now) {
return out, ErrOnlineToken
}
}
end := issued.Add(c.age)
if expires.Before(end) {
end = expires
}
if !now.Before(end) {
return out, ErrOnlineToken
}
return CurrentToken{Issuer: c.admission.Issuer, Audience: c.admission.Audience, ClientID: c.admission.ClientID,
Subject: expected.Subject, Tenant: expected.Tenant, PrincipalType: expected.PrincipalType,
TokenID: jti, TokenDigest: tokenDigest(raw), IssuedAt: issued, NotAfter: end, CheckedAt: now, claims: claims}, nil
}
type statusWire struct {
Active *bool `json:"active"`
ClientID string `json:"client_id"`
TokenType string `json:"token_type"`
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience string `json:"aud"`
Scope string `json:"scope"`
IssuedAt int64 `json:"iat"`
ExpiresAt int64 `json:"exp"`
TokenID string `json:"jti"`
}
func decodeOnlineObject(raw []byte, out interface{}) (map[string]bool, error) {
d := json.NewDecoder(bytes.NewReader(raw))
first, err := d.Token()
if err != nil || first != json.Delim('{') {
return nil, ErrOnlineToken
}
seen := map[string]bool{}
for d.More() {
key, err := d.Token()
name, ok := key.(string)
if err != nil || !ok || seen[name] {
return nil, ErrOnlineToken
}
seen[name] = true
var value json.RawMessage
if d.Decode(&value) != nil || bytes.Equal(bytes.TrimSpace(value), []byte("null")) {
return nil, ErrOnlineToken
}
}
if last, err := d.Token(); err != nil || last != json.Delim('}') {
return nil, ErrOnlineToken
}
if _, err := d.Token(); err != io.EOF {
return nil, ErrOnlineToken
}
d = json.NewDecoder(bytes.NewReader(raw))
d.DisallowUnknownFields()
if d.Decode(out) != nil {
return nil, ErrOnlineToken
}
return seen, nil
}
func decodeOnlineStatus(raw []byte) (statusWire, error) {
var out statusWire
seen, err := decodeOnlineObject(raw, &out)
if err != nil || out.Active == nil || (!*out.Active && len(seen) != 1) {
return out, ErrOnlineToken
}
if *out.Active {
if len(seen) != 10 {
return out, ErrOnlineToken
}
for _, field := range []string{"active", "client_id", "token_type", "iss", "sub", "aud", "scope", "iat", "exp", "jti"} {
if !seen[field] {
return out, ErrOnlineToken
}
}
}
return out, nil
}
func (c *OnlineClient) request(ctx context.Context, endpoint, raw string) (statusWire, error) {
var out statusWire
if c == nil || ctx == nil || ctx.Err() != nil || len(raw) == 0 || len(raw) > 32768 || (endpoint != "/introspect" && endpoint != "/revoke") {
return out, ErrOnlineToken
}
ca, err := readOnlineFile(c.admission.CAFile, false)
if err != nil {
return out, ErrOnlineToken
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(ca) {
return out, ErrOnlineToken
}
secretRaw, err := readOnlineFile(c.admission.SecretFile, true)
if err != nil {
return out, ErrOnlineToken
}
secret := strings.TrimSuffix(strings.TrimSuffix(string(secretRaw), "\n"), "\r")
if secret == "" || strings.ContainsAny(secret, "\r\n\x00") {
return out, ErrOnlineToken
}
form := url.Values{"token": {raw}, "token_type_hint": {"access_token"}}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.admission.Issuer+endpoint, strings.NewReader(form.Encode()))
if err != nil {
return out, ErrOnlineToken
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cache-Control", "no-cache")
req.SetBasicAuth(url.QueryEscape(c.admission.ClientID), url.QueryEscape(secret))
transport := &http.Transport{Proxy: nil, TLSClientConfig: &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12}, DisableKeepAlives: true, DialContext: c.dialContext}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport, Timeout: 5 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return ErrOnlineToken }}
response, err := client.Do(req)
if err != nil {
return out, ErrOnlineToken
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK || response.Header.Get("Cache-Control") != "no-store" {
return out, ErrOnlineToken
}
body, err := io.ReadAll(io.LimitReader(response.Body, 65537))
if err != nil || len(body) > 65536 {
return out, ErrOnlineToken
}
if endpoint == "/revoke" {
if len(body) != 0 {
return out, ErrOnlineToken
}
return out, nil
}
media, _, err := mime.ParseMediaType(response.Header.Get("Content-Type"))
if err != nil || media != "application/json" {
return out, ErrOnlineToken
}
return decodeOnlineStatus(body)
}
// Check verifies expected identity before accessing the private client credential,
// sends a new authenticated introspection and rechecks lifetime after the network.
func (c *OnlineClient) Check(ctx context.Context, raw string, expected ExpectedIdentity) (CurrentToken, error) {
if c == nil || ctx == nil || ctx.Err() != nil {
return CurrentToken{}, ErrOnlineToken
}
identity, err := c.verify(raw, expected, c.now().UTC())
if err != nil {
return CurrentToken{}, ErrOnlineToken
}
status, err := c.request(ctx, "/introspect", raw)
if err != nil || status.Active == nil || !*status.Active {
return CurrentToken{}, ErrOnlineToken
}
expires, _ := exactUnix(identity.claims["exp"])
if status.ClientID != c.admission.ClientID || status.TokenType != "Bearer" || status.Issuer != identity.Issuer || status.Subject != identity.Subject || status.Audience != identity.Audience || status.TokenID != identity.TokenID || status.Scope != identity.claims["scope"] || status.IssuedAt != identity.IssuedAt.Unix() || status.ExpiresAt != expires.Unix() {
return CurrentToken{}, ErrOnlineToken
}
end := c.now().UTC()
if end.Before(identity.CheckedAt) {
return CurrentToken{}, ErrOnlineToken
}
// A slow lookup cannot retain expired facts or a withdrawn JWT trust key.
return c.verify(raw, expected, end)
}
// Revoke performs cleanup only for the exact opaque token. An empty 200 is not
// proof: a separate authenticated introspection must confirm inactive-for-client.
// The caller retains original grant ownership and uncertainty/retry duties.
func (c *OnlineClient) Revoke(ctx context.Context, raw string) (InactiveForClient, error) {
if c == nil {
return InactiveForClient{}, ErrOnlineToken
}
if _, err := c.request(ctx, "/revoke", raw); err != nil {
return InactiveForClient{}, ErrOnlineToken
}
return c.ObserveInactive(ctx, raw)
}
// ObserveInactive is read-only. Unavailability or a malformed response is never
// interpreted as revocation; the negative result remains scoped to this client.
func (c *OnlineClient) ObserveInactive(ctx context.Context, raw string) (InactiveForClient, error) {
if c == nil {
return InactiveForClient{}, ErrOnlineToken
}
status, err := c.request(ctx, "/introspect", raw)
if err != nil || status.Active == nil || *status.Active {
return InactiveForClient{}, ErrOnlineToken
}
return InactiveForClient{Issuer: c.admission.Issuer, ClientID: c.admission.ClientID, TokenDigest: tokenDigest(raw), CheckedAt: c.now().UTC()}, nil
}

View file

@ -0,0 +1,160 @@
package authclient
import (
"context"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// onlineObservation is a private producer-owned command result, not a signed
// authority receipt. The invoking owner authenticates binary/config/custody and
// binds its own case, descriptor, transaction, principal and exact effect.
type onlineObservation struct {
Schema string `json:"schema"`
Status string `json:"status"`
Current *CurrentToken `json:"current,omitempty"`
AuthenticationFacts map[string]interface{} `json:"signed_authentication_facts,omitempty"`
Inactive *InactiveForClient `json:"inactive_for_client,omitempty"`
}
func validOnlineDigest(raw string) bool {
if !strings.HasPrefix(raw, "sha256:") || len(raw) != 71 || strings.ToLower(raw) != raw {
return false
}
_, err := hex.DecodeString(raw[7:])
return err == nil
}
func parseOnlineFlags(fs *flag.FlagSet, args []string) error {
seen := map[string]bool{}
for i := 0; i < len(args); i++ {
name := strings.TrimLeft(args[i], "-")
if name == args[i] || name == "" {
return ErrOnlineToken
}
name, _, inline := strings.Cut(name, "=")
if fs.Lookup(name) == nil || seen[name] {
return ErrOnlineToken
}
seen[name] = true
if !inline {
i++
if i >= len(args) {
return ErrOnlineToken
}
}
}
if fs.Parse(args) != nil || fs.NArg() != 0 {
return ErrOnlineToken
}
return nil
}
func currentOnlineOutput(file *os.File) bool {
opened, err := file.Stat()
if err != nil {
return false
}
current, err := os.Lstat(file.Name())
return err == nil && current.Mode().IsRegular() && os.SameFile(opened, current)
}
func runOnline(ctx context.Context, mode string, args []string, stderr io.Writer) error {
fs := flag.NewFlagSet(mode, flag.ContinueOnError)
fs.SetOutput(io.Discard)
admissionFile := fs.String("admission-file", "", "independently admitted public profile")
tokenFile := fs.String("token-file", "", "private owner-acquired token JSON file")
digest := fs.String("token-sha256", "", "exact owner-bound access-token digest")
subject := fs.String("subject", "", "expected signed subject for current check")
tenant := fs.String("tenant", "", "expected signed tenant for current check")
principal := fs.String("principal-type", "", "human or service for current check")
out := fs.String("out", "", "new private observation file outside Git")
if parseOnlineFlags(fs, args) != nil || *admissionFile == "" || *tokenFile == "" || !filepath.IsAbs(*out) || !validOnlineDigest(*digest) {
return ErrOnlineToken
}
if mode == "check-current-token" && (*subject == "" || *tenant == "" || (*principal != "human" && *principal != "service")) {
return ErrOnlineToken
}
if (mode == "revoke-token" || mode == "check-inactive-token") && (*subject != "" || *tenant != "" || *principal != "") {
return ErrOnlineToken
}
raw, err := readOnlineFile(*admissionFile, false)
if err != nil {
return ErrOnlineToken
}
var admission OnlineAdmission
if _, err = decodeOnlineObject(raw, &admission); err != nil {
return ErrOnlineToken
}
client, err := NewOnlineClient(admission)
if err != nil {
return ErrOnlineToken
}
raw, err = readOnlineBoundedFile(*tokenFile, true, 131072)
if err != nil {
return ErrOnlineToken
}
var tokens Tokens
if _, err = decodeOnlineObject(raw, &tokens); err != nil || tokens.TokenType != "Bearer" || tokens.AccessToken == "" || tokenDigest(tokens.AccessToken) != *digest {
return ErrOnlineToken
}
parent, err := filepath.EvalSymlinks(filepath.Dir(*out))
if err != nil {
return ErrOnlineToken
}
info, err := os.Stat(parent)
if err != nil || !info.IsDir() || info.Mode().Perm()&0077 != 0 {
return ErrOnlineToken
}
file, err := reserveOutput(*out)
if err != nil {
return ErrOnlineToken
}
success := false
defer func() {
if !success && currentOnlineOutput(file) {
_ = os.Remove(file.Name())
}
_ = file.Close()
}()
observation := onlineObservation{Schema: "keycape.current-token-observation.v1"}
switch mode {
case "check-current-token":
current, err := client.Check(ctx, tokens.AccessToken, ExpectedIdentity{Subject: *subject, Tenant: *tenant, PrincipalType: *principal})
if err != nil {
return ErrOnlineToken
}
observation.Status = "current"
observation.Current = &current
facts := current.Claims()
observation.AuthenticationFacts = map[string]interface{}{"assurance": facts["assurance"], "tenant_source": facts["tenant_source"]}
case "revoke-token":
inactive, err := client.Revoke(ctx, tokens.AccessToken)
if err != nil {
return ErrOnlineToken
}
observation.Status = "inactive-for-client"
observation.Inactive = &inactive
case "check-inactive-token":
inactive, err := client.ObserveInactive(ctx, tokens.AccessToken)
if err != nil {
return ErrOnlineToken
}
observation.Status = "inactive-for-client"
observation.Inactive = &inactive
default:
return ErrOnlineToken
}
if json.NewEncoder(file).Encode(observation) != nil || file.Sync() != nil || !currentOnlineOutput(file) {
return ErrOnlineToken
}
success = true
_, _ = fmt.Fprintln(stderr, "PASS issuer token-status observation written; execution authority remains with its owner")
return nil
}

View file

@ -0,0 +1,166 @@
package authclient
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func onlinePrivateDir(t *testing.T) string {
t.Helper()
dir := t.TempDir()
if os.Chmod(dir, 0700) != nil {
t.Fatal("private fixture directory failed")
}
return dir
}
func (f *onlineFixture) cliInput(raw string) (string, string, []string) {
f.t.Helper()
dir := f.t.TempDir()
admission, token := filepath.Join(dir, "admission.json"), filepath.Join(dir, "token.json")
a, _ := json.Marshal(f.client.admission)
b, _ := json.Marshal(Tokens{AccessToken: raw, TokenType: "Bearer", ExpiresIn: 900})
if os.WriteFile(admission, a, 0644) != nil || os.WriteFile(token, b, 0600) != nil {
f.t.Fatal("fixture input write failed")
}
return admission, token, []string{"--admission-file", admission, "--token-file", token, "--token-sha256", tokenDigest(raw)}
}
func TestOnlineCLIUsesActualProducerAndPrivateObservation(t *testing.T) {
f := onlineProvider(t)
raw := f.issue()
_, _, base := f.cliInput(raw)
dir := onlinePrivateDir(t)
out := filepath.Join(dir, "current.json")
args := append([]string{"check-current-token"}, base...)
args = append(args, "--subject", onlineExpected.Subject, "--tenant", onlineExpected.Tenant, "--principal-type", "service", "--out", out)
var log bytes.Buffer
if Run(context.Background(), args, &log) != nil {
t.Fatal("current-token command failed")
}
data, err := os.ReadFile(out)
if err != nil {
t.Fatal("observation unavailable")
}
var observation onlineObservation
if json.Unmarshal(data, &observation) != nil || observation.Schema != "keycape.current-token-observation.v1" || observation.Status != "current" || observation.Current == nil || observation.Current.TokenDigest != tokenDigest(raw) || observation.AuthenticationFacts["assurance"] == nil {
t.Fatal("wrong producer observation")
}
info, _ := os.Stat(out)
if info.Mode().Perm() != 0600 {
t.Fatal("observation is public")
}
if strings.Contains(string(data), raw) || strings.Contains(log.String(), raw) || strings.Contains(log.String(), onlineExpected.Subject) || strings.Contains(log.String(), f.issuer.ClientConfig["current-client"].ClientSecret) {
t.Fatal("command exposed private facts")
}
if Run(context.Background(), args, &log) == nil {
t.Fatal("existing observation overwritten")
}
currentBefore, _ := os.ReadFile(out)
if !bytes.Equal(currentBefore, data) {
t.Fatal("prior observation changed")
}
revokedOut := filepath.Join(dir, "inactive.json")
args = append([]string{"revoke-token"}, base...)
args = append(args, "--out", revokedOut)
if Run(context.Background(), args, &log) != nil {
t.Fatal("cleanup observation failed")
}
data, _ = os.ReadFile(revokedOut)
if json.Unmarshal(data, &observation) != nil || observation.Status != "inactive-for-client" || observation.Inactive == nil || observation.Inactive.TokenDigest != tokenDigest(raw) {
t.Fatal("cleanup scope not explicit")
}
failedOut := filepath.Join(dir, "failed.json")
args = append([]string{"check-current-token"}, base...)
args = append(args, "--subject", onlineExpected.Subject, "--tenant", onlineExpected.Tenant, "--principal-type", "service", "--out", failedOut)
if Run(context.Background(), args, &log) == nil {
t.Fatal("revoked token accepted by actual command")
}
if _, err = os.Stat(failedOut); !os.IsNotExist(err) {
t.Fatal("failed command retained positive output")
}
}
func TestOnlineCLIRejectsChangedTokenAndUntrustedInput(t *testing.T) {
for _, kind := range []string{"wrong-hash", "duplicate-flag", "token-replaced", "duplicate-admission", "duplicate-token", "public-token", "shared-output-parent", "git-output", "output-symlink", "unknown-flag", "cleanup-identity-flags"} {
t.Run(kind, func(t *testing.T) {
f := onlineProvider(t)
raw := f.issue()
admission, token, base := f.cliInput(raw)
dir := onlinePrivateDir(t)
out := filepath.Join(dir, "result.json")
mode := "check-current-token"
extra := []string{"--subject", onlineExpected.Subject, "--tenant", onlineExpected.Tenant, "--principal-type", "service"}
switch kind {
case "wrong-hash":
base[len(base)-1] = "sha256:" + strings.Repeat("0", 64)
case "duplicate-flag":
base = append(base, "--token-sha256", tokenDigest(raw))
case "token-replaced":
other := f.issue()
b, _ := json.Marshal(Tokens{AccessToken: other, TokenType: "Bearer"})
_ = os.WriteFile(token, b, 0600)
case "duplicate-admission":
b, _ := os.ReadFile(admission)
b = append([]byte(`{"issuer":"https://forged.invalid",`), b[1:]...)
_ = os.WriteFile(admission, b, 0644)
case "duplicate-token":
b, _ := os.ReadFile(token)
b = append([]byte(`{"token_type":"Bearer",`), b[1:]...)
_ = os.WriteFile(token, b, 0600)
case "public-token":
_ = os.Chmod(token, 0644)
case "shared-output-parent":
_ = os.Chmod(dir, 0755)
case "git-output":
_ = os.WriteFile(filepath.Join(dir, ".git"), []byte("gitdir: unused"), 0600)
case "output-symlink":
target := filepath.Join(t.TempDir(), "untouched")
_ = os.WriteFile(target, []byte("original"), 0600)
_ = os.Symlink(target, out)
case "unknown-flag":
extra = append(extra, "--secret-raw=must-not-echo-this")
case "cleanup-identity-flags":
mode = "revoke-token"
}
args := append([]string{mode}, base...)
args = append(args, extra...)
args = append(args, "--out", out)
var log bytes.Buffer
if Run(context.Background(), args, &log) == nil || f.introspections.Load() != 0 || f.revocations.Load() != 0 {
t.Fatal("untrusted command input reached issuer")
}
if strings.Contains(log.String(), "must-not-echo-this") || strings.Contains(log.String(), raw) {
t.Fatal("refusal echoed input")
}
})
}
}
func TestOnlineCLIFailureDoesNotDeleteReplacedObservation(t *testing.T) {
f := onlineProvider(t)
raw := f.issue()
_, _, base := f.cliInput(raw)
dir := onlinePrivateDir(t)
out := filepath.Join(dir, "result.json")
f.afterStatus.Store(func() {
if os.Remove(out) != nil || os.WriteFile(out, []byte("replacement owned by another attempt"), 0600) != nil {
t.Error("replacement fixture failed")
}
})
args := append([]string{"check-current-token"}, base...)
args = append(args, "--subject", onlineExpected.Subject, "--tenant", onlineExpected.Tenant, "--principal-type", "service", "--out", out)
var log bytes.Buffer
if Run(context.Background(), args, &log) == nil {
t.Fatal("replaced observation reported successful")
}
data, _ := os.ReadFile(out)
if string(data) != "replacement owned by another attempt" {
t.Fatal("another attempt's output removed")
}
}

View file

@ -0,0 +1,20 @@
package authclient
import (
"os/exec"
"path/filepath"
"runtime"
"testing"
)
func TestOnlineExportMatchesItsSingleProducer(t *testing.T) {
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("producer source location unavailable")
}
root := filepath.Clean(filepath.Join(filepath.Dir(file), "../../.."))
cmd := exec.Command("python3", filepath.Join(root, "tools/export_tokenstatus_client.py"), "--check")
if err := cmd.Run(); err != nil {
t.Fatal("public adapter differs from the single producer source")
}
}

View file

@ -0,0 +1,78 @@
package authclient
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"keycape/internal/domain"
"keycape/internal/server/oidc"
)
type onlineHumanDirectory struct{ user domain.User }
func (d *onlineHumanDirectory) LookupUser(context.Context, string) (*domain.User, error) {
return &d.user, nil
}
func (d *onlineHumanDirectory) LookupGroups(context.Context, string) ([]domain.Group, error) {
return nil, nil
}
func (d *onlineHumanDirectory) ListUsers(context.Context) ([]domain.User, error) {
return []domain.User{d.user}, nil
}
func (d *onlineHumanDirectory) ValidatePassword(context.Context, string, string) (bool, error) {
return false, nil
}
func TestOnlineActualHumanPKCEFactsStayDistinctFromCurrentStatus(t *testing.T) {
f := onlineProvider(t)
directory := &onlineHumanDirectory{user: domain.User{ID: "uid=operator,ou=people,dc=fixture", Username: "operator", Enabled: true, Tenant: "tenant:fixture", Groups: []string{"fixture-allowed"}}}
client := f.issuer.ClientConfig["current-client"]
client.GrantTypes = []string{"authorization_code"}
client.ServiceSubject = ""
client.AllowedScopes = []string{"openid", "profile", "groups"}
client.RedirectURIs = []string{"https://owner.fixture.invalid/callback"}
f.issuer.Users = directory
f.status.Users = directory
verifier := strings.Repeat("v", 43)
hash := sha256.Sum256([]byte(verifier))
authenticated := time.Now().Add(-time.Minute).Truncate(time.Second)
code := f.issuer.Sessions.Create(&oidc.PKCESession{ClientID: client.ClientID, Username: directory.user.Username,
RedirectURI: client.RedirectURIs[0], PKCEChallengeMethod: "S256", PKCEChallenge: base64.RawURLEncoding.EncodeToString(hash[:]),
Scopes: client.AllowedScopes, MFAVerified: true, AuthTime: authenticated, ExpiresAt: time.Now().Add(time.Minute)})
form := url.Values{"grant_type": {"authorization_code"}, "client_id": {client.ClientID}, "code": {code}, "code_verifier": {verifier}, "redirect_uri": {client.RedirectURIs[0]}}
r := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
r.SetBasicAuth(url.QueryEscape(client.ClientID), url.QueryEscape(client.ClientSecret))
w := httptest.NewRecorder()
f.issuer.ServeHTTP(w, r)
var tokens Tokens
if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &tokens) != nil {
t.Fatal("actual human PKCE fixture failed")
}
expected := ExpectedIdentity{Subject: directory.user.ID, Tenant: directory.user.Tenant, PrincipalType: "human"}
current, err := f.client.Check(context.Background(), tokens.AccessToken, expected)
if err != nil {
t.Fatal("actual human current status failed")
}
facts := current.Claims()
assurance, ok := facts["assurance"].(map[string]interface{})
if !ok || assurance["level"] != "aal2" || assurance["mfa"] != true || assurance["at"] != float64(authenticated.Unix()) || facts["tenant_source"] != "directory" {
t.Fatal("signed authentication provenance changed or was upgraded to current time")
}
before := f.introspections.Load()
if _, err = f.client.Check(context.Background(), tokens.IDToken, expected); err == nil || f.introspections.Load() != before {
t.Fatal("ID token was substituted for access-token status")
}
directory.user.Groups = []string{"netkingdom-suspended"}
if _, err = f.client.Check(context.Background(), tokens.AccessToken, expected); err == nil {
t.Fatal("suspended human remained current")
}
}

View file

@ -0,0 +1,463 @@
package authclient
import (
"context"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"sync/atomic"
"syscall"
"testing"
"time"
"keycape/internal/domain"
"keycape/internal/jose"
"keycape/internal/server/oidc"
"keycape/internal/server/telemetry"
)
type onlineFixture struct {
t *testing.T
client *OnlineClient
issuer *oidc.TokenHandler
status *oidc.TokenStatusHandler
server *httptest.Server
mode atomic.Value
introspections atomic.Int64
revocations atomic.Int64
afterStatus atomic.Value
}
func onlineProvider(t *testing.T) *onlineFixture {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal("fixture key creation failed")
}
store, _ := oidc.NewTokenStatusStore(128)
f := &onlineFixture{t: t}
f.mode.Store("")
f.afterStatus.Store(func() {})
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { f.serve(w, r) }))
t.Cleanup(server.Close)
f.server = server
clients := map[string]*domain.Client{
"current-client": {ClientID: "current-client", ClientType: "confidential", ClientSecret: "synthetic + % /: current", Audience: "resource:fixture", GrantTypes: []string{"client_credentials"}, AllowedScopes: []string{"fixture.read"}, ServiceSubject: "service:fixture", Tenant: "tenant:fixture"},
"other-client": {ClientID: "other-client", ClientType: "confidential", ClientSecret: "synthetic other", Audience: "resource:fixture", GrantTypes: []string{"client_credentials"}, AllowedScopes: []string{"fixture.read"}, ServiceSubject: "service:fixture", Tenant: "tenant:fixture"},
}
f.issuer = &oidc.TokenHandler{ClientConfig: clients, Sessions: oidc.NewSessionStore(), SigningKey: key, Issuer: server.URL, TokenLifetime: 15 * time.Minute, Emitter: telemetry.NoopEmitter{}, Tokens: store}
f.status = &oidc.TokenStatusHandler{Tokens: store, ClientConfig: clients, SigningKey: &key.PublicKey, Issuer: server.URL}
dir := t.TempDir()
ca := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw})
caFile, keysFile, secretFile := filepath.Join(dir, "ca.pem"), filepath.Join(dir, "keys.json"), filepath.Join(dir, "caller")
if os.WriteFile(caFile, ca, 0644) != nil || os.WriteFile(secretFile, []byte(clients["current-client"].ClientSecret), 0600) != nil {
t.Fatal("fixture files failed")
}
keys := oidc.NewKeySet()
keys.AddKey("key-1", &key.PublicKey)
w := httptest.NewRecorder()
oidc.NewJWKSHandler(keys).ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/jwks", nil))
if os.WriteFile(keysFile, w.Body.Bytes(), 0644) != nil {
t.Fatal("fixture public keys failed")
}
f.client, err = NewOnlineClient(OnlineAdmission{Issuer: server.URL, Audience: "resource:fixture", ClientID: "current-client", CAFile: caFile, JWKSFile: keysFile, SecretFile: secretFile, MaxTokenAge: "5m"})
if err != nil {
t.Fatal("fixture admission failed")
}
return f
}
func (f *onlineFixture) serve(w http.ResponseWriter, r *http.Request) {
mode := f.mode.Load().(string)
switch r.URL.Path {
case "/token":
f.issuer.ServeHTTP(w, r)
case "/revoke":
f.revocations.Add(1)
if mode == "revoke-no-effect" {
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
return
}
if mode == "revoke-body" {
w.Header().Set("Cache-Control", "no-store")
fmt.Fprint(w, "unexpected")
return
}
f.status.Revoke(w, r)
case "/introspect":
f.introspections.Add(1)
if mode == "redirect" {
http.Redirect(w, r, f.server.URL+"/sink", 307)
return
}
if mode == "unavailable" {
w.WriteHeader(503)
return
}
if mode == "oversize" {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, strings.Repeat("x", 65537))
return
}
recorded := httptest.NewRecorder()
f.status.Introspect(recorded, r)
for key, values := range recorded.Header() {
for _, value := range values {
w.Header().Add(key, value)
}
}
if mode == "cached" {
w.Header().Set("Cache-Control", "max-age=30")
}
if mode == "wrong-content-type" {
w.Header().Set("Content-Type", "text/plain")
}
var body map[string]interface{}
if json.Unmarshal(recorded.Body.Bytes(), &body) != nil {
w.WriteHeader(recorded.Code)
_, _ = w.Write(recorded.Body.Bytes())
return
}
switch mode {
case "missing-active":
delete(body, "active")
case "wrong-active-type":
body["active"] = "true"
case "missing-scope":
delete(body, "scope")
case "null-scope":
body["scope"] = nil
case "wrong-client":
body["client_id"] = "other-client"
case "wrong-subject":
body["sub"] = "service:other"
case "wrong-issuer":
body["iss"] = "https://other.invalid"
case "wrong-audience":
body["aud"] = "resource:other"
case "wrong-jti":
body["jti"] = "different-token-id"
case "wrong-expiry":
body["exp"] = body["exp"].(float64) + 10
case "wrong-issued":
body["iat"] = body["iat"].(float64) - 1
case "wrong-scope":
body["scope"] = "different.scope"
case "wrong-token-type":
body["token_type"] = "Other"
case "unknown-field":
body["authority"] = true
}
f.afterStatus.Load().(func())()
w.WriteHeader(recorded.Code)
encoded, _ := json.Marshal(body)
if mode == "duplicate-active" {
encoded = append([]byte(`{"active":false,`), encoded[1:]...)
}
if mode == "trailing-json" {
encoded = append(encoded, []byte(`{}`)...)
}
_, _ = w.Write(encoded)
case "/sink":
f.t.Error("credential redirect followed")
w.WriteHeader(500)
default:
w.WriteHeader(404)
}
}
func (f *onlineFixture) issue() string {
f.t.Helper()
r := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(url.Values{"grant_type": {"client_credentials"}}.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := f.issuer.ClientConfig["current-client"]
r.SetBasicAuth(url.QueryEscape(client.ClientID), url.QueryEscape(client.ClientSecret))
w := httptest.NewRecorder()
f.issuer.ServeHTTP(w, r)
var token Tokens
if w.Code != http.StatusOK || json.Unmarshal(w.Body.Bytes(), &token) != nil || token.AccessToken == "" {
f.t.Fatal("actual fixture issuance failed")
}
return token.AccessToken
}
var onlineExpected = ExpectedIdentity{Subject: "service:fixture", Tenant: "tenant:fixture", PrincipalType: "service"}
func TestOnlineActualIssuerCurrentRevocationAndFreshGrant(t *testing.T) {
f := onlineProvider(t)
first := f.issue()
ctx := context.Background()
identity, err := f.client.Check(ctx, first, onlineExpected)
if err != nil || identity.Subject != onlineExpected.Subject || identity.ClientID != "current-client" || identity.TokenDigest != tokenDigest(first) || identity.TokenID == "" || !identity.CheckedAt.Before(identity.NotAfter) {
t.Fatal("current exact identity not established")
}
claims := identity.Claims()
claims["sub"] = "changed"
if identity.Claims()["sub"] != onlineExpected.Subject {
t.Fatal("returned facts share mutable state")
}
if _, err = f.client.Check(ctx, first, onlineExpected); err != nil || f.introspections.Load() != 2 {
t.Fatal("current status was cached")
}
if _, err = f.client.ObserveInactive(ctx, first); err == nil || f.revocations.Load() != 0 {
t.Fatal("read-only negative check changed active authority")
}
closed, err := f.client.Revoke(ctx, first)
if err != nil || closed.ClientID != "current-client" || closed.TokenDigest != identity.TokenDigest {
t.Fatal("issuer negative cleanup readback failed")
}
if _, err = f.client.Check(ctx, first, onlineExpected); err == nil {
t.Fatal("revoked token accepted")
}
before := f.revocations.Load()
if _, err = f.client.ObserveInactive(ctx, first); err != nil || f.revocations.Load() != before {
t.Fatal("independent negative readback failed or mutated token")
}
second := f.issue()
current, err := f.client.Check(ctx, second, onlineExpected)
if err != nil || current.TokenID == identity.TokenID || current.TokenDigest == identity.TokenDigest {
t.Fatal("fresh grant did not establish distinct identity")
}
}
func TestOnlineIdentityMismatchRefusesBeforeNetwork(t *testing.T) {
for _, kind := range []string{"subject", "tenant", "principal", "audience", "issuer", "tamper", "key", "no-jti", "ID-token", "stale"} {
t.Run(kind, func(t *testing.T) {
f := onlineProvider(t)
raw := f.issue()
expect := onlineExpected
switch kind {
case "subject":
expect.Subject = "other"
case "tenant":
expect.Tenant = "other"
case "principal":
expect.PrincipalType = "human"
case "audience":
f.client.admission.Audience = "other"
case "issuer":
f.client.admission.Issuer = "https://different.invalid"
case "tamper":
raw += "changed"
case "key":
key, _ := rsa.GenerateKey(rand.Reader, 2048)
raw = signToken(t, key, "key-1", map[string]interface{}{"iss": f.server.URL})
case "no-jti":
f.issuer.Tokens = nil
raw = f.issue()
case "ID-token":
identity, err := f.client.verify(raw, expect, time.Now())
if err != nil {
t.Fatal("fixture identity failed")
}
claims := identity.Claims()
delete(claims, "scope")
raw, err = oidc.BuildJWT(claims, "key-1", f.issuer.SigningKey)
if err != nil {
t.Fatal("fixture purpose token failed")
}
case "stale":
f.client.now = func() time.Time { return time.Now().Add(10 * time.Minute) }
}
if _, err := f.client.Check(context.Background(), raw, expect); !errors.Is(err, ErrOnlineToken) || f.introspections.Load() != 0 {
t.Fatal("unbound identity reached credential-bearing endpoint")
}
})
}
}
func TestOnlineRefusesAmbiguousOrMismatchedCurrentStatus(t *testing.T) {
for _, mode := range []string{"missing-active", "wrong-active-type", "missing-scope", "null-scope", "wrong-client", "wrong-subject", "wrong-issuer", "wrong-audience", "wrong-jti", "wrong-expiry", "wrong-issued", "wrong-scope", "wrong-token-type", "unknown-field", "duplicate-active", "trailing-json", "redirect", "unavailable", "oversize", "cached", "wrong-content-type"} {
t.Run(mode, func(t *testing.T) {
f := onlineProvider(t)
raw := f.issue()
f.mode.Store(mode)
if _, err := f.client.Check(context.Background(), raw, onlineExpected); !errors.Is(err, ErrOnlineToken) || err.Error() != ErrOnlineToken.Error() {
t.Fatal("unsafe response accepted or disclosed detail")
}
})
}
}
func TestOnlineCleanupRequiresNegativeReadbackAndPreservesScope(t *testing.T) {
f := onlineProvider(t)
raw := f.issue()
f.mode.Store("revoke-no-effect")
if _, err := f.client.Revoke(context.Background(), raw); err == nil {
t.Fatal("empty 200 mistaken for confirmed inactive state")
}
f.mode.Store("revoke-body")
if _, err := f.client.Revoke(context.Background(), raw); err == nil {
t.Fatal("unexpected cleanup output accepted")
}
f.mode.Store("")
f.client.admission.ClientID = "other-client"
if os.WriteFile(f.client.admission.SecretFile, []byte("synthetic other"), 0600) != nil {
t.Fatal("fixture credential write failed")
}
result, err := f.client.Revoke(context.Background(), raw)
if err != nil || result.ClientID != "other-client" {
t.Fatal("wrong-client negative scope not explicit")
}
f.client.admission.ClientID = "current-client"
_ = os.WriteFile(f.client.admission.SecretFile, []byte("synthetic + % /: current"), 0600)
if _, err = f.client.Check(context.Background(), raw, onlineExpected); err != nil {
t.Fatal("another client acquired revocation authority")
}
}
func TestOnlineReloadsCredentialsKeysAndTrustFiles(t *testing.T) {
for _, kind := range []string{"secret", "keys", "ca", "secret-world-readable", "public-writable", "secret-fifo"} {
t.Run(kind, func(t *testing.T) {
f := onlineProvider(t)
raw := f.issue()
if _, err := f.client.Check(context.Background(), raw, onlineExpected); err != nil {
t.Fatal("fixture current check failed")
}
switch kind {
case "secret":
_ = os.WriteFile(f.client.admission.SecretFile, []byte("changed synthetic"), 0600)
case "keys":
_ = os.WriteFile(f.client.admission.JWKSFile, []byte(`{"keys":[]}`), 0644)
case "ca":
_ = os.WriteFile(f.client.admission.CAFile, []byte("not a certificate"), 0644)
case "secret-world-readable":
_ = os.Chmod(f.client.admission.SecretFile, 0644)
case "public-writable":
_ = os.Chmod(f.client.admission.JWKSFile, 0666)
case "secret-fifo":
_ = os.Remove(f.client.admission.SecretFile)
if syscall.Mkfifo(f.client.admission.SecretFile, 0600) != nil {
t.Fatal("FIFO fixture failed")
}
}
if _, err := f.client.Check(context.Background(), raw, onlineExpected); err == nil {
t.Fatal("withdrawn or unsafe input reused")
}
})
}
}
func TestOnlineRechecksExpiryClockAndKeyAfterStatus(t *testing.T) {
for _, kind := range []string{"expiry", "clock-backwards", "withdrawn-key"} {
t.Run(kind, func(t *testing.T) {
f := onlineProvider(t)
raw := f.issue()
now := time.Now()
calls := 0
f.client.now = func() time.Time {
calls++
if calls == 1 {
return now
}
if kind == "expiry" {
return now.Add(10 * time.Minute)
}
if kind == "clock-backwards" {
return now.Add(-time.Second)
}
return now
}
if kind == "withdrawn-key" {
f.afterStatus.Store(func() { _ = os.WriteFile(f.client.admission.JWKSFile, []byte(`{"keys":[]}`), 0644) })
}
if _, err := f.client.Check(context.Background(), raw, onlineExpected); err == nil || f.introspections.Load() != 1 {
t.Fatal("delayed status preserved withdrawn identity")
}
})
}
}
func TestOnlineCancellationAndAmbientProxyDoNotLeakInputs(t *testing.T) {
f := onlineProvider(t)
if f.server.Certificate().VerifyHostname("example.com") != nil {
t.Fatal("fixture certificate lacks test DNS identity")
}
original, _ := url.Parse(f.server.URL)
_, port, _ := net.SplitHostPort(original.Host)
dnsAddress := net.JoinHostPort("example.com", port)
f.client.admission.Issuer = "https://" + dnsAddress
f.issuer.Issuer = f.client.admission.Issuer
f.status.Issuer = f.client.admission.Issuer
f.client.dialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
if address == dnsAddress {
address = original.Host
}
return (&net.Dialer{}).DialContext(ctx, network, address)
}
raw := f.issue()
proxyCalls := atomic.Int64{}
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { proxyCalls.Add(1); w.WriteHeader(500) }))
defer proxy.Close()
t.Setenv("HTTPS_PROXY", proxy.URL)
t.Setenv("HTTP_PROXY", proxy.URL)
t.Setenv("ALL_PROXY", proxy.URL)
t.Setenv("NO_PROXY", "")
if _, err := f.client.Check(context.Background(), raw, onlineExpected); err != nil || proxyCalls.Load() != 0 {
t.Fatal("ambient proxy affected explicit owner transport")
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
before := f.introspections.Load()
if _, err := f.client.Check(ctx, raw, onlineExpected); err == nil || f.introspections.Load() != before {
t.Fatal("cancelled check reached provider")
}
if _, err := f.client.Revoke(ctx, raw); err == nil || f.revocations.Load() != 0 {
t.Fatal("cancelled cleanup reached provider")
}
}
func TestOnlineMalformedAdmissionAndNegativeStatus(t *testing.T) {
f := onlineProvider(t)
for _, issuer := range []string{"http://issuer.invalid", "https://user@issuer.invalid", "https://issuer.invalid?", "https://issuer.invalid#", "https://issuer.invalid/../other", "https://issuer.invalid/%2Fother", "https://issuer.invalid/"} {
a := f.client.admission
a.Issuer = issuer
if _, err := NewOnlineClient(a); err == nil {
t.Fatal("ambiguous issuer admitted")
}
}
for _, raw := range []string{`null`, `[]`, `{}`, `{"active":null}`, `{"active":false,"sub":"hidden"}`, `{"active":false,"active":false}`, `{"active":false}{}`} {
if _, err := decodeOnlineStatus([]byte(raw)); err == nil {
t.Fatal("ambiguous inactive response accepted")
}
}
if status, err := decodeOnlineStatus([]byte(`{"active":false}`)); err != nil || *status.Active {
t.Fatal("exact inactive response refused")
}
}
func TestOnlineUsesSharedTypedJOSEVerifier(t *testing.T) {
f := onlineProvider(t)
raw := f.issue()
data, err := os.ReadFile(f.client.admission.JWKSFile)
if err != nil {
t.Fatal("fixture public keys unavailable")
}
keys, err := jose.ParseJWKS(data)
if err != nil {
t.Fatal("fixture public keys invalid")
}
if _, err = jose.VerifyTyped(raw, keys, "JWT"); err != nil {
t.Fatal("actual producer token rejected")
}
for _, typ := range []string{"", "at+jwt", "other"} {
if _, err = jose.VerifyTyped(raw, keys, typ); err == nil {
t.Fatal("unadmitted JWT type accepted")
}
}
if _, err = jose.Verify(raw, keys); err != nil {
t.Fatal("legacy shared signature verifier changed")
}
}

View file

@ -92,10 +92,27 @@ func KeyID(token string) (string, error) {
// Verify checks a token's RS256 signature against the key set and returns its
// claims. It does not inspect any claim.
func Verify(token string, keys KeySet) (map[string]interface{}, error) {
return verify(token, keys, "")
}
// VerifyTyped applies the same signature verifier with an explicit JOSE type.
// Existing Verify callers retain their claim/type policy; online access-token
// consumers opt into the producer's exact JWT header contract here.
func VerifyTyped(token string, keys KeySet, expectedType string) (map[string]interface{}, error) {
if expectedType == "" {
return nil, ErrVerification
}
return verify(token, keys, expectedType)
}
func verify(token string, keys KeySet, expectedType string) (map[string]interface{}, error) {
header, signingInput, signature, err := split(token)
if err != nil {
return nil, err
}
if expectedType != "" && header.Typ != expectedType {
return nil, ErrVerification
}
public, ok := keys[header.Kid]
if !ok {
return nil, ErrVerification
@ -117,6 +134,7 @@ func Verify(token string, keys KeySet) (map[string]interface{}, error) {
type joseHeader struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
Kid string `json:"kid"`
Crit []string `json:"crit"`
}

View file

@ -0,0 +1,43 @@
"""Export the producer's one online client; never maintain a consumer fork."""
from pathlib import Path
import argparse
import hashlib
import json
ROOT = Path(__file__).resolve().parent.parent
DESTINATION = ROOT / 'adapters/go/tokenstatus'
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--check', action='store_true')
args = parser.parse_args()
client = (ROOT / 'src/internal/authclient/online.go').read_bytes()
assert client.startswith(b'package authclient\n')
output = {
'pkg/tokenstatus/client.go': client.replace(b'package authclient\n', b'package tokenstatus\n', 1),
'internal/jose/jose.go': (ROOT / 'src/internal/jose/jose.go').read_bytes(),
'LICENSE': (ROOT / 'LICENSE').read_bytes(),
'go.mod': b'module keycape\n\ngo 1.23.0\n',
}
metadata = {
'source_of_truth': 'key-cape source; generated by tools/export_tokenstatus_client.py',
'source_files': ['src/internal/authclient/online.go', 'src/internal/jose/jose.go', 'LICENSE'],
'transformation': 'replace only the first package declaration authclient with tokenstatus; all remaining client bytes and shared JOSE bytes exact',
'module': 'keycape', 'profile': 'issuer online access-token status; no policy evaluator or deployment',
'supported_file_boundary': 'Linux owner execution profile; POSIX mode checks; not a Windows ACL contract',
'sha256': {name: 'sha256:' + hashlib.sha256(raw).hexdigest() for name,raw in sorted(output.items())},
}
output['producer-source.json'] = (json.dumps(metadata, indent=2) + '\n').encode()
actual = {str(p.relative_to(DESTINATION)) for p in DESTINATION.rglob('*') if p.is_file()}
if actual - set(output):
raise SystemExit('unexpected generated adapter file; reconcile explicitly')
if args.check:
if any(not (DESTINATION / name).is_file() or (DESTINATION / name).read_bytes()!=raw for name,raw in output.items()):
raise SystemExit('producer/consumer token-status source drift')
else:
for name, raw in output.items():
path=DESTINATION/name;path.parent.mkdir(parents=True, exist_ok=True);path.write_bytes(raw)
print(json.dumps({'status':'verified' if args.check else 'exported','files':len(output)}))
if __name__ == '__main__':
main()

View file

@ -82,3 +82,35 @@ An isolated control omitting the revoked-state predicate fails seven intended
checks. This closes source task T02 only. T01 independent issuer/profile review,
T03 native admission and all existing consumer operation/effect bindings remain
open. Evidence is retained by CUST-WP-0080 under keycape-token-status-r1.
## Provide the shared producer client and owner-private command observations
```task
id: KEY-WP-0042-T04
status: done
priority: high
state_hub_task_id: "4c71c9f7-8e14-5fec-8491-d7a7995f97e2"
```
2026-10-18 source outcome: `internal/authclient/online.go` owns strict current
token/status joining, explicit HTTPS/CA/JWKS/current-credential admission,
identity-first credential access, per-check rereads, response/clock/lifetime
rechecks and independent read-only inactive observation. The existing executable
exposes `check-current-token`, `revoke-token`, `check-inactive-token`, with exact
token digest binding and new private observation outputs outside Git. Negative
output is explicitly inactive-for-client, never global revocation/ownership.
The public five-file Go adapter is generated from the same client and JOSE bytes;
ordinary source checks refuse drift. Source/contract: `docs/current-token-client.md`.
The full source race suite passes 724 test/subtest results with the same six
native/external skips. Human PKCE tests retain actual subject/assurance/auth-time/
tenant provenance and refuse ID-token substitution. A separate source fixture
uses the built CLI, generated Go consumer and actual Warden kernel across fresh
owner processes, with real fixture HTTPS status and independent negative cleanup
checks. Owner approvals/case/catalog and target are synthetic; there is no native
admission or second real application acceptance. The first CLI fixture's directory
permissions, archive-only Warden Git metadata failure and original API absence are
retained as failures, not backfilled successes. Source/evidence:
`the-custodian/docs/changes/netkingdom-target-2026-10-18/keycape-current-client-r1/`.
T01 independent issuer/profile review, T03 native lifecycle, NK48/Apps actual
operation/inverse/ongoing-resource bindings and all full target gates remain open.