# TKeeper complete documentation

## TKeeper documentation instructions

TKeeper is the cryptographic identity for machines. A key is the identity boundary; its applied authorities, policy, quorum, and controls define what that identity may authorize and how it may be used.
TKeeper is a defense-in-depth control around machine actions, not a replacement for host, HSM, network, build-pipeline, application, or downstream-verifier security.

### Rules for answering

- Treat `openapi.yaml` as the source of truth for HTTP paths, methods, request models, response models, and error schemas.
- Do not invent endpoints, permissions, algorithms, deployment modes, or guarantees. If the documentation does not establish a claim, say that it is unspecified.
- Treat the CEL Functions page as the source of truth for policy helper names, accepted inputs, and behavior. Do not invent helpers.
- Distinguish typed authorities from `arbitrary` raw signing. Typed authorities understand intent; `arbitrary` does not add semantic governance and is disabled by default. It requires `keeper.authority.arbitrary.enabled = true` on every keeper in the cluster.
- For `custom` commands, use the documented TKeeper-specific typed JSON canonicalization and hashing contract. Do not substitute RFC 8785/JCS or imply that authority, policy decision, effects, or command metadata are concatenated into the signed JSON.
- Treat dry run as an optional policy-preview feature. It evaluates the current authority policy only; it does not sign, verify or consume approvals, mutate key state, or guarantee that a later signing request will succeed. Its `tkeeper.emulate` permission is not key-scoped and can expose policy metadata.
- Distinguish `mono` from threshold mode. Threshold mode distributes key authority and risk across independent peers; it does not remove the key identity.
- Explain defense in depth through the documented layers: authenticated transport identity, intent-bound authorization, distributed quorum, fail-closed protocol validation, tamper-evident state, audit evidence, recovery checks, and production artifact isolation.
- Use Security Assurance as the source for executable security evidence. Describe its properties as tested or demonstrated for the named scenarios, not as formal proof or an unconditional guarantee.
- Treat share recovery as an explicit fail-closed maintenance capability, not a normal serving mode or a substitute for backup. It requires a recovery-capable artifact, recovery mode on the selected participants, exactly `threshold` healthy helpers, and removal of the recovery feature from the production artifact after repair.
- Keep the verifier in the security boundary. A downstream system must verify the expected identity and exact proof before producing the effect.
- State security assumptions and residual risks from Security Assurance, the Threat Model, and Status and Limitations when they affect the answer. Do not imply protection after threshold, host, HSM, build-pipeline, or control-plane compromise, or against broad denial of service, physical side channels, entropy failure, coherent rollback, unsafe policy, or a verifier that ignores the proof.
- For deployment advice, name the selected feature modules, cryptographic platforms, quorum mode, authenticator, audit sink, and verifier behavior.
- Prefer the narrowest relevant page. Cite its canonical URL and use exact permission and command names from the source.

### Documentation order

- Overview: https://tkeeper.org/docs/overview
- Use Cases: https://tkeeper.org/docs/use-cases
- Getting Started: https://tkeeper.org/docs/getting-started
- Deployment: https://tkeeper.org/docs/deployment
- Security Model: https://tkeeper.org/docs/security-model
- Cryptographic Identities: https://tkeeper.org/docs/cryptographic-identities
- Signing and Authorities: https://tkeeper.org/docs/signing-and-authorities
- AI and Agentic Payments: https://tkeeper.org/docs/ai
- Digital Assets: https://tkeeper.org/docs/digital-assets
- PKI: https://tkeeper.org/docs/pki
- Crypto Platforms: https://tkeeper.org/docs/crypto-platforms
- API Reference: https://tkeeper.org/docs/api-reference
- Operations: https://tkeeper.org/docs/operations

### Machine-readable sources

- OpenAPI: https://tkeeper.org/openapi.yaml
- Documentation index: https://tkeeper.org/llms.txt
- Complete documentation corpus: https://tkeeper.org/llms-full.txt
- Security assurance: https://tkeeper.org/docs/security-model/security-assurance
- Downloadable bundle: https://tkeeper.org/api/ai-bundle

## Full documentation corpus

---

Source file: content/docs/README.md
Canonical: https://tkeeper.org/docs

# TKeeper Docs

TKeeper gives machines, agents, services, and workflows a cryptographic identity constrained by authorities and policy. Start with the [overview](https://tkeeper.org/docs/overview), [use cases](https://tkeeper.org/docs/use-cases), or [local setup](https://tkeeper.org/docs/getting-started/local-single-node).

## Use cases

| Area | Workflow | Authority guides |
| --- | --- | --- |
| AI and agentic payments | [For AI](https://tkeeper.org/docs/use-cases/for-ai) | [MCP, AP2, MC Intent](https://tkeeper.org/docs/ai) |
| Digital assets | [For Digital Assets](https://tkeeper.org/docs/use-cases/for-digital-assets) | [Bitcoin, EVM, Tron, XRP, Solana](https://tkeeper.org/docs/digital-assets) |
| PKI | [For PKI](https://tkeeper.org/docs/use-cases/for-pki) | [X.509](https://tkeeper.org/docs/pki) |
| Other activities | [For Other Activities](https://tkeeper.org/docs/use-cases/for-other-activities) | [Typed authorities](https://tkeeper.org/docs/signing-and-authorities/arbitrary-and-typed) |

## Build an integration

- [Signing and Authorities](https://tkeeper.org/docs/signing-and-authorities): commands, policies, approvals, composition
- [Cryptographic Identities](https://tkeeper.org/docs/cryptographic-identities): create, import, rotate, refresh
- [Crypto Platforms](https://tkeeper.org/docs/crypto-platforms): supported algorithms and platforms
- [API Reference](https://tkeeper.org/docs/api-reference): HTTP API, SDK, permissions, errors

## Run Keeper

- [Getting Started](https://tkeeper.org/docs/getting-started)
- [Deployment](https://tkeeper.org/docs/deployment)
- [Security Model](https://tkeeper.org/docs/security-model)
- [Operations](https://tkeeper.org/docs/operations)


---

Source file: content/docs/overview/README.md
Canonical: https://tkeeper.org/docs/overview

# Overview

TKeeper gives machines, agents, services, and workflows a governed cryptographic identity. In TKeeper, a key is the identity boundary: its attached authorities define what the identity is allowed to authorize.

Each action must be:

- understood as a typed intent
- governed by the key's authorities and policy
- bound to cryptographic proof that a downstream system can verify before execution

The enforcement rule for typed authorities:

```text
No understood and approved intent -> no cryptographic proof -> no effect.
```

## In this section

- [What is TKeeper?](https://tkeeper.org/docs/overview/what-is-tkeeper)
- [Governed Cryptographic Identity](https://tkeeper.org/docs/overview/governed-cryptographic-identity)
- [Use Cases](https://tkeeper.org/docs/overview/use-case-fit)
- [Architecture](https://tkeeper.org/docs/overview/architecture)
- [Status and Limitations](https://tkeeper.org/docs/overview/status-and-limitations)
- [Glossary](https://tkeeper.org/docs/overview/glossary)

## What TKeeper governs

| Area | What TKeeper controls |
| --- | --- |
| AI agents | Typed tool/action intents, spending, production actions, signed decisions |
| Crypto assets | EVM and Bitcoin transaction signing, treasury workflows |
| Certificates | X.509 issuance and workload identity operations |
| Internal systems | Typed commands, sensitive automation, privileged operations |
| Key lifecycle | DKG, import, refresh, rotate, destroy |

## What to read next

After this overview:

- run the local flow in [Getting Started](https://tkeeper.org/docs/getting-started)
- choose a deployment shape in [Quorum Modes](https://tkeeper.org/docs/security-model/quorum-modes)
- select build modules in [Build and Features](https://tkeeper.org/docs/deployment/build-and-features)
- review guarantees and non-goals in [Threat Model](https://tkeeper.org/docs/security-model/threat-model)


---

Source file: content/docs/overview/what-is-tkeeper.md
Canonical: https://tkeeper.org/docs/overview/what-is-tkeeper

# What is TKeeper?

TKeeper is an authority layer for cryptographic identity. It treats each key as an identity with attached authorities: what the identity can authorize, how a request is understood, which policy governs it, and what proof another system should verify before execution.

It is useful when a machine action has real consequences:

- move funds
- sign a transaction
- issue a certificate
- approve a spender
- rotate, import, refresh, or destroy a key
- let an automated agent request a governed tool or production action

Classic access control answers whether a caller can reach an API. TKeeper also answers whether the selected cryptographic identity may authorize the exact requested action.

## The basic flow

```text
Caller -> typed intent -> TKeeper controls -> bound proof -> downstream effect
```

The downstream system should execute the effect only after it verifies the expected identity, proof, and exact intent. It must also enforce freshness or replay protection where the action requires it.

If TKeeper does not approve the intent, it does not produce the proof. Without the proof, the action cannot continue in systems that depend on the governed identity.

## Product boundaries

TKeeper combines governed signing, key lifecycle control, threshold cryptography, and audit at the point where an identity uses its key.

It is not a generic secrets manager, a replacement for host or network security, or a standalone fraud, AML, prompt-injection, or risk engine. It can participate in wallet, CA, KMS, and agent workflows, but it does not replace the surrounding transaction builder, certificate authority, business system, or verifier.

External systems can detect, score, or decide. TKeeper enforces the cryptographic boundary: no approval, no signature; no approval, no lifecycle operation.

## Main concepts

| Concept | Meaning |
| --- | --- |
| Intent | The exact action being requested in a form TKeeper understands |
| Authority | The declared capability attached to a key identity |
| Policy | Rules attached to the key, authority, caller, and operation |
| Quorum mode | Whether one node or a threshold of peers controls key use |
| Proof | The cryptographic output bound to the approved action |

## Quorum modes

TKeeper supports two operating modes:

| Mode | Use when |
| --- | --- |
| `mono` | You need the same authority controls with local key material |
| `threshold` | You need key shares split across peers so one node cannot act alone |

Mono is useful for development, lower-impact deployments, and bootstrap phases. Threshold mode is the safer default for high-stakes keys because key use requires quorum participation.

## Build-time platforms

Cryptographic implementations are selected at build time:

| Platform | Provides |
| --- | --- |
| `platform-ecc` | ECC algorithms and protocols such as ECDSA, FROST, BIP-340, Taproot, and ECIES |
| `platform-pqc` | ML-DSA algorithms, threshold ML-DSA DKG, and threshold ML-DSA signing |

A deployable build must include at least one platform.


---

Source file: content/docs/overview/governed-cryptographic-identity.md
Canonical: https://tkeeper.org/docs/overview/governed-cryptographic-identity

# Governed Cryptographic Identity

TKeeper treats a key as a governed cryptographic identity.

Authentication identifies the caller, while key possession enables cryptographic action. A governed cryptographic identity adds a declared authority boundary: which actions the identity may authorize and which verifiable proof represents that authorization.

The identity is defined by:

- the key
- the authorities attached to that key
- the typed intents those authorities understand
- the policy that governs those intents
- the proof format downstream systems verify

Examples:

- a treasury identity that may sign only understood transaction intents
- a CA identity that may sign only certificate requests matching its authority policy
- an AI-agent identity that may sign only typed tool/action intents declared by an authority document attached to the key
- an internal-service identity that may sign only typed commands accepted by a backend
- a key lifecycle operation accepted by a quorum

These outputs matter because other systems trust them. TKeeper places controls before the output exists.

For AI agents, the model is not "sign whatever tool call the agent produced." The tool or action shape should be represented as a typed authority document, or manifest, attached to the key. TKeeper materializes the request into an intent, evaluates the declared policy and effects, and signs only the exact approved intent.

## Why this matters

Many automated systems can already decide what they want to do. The risky part is when the system can make the decision real.

TKeeper separates the request from the authority to complete it:

```text
Requesting an action is not the same as being allowed to authorize it as this cryptographic identity.
```

The caller submits an intent. TKeeper checks authentication, permissions, authority rules, key lifecycle state, optional four-eye approvals, audit requirements, quorum participation, and platform-specific cryptographic constraints.

Only then does it produce the signature, certificate, or key operation result.

## Enforcement boundary

TKeeper is strongest when the downstream system requires the cryptographic proof before executing the effect.

Good integration shape:

```text
1. Caller prepares intent
2. TKeeper approves or rejects the intent
3. TKeeper returns proof only if approved
4. Downstream system verifies proof
5. Downstream system executes the effect
```

Weak integration shape:

```text
1. Caller asks TKeeper for an opinion
2. Downstream system can ignore the answer
```

If the downstream system can bypass the governed identity, TKeeper cannot enforce that boundary by itself.

## Verifier contract

A valid signature proves that the corresponding key signed specific bytes. A secure integration must also decide what those bytes mean and whether they are acceptable now.

The verifier should:

- trust the expected identity or public key, not any mathematically valid key
- reconstruct or validate the exact intent that will be executed
- bind the proof to the correct environment, target, and operation
- enforce nonce, expiry, sequence, or idempotency rules where replay matters
- reject fields or effects that were not covered by the governed intent

TKeeper cannot repair a verifier that accepts a different payload, ignores unsigned fields, or allows the same proof to authorize unintended repeats.

## Authority path

The authority path is the part of the system where an intent becomes a consequence.

TKeeper keeps these pieces together:

| Stage | Question |
| --- | --- |
| Intent | What exact action is being requested, and is it understood? |
| Policy | Is this action allowed for this key identity, caller, and authority? |
| Quorum | Do enough peers accept the same action? |
| Audit | Can the decision be recorded before the effect? |
| Proof | What cryptographic output binds this identity to the approved action? |

If these pieces split apart, control weakens. A valid signature over the wrong data, a policy check not bound to the signed action, or an unverifiable approval record can all create bypasses.

## Relation to external risk systems

TKeeper does not need to be the system that detects every risk.

Other systems may provide verdicts:

- prompt-injection detection
- AML or KYT checks
- fraud scoring
- business approvals
- SIEM or SOAR decisions
- human review

TKeeper's job is to make the final cryptographic action depend on the accepted verdict and local policy state. The integration must bind that verdict to the same intent that is ultimately signed and executed.


---

Source file: content/docs/overview/use-cases.md
Canonical: https://tkeeper.org/docs/overview/use-case-fit

# Use-case fit

TKeeper fits workflows where a cryptographic identity is the final authority for an action and the consumer can verify proof before producing the effect.

| Scenario | Governed intent | Verifier | TKeeper does not replace |
| --- | --- | --- | --- |
| AI | MCP tool call, AP2 or MC VI payment, production action | tool backend or credential verifier | model security, sandboxing, or risk detection |
| Digital assets | Bitcoin, EVM, Tron, XRP, or Solana transaction | chain client, broadcaster, or custody backend | transaction construction, broadcast, or settlement monitoring |
| PKI | DER-encoded TBS certificate | relying party or CA pipeline | enrollment, serial allocation, revocation, or certificate publication |
| Other activities | typed privileged command | service that performs the command | business logic or host authorization |

Detailed integration guidance:

- [For AI](https://tkeeper.org/docs/use-cases/for-ai)
- [For Digital Assets](https://tkeeper.org/docs/use-cases/for-digital-assets)
- [For PKI](https://tkeeper.org/docs/use-cases/for-pki)
- [For Other Activities](https://tkeeper.org/docs/use-cases/for-other-activities)

## Fit test

Before adopting TKeeper, answer these questions:

1. What exact effect requires cryptographic authorization?
2. Can that effect be represented as a stable, canonical intent?
3. Which key identity is trusted to authorize it?
4. Which system verifies the proof, and what does it check besides signature validity?
5. Can the effect be produced through a path that bypasses that verifier?
6. Which context must be covered: environment, target, amount, chain, expiry, nonce, or approver verdict?
7. Is one compromised TKeeper node allowed to act as the identity?

If the effect can occur without the proof, TKeeper is advisory on that path. If the payload has no stable meaning, use of raw `arbitrary` signing must be an explicit security decision rather than a governed-intent claim.


---

Source file: content/docs/overview/architecture.md
Canonical: https://tkeeper.org/docs/overview/architecture

# Architecture

TKeeper is split into a core runtime, build-time feature modules, and build-time cryptographic platforms.

## Main path

```text
Client
  -> public API
  -> authentication and permissions
  -> intent materialization
  -> authority and key policy checks
  -> audit gate
  -> key/session manager
  -> mono or threshold cryptographic operation
  -> proof/result
```

For threshold operations, the coordinator starts the session, but peers validate the same operation before contributing.

## Runtime parts

| Part | Responsibility |
| --- | --- |
| Public API | External HTTP API for clients and operators |
| Internal API | Peer-to-peer protocol calls inside a cluster |
| Auth layer | Client authentication, internal peer authentication, permission checks |
| Authority layer | Converts command data into governed intent and policy checks |
| Key lifecycle | DKG, import, refresh, rotate, destroy, generation tracking |
| Session managers | Coordinate signing, DKG, destroy, and feature-specific multi-step operations |
| Audit layer | Records security-relevant decisions and can block operations if required sinks fail |
| Storage | Local key share state, metadata, authorities, audit state, and side data |

## Features

Features add product surface area. They are selected at build time.

Examples:

| Feature | Adds |
| --- | --- |
| `digital-assets:evm` | EVM transaction intent support |
| `digital-assets:bitcoin` | Bitcoin transaction intent support |
| `digital-assets:tron` | Tron transaction intent and composition |
| `digital-assets:xrp` | XRP transaction intent and composition |
| `digital-assets:solana` | Solana transaction intent and composition |
| `agentic-payments` | AP2 and MC VI mandate signing and composition |
| `mcp` | MCP discovery, utility, signing, and composition tools |
| `authority-x509` | Certificate issuance intent support |
| `ecies` | ECIES encryption and threshold decryption |
| `ui` | Control-plane UI |
| `seal-aws` | AWS KMS seal provider |
| `seal-gcloud` | Google Cloud KMS seal provider |

If a feature is not included in the artifact, its endpoints or command types are unavailable.

## Platforms

Platforms add algorithm implementations. They are selected separately from features.

| Platform | Adds |
| --- | --- |
| `ecc` | `SECP256K1`, `P256`, `ED25519`, ECC signing protocols, ECC key derivation, ECIES support |
| `pqc` | `MLDSA44`, `MLDSA65`, `MLDSA87`, ML-DSA DKG, ML-DSA signing |

Features that depend on a platform require that platform explicitly. Digital assets, agentic payments, X.509, and ECIES require `ecc`.

## Mono and threshold

| Mode | Crypto control point | Operational meaning |
| --- | --- | --- |
| `mono` | One local node | Same policy controls, no distributed key custody |
| `threshold` | Quorum of peers | Key use requires enough peers to accept and participate |

Threshold mode protects against a single peer using the key alone. It does not remove the need for host security, network security, backups, monitoring, and careful permission design.

## Integration boundary

TKeeper should sit on the authority path, not beside it.

Good placement:

```text
Business system requires TKeeper proof before executing the effect.
```

Weak placement:

```text
Business system can execute the same effect without the governed identity.
```

The second shape may still provide logging or advisory checks, but it is not strong enforcement.


---

Source file: content/docs/overview/status-and-limitations.md
Canonical: https://tkeeper.org/docs/overview/status-and-limitations

# Status and Limitations

These boundaries are part of the security contract, not implementation footnotes.

## Current shape

TKeeper currently provides:

- mono and threshold key modes
- governed signing
- key lifecycle operations: DKG, import, refresh, rotate, destroy
- authority-based signing commands
- four-eye control for supported operations
- signed audit records and audit sink enforcement
- build-time feature selection
- build-time cryptographic platform selection
- ECC and ML-DSA platform support
- optional ECIES support when the ECIES feature is built
- optional ECC and ML-DSA peer-share recovery when the recovery feature is built

## Build-time module limits

Features and platforms are selected when the artifact is built.

If a feature is missing, the related endpoint or command type is unavailable. If a required platform is missing, the build should fail early or the runtime cannot find the algorithm provider.

Use the build docs before deploying a custom artifact:

- [Build and Features](https://tkeeper.org/docs/deployment/build-and-features)

## ML-DSA limits

Threshold ML-DSA signing is probabilistic. A healthy operation can abort during rejection sampling and retry with fresh session state.

The retry cap is controlled by:

```text
keeper.session.mldsa.max-rounds
```

The default is `12`.

If the cap is exhausted, TKeeper returns `SESSION_MAX_ROUNDS_EXCEEDED`. This is an availability outcome. It does not automatically prove that a peer is dead or malicious.

ML-DSA refresh advances the generation while carrying each peer's existing share and public key forward unchanged. It does not replace shares or refresh cryptographic material. Use rotate or a new DKG when new ML-DSA material is required.

## Failure injection

Failure injection is only for integration tests.

The integration image includes every default production feature, the explicit recovery and
development-authentication features, every platform, both recovery platform modules, and the
test-only failure-injection module. Regular production builds do not include failure injection;
recovery is included only when selected.

Do not deploy the integration image as a production runtime.

## Trusted dealer import

Trusted dealer import intentionally starts from reconstructed or externally held key material. Use it only when that trust model is acceptable.

Threshold mode after trusted dealer import still requires quorum for later operations, but the import path itself depends on the dealer and the imported material being trusted.

## Policy limits

TKeeper can enforce only the boundaries it controls.

It cannot prevent an action if:

- the downstream system accepts another key
- the caller can bypass the governed proof
- policy is checked but not bound to the signed intent
- broad permissions allow unintended operations
- operators enable dev authentication without protecting its token and permissions as production credentials

Cryptographic validity alone does not establish business validity, freshness, or replay safety. The verifier must accept the expected key, validate the exact governed payload, and enforce any nonce, expiry, environment, or idempotency rules required by the action.

## Operational limits

Threshold cryptography adds distributed-system failure modes:

- peers can be unavailable
- sessions can time out
- one peer can see a partial operation while another does not
- consistency repair may be needed after crashes or partitions
- latency depends on quorum participation and protocol rounds

These costs must be included in availability targets and incident runbooks.

Quorum is not backup. Each peer has local state and independent seal dependencies; recovery must preserve enough shares without collapsing them into one administrative failure domain.

Do not assume mixed-version peer compatibility. Validate the exact upgrade path and keep the cluster on a consistent artifact, feature set, and platform set.

## Security review boundary

TKeeper relies on Anvil for protocol-level cryptographic implementations. Review TKeeper docs for product behavior and operational controls. Review Anvil materials for protocol-level assumptions, proofs, and implementation details.


---

Source file: content/docs/overview/glossary.md
Canonical: https://tkeeper.org/docs/overview/glossary

# Glossary

## Authority

The declared capability attached to a key identity. An authority defines what kind of action the identity can authorize and how TKeeper should understand and govern requests for that action.

Examples: arbitrary bytes signing, typed commands, EVM transactions, Bitcoin transactions, X.509 certificate issuance.

## Authority path

The path where a requested action becomes a real effect. TKeeper is meant to sit on this path, so the effect depends on the cryptographic proof TKeeper controls.

## Command

The API object that describes what the caller wants to sign or authorize. A command is materialized into an intent before policy and signing.

## Cryptographic proof

The output another system can verify before executing an effect. Usually this is a signature or certificate. Some optional features expose other governed cryptographic results.

## DKG

Distributed key generation. In threshold mode, peers create key shares without reconstructing a private key on one machine.

## Effect

A normalized consequence derived from an intent and exposed to policy, such as a token transfer, approval, certificate issuance, or typed internal action.

## Feature

A build-time module that adds product functionality, endpoints, authority types, UI, or providers.

Examples: `digital-assets`, `agentic-payments`, `authority-x509`, `ecies`, `ui`, `seal-aws`, `seal-gcloud`.

## Four-eye control

A control that requires independent approval before an operation can continue.

## Generation

A version of key material or key-share state for the same key id. Lifecycle operations can create new generations.

## Governed cryptographic identity

A key identity whose allowed actions are declared through authorities, governed by policy, and bound to cryptographic proof that downstream systems can verify before execution.

## Intent

The exact action being requested after TKeeper has parsed and normalized command data.

## Mono

The quorum mode where one local node holds and uses key material. Mono still uses TKeeper policy, authority, audit, and lifecycle controls.

## Platform

A build-time module that provides cryptographic algorithms and protocol implementations.

Examples: `platform-ecc`, `platform-pqc`.

## Quorum

The number of peers required to complete a threshold operation.

## Refresh

A lifecycle operation that creates a new generation without changing the aggregate key. Threshold ECC replaces the peer shares. ML-DSA carries each peer's existing share and public key forward unchanged.

## Rotate

A lifecycle operation that creates new key material.

## Threshold

The quorum mode where key material is split across peers and an operation needs enough peer participation to complete.

## Trusted dealer

An import model where a trusted source splits or provides key material to peers. It is useful for migration and external key onboarding, but the import path depends on trusting the dealer and imported material.

## Verifier

The downstream component that accepts a TKeeper proof and decides whether to execute the corresponding effect. It must validate the expected identity and exact intent, not only the signature equation.


---

Source file: content/docs/use-cases/README.md
Canonical: https://tkeeper.org/docs/use-cases

# Use Cases

Every supported action follows the same path:

```text
identity -> understood action -> policy -> proof -> verified execution
```

| Use case | Workflow | Authority guide |
| --- | --- | --- |
| AI | [For AI](https://tkeeper.org/docs/use-cases/for-ai) | [MCP and agentic payments](https://tkeeper.org/docs/ai) |
| Digital assets | [For Digital Assets](https://tkeeper.org/docs/use-cases/for-digital-assets) | [Chain authorities](https://tkeeper.org/docs/digital-assets) |
| PKI | [For PKI](https://tkeeper.org/docs/use-cases/for-pki) | [X.509](https://tkeeper.org/docs/pki) |
| Other activities | [For Other Activities](https://tkeeper.org/docs/use-cases/for-other-activities) | [Typed authorities](https://tkeeper.org/docs/signing-and-authorities/arbitrary-and-typed) |

The downstream system verifies the proof before acting. If an action can bypass that check, Keeper does not govern it.


---

Source file: content/docs/use-cases/for-ai.md
Canonical: https://tkeeper.org/docs/use-cases/for-ai

# For AI

TKeeper lets an agent host discover permitted identities and request signatures through MCP. An authority document limits what each identity can sign. The system that performs the action verifies the signature before acting.

## MCP

Build the `mcp` feature with the authority modules the agent needs:

```sh
./gradlew shadowJar -Pkeeper.features=mcp,agentic-payments,digital-assets -Pkeeper.platforms=ecc
```

`POST /mcp` serves [MCP `2026-07-28`](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/transports/streamable-http.mdx) over Streamable HTTP. It uses the public Keeper server's authentication. The agent host supplies its Keeper token; the model does not need the token. The endpoint supports `server/discover`, `tools/list`, and `tools/call`.

| Tool | Permission | Use |
| --- | --- | --- |
| `identity.list`, `identity.describe_authority` | `tkeeper.key.<keyId>.sign` | Find active signing identities and their command schemas |
| `utility.keeper_status` | `tkeeper.system.status` | Check readiness |
| `utility.get_public_key` | `tkeeper.key.<keyId>.public` | Read a public key |
| `utility.verify_signature` | `tkeeper.key.<keyId>.verify` | Verify a command signature |
| `action.sign`, `action.compose` | `tkeeper.key.<keyId>.sign` | Sign; compose a credential or transaction when supported |

The tool list reflects the token's permissions. Every call checks permission again for its target key. `identity.describe_authority` returns `metadata.description` and a JSON Schema for the command. The schema describes valid input; the authority policy decides whether that input is allowed. See [MCP connection and tools](https://tkeeper.org/docs/ai/mcp) for setup and the full tool list.

## Govern an MCP action

A package release is one example of a governed MCP action. The same tool can request signatures for other typed actions when their executing services verify the result. This authority binds release approval to the package name, version, registry, and artifact digest:

```yaml
schemaVersion: verdict.authority/v1
id: package-release
type: custom
version: 1.0.0
metadata:
  description: Authorize a package release to the configured registry.
config:
  fields:
    packageName: { type: string }
    version: { type: string }
    registry: { type: string }
    artifactSha256: { type: string }
    nonce: { type: string }
    expiresAt: { type: time }
  effects:
    - type: package.publish
      fields:
        packageName: "$packageName"
        version: "$version"
        registry: "$registry"
        artifactSha256: "$artifactSha256"
policy:
  id: package-release
  fallback: DENY
  approvers:
    release-owner:
      algorithm: ED25519
      publicKey64: "..."
  allow:
    - id: publish-sdk
      where:
        - "packageName == '@example/sdk'"
        - "registry == 'https://registry.npmjs.org'"
        - "effect.one(effects, 'package.publish')"
      approvals:
        threshold: 1
        approvers: [release-owner]
```

Attach the authority to the signing key through a digest-pinned OCI reference. Give the agent host `tkeeper.key.<keyId>.sign` only for that key. It can call `identity.describe_authority` to inspect the command schema, then submit:

```http
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: action.sign
X-JWT-TOKEN: <raw-jwt>

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"action.sign","arguments":{"keyId":"agent-release","command":{"type":"custom","authorityId":"package-release","artifact":{"scheme":"ECDSA","hash":"SHA256","typed":{"packageName":"@example/sdk","version":"2.4.0","registry":"https://registry.npmjs.org","artifactSha256":"<sha256-of-package-tarball>","nonce":"n-123","expiresAt":"2030-01-02T03:04:05Z"}}}},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}
```

TKeeper first returns `APPROVAL_REQUIRED`. The release owner reviews the package and build, signs the exact request hash, and the host resubmits the unchanged command with that approval. See [four-eye control](https://tkeeper.org/docs/security-model/four-eye-control) for the proof format.

The publishing service has registry access. It verifies the TKeeper signature against `agent-release` and the exact command, compares `artifactSha256` with the package tarball it will publish, checks `expiresAt`, and consumes `nonce` once. A different tarball needs a new approval. The publisher must be the only path to the registry for this workflow; direct publish credentials would bypass the check. TKeeper authorizes the release command and does not inspect the package for malware.

## Agentic payments

The `agentic-payments` feature adds AP2 (`ap2.mandate`) and Mastercard Verifiable Intent (`mcintent.mandate`). Both require a P-256 key and ES256. A policy can limit merchants, payment methods, each purchase, and the total request. For example, the [AP2 purchase authority](https://tkeeper.org/docs/ai/agentic-payments/ap2) allows the configured shop and card, at most USD 100 per purchase and USD 150 per request; the [MC VI authority](https://tkeeper.org/docs/ai/agentic-payments/mastercard-verifiable-intent) applies the same limits.

```java
var artifact = new Ap2Mandates(PaymentRequestMode.PAIRED, signingInput, disclosures);
var command = Command.of("test:ap2/purchases", artifact);
var credential = client.signature().compose(
        Sign.of(p256KeyId, command), PaymentCredential.class).credential();
```

`signingInput` is the encoded JWS header and payload joined by `.`; `disclosures` contains every referenced SD-JWT disclosure. Keeper checks their commitments, runs the authority policy, signs the exact JWS input, and returns a credential layer. Use `McMandates` for MC VI. The recipient must verify the credential chain, audience, expiry, replay rules, and merchant identity.


---

Source file: content/docs/use-cases/for-digital-assets.md
Canonical: https://tkeeper.org/docs/use-cases/for-digital-assets

# For Digital Assets

TKeeper parses an unsigned transaction, evaluates its effects against the key's authority, then signs. The transaction builder handles nonce or UTXO selection, fees, and broadcast.

```sh
./gradlew shadowJar -Pkeeper.features=digital-assets -Pkeeper.platforms=ecc
```

Select `bitcoin`, `evm`, `tron`, `xrp`, or `solana` to build one authority.

| Authority | SDK artifact | Key | Compose result |
| --- | --- | --- | --- |
| `bitcoin.transaction` | `UtxoInput` | secp256k1 | Signature only |
| `evm.transaction` | `UnsignedEvmTransaction.message64` (Base64) | secp256k1 | Signed transaction hex and hash |
| `tron.transaction` | `UnsignedTronTransaction.transaction` (JSON) | secp256k1 | Signed transaction JSON and hash |
| `xrp.transaction` | `UnsignedXrpTransaction.transaction` (hex) | secp256k1 or Ed25519 | Signed transaction hex and hash |
| `solana.transaction` | `UnsignedSolanaTransaction.transaction` (Base64) | Ed25519 | Signed transaction Base64, `complete`, and hash when complete |

## Example: limit a Tron transfer

This authority permits one recipient and at most 100 TRX. Tron transfer amounts and `feeLimit` are measured in [sun](https://developers.tron.network/docs/token-standards-trx); 100 TRX is 100,000,000 sun.

```yaml
schemaVersion: verdict.authority/v1
id: test:tron/native-transfer
type: tron.transaction
version: 1.0.0
config: {}
policy:
  id: tron-native-transfer
  fallback: DENY
  variables:
    recipient: "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"
  allow:
    - id: small-transfer
      where:
        - "feeLimit <= 30000000"
        - "effect.onlyTypes(effects, ['native.transfer'])"
        - "effect.one(effects, 'native.transfer')"
        - "effect.all(effects, 'native.transfer', {'asset': 'trx', 'to': recipient})"
        - "bigint.gt(effect.amount(effects, 'native.transfer'), '0')"
        - "bigint.lte(effect.amount(effects, 'native.transfer'), '100000000')"
```

Attach the [complete authority document](https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/src/testFixtures/resources/authorities/tron/native-transfer.yaml) to a secp256k1 key through a digest-pinned OCI reference. Build the unsigned transaction with that key as its owner. Sign and compose it:

```java
var artifact = new UnsignedTronTransaction(unsignedTronJson);
var command = Command.of("test:tron/native-transfer", artifact);
var signed = client.signature().compose(
        Sign.of(tronKeyId, command), SignedTronTransaction.class);
String transactionJson = signed.rawTransaction();
String transactionHash = signed.transactionHash();
```

The integration test checks the owner key, policy boundary, unchanged transaction body, signature, and returned hash. The broadcaster still decides when and where to submit `transactionJson`.

When signing with a `tweak`, build Bitcoin, Tron, XRP, or Solana transactions from the public key returned by `central().getPublicKey(keyId, tweak)`. Keeper checks the transaction's owner or required signer against that derived key on every participating peer.

For XRP, policy amounts are [drops](https://xrpl.org/docs/introduction/transactions-and-requests); for Solana, native transfer amounts are [lamports](https://solana.com/docs/references/terminology). Use raw protocol units in limits. EVM composition covers legacy and typed transactions 1–4; type 3 broadcast also needs its blob sidecar. Solana can return a partial transaction: check `complete` before broadcasting.

See the separate [Bitcoin](https://tkeeper.org/docs/digital-assets/bitcoin), [EVM](https://tkeeper.org/docs/digital-assets/evm), [Tron](https://tkeeper.org/docs/digital-assets/tron), [XRP](https://tkeeper.org/docs/digital-assets/xrp), and [Solana](https://tkeeper.org/docs/digital-assets/solana) authority guides.


---

Source file: content/docs/use-cases/for-pki.md
Canonical: https://tkeeper.org/docs/use-cases/for-pki

# For PKI

An X.509 authority governs a CA key's signature over a DER-encoded `TBSCertificate`. The CA service builds the certificate; TKeeper checks its parsed fields and signs only after policy and any required approvals pass.

```sh
./gradlew shadowJar -Pkeeper.features=authority-x509 -Pkeeper.platforms=ecc
```

## Example: service leaf certificate

This authority accepts a server certificate for `api.svc.test`, with no CA privilege:

```yaml
schemaVersion: verdict.authority/v1
id: test:x509/service-leaf
type: x509.tbs-certificate
version: 1.0.0
config: {}
policy:
  id: test-x509-service-leaf
  fallback: DENY
  allow:
    - id: allow-service-leaf
      where:
        - "version == 3"
        - "subject.commonName == 'api.svc.test'"
        - "extensions.basicConstraints.present"
        - "!extensions.basicConstraints.cA"
        - "extensions.keyUsage.digitalSignature"
        - "extensions.extKeyUsage.serverAuth"
```

The [integration fixture](https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/src/testFixtures/resources/authorities/x509/service-leaf.yaml) uses this rule. The [X.509 authority guide](https://tkeeper.org/docs/pki/x509#authority-example-workload-server-certificate) adds issuer, DNS SAN, validity, signature algorithm, and operator approval checks for a stricter profile.

The CA service submits exactly the TBS bytes it will publish:

```java
var tbs = new TBSDerEncoded(Base64.getEncoder().encodeToString(tbsDer));
var command = Command.of("test:x509/service-leaf", tbs);
var signature = client.signature().sign(Sign.of(issuingKeyId, command));
byte[] signatureBytes = Base64.getDecoder().decode(signature.signature64());
```

The CA service assembles the final certificate and checks that its signature algorithm matches the key. Enrollment authorization, proof of possession, serial-number uniqueness, revocation, and publication remain in the PKI. Relying parties validate the finished certificate and chain.


---

Source file: content/docs/use-cases/for-other-activities.md
Canonical: https://tkeeper.org/docs/use-cases/for-other-activities

# For Other Activities

Use a `custom` authority when a service will execute a sensitive typed command after verifying a TKeeper signature. The authority declares input fields and policy effects. Raw `arbitrary` signing has no intent policy.

## Example: administrative change

This authority allows a normal rotation, reconciliation, or freeze tied to an approved change ticket:

```yaml
schemaVersion: verdict.authority/v1
id: test:custom/admin-ops
type: custom
version: 1.0.0
config:
  fields:
    purpose: { type: string }
    operation: { type: string }
    ticket: { type: string }
    emergency: { type: bool, required: false, default: false }
policy:
  id: test-custom-admin-ops
  fallback: DENY
  allow:
    - id: allow-normal-admin-change
      where:
        - "purpose == 'admin'"
        - "operation in ['rotate', 'reconcile', 'freeze']"
        - "ticket in ['INC-0001', 'INC-0002']"
      unless:
        - "emergency"
```

Attach the [authority document](https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/src/testFixtures/resources/authorities/custom/admin-ops.yaml) by digest-pinned OCI reference to the administrative key. A matching command is:

```json
{
  "keyId": "admin-automation",
  "command": {
    "type": "custom",
    "authorityId": "test:custom/admin-ops",
    "artifact": {
      "scheme": "ECDSA",
      "hash": "SHA256",
      "typed": {
        "purpose": "admin",
        "operation": "rotate",
        "ticket": "INC-0001",
        "emergency": false
      }
    }
  }
}
```

Submit it to `POST /v2/keeper/sign` or through `action.sign` on MCP. The executing service verifies the signature against the expected key and exact command, then enforces its own ticket state and replay rules. Add target, environment, expiry, and nonce fields when they affect the action. The backend must not act on undeclared fields. [Typed signing material](https://tkeeper.org/docs/signing-and-authorities/arbitrary-and-typed#typed-json-signing-material) defines the byte representation used for verification.


---

Source file: content/docs/getting-started/README.md
Canonical: https://tkeeper.org/docs/getting-started

# Getting Started

Start with:

- [Local Single Node](https://tkeeper.org/docs/getting-started/local-single-node)

The local guide is a transport and cryptography smoke test. It uses developer authentication, one node, `mono` mode, and `arbitrary` raw signing. It does not demonstrate typed intent policy or production custody.

After the smoke test:

- [Authorities](https://tkeeper.org/docs/signing-and-authorities/authorities)
- [Quorum Modes](https://tkeeper.org/docs/security-model/quorum-modes)
- [Build and Features](https://tkeeper.org/docs/deployment/build-and-features)
- [Authentication and Authorization](https://tkeeper.org/docs/security-model/authentication-and-authorization)


---

Source file: content/docs/getting-started/local-single-node.md
Canonical: https://tkeeper.org/docs/getting-started/local-single-node

# Local Single Node

This guide runs one local node with developer authentication, initializes it, creates one demo key identity, signs one request, and verifies the returned proof.

This is not a production deployment guide. It uses:

- one node
- `mono` key mode
- Shamir seal provider with `1-of-1` recovery
- developer token authentication
- the `arbitrary` authority only to demonstrate the sign/verify path

For real governed identities, use typed or concrete authorities so TKeeper can understand the requested action before signing it. `arbitrary` does not give TKeeper a structured intent or policy surface.

## What this proves

The local flow proves that configuration, initialization, sealing, key creation, signing, and verification work end to end:

```text
request -> raw-signing identity -> signature -> verification
```

It does not prove that TKeeper understands a business action or enforces intent policy. That begins when `arbitrary` is replaced by a `custom` or native typed authority.

## Requirements

- Java 25
- a built TKeeper jar
- `curl`

Build the jar with the explicit developer-auth feature:

```bash
./gradlew :build -Pkeeper.features=all,auth-dev -Pkeeper.platforms=all
```

The jar is:

```text
build/libs/tkeeper-2.5.0.jar
```

## Create local config

Create `/tmp/tkeeper/application.conf`:

```hocon
auth { type = "dev" }

boot { token = "local-boot-token" }

keeper {
  database { path = "/tmp/tkeeper/db" }

  authority { arbitrary { enabled = true } }

  providers {
    selected = "shamir"
    shamir {
      threshold = 1
      total = 1
    }
  }

  server {
    public {
      host = "0.0.0.0"
      port = 8080
    }
    internal {
      host = "0.0.0.0"
      port = 9090
    }
  }
}
```

Create `/tmp/tkeeper/dev.conf`:

```hocon
keeper.dev {
  token = "dev-token"
  permissions = [
    "tkeeper.system.init",
    "tkeeper.system.unseal",
    "tkeeper.system.seal",
    "tkeeper.dkg.create",
    "tkeeper.key.*.public",
    "tkeeper.key.*.sign",
    "tkeeper.key.*.verify",
    "tkeeper.compliance.inventory"
  ]
}
```

This guide uses developer authentication locally. The feature may be selected for any build, but enabling it in production is an explicit operator risk decision and requires production-grade token protection and least-privilege permissions.

## Run TKeeper

```bash
java \
  --enable-native-access=ALL-UNNAMED \
  -Dkeeper.config.location=/tmp/tkeeper \
  -Dkeeper.dev.enabled=true \
  -Dkeeper.dev.config.location=/tmp/tkeeper \
  -Dkeeper.coordinator.enabled=true \
  -jar build/libs/tkeeper-2.5.0.jar
```

## Initialize the node

```bash
curl -s \
  -H 'X-DEV-TOKEN: dev-token' \
  -H 'Content-Type: application/json' \
  -d '{"peerId":1,"threshold":1,"total":1}' \
  http://localhost:8080/v1/keeper/system/init
```

With the Shamir provider, the response contains `shares64`. Save the share outside the node. You need it to unseal TKeeper after initialization or restart.

## Unseal

Replace `share-from-init` with one value from the `shares64` response.

```bash
curl -s \
  -H 'X-DEV-TOKEN: dev-token' \
  -H 'Content-Type: application/json' \
  -d '{"payload64":"share-from-init"}' \
  http://localhost:8080/v1/keeper/system/unseal
```

## Create a demo key identity

This creates a local `SECP256K1` identity that can authorize `arbitrary` signing requests.

```bash
curl -s \
  -H 'X-DEV-TOKEN: dev-token' \
  -H 'Content-Type: application/json' \
  -d '{
    "keyId": "demo-identity",
    "algorithm": "SECP256K1",
    "mode": "CREATE",
    "authorities": [
      { "id": "arbitrary" }
    ]
  }' \
  http://localhost:8080/v2/keeper/dkg
```

For production-like flows, attach a real authority document to the key, such as `custom`, `evm.transaction`, `bitcoin.transaction`, or `x509.tbs-certificate`. Those authorities let TKeeper parse the request into an understood intent and evaluate policy before signing.

## Sign

This signs `hello`, base64-encoded as `aGVsbG8=`.

```bash
curl -s \
  -H 'X-DEV-TOKEN: dev-token' \
  -H 'Content-Type: application/json' \
  -d '{
    "keyId": "demo-identity",
    "command": {
      "type": "arbitrary",
      "authorityId": "arbitrary",
      "artifact": {
        "scheme": "ECDSA",
        "hash": "SHA256",
        "data64": "aGVsbG8="
      }
    }
  }' \
  http://localhost:8080/v2/keeper/sign
```

The response contains the signature proof:

```json
{
  "signature64": "...",
  "type": "ECDSA",
  "generation": 1,
  "imposters": []
}
```

## Verify

Replace `signature-from-sign-response` with `signature64` from the sign response.

```bash
curl -s \
  -H 'X-DEV-TOKEN: dev-token' \
  -H 'Content-Type: application/json' \
  -d '{
    "keyId": "demo-identity",
    "command": {
      "type": "arbitrary",
      "artifact": {
        "scheme": "ECDSA",
        "hash": "SHA256",
        "data64": "aGVsbG8="
      }
    },
    "signature64": "signature-from-sign-response"
  }' \
  http://localhost:8080/v2/keeper/sign/verify
```

Expected response:

```json
{ "valid": true }
```

This verifies raw bytes. A production verifier must additionally trust the expected identity, bind the proof to the action it will execute, and enforce replay or expiry rules required by that action.

## Turn the smoke test into a governed integration

- Define the action schema and effects, then use [Authorities](https://tkeeper.org/docs/signing-and-authorities/authorities) to replace `arbitrary` with a structured authority.
- Make the downstream service reject the action unless proof for the exact governed intent verifies.
- Use [Quorum Modes](https://tkeeper.org/docs/security-model/quorum-modes) before creating high-risk keys.
- Use [Build and Features](https://tkeeper.org/docs/deployment/build-and-features) to select only the features and platforms your deployment needs.
- Use [Authentication and Authorization](https://tkeeper.org/docs/security-model/authentication-and-authorization) before leaving local development.


---

Source file: content/docs/deployment/README.md
Canonical: https://tkeeper.org/docs/deployment

# Deployment

Deployment has three decisions:

1. Which product features are included in the artifact.
2. Which cryptographic platforms are included in the artifact.
3. Which runtime mode the node is initialized with.

Read:

- [Build and Features](https://tkeeper.org/docs/deployment/build-and-features)
- [Configuration](https://tkeeper.org/docs/deployment/configuration)
- [Initialization and Unseal](https://tkeeper.org/docs/deployment/initialization-and-unseal)
- [Sealing and Unsealing](https://tkeeper.org/docs/deployment/sealing-and-unsealing)
- [Clustering](https://tkeeper.org/docs/deployment/clustering)
- [Backup and Recovery](https://tkeeper.org/docs/deployment/backup-and-recovery)
- [Control Plane UI](https://tkeeper.org/docs/deployment/control-plane-ui)
- [Production Checklist](https://tkeeper.org/docs/deployment/production-checklist)

## Deployment shapes

| Shape | Use when | Key boundary |
| --- | --- | --- |
| Local mono | Development and demos | One local key identity |
| Production mono | Lower-impact workflows that accept single-node compromise | One node holds full key authority |
| Threshold cluster | High-risk workflows | Quorum of peers must authorize and participate |

Mono and threshold use the same public API model. The difference is where key authority lives.

## Production defaults

For production:

- build only required features
- include recovery only in temporary maintenance artifacts and replace them after repair
- include every required platform explicitly
- use a production authenticator
- keep the internal API private to the cluster
- terminate public API access behind explicit network policy and rate limits
- configure audit sinks before relying on compliance evidence
- keep lifecycle/import/destroy permissions separate from signing
- use threshold mode for identities with real financial, security, or compliance consequences

Do not deploy the integration image. It includes test-only failure-injection controls.


---

Source file: content/docs/deployment/build-and-features.md
Canonical: https://tkeeper.org/docs/deployment/build-and-features

# Build and Features

TKeeper has two build-time selectors:

- features: product surface such as authorities, ECIES, UI, and seal providers
- platforms: cryptographic algorithms and protocols

A usable production artifact must include at least one platform.

## Build default production modules

```bash
./gradlew build -Pkeeper.features=all -Pkeeper.platforms=all
```

`build` runs the root and SDK tests plus the unit tests of every selected feature and platform module before producing the artifact.

The root `build` task runs the normal verification lifecycle and produces the deployable fat jar through `shadowJar`.

Equivalent:

```bash
./gradlew :build -Pkeeper.features.all=true -Pkeeper.platforms.all=true
```

The jar lands under:

```text
build/libs/tkeeper-2.5.0.jar
```

TKeeper requires Java 25.

## Build a smaller artifact

Example: EVM signing, ECIES, and the UI:

```bash
./gradlew :build -Pkeeper.features=evm,ecies,ui -Pkeeper.platforms=ecc
```

Example: ML-DSA only:

```bash
./gradlew :build -Pkeeper.platforms=pqc
```

Feature names match child project names. `:features:digital-assets:evm` is selected with `evm`.
Use `digital-assets` for Bitcoin, EVM, Tron, XRP, and Solana, and `agentic-payments` for both AP2 and MC VI:

```bash
./gradlew :build -Pkeeper.features=agentic-payments,digital-assets -Pkeeper.platforms=ecc
```

The earlier `authority-bitcoin` and `authority-evm` selectors remain accepted.

## Select a native OS/CPU target

`keeper.platforms` selects cryptographic modules (`ecc` and `pqc`). The separate `target`
property selects native libraries packaged into the **production** fat jar. By default,
`target=all` keeps native binaries for every bundled OS/CPU combination and produces
`build/libs/tkeeper-2.5.0.jar`.

Build the complete feature set for a Linux amd64 host with a smaller jar:

```bash
./gradlew shadowJar -Pkeeper.features=all -Pkeeper.platforms=all -Ptarget=linux-amd64
```

The output is `build/libs/tkeeper-2.5.0-linux-amd64.jar`. To keep every bundled native variant, use
`-Ptarget=all` or omit `target`:

```bash
./gradlew shadowJar -Pkeeper.features=all -Pkeeper.platforms=all -Ptarget=all
```

| `target` | Output classifier |
| --- | --- |
| `all` (default) | none |
| `linux-amd64` | `linux-amd64` |
| `linux-arm64` | `linux-arm64` |
| `macos-amd64` | `macos-amd64` |
| `macos-aarch64` | `macos-aarch64` |
| `windows-amd64` | `windows-amd64` |

Targeted jars keep only the matching RocksDB, Netty, gRPC Netty, Conscrypt, Zstd, Jansi,
GMP, secp256k1, and libsodium native files that are bundled by the dependencies. Java
classes and selected features remain the same. The integration and production test jars
always retain their full native set, regardless of `target`.

On Linux arm64 and macOS amd64, GMP and libsodium are loaded from the system;
secp256k1 is also loaded when `ecc` is selected.
At startup Keeper tries the bundled libraries first, then calls `System.loadLibrary` for
any that are absent. Install these shared libraries where the JVM can find them (for
example via `-Djava.library.path`). The production Dockerfile builds all three for Linux
and sets the JVM library path; a standalone jar does not install system libraries.
Linux targets use glibc binaries. For musl-based distributions, use `target=all` and
verify the remaining native dependencies. A targeted jar is specific to its OS/CPU.

`shadowJar` checks the finished targeted archive: it requires the selected RocksDB
binary and runtime classes, requires bundled GMP, secp256k1, and libsodium when the
target has them, and rejects native files for other targets. This check also runs during
a cross build. It verifies archive contents, not whether the libraries can load on the
target machine.

On the target host, load the native libraries from the built jar with:

```bash
./gradlew smokeTargetJar -Pkeeper.features=all -Pkeeper.platforms=all -Ptarget=linux-amd64
```

This checks the JVM OS/CPU against `target`, then loads RocksDB, GMP, libsodium, and
secp256k1 when `ecc` is selected. It fails if a required library cannot load. For
Linux arm64 or macOS amd64, provide these system libraries in the JVM library path.
For example, set `JAVA_TOOL_OPTIONS=-Djava.library.path=/path/to/libs` for a standalone
run. A cross build needs this smoke command run on the target OS/CPU; it cannot validate
native loading on the build host.

## Feature and platform matrix

| Need | Feature selector | Platform selector |
| --- | --- | --- |
| EVM transaction authority | `evm` or `digital-assets` | `ecc` |
| Bitcoin transaction authority | `bitcoin` or `digital-assets` | `ecc` |
| Tron transaction authority | `tron` or `digital-assets` | `ecc` |
| XRP transaction authority | `xrp` or `digital-assets` | `ecc` |
| Solana transaction authority | `solana` or `digital-assets` | `ecc` |
| AP2 payment authority | `ap2` or `agentic-payments` | `ecc` |
| MC VI payment authority | `mc-vi` or `agentic-payments` | `ecc` |
| X.509 certificate authority | `authority-x509` | `ecc` |
| ECIES | `ecies` | `ecc` |
| Peer share recovery | `recovery` (explicit opt-in) | `ecc`, `pqc`, or both |
| Control-plane UI | `ui` | any required crypto platform |
| AWS KMS seal provider | `seal-aws` | any required crypto platform |
| Google Cloud KMS seal provider | `seal-gcloud` | any required crypto platform |
| Developer token authentication | `auth-dev` (explicit opt-in, excluded from `all`) | any required crypto platform |
| Authority policy dry run | `dry-run` (explicit opt-in, excluded from `all`) | any required crypto platform |
| MCP discovery, utilities, signing, and composition | `mcp` (explicit opt-in, excluded from `all`) | any required crypto platform |
| ML-DSA identities | none | `pqc` |
| Default production set | `all` | `all` |

Features with platform dependencies require the matching platform. The build should fail early instead of producing an artifact with a missing runtime provider.

Recovery is an explicit artifact capability. Selecting it adds the base recovery API and the
recovery module for each selected platform:

```bash
./gradlew :build -Pkeeper.features=recovery -Pkeeper.platforms=ecc
./gradlew :build -Pkeeper.features=recovery -Pkeeper.platforms=pqc
./gradlew :build -Pkeeper.features=recovery -Pkeeper.platforms=ecc,pqc
```

The first command includes `:features:recovery` and `:features:recovery:ecc`; the second includes
`:features:recovery` and `:features:recovery:pqc`; the third includes all three. The platform modules
are not selected separately. Recovery, `auth-dev`, `dry-run`, and `mcp` are excluded from `keeper.features=all` and
must be requested explicitly.

Treat this as a maintenance artifact. After recovery, rebuild and redeploy the normal production
artifact without the `recovery` selector; setting `keeper.recovery=false` alone leaves the recovery
code and routes in the artifact.

`auth-dev` is deliberately excluded from `all`, but it may be included in any deployable artifact by requesting it explicitly:

```bash
./gradlew :build -Pkeeper.features=auth-dev -Pkeeper.platforms=ecc
```

Build the dry-run endpoint explicitly in the same way:

```bash
./gradlew :build -Pkeeper.features=dry-run -Pkeeper.platforms=ecc
```

Build the MCP endpoint into the same public Keeper server:

```bash
./gradlew :build -Pkeeper.features=mcp,digital-assets -Pkeeper.platforms=ecc
```

The endpoint is `POST /mcp` and uses the configured Keeper authentication provider. See
[MCP connection and tools](https://tkeeper.org/docs/ai/mcp) for setup and request format.

## Selection properties

| Scope | Features | Platforms |
| --- | --- | --- |
| Runtime jar | `keeper.features` | `keeper.platforms` |
| Docker build | `keeper.docker.features` | `keeper.docker.platforms` |
| Select all | `keeper.features.all=true` | `keeper.platforms.all=true` |

Comma-separated selectors accept short names such as `ecies`, `ecc`, and `pqc`. `all` selects every
default production module in that category. Explicit features such as `recovery`, `auth-dev`, `dry-run`, and `mcp` are
not included.

`target` is independent of these selectors. Invalid target names fail the Gradle build
instead of silently producing a jar with mismatched native libraries.

## Docker

Build the production Docker image:

```bash
./gradlew dockerBuild -Pkeeper.features=all -Pkeeper.platforms=all
```

For a Linux amd64 or arm64 image, pass `-Ptarget=linux-amd64` or
`-Ptarget=linux-arm64`. The Docker build uses the matching `--platform` and the
matching production jar. `target=all` preserves the existing Docker build with the
multi-target native jar. macOS and Windows targets cannot be used with `dockerBuild` because the
Dockerfile produces Linux images.

`dockerBuild` also loads the jar's native libraries inside the target image before
finishing. For `linux-arm64` on an amd64 host, Docker must be able to execute arm64
build steps (for example through an arm64 builder or emulation).

Build a recovery image with both platform implementations:

```bash
./gradlew dockerBuild \
  -Pkeeper.docker.features=recovery \
  -Pkeeper.docker.platforms=ecc,pqc
```

Production image tags:

```text
exploit/tkeeper:2.5.0
exploit/tkeeper:latest
```

The Dockerfile adds the JVM flag required by the FFI Java API:

```text
--enable-native-access=ALL-UNNAMED
```

Run the image:

```bash
docker run --rm \
  -p 8080:8080 \
  -p 9090:9090 \
  -v "$PWD/config:/etc/tkeeper:ro" \
  -v "$PWD/data:/var/lib/tkeeper" \
  -e KEEPER_CONFIG_LOCATION=/etc/tkeeper \
  exploit/tkeeper:2.5.0
```

## Integration image

Run the complete release verification with:

```bash
./gradlew releaseGate
```

This includes every module's unit tests, artifact isolation, both test-container builds, and the functional integration suite. Performance benchmarks are separate.

Build both images used by functional integration tests with:

```bash
./gradlew buildTestContainers
```

The test task reuses these images. Re-run the build after changing application code, dependencies, or Dockerfiles.

Do not pass `keeper.features` or `keeper.platforms` to this task. The development integration image
uses a dedicated classpath containing every default production feature, the explicit `auth-dev`,
`dry-run`, and `recovery` features, both recovery platform modules, every platform, and the test-only
failure-injection module.

Never deploy either test image as production runtime.

## Common failures

### Feature endpoint returns 404

The feature was not included in the artifact.

Rebuild with the required feature and platform.

For recovery, select the base feature and the required platforms:

```bash
./gradlew :build -Pkeeper.features=recovery -Pkeeper.platforms=ecc,pqc
```

### No provider for algorithm

The platform was not included in the artifact.

Rebuild with the required platform, for example:

```bash
./gradlew :build -Pkeeper.features=evm -Pkeeper.platforms=ecc
```

### Native access warning

Add:

```text
--enable-native-access=ALL-UNNAMED
```

The Docker image already does this.


---

Source file: content/docs/deployment/configuration.md
Canonical: https://tkeeper.org/docs/deployment/configuration

# Configuration

TKeeper reads config in this order:

1. JVM system properties
2. external config from `KEEPER_CONFIG_LOCATION` or `-Dkeeper.config.location`
3. profile config from `KEEPER_PROFILE` or `-Dkeeper.profile`
4. bundled `application.conf`
5. bundled `reference.conf`

External config can be:

- a file
- a directory with `application.conf`, `application.json`, or `application.properties`
- `classpath:...`

Multiple external locations are comma-separated. Earlier locations win because they are loaded first.

Configuration can contain bootstrap tokens, HSM PINs, registry credentials, and trust-store passwords. Keep configuration files out of source control, restrict filesystem access, and inject secrets through the deployment's protected configuration mechanism. Do not expose resolved configuration in logs or support bundles.

Profile config uses bundled files named `application-{profile}.conf`, `application-{profile}.json`, or `application-{profile}.properties`.

Dev auth config is separate. Include the feature when building:

```bash
./gradlew :build -Pkeeper.features=auth-dev -Pkeeper.platforms=ecc
```

Enable it at runtime with:

```bash
-Dkeeper.dev.enabled=true
-Dkeeper.dev.config.location=/etc/tkeeper
```

`auth-dev` is not included by `keeper.features=all`; it must be selected explicitly.

When the dev location is a directory, TKeeper looks for `dev.conf`, `dev.json`, or `dev.properties`. A direct file path also works.

Example:

```bash
java \
  -Dkeeper.config.location=/etc/tkeeper \
  -Dkeeper.dev.config.location=/etc/tkeeper \
  -jar build/libs/tkeeper-2.5.0.jar
```

Minimal node config:

```hocon
auth { type = "dev" }

boot { token = "change-me" }

keeper {
  database { path = "/var/lib/tkeeper/db" }

  providers {
    selected = "shamir"
    shamir {
      total = 5
      threshold = 3
    }
  }

  server {
    public {
      host = "0.0.0.0"
      port = 8080
    }

    internal {
      host = "0.0.0.0"
      port = 9090
    }
  }

  peers = [
    { id = 2, internal-url = "http://keeper-2:9090" },
    { id = 3, internal-url = "http://keeper-3:9090" }
  ]
}
```

Common fields:

| Field | Meaning |
| --- | --- |
| `keeper.database.path` | RocksDB path |
| `keeper.server.public` | API users call this |
| `keeper.server.internal` | Peers call this |
| `keeper.peers` | Other peers in the cluster; self is omitted |
| `keeper.providers.selected` | Seal provider id |
| `keeper.client.tls` | TLS for peer clients |
| `keeper.recovery` | Recovery-only runtime mode; defaults to `false` |
| `keeper.authority.arbitrary.enabled` | Enables raw `arbitrary` signing; defaults to `false` |
| `keeper.approval.ttl` | Four-eye approval lifetime and persistent nonce replay-retention window |
| `keeper.session.*` | DKG, FROST, GG20, ML-DSA, ECIES, destroy session limits |

Raw `arbitrary` signing must be enabled explicitly and consistently on every keeper in the cluster:

```hocon
keeper.authority.arbitrary.enabled = true
```

The equivalent environment variable is `KEEPER_AUTHORITY_ARBITRARY_ENABLED`. Leave the setting
disabled when every key uses a concrete OCI authority.

Coordinator-only endpoints can be disabled on a node:

```bash
-Dkeeper.coordinator.enabled=false
```

or:

```text
KEEPER_COORDINATOR_ENABLED=false
```

Use that for peers that only participate in threshold protocols.

`keeper.recovery=true` is fail-closed maintenance mode. Build the artifact with
`-Pkeeper.features=recovery` and the required platform selectors. The mode requires the recovery
feature in the artifact, public TLS, internal mTLS with client authentication, outbound client mTLS,
and a distinct `tls-spki-sha256` binding for every peer. Normal key operations and readiness are
blocked until the keeper is restarted with recovery disabled. See
[Backup and Recovery](https://tkeeper.org/docs/deployment/backup-and-recovery).

After repair, replace the maintenance artifact with a production build that does not include the
`recovery` feature. Do not use the runtime flag as the only recovery deactivation step.

## Server TLS

TLS can use a keystore:

```hocon
keeper.server.public.tls {
  enabled = true
  key-store-path = "/etc/tkeeper/public.p12"
  key-store-password = "..."
  key-store-type = "PKCS12"
}
```

or certificate files:

```hocon
keeper.server.public.tls {
  enabled = true
  certificate-chain-path = "/etc/tkeeper/tls.crt"
  private-key-path = "/etc/tkeeper/tls.key"
}
```

When PEM refresh is enabled, TKeeper validates that the candidate private key matches the leaf
certificate before publishing it. A partial, malformed, or mismatched update is logged and the last
valid identity remains active until both files form a valid pair.

Public and internal servers have separate TLS blocks:

```hocon
keeper.server.public.tls { enabled = true }
keeper.server.internal.tls { enabled = true }
```

JWT authentication requires public-server TLS and fails startup when it is disabled.

The peer client must trust the internal server certificate when internal TLS is enabled:

```hocon
keeper.client {
  tls = true
  trust-store-path = "/etc/tkeeper/internal-truststore.p12"
  trust-store-password = "..."
}
```

For mutual TLS, require client certificates on every internal server and configure the peer client key store:

```hocon
keeper.server.internal.tls {
  enabled = true
  client-auth = true
  trust-store-path = "/etc/tkeeper/peer-ca.p12"
  trust-store-password = "..."
  trust-store-type = "PKCS12"
}

keeper.client {
  tls = true
  trust-store-path = "/etc/tkeeper/internal-truststore.p12"
  trust-store-password = "..."
  key-store-path = "/etc/tkeeper/peer-client.p12"
  key-store-password = "..."
  key-store-type = "PKCS12"
}
```

Give every peer a distinct client certificate, then bind its configured peer id to that certificate's SPKI digest:

```hocon
keeper.peers = [
  {
    id = 2
    internal-url = "https://keeper-2:9090"
    tls-spki-sha256 = "base64-sha256-of-peer-2-client-certificate-spki"
  }
]
```

Compute the value from the client certificate:

```bash
openssl x509 -in peer-2-client.crt -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary \
  | openssl base64 -A
```

If one peer entry has `tls-spki-sha256`, every peer entry must have a valid, distinct pin. TKeeper also requires internal TLS client authentication and an outbound client key store in that configuration.

mTLS limits internal API access, protects forwarded actor credentials from network interception, and—when SPKI pins are configured—binds the claimed peer id to its certificate. TKeeper still verifies application-level peer signatures because TLS does not bind protocol messages to TKeeper sessions.

Outside dev mode, protected peer protocol routes reject plaintext even if request signatures are otherwise valid. Health and integrity-public-key discovery remain available for deployment health checks and enrollment.

## Authentication

JWT authentication is configured under `auth.jwt`:

```hocon
auth {
  type = "jwt"

  jwt {
    jwks-location = "https://issuer.example/.well-known/jwks.json"
    issuer = "https://issuer.example"
    audience = "tkeeper"
    refresh = 15m
    clock-skew = 15s
  }
}
```

When `issuer` is configured, it must match the token `iss` claim. Configure it in production to bind tokens to the expected identity provider.

`audience` must be present in the token `aud` claim. Tokens must contain `exp`; `nbf` is honored when present.

`clock-skew` defaults to `15s` and must not be negative.

Remote `jwks-location` and OIDC discovery URLs must use HTTPS. A local JWKS file path or
`file:` URI is also supported. OIDC callbacks must use HTTPS, except for loopback callbacks
used during local development.

## Sessions

Session limits live under `keeper.session`:

```hocon
keeper.session {
  dkg { expire = 5m }
  destroy { expire = 5m }

  frost {
    expire = 5m
    max-rounds = 5
  }

  gg20 {
    expire = 15m
    max-rounds = 3
  }

  mldsa {
    expire = 5m
    max-rounds = 12
  }

  ecies {
    max-rounds = 3
  }
}
```

For ML-DSA, `max-rounds` is the maximum number of complete signing attempts, not the number of protocol messages and not the internal parallel-instance count `K`. Each attempt has three signing rounds and may abort by design during rejection sampling. The default of `12` bounds latency; exhausting it returns `SESSION_MAX_ROUNDS_EXCEEDED` and does not identify a dead or malicious peer. Increase it only together with an end-to-end request deadline and latency monitoring.

## Audit

Minimal file audit:

```hocon
keeper.audit {
  enabled = true
  timeout = 1000

  file {
    directory = "/var/lib/tkeeper/audit"
    extension = "ndjson"
  }
}
```

Socket audit supports TLS, SPKI pins, client certificates, batching, timeouts, and reconnect backoff. See [Audit Logging](https://tkeeper.org/docs/security-model/audit-logging).

## ORAS

ORAS config is used by authority OCI pulls:

```hocon
oras {
  insecure = false
  allowed-registries = ["registry.example.com"]
  username = "robot"
  password = "secret"
}
```

`allowed-registries` is mandatory for OCI authorities and matches the exact registry authority, including the port. An empty list denies all OCI pulls. HTTPS is the default; use `insecure = true` only for an explicitly allowed local plain HTTP registry.

## UI CSP

The UI has its own CSP config under `keeper.csp`. See [Control Plane UI](https://tkeeper.org/docs/deployment/control-plane-ui).

## Environment aliases

Common environment variables:

| Variable | Config field |
| --- | --- |
| `KEEPER_AUTH_TYPE` | `auth.type` |
| `KEEPER_BOOT_TOKEN` | `boot.token` |
| `KEEPER_DATABASE_PATH` | `keeper.database.path` |
| `KEEPER_AUDIT_ENABLED` | `keeper.audit.enabled` |
| `KEEPER_SEAL_SELECTED` | `keeper.providers.selected` |
| `KEEPER_SEAL_SHAMIR_TOTAL` | `keeper.providers.shamir.total` |
| `KEEPER_SEAL_SHAMIR_THRESHOLD` | `keeper.providers.shamir.threshold` |
| `KEEPER_HOST` | `keeper.server.public.host` |
| `KEEPER_PORT` | `keeper.server.public.port` |
| `KEEPER_INTERNAL_HOST` | `keeper.server.internal.host` |
| `KEEPER_INTERNAL_PORT` | `keeper.server.internal.port` |
| `KEEPER_TLS_ENABLED` | `keeper.server.public.tls.enabled` |
| `KEEPER_INTERNAL_TLS_ENABLED` | `keeper.server.internal.tls.enabled` |
| `KEEPER_CLIENT_TLS` | `keeper.client.tls` |
| `KEEPER_INTERNAL_TLS_CLIENT_AUTH` | `keeper.server.internal.tls.client-auth` |
| `KEEPER_INTERNAL_TLS_TRUST_STORE_PATH` | `keeper.server.internal.tls.trust-store-path` |
| `KEEPER_CLIENT_KEY_STORE_PATH` | `keeper.client.key-store-path` |

## Common problems

### Peer calls fail

Check `keeper.peers`. Each node lists the other peers, not itself.

### Authority OCI pull fails with TLS errors

Local registry over plain HTTP:

```hocon
oras {
  insecure = true
  allowed-registries = ["registry:5000"]
}
```

Real registry over HTTPS:

```hocon
oras {
  insecure = false
  allowed-registries = ["registry.example.com"]
}
```


---

Source file: content/docs/deployment/initialization-and-unseal.md
Canonical: https://tkeeper.org/docs/deployment/initialization-and-unseal

# Initialization and Unseal

Init writes the keeper identity and quorum settings into the sealed store.

TKeeper has two quorum modes:

- `mono`: `threshold = 1`, `total = 1`
- `threshold`: `threshold > 1`, `total >= threshold`

The mode is not a separate request field. TKeeper derives it from `threshold` and `total`.

Endpoint:

```http
POST /v1/keeper/system/init
```

Body:

```json
{
  "peerId": 1,
  "threshold": 2,
  "total": 3
}
```

Mono body:

```json
{
  "peerId": 1,
  "threshold": 1,
  "total": 1
}
```

Threshold body for one peer:

```json
{
  "peerId": 2,
  "threshold": 2,
  "total": 3
}
```

Required permission:

```text
tkeeper.system.init
```

Rules:

- `peerId` starts from 1
- `threshold` must be greater than 0
- `total` must be greater than 0
- `threshold` cannot be greater than `total`
- if `threshold` is `1`, `total` must also be `1`
- every peer in a threshold cluster must use the same `threshold` and `total`
- run init once per peer

For threshold mode, all peers must be initialized with the same `threshold` and `total`. If one peer is initialized with different cluster parameters, reset that peer's database and initialize it again with the correct parameters.

Peers can be initialized independently. The threshold parameters are the part that must match.

## Choosing a mode

Use mono when one Keeper is enough for custody, but you still want TKeeper's authority controls around the key. Mono operations are local. There is no peer quorum, no distributed signing protocol, and no protection against compromise of that one node.

Use threshold when no single machine should be able to use the key alone. Keys are split across peers. Signing and decrypting need enough healthy peers to participate. For ECDSA TKeeper uses GG20. For Schnorr-style schemes it uses FROST. Threshold ECIES decrypts through peer partial decrypts.

Use threshold mode when one compromised node must not be enough to act as the identity. Mono is appropriate for development, explicitly lower-impact deployments, bootstrap phases, and systems that plan to promote into a quorum later.

## Promoting mono to threshold

See [Quorum Promotion](https://tkeeper.org/docs/cryptographic-identities/quorum-promotion).

If the selected seal provider needs recovery material, init returns it. With manual Shamir, that means unseal shares. Store them outside the node. Without enough shares, the node stays sealed.

Manual Shamir response:

```json
{
  "threshold": 2,
  "total": 3,
  "shares64": ["...", "...", "..."]
}
```

Auto-unseal providers return `204 No Content` after successful init.

Status response:

```json
{
  "sealedBy": "shamir",
  "state": "SEALED",
  "progress": {
    "threshold": 2,
    "total": 3,
    "progress": 0,
    "ready": false
  }
}
```

If the caller does not have `tkeeper.system.unseal`, `sealedBy` is hidden.

Status endpoints:

```http
GET /v1/keeper/system/status
GET /v1/keeper/system/health
GET /v1/keeper/system/ready
GET /v1/keeper/peerId
GET /v1/keeper/ping
```

## Common problems

### `KEEPER_ALREADY_INITIALIZED`

Init is one-time per node. Local re-init requires a fresh local DB.

### Wrong peer id

In threshold mode, peer ids are part of protocol state. Each node needs its own `peerId`.

### Wrong threshold or total

Reset the local database for the bad peer and run init again with the same `threshold` and `total` as the rest of the cluster.


---

Source file: content/docs/deployment/sealing-unsealing.md
Canonical: https://tkeeper.org/docs/deployment/sealing-and-unsealing

# Sealing & Unsealing

TKeeper starts sealed. While sealed, most public endpoints reject the request with `KEEPER_SEALED`.

Manual unseal:

```http
POST /v1/keeper/system/unseal
```

One share:

```json
{
  "payload64": "..."
}
```

Several shares:

```json
{
  "payloads64": ["...", "..."]
}
```

Reset in-progress unseal first:

```json
{
  "payloads64": ["...", "..."],
  "reset": true
}
```

Shamir unseal progress:

```json
{
  "threshold": 2,
  "total": 3,
  "progress": 1,
  "ready": false
}
```

When enough shares are submitted, `ready` becomes `true`.

Seal again:

```http
POST /v1/keeper/system/seal
```

Required permissions:

```text
tkeeper.system.unseal
tkeeper.system.seal
```

Seal providers:

| Provider | Where it lives |
| --- | --- |
| `shamir` | core |
| `hsm` | core |
| `aws` | `:features:seal-aws` |
| `google` | `:features:seal-gcloud` |

Feature seal providers must be included at build time. If `keeper.providers.selected = "aws"` but the jar was built without `:features:seal-aws`, startup will not find the provider.

## Provider selection

Provider selection lives under `keeper.providers`:

```hocon
keeper.providers {
  selected = "shamir"
  auto-unseal = false
}
```

`auto-unseal` applies to automatic providers. Manual Shamir unseal uses submitted shares.

For automatic providers, `GET /v1/keeper/system/unseal` asks the selected provider to decrypt TKeeper's internal master key. That is how HSM, AWS KMS, and Google Cloud KMS unseal without submitted shares.

## Shamir provider

`shamir` is built in.

Config:

```hocon
keeper.providers {
  selected = "shamir"

  shamir {
    total = 5
    threshold = 3
  }
}
```

Behavior:

- `/v1/keeper/system/init` returns Shamir unseal shares
- `POST /v1/keeper/system/unseal` accepts one or more shares
- `GET /v1/keeper/system/unseal` is not supported for manual Shamir

## HSM provider

`hsm` is built in. It uses PKCS#11 through `SunPKCS11`.

Config:

```hocon
keeper.providers {
  selected = "hsm"
  auto-unseal = true

  hsm {
    name = "softhsm"
    library = "/usr/lib/softhsm/libsofthsm2.so"
    key-alias = "tkeeper-kek"
    pin = "1234"
    cipher = "AES_GCM"

    slot-list-index = 0
    extra-attributes = []
  }
}
```

Supported ciphers:

```text
AES_GCM
AES_CBC
```

`slot` and `slot-list-index` are mutually exclusive.

## AWS KMS provider

`aws` lives in `:features:seal-aws`.

Build with it:

```bash
./gradlew shadowJar -Pkeeper.features=seal-aws
```

Config:

```hocon
keeper.providers {
  selected = "aws"
  auto-unseal = true

  aws {
    key-id = "arn:aws:kms:eu-central-1:123456789012:key/..."
    region = "eu-central-1"
  }
}
```

## Google Cloud KMS provider

`google` lives in `:features:seal-gcloud`.

Build with it:

```bash
./gradlew shadowJar -Pkeeper.features=seal-gcloud
```

Config:

```hocon
keeper.providers {
  selected = "google"
  auto-unseal = true

  google {
    project = "my-project"
    location = "global"
    key-ring = "tkeeper"
    crypto-key = "seal-key"
  }
}
```

## Common problems

### `KEEPER_SEALED`

Unseal the node first:

```http
POST /v1/keeper/system/unseal
```

### Auto-unseal provider is selected but missing

Rebuild the jar with the provider feature.

### Google provider is not found

The provider id is `google`, not `gcloud`. The build feature name is still `seal-gcloud`.


---

Source file: content/docs/deployment/clustering.md
Canonical: https://tkeeper.org/docs/deployment/clustering

# Clustering

In threshold mode, each peer runs its own TKeeper instance. A coordinator can start a session, but the peers still validate the same intent before contributing.

## Cluster invariants

Every peer must agree on:

- `threshold`
- `total`
- peer ids
- internal peer URLs
- internal authentication trust
- mTLS certificate-to-peer SPKI bindings, when enabled
- selected seal provider behavior
- artifact feature and platform set

Each peer has its own local database and seal state.

## Peer ids

Peer ids start at `1`.

Each peer is initialized once with its own `peerId` and the same `threshold` and `total` as the rest of the cluster.

Example for a `2-of-3` cluster:

| Node | `peerId` | `threshold` | `total` |
| --- | --- | --- | --- |
| keeper-1 | `1` | `2` | `3` |
| keeper-2 | `2` | `2` | `3` |
| keeper-3 | `3` | `2` | `3` |

## Public and internal APIs

TKeeper exposes two network surfaces:

| API | Used by | Boundary |
| --- | --- | --- |
| Public API | clients and operators | external auth and permissions |
| Internal API | TKeeper peers | cluster-only peer authentication |

Keep the internal API private to the cluster. Do not expose it as a public service.

## Coordinator

Coordinator-enabled nodes accept operations that start sessions, such as signing and DKG. Non-coordinator peers can still participate in threshold protocols.

Disable coordinator endpoints on a peer:

```text
KEEPER_COORDINATOR_ENABLED=false
```

or:

```bash
-Dkeeper.coordinator.enabled=false
```

## Failure model

Threshold mode removes a single cryptographic control point, but it adds distributed-system failure modes:

- a peer may be sealed
- a peer may be unreachable
- an internal certificate or trust setting may be wrong
- peers may disagree on key generation state
- a session may time out
- consistency repair may be required after partial failure

Use [Troubleshooting](https://tkeeper.org/docs/operations/troubleshooting) for operator symptoms.

## Upgrade discipline

Run the same release artifact, feature set, and platform set on every peer. Do not assume mixed-version protocol compatibility unless that exact upgrade path has been tested.

During a rollout:

- preserve enough healthy, unsealed peers for quorum
- avoid starting lifecycle or repair operations across a partially upgraded cluster
- verify internal API trust and protocol health before proceeding to the next peer
- keep a validated database backup and rollback decision for the target release

## Security notes

- one compromised peer should not be enough to authorize as the identity
- policy integrity becomes quorum-bound only if enough peers enforce the same policy state
- lifecycle permissions are more dangerous than signing permissions
- trusted-dealer import depends on trusting the dealer path
- threshold mode does not replace host, network, seal, or audit hardening


---

Source file: content/docs/deployment/backup-and-recovery.md
Canonical: https://tkeeper.org/docs/deployment/backup-and-recovery

# Backup and recovery

Each TKeeper peer owns local state. Cluster quorum is not a backup: surviving peers may keep operations available, but they do not make loss or rollback of one peer's database harmless.

## Recovery assets

The recovery plan must account for:

| Asset | Why it is needed |
| --- | --- |
| Peer database | encrypted key shares, generations, authorities, peer identity, integrity state, and platform side state |
| Runtime configuration | peer id, cluster topology, TLS, auth, selected features, platforms, and seal provider |
| Seal dependency | Shamir shares, HSM key, or cloud KMS key and its authorization path |
| TLS and trust material | public/internal API identity and peer connectivity |
| Audit integrity public keys | verification of retained audit events across integrity-key versions |
| Authority artifacts | exact digest-pinned policy and intent documents referenced by identities |

Back up each peer independently. Do not collect enough peer databases, unseal shares, or seal credentials into one backup account or location to defeat the threshold and seal boundaries.

## Snapshot rules

- Use a storage snapshot procedure validated for the peer database. Do not assume an arbitrary live filesystem copy is consistent.
- Encrypt backups and restrict restore access as tightly as live key-share storage.
- Record the TKeeper version, artifact digest, features, platforms, peer id, and snapshot time with each backup.
- Protect backups from silent rollback or replacement and retain the audit trail for backup and restore operations.
- Test access to external HSM or KMS keys; a database backup without its seal dependency may be intentionally unrecoverable.

## Restore validation

Restore a peer in an isolated environment before reconnecting it to the cluster.

1. Use the expected TKeeper artifact and configuration for that peer id.
2. Restore the database and required TLS/trust material.
3. Confirm the configured seal provider can unseal the restored state.
4. Check node status, inventory integrity, public keys, active generations, authorities, and platform side state.
5. Compare the restored generation state with healthy peers and the audit history.
6. Rejoin only after the state difference is understood.

An older backup may represent a generation that the cluster has already rotated, refreshed, destroyed, or repaired. Do not run consistency fix as an automatic restore step; use it only when the operator can establish which quorum state is safe.

## Share recovery mode

Share recovery is an explicit build capability. Select `recovery` and the platforms used by the
key histories that need repair. Gradle adds the corresponding platform recovery modules.

ECC recovery artifact:

```bash
./gradlew :build -Pkeeper.features=recovery -Pkeeper.platforms=ecc
```

ML-DSA recovery artifact:

```bash
./gradlew :build -Pkeeper.features=recovery -Pkeeper.platforms=pqc
```

Artifact for both platforms:

```bash
./gradlew :build -Pkeeper.features=recovery -Pkeeper.platforms=ecc,pqc
```

The `all` feature selector does not include recovery. Build recovery as a separate maintenance
artifact and run that artifact on every helper and target only for the repair window.

### Preconditions

- The configured quorum satisfies `1 < threshold < total`.
- The operator has selected a sorted list of exactly `threshold` healthy helper peer ids.
- The target peer id is not in the helper list.
- The helpers agree on the complete key history and have no pending state for the key.
- Public TLS, internal mTLS with client authentication, outbound client mTLS, and a distinct
  `tls-spki-sha256` pin for every peer are configured.
- Initialization, database identity, integrity, TLS, and seal state are intact on the target.

Legacy unversioned generation zero is outside the recovery boundary.

### Run recovery

1. Stop normal traffic to the participating keepers.
2. Deploy the recovery-capable artifact to the target and selected helpers.
3. Restart each participant with recovery mode enabled:

   ```bash
   java -Dkeeper.recovery=true -jar build/libs/tkeeper-2.5.0.jar
   ```

4. Unseal the keepers if required and confirm health and status.
5. Call the recovery endpoint on the damaged keeper with a principal that has
   `tkeeper.recover`:

   ```bash
   curl --fail-with-body \
     --cacert public-ca.crt \
     -H 'X-JWT-TOKEN: <raw-jwt>' \
     -H 'Content-Type: application/json' \
     -d '{"keyId":"signing-key","helperIds":[1,2,3]}' \
     https://keeper-4:8080/v1/keeper/recovery/recover
   ```

6. Compare the returned algorithm, current generation, recovered generations, and helpers with
   the expected state. Read every generation's public key while recovery mode is still enabled.
7. Repeat for each damaged logical key.
8. Rebuild the normal production artifact with the deployment's regular feature and platform
   selectors, without `recovery`:

   ```bash
   ./gradlew :build -Pkeeper.features=<production-features> -Pkeeper.platforms=<production-platforms>
   ```

9. Replace the maintenance artifact on every participant and restart with
   `keeper.recovery=false`. Do not return the recovery-capable artifact to normal service.
10. Verify historical inventory, current generation, public keys, owner and expiration indexes,
   destroyed generations, and a normal signing operation.

The endpoint requires the operator-provided helper list. It does not discover or replace helpers.
If a selected helper is missing or has conflicting key state, the request fails before target writes.

### Rebuilt state

The target first obtains an identical manifest from every selected helper. Only after validation
does it replace all local state for that logical key in one database transaction. The rebuild covers:

- active key material for every non-destroyed generation;
- signed versioned metadata, policies, authorities, owner, and expiration data;
- the current-generation pointer and historical inventory;
- destroy markers;
- ECC commitments or ML-DSA public keys and recovery commitments; and
- derived owner and expiration indexes.

Target-local pending, missing, stale, or conflicting records for the key are discarded. A failure
inside the replacement transaction rolls back the reset and install together. Prefix-adjacent key
ids are not part of the reset.

If the helpers mark a generation destroyed, recovery restores its metadata and destroy marker but
does not install secret material. Recovery can rebuild a peer with no local records for the logical
key, provided the target database identity and other node-scoped state still exist.

### Platform behavior

ECC recovery uses exactly `threshold` remote helpers. Helpers exchange fresh pairwise masks and
return masked Lagrange contributions. The target validates each reconstructed share against the
generation's polynomial commitments. It works for any configured quorum with `1 < threshold < total`;
`threshold = total` cannot recover a missing share because only `total - 1` shares survive.

ML-DSA helpers return only replicated RSS components that belong to the target. The target validates
each component against the signed per-subset commitments stored during DKG or dealer split. Legacy
ML-DSA generations without these commitments remain usable but return `RECOVERY_NOT_POSSIBLE`.
ML-DSA parameter changes between generations are supported. A history that crosses between ECC and
PQC returns `RECOVERY_NOT_POSSIBLE` without installing partial state.

Recovery is limited to 64 historical generations per logical key.

### Recovery mode boundary

Recovery mode blocks normal key operations. The public API allows health, status, seal, unseal,
public-key reads, and recovery routes. The internal API allows health, integrity-public-key discovery,
and the selected platform's recovery routes. Helpers reject recovery calls while running in normal
mode. Startup fails if the artifact lacks the recovery feature or the required TLS and peer-pin
configuration.

Recovery repairs keeper state. It does not determine whether the lost or modified share was exposed.
Rotate or reshare after recovery when required by the incident response decision.

Disabling the runtime flag is not the final deployment state. The normal artifact must exclude the
recovery base module and both platform recovery implementations.

### ECC mask boundary

The mask construction hides an honest helper contribution from the target colluding with up to
`t - 2` helpers. A malicious helper can cause the final commitment check to fail; this version does
not include a zero-knowledge proof that attributes that denial of service. Secret scalar arithmetic
uses random quotient blinding, pair masks, and one secure modular reduction, but the bigint backend
is not a formally verified fixed-width constant-time field implementation. Co-resident timing and
microarchitectural attackers remain outside the protocol's threat model.

Within one recovery session each helper caches its contribution, so retries return the same value
and do not create a second equation. Every new session uses independent CSPRNG pair masks. Reusing a
pair mask in a session with a different helper set (and therefore a different Lagrange coefficient)
would reveal that helper's share; the signed transcript binds the session id, target, canonical
helper set, algorithm, generation, commitments, metadata, and expiry, and rejects such reuse.

The complete graph contains `t(t - 1) / 2` mask deliveries. The implementation bounds setup to 64
concurrent requests; validate recovery latency at the deployment's largest supported `t` before
relying on the default session lifetime.

## Recovery objectives

Define and test separately:

- loss of one peer while quorum remains available
- loss of a peer database with its seal dependency intact
- seal-provider outage with peer data intact
- rollback to an older peer snapshot
- loss of an audit sink or integrity-key history
- regional failure affecting multiple peers or recovery operators

If the recovery design cannot restore enough independent shares and seal dependencies, document key loss as the expected outcome. If recovery storage contains enough material to use the identity unilaterally, document that storage as part of the key-custody boundary.


---

Source file: content/docs/deployment/control-plane-ui.md
Canonical: https://tkeeper.org/docs/deployment/control-plane-ui

# Control Plane UI

The control-plane UI lives in `:features:ui`.

Build with it:

```bash
./gradlew shadowJar -Pkeeper.features=ui
```

Open:

```http
GET /ui/
```

Disable the UI at startup:

```bash
java \
  -Dkeeper.ui.enabled=false \
  -jar build/libs/tkeeper-2.5.0.jar
```

## Authentication

The UI uses the same external auth mode as the API.

With dev auth, the browser sends:

```text
X-DEV-TOKEN
```

With JWT auth, the browser sends:

```text
X-JWT-TOKEN
```

If `auth.jwt.oidc` is configured, the UI reads OIDC settings from:

```http
GET /v1/keeper/control/auth/config
```

After authentication, the UI discovers the key algorithms included in the running build from:

```http
GET /v1/keeper/control/capabilities
```

Key generation, trusted-dealer import, and four-eye approver forms use this runtime list instead of assuming a fixed platform set.

## Content Security Policy

The UI feature adds security headers under `/ui`.

Default CSP:

```hocon
keeper.csp {
  default-src = ["'self'"]
  base-uri = ["'self'"]
  object-src = ["'none'"]
  frame-ancestors = ["'none'"]
  script-src = ["'self'"]
  style-src = ["'self'"]
  img-src = ["'self'", "data:"]
  font-src = ["'self'", "data:"]
  connect-src = ["'self'"]
  form-action = ["'self'"]
}
```

Report-only mode:

```hocon
keeper.csp {
  report-only = true
}
```

Extra origins:

```hocon
keeper.csp {
  connect-extra = [
    "https://issuer.example"
  ]

  form-action-extra = [
    "https://issuer.example"
  ]

  img-extra = [
    "https://assets.example"
  ]
}
```

OIDC origins are added to `connect-src` automatically when:

```hocon
keeper.csp {
  oidc-auto-connect = true
}
```

Set it to `false` when `connect-src` is managed manually.

## Headers

The UI also sends:

```text
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Permissions-Policy: geolocation=(), microphone=(), camera=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
```

When public TLS is enabled:

```text
Strict-Transport-Security: max-age=31536000; includeSubDomains
```

## Common problems

### `/ui/` returns 404

Rebuild with `:features:ui`.

### OIDC login cannot reach the issuer

Check `keeper.csp.connect-extra` or keep `keeper.csp.oidc-auto-connect = true`.


---

Source file: content/docs/deployment/production-checklist.md
Canonical: https://tkeeper.org/docs/deployment/production-checklist

# Production Checklist

## Artifact

- [ ] Build only required production features.
- [ ] Include required platforms explicitly.
- [ ] Do not deploy `exploit/tkeeper:dev`.
- [ ] Include `auth-dev` only when explicitly required; otherwise confirm it is absent.
- [ ] Do not include failure-injection outside integration tests.
- [ ] Pin the release artifact used by every peer.
- [ ] Verify artifact provenance and integrity before rollout.
- [ ] Confirm every peer runs the same feature and platform set.
- [ ] Confirm every peer uses the same Anvil protocol version; do not mix Fiat-Shamir domain or GG20 MtA wire versions during a rolling upgrade.

## Authentication and permissions

- [ ] If using developer authentication, protect its token and config as production credentials and grant least privilege.
- [ ] Configure the explicitly selected authenticator for the deployment.
- [ ] Bind JWT tokens to expected issuer and audience when using JWT.
- [ ] Keep signing, lifecycle, import, destroy, and audit permissions separate.
- [ ] Review wildcard permissions.

## Network

- [ ] Public API is reachable only where intended.
- [ ] Public API TLS is enabled; JWT mode fails startup when the public server is plaintext.
- [ ] Internal API is reachable only by TKeeper peers.
- [ ] Protected internal routes use TLS; configure mTLS with a distinct `tls-spki-sha256` binding for every peer.
- [ ] Peer URLs use the expected internal addresses.
- [ ] Public API rate limits and request-size limits are enforced at the edge.
- [ ] Hosts have synchronized clocks for JWT, approval, expiry, and audit checks.

## Seal and recovery

- [ ] Use the intended seal provider.
- [ ] Store Shamir shares or provider recovery material outside the node.
- [ ] Test unseal after restart.
- [ ] Restrict access to HSM/KMS keys used for sealing.
- [ ] Back up each peer's database and platform side state according to the recovery design.
- [ ] Test restoring a peer without placing enough key shares and unseal material in one failure domain.
- [ ] Validate the full [Backup and Recovery](https://tkeeper.org/docs/deployment/backup-and-recovery) procedure before go-live.

## Authorities

- [ ] Use structured authorities for governed identities.
- [ ] Leave `keeper.authority.arbitrary.enabled` disabled unless raw signing is explicitly required.
- [ ] Treat `arbitrary` identities as raw-signing identities.
- [ ] Pin authority OCI references by digest.
- [ ] Do not mix `arbitrary` with concrete authorities on the same identity.
- [ ] Confirm downstream systems verify TKeeper proof before execution.
- [ ] Confirm verifiers pin the expected identity and cover every effect-changing field.
- [ ] Define replay, nonce, expiry, and idempotency behavior for each signed action.

## Quorum

- [ ] Use threshold mode for high-impact identities.
- [ ] Initialize every peer with the same `threshold` and `total`.
- [ ] Confirm each peer is unsealed and ready.
- [ ] Monitor threshold session latency and failure rates.
- [ ] Place peers and their seal dependencies in independent failure domains where the threat model requires it.

## Audit

- [ ] Configure at least one audit sink.
- [ ] Decide whether audit sink failure blocks operations.
- [ ] Test audit verification.
- [ ] Alert on audit sink outage.
- [ ] Restart all peers after an integrity-key rotation so process-local peer pins are re-enrolled.

## ML-DSA

- [ ] Monitor retry counts.
- [ ] Keep `keeper.session.mldsa.max-rounds` bounded.
- [ ] Set end-to-end request deadlines.
- [ ] Use rotate, not refresh, when new ML-DSA material is required.

## Incident readiness

- [ ] Document who can seal, unseal, rotate, destroy, import, and repair consistency.
- [ ] Test recovery from one sealed or unavailable peer.
- [ ] Test denied policy and denied permission paths.
- [ ] Keep a runbook for `SESSION_MAX_ROUNDS_EXCEEDED`, quorum failure, and audit outage.
- [ ] Define the response to suspected mono-key exposure before quorum promotion; promotion alone does not remove prior copies.
- [ ] Rotate, rather than refresh, any GG20 generation exposed to a protocol version that transmitted MtA masks or permitted reusable signing state.


---

Source file: content/docs/security-model/README.md
Canonical: https://tkeeper.org/docs/security-model

# Security Model

For typed authorities, TKeeper enforces a cryptographic authority boundary:

```text
no accepted identity intent -> no proof -> no downstream effect
```

This boundary is effective only when the downstream system refuses to execute the action without verifying TKeeper proof.

Read:

- [Threat Model](https://tkeeper.org/docs/security-model/threat-model)
- [Security Assurance](https://tkeeper.org/docs/security-model/security-assurance)
- [Quorum Modes](https://tkeeper.org/docs/security-model/quorum-modes)
- [Authentication and Authorization](https://tkeeper.org/docs/security-model/authentication-and-authorization)
- [Audit Logging](https://tkeeper.org/docs/security-model/audit-logging)
- [Four Eye Control](https://tkeeper.org/docs/security-model/four-eye-control)

## Security goals

TKeeper is designed to:

- prevent unauthenticated or unauthorized clients from using key identities
- bind signatures to understood intents where concrete authorities are used
- keep raw `arbitrary` signing isolated from concrete authorities
- require quorum participation in threshold mode
- reject invalid peer contributions where protocols can verify them
- record security-relevant operations in signed audit logs
- fail closed when required audit sinks are unavailable
- keep integration-only failure injection out of production builds

## Non-goals

TKeeper does not:

- protect an action that can bypass the governed identity
- make a bad authority policy safe
- protect against compromise of at least `threshold` peers
- replace host, network, container, or HSM hardening
- act as a standalone AI firewall, AML system, or fraud engine
- make `arbitrary` raw signing semantically governed
- guarantee availability when fewer than the required peers are healthy
- prevent replay when the signed intent and verifier do not enforce freshness

## Security review checklist

Ask these questions during review:

- Does the downstream system verify TKeeper proof before executing the effect?
- Does it trust the expected key identity and reject unsigned effect-changing fields?
- Are replay, expiry, nonce, and idempotency rules explicit?
- Are high-risk identities in threshold mode?
- Are lifecycle/import/destroy permissions separated from signing?
- Are authority artifacts digest-pinned?
- Are `arbitrary` identities intentionally accepted as raw-signing identities?
- Is developer authentication disabled, or explicitly accepted and protected as a production credential?
- Is the internal peer API private?
- Are audit sinks configured and tested?
- Is the integration image blocked from production deployment?
- Are ML-DSA retry limits and request deadlines monitored?


---

Source file: content/docs/security-model/threat-model.md
Canonical: https://tkeeper.org/docs/security-model/threat-model

# Threat Model

This page covers TKeeper service-level threats and residual risks.

TKeeper's security boundary is the governed cryptographic identity. With a concrete authority, an identity can authorize an action only after TKeeper materializes the intent, accepts the authority and policy decision, passes audit and key controls, and produces proof that the downstream system verifies before execution. An `arbitrary` authority is raw signing and does not provide this semantic guarantee.

For protocol-level details in FROST, GG20, threshold ML-DSA, ECIES, ZK proofs, nonce handling, Paillier, and elliptic-curve math, use the Anvil threat model:

[Anvil threat model](https://github.com/exploit-org/anvil/blob/main/THREAT_MODEL.md)

## Scope

In scope:

- client authentication and authorization
- authority policy
- four eye approvals
- peer authentication and Byzantine behavior
- seal and unseal
- local key share storage
- key lifecycle operations
- trusted dealer import
- audit log integrity and sink enforcement
- UI exposure through `:features:ui`

Out of scope:

- compromise of at least `threshold` peers
- downstream systems that execute effects without verifying TKeeper proof
- bad authority policies approved by operators
- physical side channels
- host hardening
- kernel, container runtime, and hypervisor compromise
- bugs inside cloud KMS, HSM firmware, or external identity providers

## Assumptions

TKeeper runs as a cluster of peers. Each peer has one local key share. A threshold operation needs enough peers to participate.

Compromising fewer than `threshold` peers must not give the attacker a usable private key. Compromising at least `threshold` peers breaks the threshold model.

All protected operations run only after the keeper is unsealed.

Production artifacts include only explicitly selected platforms and features. The dedicated integration artifact additionally contains failure-injection controls and must not be deployed as a production image.

Authorities are part of the signing boundary. A key either uses `arbitrary` for raw signing or uses concrete authority policies. `arbitrary` cannot be mixed with concrete authorities on the same key.

Concrete authorities use digest-pinned OCI references. Tags are mutable and are only useful for local development.

Audit events are signed with the integrity key. When audit is enabled, at least one configured sink must accept the event.

## Assets

| Asset | Confidentiality | Integrity | Availability |
| --- | --- | --- | --- |
| Local key shares | Critical | Critical | High |
| DEK | Critical | Critical | High |
| KEK or unseal material | Critical | Critical | High |
| Shamir unseal shares | Critical | Critical | High |
| HSM or cloud KMS credentials | Critical | Critical | High |
| JWT signing keys and JWKS | High | Critical | High |
| Key metadata and authorities | Low | Critical | High |
| Public keys and commitments | Public | Critical | High |
| Authority OCI artifacts | Low | Critical | High |
| Four eye approver keys | High | Critical | Medium |
| Audit records | Low | Critical | High |
| Session context | Low | Critical | High |

## Trust boundaries

Client to Keeper:

Requests are untrusted until authenticated and authorized. JWT mode validates token signature, `kid`, audience, configured issuer, token lifetime, and subject. JWT mode also requires TLS on the public server so bearer credentials are not accepted over plaintext. Dev token mode is for controlled environments.

Keeper to Keeper:

Protected internal routes require TLS outside dev mode. Requests and authenticated responses are signed and bound to the claimed peer id, request nonce, request hash, status, and body. Configure mTLS with a distinct SPKI pin per peer to bind that claimed id to the transport certificate; without it, first enrollment still trusts the shared bootstrap token and network path. Actor credentials are forwarded only to internal operation entrypoints that enforce their permissions. Peers still verify protocol data. Bad FROST, GG20, ML-DSA, or ECIES contributions are rejected. Protocols report an imposter only where the sender can be identified; a normal ML-DSA rejection-sampling abort is not Byzantine evidence.

Keeper to OCI registry:

Authorities are loaded only from explicitly allowed OCI registries. Digest-pinned references protect against tag drift. The authority id inside the artifact must match the id configured on the key.

Keeper to seal provider:

Seal providers protect the DEK. Built-in providers are Shamir and HSM. External providers are AWS and Google Cloud features.

Keeper storage:

Stored key material is AEAD-encrypted. Integrity-sensitive records use one of two location-bound formats: signed records bind the column family, record id, and payload, while integrity private keys and audit-HMAC keys use encrypted envelopes bound to their exact record ids. This prevents a valid record or ciphertext from being moved into another storage slot. On a legacy V1 upgrade, rebinding internal secrets, signing the initialization envelope, relocating legacy key generations, deleting their source records, and advancing the marker are one cross-column-family transaction. Any validation failure rolls the whole V1 transaction back. The keeper reports not-ready throughout migration and stays sealed on failure, including auto-unseal failure. Relocation is allowed only when the isolated key-version store is empty; pending or mixed state is rejected; generation pointers must be canonical and match signed head/version metadata. Orphan records are not migration roots. Relocated key material remains explicitly legacy and unsigned; only a later refresh or rotate creates a signed, location-bound generation. The signed initialization envelope binds the peer id and quorum tuple after unseal. Integrity and HMAC version pointers are checked against the latest stored version; integrity-key rotation retains historical public keys but removes historical private keys.

Browser to UI:

`:features:ui` exposes the control-plane UI. It uses the same external API permissions as direct API clients. CSP configuration controls what the browser may load or connect to.

## Threats

### T-1: Client Impersonation

Attack:

An attacker uses a forged or stolen token to call signing, decrypt, lifecycle, or inventory APIs.

Mitigation:

- JWT signature and `kid` are checked against JWKS.
- Audience, configured issuer, lifetime, and subject are checked.
- Permissions are enforced per operation.
- Key operations use key-scoped permissions such as `tkeeper.key.<keyId>.sign`.

Residual risk:

A compromised IdP or long-lived token gives the attacker the permissions inside that token until it expires or is revoked.

### T-2: Permission Misconfiguration

Attack:

A principal gets broad permissions and uses a key or lifecycle operation outside its intended scope.

Mitigation:

- Permissions are explicit.
- Key operations can be scoped per key.
- Deny entries can restrict broad grants.
- Destructive operations use separate permissions.

Residual risk:

An operator can still grant broad access. Review wildcard grants before production use.

### T-3: Authority Downgrade

Attack:

An attacker tries to create or import a key with `arbitrary` authority and concrete authorities together, or tries to move a key from typed policy to raw signing.

Mitigation:

- `arbitrary` must be the only authority on a key.
- Concrete authorities require OCI references.
- Authority ids are validated.
- Authority policy is evaluated before signing starts.
- Asset Inventory exposes key authorities for review and export.

Residual risk:

An authorized operator can create a raw-signing key on purpose. Treat `arbitrary` keys as high risk.

### T-4: Authority Artifact Tampering

Attack:

An attacker changes the authority artifact in the registry or points a key at a different policy.

Mitigation:

- Production references use `@sha256:...`.
- `oras.allowed-registries` restricts outbound pulls to exact registry authorities.
- TKeeper verifies the loaded authority id against the configured id.
- Authority metadata is part of signed key state.
- Audit logs record authority-related operations.

Residual risk:

If a bad policy is approved and pushed under its digest, TKeeper will enforce that bad policy. Review authority artifacts before attaching them to keys.

### T-5: Policy or Intent Modeling Gap

Attack:

The policy sees a weak model of the requested action and allows a command whose real effect is unsafe.

Mitigation:

- Typed authorities materialize commands into policy input.
- EVM, Bitcoin, X.509, and custom authorities have separate intent builders.
- `arbitrary` is isolated from typed authority keys.

Residual risk:

Policy can only enforce the effects it can model. Raw bytes give policy almost no semantic context. Custom authorities ignore undeclared JSON fields, so a backend that acts on those fields can create a policy bypass.

### T-6: Four Eye Replay or Bypass

Attack:

An attacker reuses approval proofs for another request or tries to submit duplicate approvers.

Mitigation:

- Approvers sign a hash of the exact operation fields.
- The hash includes nonce and timestamp.
- Duplicate approver keys are rejected.
- A coordinator consumes the nonce only after enough signatures have verified and persists it across restarts for the configured approval TTL.
- `m` must be at least `2`, and `m` cannot exceed `n`.

Residual risk:

Compromised approver keys can approve malicious requests. Store approver keys separately from TKeeper peers. Protocol retries reuse the same approval, so non-coordinator peers verify its signatures, request-field binding, and TTL but do not independently consume its nonce. A compromised coordinator that bypasses its own nonce check can therefore replay a still-fresh approval with the same approved request fields. Current lifecycle and session checks still apply, but independent peer-side replay prevention is not provided.

### T-7: Byzantine Peer During Signing

Attack:

A peer sends invalid FROST, GG20, or threshold ML-DSA data to corrupt a signature or bias the result.

Mitigation:

- FROST verifies peer proof material and signing contributions.
- FROST signing nonces are atomically consumed and cannot produce a second partial signature.
- GG20 verifies the ZK proof flow used by the protocol; MtA responses expose only the ciphertext and proof, never the respondent mask or encryption randomness.
- GG20 signing state emits at most one partial signature.
- FROST and GG20 session clients are destroyed on success, abort, expiry, and shutdown.
- Threshold ML-DSA binds commitments, reveals, participants, message context, and partial responses, then verifies the combined standard ML-DSA signature.
- Identified bad peers are returned as `imposters`.
- Signing restarts with fresh session state where the protocol reports an imposter.

Residual risk:

Byzantine peers can cause availability loss by aborting sessions. Threshold ML-DSA does not provide identifiable abort for every failure. A defect that exposes an MtA mask or permits reuse of FROST/GG20 signing state can invalidate the threshold confidentiality assumption; affected key generations require rotation, not refresh.

### T-8: Byzantine Peer During ECIES Decrypt

Attack:

A peer returns a forged partial decrypt.

Mitigation:

- Each partial decrypt carries a DLEQ proof.
- The coordinator verifies the proof against the ciphertext point, derived peer public share, and partial decrypt.
- Bad partial decrypts are skipped and reported as imposters.
- Decrypt succeeds only if enough honest partial decrypts remain.

Residual risk:

Too many unavailable or dishonest peers can stop decryption.

### T-9: Local Storage Read or Tamper

Attack:

An attacker reads RocksDB files or changes stored records.

Mitigation:

- Key shares are encrypted at rest.
- The DEK is wrapped by seal material.
- Signed records protect integrity-sensitive state.
- A sealed keeper refuses protected operations.

Residual risk:

Memory forensics against an unsealed keeper can expose runtime secrets. Signatures detect altered or relocated records, but they cannot by themselves detect a coherent same-location replay of an older record set. This includes replaying a key head with its matching key and metadata records, or rolling back the complete database. During the first upgrade from legacy V1 storage, TKeeper can authenticate the legacy ciphertext with the DEK and verify its signed metadata, but it cannot prove that an unbound ciphertext was not substituted before that migration. Relocation does not add a signature to that key material; it remains legacy until refresh or rotate. Pre-2.2 platform side-state and sessionless destroy-marker formats that do not embed their identity remain read-compatible until the corresponding key lifecycle rewrite; relocating one fails later consistency checks or can force a fail-closed denial of service. Verify and protect the existing database and backups before the first 2.2 unseal. Use host storage controls, encrypted swap, durable external audit export, an independently protected monotonic checkpoint where rollback detection is required, and protected backups.

### T-10: Unseal Material Compromise

Attack:

An attacker obtains enough Shamir shares, HSM access, or cloud KMS rights to unwrap the DEK.

Mitigation:

- Shamir uses configurable `threshold` and `total`.
- HSM keeps wrapping keys outside TKeeper storage.
- AWS and Google Cloud providers rely on provider IAM and KMS audit logs.
- Auto-unseal can be disabled.

Residual risk:

Seal providers move trust into operators, HSM policy, or cloud IAM. Treat that material like root recovery access.

### T-11: Audit Tampering or Sink Failure

Attack:

An attacker edits audit logs or makes sinks unavailable.

Mitigation:

- Audit events are Ed25519-signed payloads.
- Verification uses the integrity public key version recorded in the event.
- If audit is enabled, protected operations require at least one available sink.
- If all configured sinks fail or time out while writing, the operation fails.

Residual risk:

Deployments without audit enabled lose this control.

### T-12: Trusted Dealer Abuse

Attack:

An authorized caller imports weak or unauthorized key material through trusted dealer flow.

Mitigation:

- Trusted dealer import is separately permissioned.
- Import runs through key metadata, authorities, commitments, and audit.
- Stored key records are integrity-protected.

Residual risk:

Trusted dealer mode trusts the importer to bring valid key material. Use it only for migration or recovery flows that need it.

### T-13: Key Lifecycle Abuse

Attack:

An attacker rotates, refreshes, destroys, or runs consistency repair on a key to cause denial of service or move the key into an unexpected state.

Mitigation:

- Lifecycle operations use separate permissions.
- Destructive operations are audit-logged.
- Key metadata and active generations are integrity-protected.
- Refresh and rotate write a new signed generation and never overwrite a legacy generation in place.
- Threshold destroy commit and abort messages are accepted only from the peer that prepared the signed destroy session.
- ECC refresh reshapes the existing secret shares without changing the public key. Rotate creates new key material.
- ML-DSA refresh carries each peer's existing share and public key into the new generation unchanged; it does not replace shares or refresh cryptographic material. ML-DSA rotate runs a new DKG and creates a new key.
- Consistency repair is an explicit API, not part of normal signing flow.

Residual risk:

Authorized lifecycle operators can still break availability. A compromised ML-DSA share remains compromised after refresh; use rotate when new cryptographic material is required. Keep lifecycle permissions narrower than signing permissions.

### T-14: UI Exposure

Attack:

An attacker uses the UI to trigger privileged operations from a browser session.

Mitigation:

- UI calls the same authenticated external APIs.
- UI can be disabled.
- CSP limits script, connect, image, and form targets.
- Browser-originated operations still require permissions and approvals.

Residual risk:

Bad CSP or weak browser session handling can expose operators to web attacks. Keep UI access narrow.

### T-15: Probabilistic ML-DSA Signing Exhaustion

Attack or failure:

A healthy threshold ML-DSA signing operation repeatedly aborts during rejection sampling, or an attacker amplifies the cost by submitting many authorized signing requests.

Mitigation:

- Each retry creates fresh session state and fresh signing randomness.
- `keeper.session.mldsa.max-rounds` bounds complete signing attempts; it defaults to `12`.
- Session expiry and caller-side deadlines bound retained state and end-to-end latency.
- Exhaustion fails closed with `SESSION_MAX_ROUNDS_EXCEEDED`; no signature is returned.

Residual risk:

Threshold ML-DSA is probabilistic and does not provide a hard success-latency guarantee. Exhaustion with empty `dead` and `imposters` sets is not proof of corruption. Monitor retry counts and latency; increasing the attempt cap trades availability for resource use and worst-case response time.

### T-16: Integration Artifact in Production

Attack:

An operator deploys the integration image, exposing the failure-injection module used to corrupt, demote, delete, or replace test key state.

Mitigation:

- Regular `shadowJar` and `dockerBuild` include only selected production platforms and features.
- `:integration-tests:failure-injection` is wired only into `shadowJarIntegration` and `dockerBuildIntegration`.
- The integration image uses the separate `exploit/tkeeper:dev` tag.

Residual risk:

Build separation cannot prevent an operator from deploying the wrong artifact. Production admission and release policy must reject the integration jar and `exploit/tkeeper:dev` image.

### T-17: Downstream Proof Misuse

Attack:

A downstream service accepts a valid signature for the wrong identity, executes a payload that differs from the governed intent, relies on unsigned fields, or replays a previously valid proof.

Mitigation:

- Verifiers pin the expected identity or public key.
- The executed effect is reconstructed from the exact governed command.
- Security-relevant context is included in the signed intent or checked before execution.
- Nonces, expiry, sequence numbers, or idempotency keys are enforced where replay matters.
- Custom-authority integrations reject or ignore undeclared fields before execution.

Residual risk:

TKeeper cannot enforce a downstream path that misinterprets or bypasses its proof. Cryptographic validity does not establish business validity, freshness, or correct environment by itself.

## Security properties

| Property | Mechanism |
| --- | --- |
| No unilateral key use in threshold mode | `t-of-n` threshold protocols |
| No key reconstruction in normal threshold flows | FROST, GG20, threshold ML-DSA, and threshold ECIES use shares |
| Raw signing isolated | `arbitrary` cannot be mixed with concrete authorities |
| Typed signing policy | authorities materialize commands before signing |
| Authority immutability | digest-pinned OCI references |
| Four eye binding | approver signatures over operation hash |
| Byzantine detection | protocol proofs, DLEQ checks, imposter reporting |
| Sealed state | protected operations refused until unseal |
| Storage confidentiality | DEK/KEK envelope encryption |
| Storage integrity | signed key and metadata records |
| Audit integrity | Ed25519-signed events |

## Operational checklist

- Use short-lived JWTs.
- Configure the expected JWT issuer and audience explicitly.
- Do not enable dev token mode in production unless its risk is explicitly accepted and its token and permissions are protected as production credentials.
- Avoid broad wildcard permissions.
- Treat `arbitrary` keys as raw signing keys.
- Use digest-pinned OCI authorities.
- Review Asset Inventory exports.
- Distribute Shamir shares across separate operators.
- Restrict HSM, AWS KMS, and Google Cloud KMS access to TKeeper identities.
- Enable audit with more than one sink.
- Watch `imposters` and `dead` fields after failed threshold operations.
- Put rate limiting in front of public endpoints.


---

Source file: content/docs/security-model/security-assurance.md
Canonical: https://tkeeper.org/docs/security-model/security-assurance

# Security Assurance

TKeeper security assurance currently comprises **367 automated functional scenarios across 19 test
classes**, including **90 protocol and corruption failure-injection scenarios** and one 3-of-5
share-recovery scenario, executed against multi-node Keeper deployments.

The standard Testcontainers topology runs a 2-of-3 quorum with peer communication, storage,
SoftHSM, restarts, and malicious protocol injection. Production integration uses a three-node
transport cluster and a 3-of-5 recovery cluster with per-run PKI, TLS, mTLS, peer authentication,
and SPKI pinning. The transport cluster also exercises JWT and JWKS behavior.

Every pull request targeting `main` and every commit pushed to `main` runs the Release Gate. A
passing revision completes the module test tasks, all 367 functional scenarios, artifact isolation,
and both container builds.

> **In short:** TKeeper tests cover production identity and transport,
> authorization and four-eye policy, malicious coordinators and Byzantine
> peers, FROST/GG20/ML-DSA transcript attacks, ECIES contribution integrity,
> tamper-evident key state and lifecycle, generative and coverage-guided binary
> parser and protocol-state testing, invalid transition order, concurrent
> duplicate delivery, crash-safe session cleanup, audit persistence, ECC and
> ML-DSA share recovery, and production artifact isolation.

Every claim below maps to an executable scenario, generated property, fuzz
target, or release check.

## Security posture

The tests demonstrate these properties:

- **Quorum-enforced key use.** Threshold signing and decryption operate on
  shares while key material remains distributed.
- **Fail-closed peer validation.** Peers validate signer sets, proofs,
  commitments, contributions, and one-shot protocol state at the consuming
  trust boundary.
- **Intent-bound authorization.** Authorities, typed commands, policy, and
  four-eye approvals are bound to the requested cryptographic operation and
  cannot be substituted or replayed through the tested paths.
- **Authenticated transport identity.** Production public access uses TLS and
  JWT validation; peer access combines mTLS, signed peer authentication, and
  per-peer certificate pinning.
- **Tamper-evident state.** Signed metadata, location-bound key records,
  generation pointers, migrations, and audit records fail closed under the
  tested storage mutations.
- **Production build separation.** Development authentication, failure injection, and recovery are
  verified absent from the default production test artifact. Recovery appears only when selected.

## Evidence quality

| Evidence layer | How it is exercised | Security signal |
| --- | --- | --- |
| Distributed execution | Functional tests run multi-node Keeper clusters with key generation, shares, network calls, storage, and cryptographic implementations. | Exercises system boundaries and protocol composition across deployed components. |
| Production transport topology | The production image runs with generated PKI, TLS, mTLS, certificate pins, JWT, JWKS rotation, and connection-rejection cases. | Exercises deployed authentication and transport failure modes. |
| Protocol failure injection | A test-only module introduces one security-relevant mutation at a time into FROST, GG20, ML-DSA, ECIES, and keeper protocol transitions. | Demonstrates rejection at the peer that consumes untrusted protocol data. |
| Failure contracts | Negative cases assert rejection reason and, where the protocol supports it, attribution of the malicious peer. | Detects regressions that crash or reject for the wrong reason. |
| Recovery checks | Every FROST, ML-DSA, and keeper-transition mutation is followed by a distributed signature. A separate 3-of-5 topology rebuilds ECC and ML-DSA histories on two damaged peers. | Confirms continued key use after rejected protocol input and checks full key-scoped state reconstruction. |
| Generative parser testing | Five serialization properties generate 2,500 cases per run with shrinking; a seeded Jazzer target coverage-guides malformed inputs through five security-sensitive binary decoders. | Checks round-trip, canonical encoding, record binding, key-kind preservation, bounded parsing, and controlled rejection beyond hand-written examples. |
| Stateful protocol modeling | Fifteen lifecycle and protocol-state properties exercise 7,350 generated participant topologies, action sequences, and concurrent schedules per run. Jazzer targets cover the protocol state containers used by ECC DKG, PQC DKG, FROST, GG20, threshold ML-DSA signing, and ECC/PQC recovery payload handling. | Compares state containers against legal-transition, uniqueness, operation-isolation, order-independence, single-winner consumption, destroy-state, transcript-binding, and recovery-payload models. |
| Concurrency and crash recovery | Eight simultaneous deliveries race for one signing session or one keeper protocol transition. Container-restart cases stop a keeper after FROST nonce generation, GG20 ephemeral initialization, or ML-DSA round 1. | Checks one-winner transitions, terminal session state, durable key state, safe session recreation, and post-failure signing. |
| Release isolation | The release gate checks module tests, functional behavior, container builds, and separation of integration-only and explicit recovery code from the default production artifact. | Prevents the security harness from entering production and prevents recovery endpoints from appearing unless selected. |

## Assurance by domain

| Security domain | Demonstrated assurance | Detailed evidence |
| --- | --- | --- |
| Threshold protocol integrity | Malformed signer sets, invalid transition order, sequential or concurrent replay, invalid proofs, transcript mutations, bad partial contributions, and consumed-state reuse fail closed across ECC/PQC DKG, FROST, GG20, and threshold ML-DSA. Generated and concurrent action schedules additionally check the protocol state containers against explicit transition models. | [Threshold protocols](#threshold-protocols) |
| Byzantine tolerance and recovery | Invalid attributable FROST, GG20, and ECIES contributions identify the responsible peer; an honest quorum continues where the protocol permits retry. Mismatched ML-DSA shares cannot produce a signature. Operator-selected helper consensus reconstructs ECC and ML-DSA histories on damaged peers without restoring destroyed secret material. | [Byzantine peers](#byzantine-peers-and-recovery) |
| Authentication and transport | Forged or malformed JWTs, missing permissions, TLS downgrade, unknown CAs, hostname mismatch, invalid client certificates, peer impersonation, and unsafe production configuration are rejected. | [Identity and transport](#identity-and-transport) |
| Intent, policy, and approvals | Authority confusion, typed-intent mutation, policy deletion, incomplete approvals, approval substitution, replay, and nonce races cannot authorize a tested protected operation. | [Authorization and policy](#authorization-and-policy) |
| Key state and lifecycle | Signed-record tampering, relocation, pointer rollback, unsafe migration, conflicting lifecycle mutations, and invalid promotion state fail closed or preserve the documented identity invariant. Security-sensitive binary records additionally have generative canonicality, binding, and malformed-input coverage. | [State and lifecycle](#state-lifecycle-and-audit) |
| Signature and decryption correctness | Threshold and mono outputs verify across supported ECC and ML-DSA paths; modified signatures, payloads, ML-DSA components, ECIES ciphertexts, and tweaks are rejected. | [Output integrity](#output-integrity) |
| Operational security | Credential rotation retains the last known good identity on invalid updates. Durable keys and HSM state survive a tested keeper restart while process-local signing state is purged; audit records remain verifiable across restart and migration, and integration-only modules remain outside production artifacts. | [Operational controls](#operational-controls) |

## Adversarial protocol coverage

Each adversarial vector is reported as a separate JUnit invocation. The
FROST, GG20, and ML-DSA suite contains 53 negative cases and three valid
controls:

| Protocol | Malicious signing packages | Transcript or protocol-input mutations | Valid controls | Reported cases |
| --- | ---: | ---: | ---: | ---: |
| FROST | 6 | 11 | 1 | 18 |
| GG20 | 6 | 12 | 1 | 19 |
| Threshold ML-DSA | 6 | 12 | 1 | 19 |
| **Total** | **18** | **35** | **3** | **56** |

FROST commitments and signature shares are produced by a remote Keeper.
GG20 controls generate valid Paillier and zero-knowledge proof material before
each mutated input is sent to the production respondent. ML-DSA probes exercise
the stored-key commit/reveal state machine. The exact vectors and expected
failure contracts are listed in the [exact protocol vector catalog](#exact-protocol-vector-catalog).

ECIES adds participant-set validation, DLEQ verification of partial decryptions,
and negative ciphertext and tweak coverage outside the 56 signing cases above.

Keeper protocol-order coverage adds seven failure-injection invocations:

| State machine | Reordered transition | Replay | Eight-way race | Reported cases |
| --- | ---: | ---: | ---: | ---: |
| ECC DKG | 1 | 0 | 0 | 1 |
| PQC DKG | 0 | 1 | 1 | 2 |
| FROST signing | 1 | 0 | 0 | 1 |
| GG20 signing | 1 | 0 | 0 | 1 |
| Threshold ML-DSA signing | 0 | 1 | 1 | 2 |
| **Total** | **3** | **2** | **2** | **7** |

These cases cross the internal peer transport. The race cases require one accepted transition and
seven rejected duplicates. Each case ends with a normal distributed signature.

## Research lineage and interpretation

| Source | Failure cases converted into executable regressions |
| --- | --- |
| [RFC 9591] and the [NCC Group Zcash FROST assessment] | Distinct and valid signer identities, exact commitment sets, participant binding, proof validation, partial-signature verification, and one-shot nonce state. |
| [BitForge GG18/GG20 finding] | Invalid Paillier modulus, generator, ciphertext, range proof, biprime proof, and no-small-factor proof inputs. |
| [CGGMP21 modulus-proof advisory], [CGGMP24 Pi-enc hardening], and [CGGMP24 aff-g hardening] | Multiplicative-group membership variants across Paillier ciphertexts, randomizers, range commitments, and Ring-Pedersen proof elements; non-coprime biprime witnesses. |
| [Efficient Threshold ML-DSA] | Commit/reveal binding, exact round participants, malformed transcript rejection, and one-shot round state. |
| [NIST ACVP ML-DSA specification] | Independent mutation of the final signature commitment, `z`, hint, and message across ML-DSA-44, ML-DSA-65, and ML-DSA-87. |
| TKeeper variant analysis and [Anvil Paillier proof regressions] | Truncated and non-canonical binary inputs, degenerate zero-ciphertext proofs, concurrent state resurrection, session replay, consumed-state reuse, and in-flight keeper crash schedules. |

## Reproducing the assurance

Run the release gate:

```bash
./gradlew releaseGate
```

This builds the integration and production transport images, runs module and
functional tests, and verifies artifact isolation. A shorter command for the
protocol-input package is documented under
[Integration Tests](https://tkeeper.org/docs/operations/integration-tests).

The property and coverage-guided parser layer can be reproduced separately:

```bash
./gradlew :platform-ecc:test \
  --tests 'org.exploit.keeper.platform.ecc.property.SecuritySerializationProperties' \
  --tests 'org.exploit.keeper.platform.ecc.fuzz.SecurityBinaryParserFuzzTest'
./gradlew :platform-ecc:fuzzSecurityParsers
```

Run the generated and coverage-guided protocol-state layer with:

```bash
./gradlew :platform-ecc:test \
  --tests 'org.exploit.keeper.platform.ecc.property.ProtocolStateMachineProperties' \
  --tests 'org.exploit.keeper.platform.ecc.fuzz.SecurityProtocolStateFuzzTest' \
  --tests 'org.exploit.keeper.platform.ecc.fuzz.KeeperProtocolStateFuzzTest'
./gradlew :platform-pqc:test \
  --tests 'org.exploit.keeper.platform.pqc.property.MLDSAStateMachineProperties' \
  --tests 'org.exploit.keeper.platform.pqc.fuzz.MLDSAStateMachineFuzzTest' \
  --tests 'org.exploit.keeper.platform.pqc.fuzz.KeeperMLDSAProtocolStateFuzzTest'
./gradlew :test \
  --tests 'org.exploit.keeper.tests.temporary.InMemoryTemporaryMapConcurrencyTest'
./gradlew securityFuzz
```

`securityFuzz` runs seven coverage-guided campaigns: binary decoding, the existing ECC and ML-DSA
state containers, keeper-level ECC and ML-DSA protocol transitions, ECC recovery sessions, and PQC
recovery payloads.

## Deployment and operational boundaries

The following conditions require deployment or operational controls outside
the Keeper process:

- compromise of at least the configured threshold, or compromise of the host,
  HSM, cloud unseal authority, build pipeline, or deployment control plane;
- broad denial of service, physical side channels, fault injection, entropy
  failure, and platform-specific constant-time behavior;
- coherent full-database rollback without an external monotonic control;
- unsafe authority policy, use of raw `arbitrary` signing where semantic
  governance is required, or a downstream verifier that ignores identity,
  intent, expiry, or replay requirements.

The [Threat Model](https://tkeeper.org/docs/security-model/threat-model) specifies the corresponding trust
assumptions and mitigations. Exact test traceability continues below in
[Executable evidence](#executable-evidence).

## Executable evidence

This catalog maps each demonstrated security property to executable evidence
and records the exact adversarial protocol vectors. Threat identifiers refer
to the [Threat Model](https://tkeeper.org/docs/security-model/threat-model). Evidence is revision-specific: the test
names identify the executable contract, while the release gate determines
whether that contract passes for a given artifact.

### Threshold protocols

| Control boundary | Verified behavior | Evidence |
| --- | --- | --- |
| Coordinator-supplied signing package (T-7) | FROST, GG20, and threshold ML-DSA peers reject omission of the local participant, duplicate, undersized, or out-of-range participant sets, and sequential replay of an active session id. Under an eight-way concurrent replay, exactly one creator wins and all seven duplicates receive `SESSION_ALREADY_EXISTS`. Each protocol/vector pair is independently reported. | [FailureInjectionTests]: `frostRejectsInvalidSigningPackage`, `gg20RejectsInvalidSigningPackage`, `mldsaRejectsInvalidSigningPackage` |
| FROST commitment and signature transcript (T-7) | A remote Keeper produces the commitment and signature share. Changed proof material, malformed encoding, identity mismatch, missing or duplicate contributions, cross-context replay, changed nonce commitment, changed signature share, and consumed nonce reuse fail closed. Attributable failures identify the remote peer. | [FailureInjectionTests]: `validFrostSigningTranscriptPasses`, `frostSigningTranscriptRejectsMaliciousInput` |
| GG20 MtA and Paillier input (T-7) | Valid transcripts pass on both supported GG20 curves. Small, even, or oversized moduli, invalid generator, zero, non-coprime, or out-of-range ciphertexts, non-coprime biprime witnesses, and mutated or truncated range, biprime, and no-small-factor proof material produce an identifiable abort attributed to the initiator. | [FailureInjectionTests]: `validGg20MtATranscriptsPassOnSupportedCurves`, `gg20MtARejectsMaliciousInput` |
| Threshold ML-DSA commit/reveal transcript (T-7) | A valid stored-key transcript passes. Changed or truncated commitments and reveals, duplicate, missing, or out-of-range round senders, commitment-opening mismatch, and reuse of consumed round state fail closed as independently reported cases. | [FailureInjectionTests]: `validMLDSASigningTranscriptPasses`, `mldsaSigningTranscriptRejectsMaliciousInput` |
| Generated protocol-state transitions (T-7) | Valid threshold participant sets are order-independent; omission, duplication, wrong size, and out-of-range mutation fail with the expected contract. FROST nonce pairs and shares remain operation-scoped and one-shot, GG20 MtA values remain per-peer unique and order-independent, and ML-DSA batches and round stores match explicit transition and destruction models across generated action sequences. Concurrent nonce/share/MtA/round-state races have exactly one winner. Session-map close is terminal and revokes each remaining value once; the first concurrency run exposed and fixed post-close state resurrection. | [ProtocolStateMachineProperties]; [SecurityProtocolStateFuzzTest]; [MLDSAStateMachineProperties]; [MLDSAStateMachineFuzzTest]; [InMemoryTemporaryMapConcurrencyTest] |
| Keeper protocol transition order (T-7) | ECC DKG completion before computation, FROST signing before commitment collection, and GG20 signing before setup are rejected. PQC DKG and ML-DSA signing reject sequential round-1 replay; eight simultaneous round-1 deliveries produce one winner. Generated actions cover these production transition guards and failure rollback. | [FailureInjectionTests]: `keeperProtocolStateRejectsReorderedReplayAndConcurrentTransitions`; [KeeperProtocolStateFuzzTest]; [KeeperMLDSAProtocolStateFuzzTest] |
| In-flight keeper crash (T-7) | A keeper is restarted after FROST nonce generation, GG20 ephemeral initialization, or ML-DSA round 1. Persistent RocksDB and HSM key state survive, but the process-local session cannot resume and returns `SESSION_NOT_FOUND`. The same session id can then be created and cleared safely, followed by a valid distributed signature. | [FailureInjectionTests]: `inFlightProtocolStateDoesNotSurviveKeeperRestart` |
| Supported protocol paths | Threshold and mono signing and verification execute across GG20 ECDSA, FROST Schnorr/BIP-340/Taproot, threshold Ed25519, and ML-DSA-44/65/87, including supported tweak paths. ECIES executes with AES-GCM and ChaCha20-Poly1305 on secp256k1 and P-256. | [SignatureTests]: scheme-specific sign/verify tests; [ECIESTests]: `encryptDecryptSuccessful`, `encryptDecryptSuccessfulWithTweak`, `ensureDleqProofPassesAfterRefresh` |

The FROST participant, proof, commitment-set, signature-share, duplicate, and
nonce-consumption invariants execute in the ciphersuite-independent shared
signing state machine. Ciphersuite-specific valid signing and verification
paths are covered in [SignatureTests].

### Byzantine peers and recovery

| Failure mode | Verified behavior | Evidence |
| --- | --- | --- |
| Corrupted FROST share (T-7) | A peer with internally coherent but incorrect local share and commitments initializes from its own stored public state; its invalid contribution is then rejected and attributed while an honest threshold still produces a verifiable Schnorr signature. | [FailureInjectionTests]: `corruptedKeyMaterialOnOnePeerDoesNotForgeOrStopThreshold` |
| Corrupted GG20 share (T-7) | A peer with internally coherent but incorrect local share and commitments initializes from its own stored public state; its invalid contribution is then rejected and attributed while an honest threshold still produces a verifiable ECDSA signature. | [FailureInjectionTests]: `corruptedEcdsaKeyMaterialOnOnePeerDoesNotForgeOrStopThreshold` |
| ML-DSA public side-state mismatch (T-7) | A peer whose stored aggregate public state has been changed cannot silently contribute; the coordinator retries with a healthy peer. | [FailureInjectionTests]: `tamperedMLDSAPublicKeySideStateRetriesWithHealthyPeer` |
| ML-DSA quorum share mismatch (T-7) | A quorum assembled from shares belonging to different keys cannot produce a signature. | [FailureInjectionTests]: `mismatchedMLDSAPartySharesCannotProduceSignature` |
| Corrupted ECIES partial decrypt (T-8) | The DLEQ proof rejects the contribution, attributes the peer, and an honest threshold still decrypts. | [FailureInjectionTests]: `corruptedEciesKeyMaterialOnOnePeerIsRejectedByDleqProof` |
| Malicious ECIES participant set (T-8) | Omitted local peer, duplicate identifier, undersized set, and configured-range violation are rejected before threshold decryption. | [FailureInjectionTests]: `maliciousCoordinatorCannotInjectInvalidEciesParticipantSet` |
| Damaged peer reconstruction (T-9, T-13) | In a production-TLS 3-of-5 cluster, two peers with complementary missing, rolled-back, and corrupted ECC/ML-DSA state are rebuilt from three explicit healthy helpers. Helper disagreement fails before writes. Recovery restores metadata, policy, authorities, public side-state, history, owner indexes, and active signing while destroyed generations remain without secret material. A forced mid-transaction failure rolls back, and a prefix-adjacent key remains unchanged. | [RecoveryFailureInjectionTests]: `recoversComplementaryDamageAndHistoryAcrossTwoPeers` |

### Identity and transport

| Attack or failure | Verified behavior | Evidence |
| --- | --- | --- |
| Forged or malformed production JWT (T-1) | Missing token, malformed JWT, missing or unknown `kid`, bad signature, RS256/HS256 confusion, invalid issuer, audience or lifetime, and missing or blank subject are rejected with `401`. The development token is not accepted by the production topology. | [ProductionTransportSecurityTests]: `protectedEndpointRejectsMissingJwt`, `developmentTokenHeaderCannotAuthenticateProductionTopology`, `rejectsInvalidJwtVariants` |
| Authenticated principal without permission (T-2) | A valid identity without the required permission is rejected with `403` on a representative protected control-plane endpoint. | [ProductionTransportSecurityTests]: `authenticatedPrincipalWithoutPermissionIsDenied` |
| Public transport downgrade or impersonation (T-1) | The public API accepts TLS 1.2 and 1.3, refuses plaintext on the TLS port, and clients reject unknown CAs and hostname mismatch. | [ProductionTransportSecurityTests]: `publicApiNegotiatesEverySupportedProductionTlsProtocol`, `plaintextCannotBeUsedOnPublicTlsPort`, `publicCertificateFromUnknownCaIsRejected`, `publicCertificateHostnameMismatchIsRejected` |
| Peer transport impersonation (T-7, T-8) | The internal listener requires an appropriate CA-trusted client certificate and client-auth EKU. A CA-trusted outsider still fails signed peer authentication. Outbound peers reject unknown CAs, hostname mismatch, and swapped per-peer SPKI pins. | [ProductionTransportSecurityTests]: `internalListenerRequiresClientCertificate`, `internalListenerRejectsClientCertificateFromUnknownCa`, `internalListenerRejectsCertificateWithoutClientAuthEku`, `caTrustedClientCertificateStillRequiresSignedPeerAuthentication`, `outboundPeerTlsRejectsServersFromUnknownCa`, `outboundPeerTlsRejectsServerHostnameMismatch`, `swappedPeerPinsBreakProtocolAndRestoringPinsRecoversIt` |
| Unsafe production security configuration | Startup fails for disabled public or peer TLS, insecure peer/JWKS/OIDC URLs, credential-bearing or fragmented peer URLs, disabled internal client authentication, and duplicate or malformed peer identities and pins. | [ProductionTransportSecurityTests]: `rejectsUnsafeProductionStartupConfiguration` |
| Credential disclosure | Private test credentials are checked for absence from container logs. | [ProductionTransportSecurityTests]: `privateCredentialsNeverAppearInContainerLogs` |

### Authorization and policy

| Attack or failure | Verified behavior | Evidence |
| --- | --- | --- |
| Authority downgrade or type confusion (T-3, partial T-4) | Raw `arbitrary` authority cannot be mixed with OCI authorities or declared by an OCI authority document. Empty, duplicate, malformed, missing-reference, reference/id-mismatched, unsupported, command-unattached, and artifact-type-mismatched authorities fail closed. | [AuthorityPolicyTests]: `rejectsAuthorityThatIsNotAllowedForKey`, `rejectsArtifactTypeThatDoesNotMatchAuthorityType`, `rejectsGenerateWithArbitraryMixedWithOciAuthority`, `rejectsOciAuthorityWithArbitraryType`, `rejectsDuplicateAuthorityIdsOnGenerate`, `rejectsGenerateWithEmptyAuthorities`, `rejectsGenerateWithInvalidAuthorityId`, `rejectsOciAuthorityWithoutReference`, `rejectsOciReferenceWhoseAuthorityIdDoesNotMatch`, `rejectsUnsupportedAuthorityTypeBeforeCreatingKey`, `verifyRejectsMalformedEvmMaterialWithoutResolvingAuthority` |
| Disabled arbitrary authority (T-3) | A keeper with `arbitrary` disabled rejects both creation of a raw-signing key and signing with an existing key whose stored authority is `arbitrary`. | [ArbitraryAuthorityConfigTests]: `rejectsArbitraryKeyCreationWhenDisabled`, `rejectsArbitrarySigningForExistingKeyWhenDisabled` |
| Typed intent or effect-policy bypass (T-5) | Allowed commands sign while policy violations are denied for custom payments, EVM native/ERC-20 transfer, approval, `transferFrom`, vault operations, Bitcoin spend, and X.509 issuance. Unknown fields, wrong types, wrong principals, and over-limit amounts are exercised. | [AuthorityPolicyTests]: `deniesTypedCommandRejectedByAuthorityPolicy`, `rejectsTypedCommandWithUnknownField`, `rejectsTypedCommandWithWrongFieldType`, `deniesEvmErc20ApprovalWhenAmountExceedsPolicy`, `deniesEvmErc20TransferFromWhenOwnerDoesNotMatchPolicy`, `deniesEvmVaultWithdrawWhenRecipientDoesNotMatchPolicy`, `deniesBitcoinSpendRejectedByAuthorityPolicy`, `deniesX509TbsCertificateRejectedByAuthorityPolicy` |
| Dry-run policy bypass (T-1, T-3, T-5) | The optional endpoint reports allow, deny, and four-eye-control decisions with approval requirements; it requires authentication, validates key identifiers and existence, and rejects authorities not assigned to the key. | [DryRunTests]: `returnsAllowForAcceptedCommand`, `returnsDenyAsEvaluationResult`, `returnsAllowWithRequirementsForFourEyeControl`, `requiresAuthentication`, `rejectsInvalidKeyId`, `rejectsMissingKey`, `rejectsAuthorityNotAssignedToKey` |
| Four-eye bypass or incomplete approval group (T-6) | Sign, decrypt, refresh, rotate, and destroy fail without required proofs. Replaced approver sets invalidate old proofs; every matching authority group is required; key and authority requirements are cumulative. | [FourEyeControlTests]: `ensureDecryptRequiresProofs`, `ensureSignRequiresProofs`, `ensureRefreshRequiresProofs`, `ensureOldKeySetNoLongerWorks`, `ensureRotateRequiresProofs`, `authorityPolicyRequiresEveryMatchingApprovalGroup`, `keyAndAuthorityPoliciesShareOneApprovalNonce`, `lenientFourEyeControlAppliesToDestroy` |
| Approval substitution, replay, or race (T-6) | Changing an approved command invalidates its proof. A consumed approval cannot replay; concurrent submissions yield only one success; consumption survives coordinator restart. | [FourEyeControlTests]: `authorityPolicyBindsProofsAndRejectsReplay`, `approvalNonceIsConsumedAtomically`, `approvalNonceRemainsConsumedAfterCoordinatorRestart` |
| Single-peer policy tampering (T-3, T-5, T-6) | Mutating the coordinator's authority or deleting its four-eye or expiry policy does not reach threshold. A healthy quorum still accepts a valid command when one non-coordinator peer has missing or swapped authority metadata. | [FailureInjectionTests]: `arbitraryAuthorityInjectedOnOneCoordinatorPeerDoesNotReachThreshold`, `authoritySwappedOnCoordinatorCannotAuthorizeDifferentCommand`, `fourEyePolicyDeletedOnCoordinatorStillRequiresPeerApprovals`, `expiredApplyPolicyClearedOnCoordinatorStillBlocksAtPeers`, `validCommandStillSignsWhenOnePeerRemovedAuthorities`, `validCommandStillSignsWhenOnePeerSwappedAuthorities` |
| Coordinator role boundary (T-13) | DKG, ECIES, signing, destroy, and consistency mutation requests sent to a non-coordinator are rejected with `NOT_COORDINATOR`. | [CoordinatorDisabledTest]: `ensureDkgDisabled`, `ensureCipherDisabled`, `ensureSignaturesDisabled`, `ensureDestroyDisabled`, `ensureConsistencyFixDisabled` |

### State, lifecycle, and audit

| Attack or failure | Verified behavior | Evidence |
| --- | --- | --- |
| Stored-record tampering, relocation, or pointer rollback (T-9) | Changed metadata and signed generations are exposed as tampered. Moving a location-bound active record, restoring an old generation pointer, or mixing legacy and signed storage fails closed with `TAMPERED_KEEPER`. | [FailureInjectionTests]: `tamperedMetadataIsVisibleInInventory`, `tamperedSignedKeyGenerationIsFlaggedInInventory`, `relocatedSignedActiveRecordFailsClosed`, `rolledBackGenerationPointerFailsClosedWithoutFallback`, `refreshRejectsMixedLegacyAndSignedStorage` |
| Malicious or mixed migration state (T-9, T-13) | Migration refuses non-empty targets and synthetic roots. Failed migration is not committed, remains sealed across restart, and does not expose protected operations. Valid migration runs once and preserves key and audit state. | [LegacyStorageMixedStateTests]: `migrationRefusesToOverwriteNonEmptyTargetStore`; [LegacyStorageUntrustedRootTests]: `migrationRejectsRandomAuthenticatedDataWithSyntheticPointer`; [LegacyStorageMigrationTests]: `v211V1KeyStorageMigratesOnceBeforeRefreshAndRotate` |
| Destructive or conflicting lifecycle mutation (partial T-12, T-13) | Duplicate create/import, invalid imported encoding, algorithm-changing refresh, and current-generation destruction fail. Old-generation destruction propagates across peers. | [KeyImportTests]: `invalidBase64ImportFails`, `duplicateImportFails`; [KeyLifecycleTests]: `duplicateCreateFails`, `refreshMLDSAWithDifferentAlgorithmFailsWithoutChangingGeneration`, `destroyActualGenerationFails`, `ensureDestroyedSecp256k1KeyGenerationOnAllKeepers` |
| Mono-to-threshold promotion | Promotion preserves the active identity, destroys mono history, and requires restart before threshold use. | [QuorumPromotionTests]: `promoteMonoKeeperAndRequireRestart`, `promotedInventoryKeepsMetadataAndDestroysMonoHistory`, `promotedKeysSignAndVerifyAsThresholdKeys` |
| Inventory query scope | Historical inventory without a logical id and cursors outside that logical scope are rejected. Owner filters do not expose records owned by another or unknown owner. | [InventoryIndexTest]: `inventoryHistoricalRequiresLogicalId`, `inventoryRejectsCursorOutsideLogicalScope`, `historicalInventoryForMismatchedOwnerReturnsEmptyPage`, `monoInventoryIndexesOwnerAndValidatesCursors` |
| Audit integrity and persistence (partial T-11) | Audit records have distinct verifiable signatures and remain present and verifiable across storage migration and restart. | [LegacyStorageMigrationTests]: `v211V1KeyStorageMigratesOnceBeforeRefreshAndRotate` |
| Binary record parsing (T-9) | Signed payloads, record-bound secrets, typed keys, DKG commitments, and imported keys preserve their security metadata across canonical round trips. Generated malformed inputs terminate at the documented controlled error boundary. The first generative run exposed truncated commitment inputs escaping as `BufferUnderflowException`; the parser now performs bounded reads, canonical UTF-8 validation, and trailing-byte rejection. | [SecuritySerializationProperties]; [SecurityBinaryParserFuzzTest] |

### Output integrity

| Mutation | Verified behavior | Evidence |
| --- | --- | --- |
| Signature or intent substitution (T-5, T-17 boundary) | Modified signature, wrong payload, signature from another key, policy-rejected typed payload, invalid encoding, and incompatible scheme, curve, payload, or tweak combinations are rejected. | [SignatureTests]: `verifyTamperedSignatureReturnsFalse`, `verifyWrongPayloadReturnsFalse`, `verifyInvalidSignatureBase64Fails`; [AuthorityPolicyTests]: `verifyReturnsFalseEvenWhenTypedPayloadIsRejectedByPolicy`, `verifyReturnsFalseWhenUsingSignatureFromAnotherKey` |
| ML-DSA signature component mutation | Commitment, `z`, hint, and message mutations are rejected for threshold and mono ML-DSA-44, ML-DSA-65, and ML-DSA-87 outputs. | [SignatureTests]: `testThresholdMLDSA44Signature`, `testThresholdMLDSA65Signature`, `testThresholdMLDSA87Signature`, `monoMLDSA44SignsAndVerifies`, `monoMLDSA65SignsAndVerifies`, `monoMLDSA87SignsAndVerifies` |
| ECIES ciphertext or tweak mutation | Changed ciphertext and wrong tweak fail in threshold and mono modes. | [ECIESTests]: `decryptTamperedCiphertextFails`, `decryptWithWrongTweakFails`, `monoDecryptTamperedCiphertextFails`, `monoDecryptWithWrongTweakFails` |

### Operational controls

| Operational event | Verified behavior | Evidence |
| --- | --- | --- |
| Invalid TLS identity rotation | A mismatched PEM key/certificate update retains the last known good identity. | [ProductionTransportSecurityTests]: `publicPemRotationRetainsLastKnownGoodIdentityWhileFilesMismatch` |
| JWKS rotation or refresh failure | Rotation accepts the new key and removes the retired key; failed refresh retains the last known good set. | [ProductionTransportSecurityTests]: `jwksRotationAcceptsNewSigningKeyWithoutRestart`, `failedJwksRefreshKeepsLastKnownGoodKeys` |
| Optional-module isolation | Development authentication, dry run, failure injection, and recovery modules are present in the integration artifact and absent from the production test artifact. | `./gradlew artifactIsolationTest` |
| Release evidence | Module tests, artifact isolation, container builds, functional suites, and production transport tests execute as one release gate. | `./gradlew releaseGate` |

### Exact protocol vector catalog

#### Shared signing-package boundary

Each vector runs independently against FROST, GG20, and threshold ML-DSA.

| Vector | Expected contract |
| --- | --- |
| `OMIT_LOCAL_PARTICIPANT` | `NOT_PARTICIPANT` |
| `DUPLICATE_PARTICIPANT` | `INVALID_REQUEST_BODY` |
| `UNDERSIZED_PARTICIPANT_SET` | `INVALID_REQUEST_BODY` |
| `OUT_OF_RANGE_PARTICIPANT` | `INVALID_REQUEST_BODY` |
| `REPLAY_SESSION_ID` | `SESSION_ALREADY_EXISTS` |
| `CONCURRENT_REPLAY_SESSION_ID` | Exactly one of eight simultaneous creators succeeds; seven receive `SESSION_ALREADY_EXISTS`. |

#### FROST transcript

| Vectors | Security property | Expected contract |
| --- | --- | --- |
| `TAMPER_PROOF_POINT`, `TAMPER_PROOF_SCALAR` | Proof of possession binds the contribution to the participant public share and additional context. | Identifiable abort attributed to the remote peer. |
| `TRUNCATE_COMMITMENT_POINT` | Malformed point encoding does not enter protocol state. | `IllegalArgumentException` |
| `MISMATCH_COMMITMENT_INDEX`, `DUPLICATE_COMMITMENT` | Commitment identity is exact and unique. | Identifiable abort attributed to the remote peer. |
| `OMIT_PARTICIPANT_COMMITMENT` | The commitment set must equal the signer set. | `IllegalStateException` |
| `REPLAY_COMMITMENT_CONTEXT` | A commitment cannot be replayed under different additional context. | Identifiable abort attributed to the remote peer. |
| `TAMPER_NONCE_COMMITMENT`, `TAMPER_SIGNATURE_SHARE` | The partial signature must verify against its nonce commitment and public share. | Identifiable abort attributed to the remote peer. |
| `DUPLICATE_SIGNATURE_SHARE`, `REUSE_NONCE_STATE` | Signature shares are unique and local signing nonces are one-shot. | `IllegalStateException` |

#### GG20 MtA and proofs

Every negative below produces `IdentifiableAbortException` attributed to the
initiating peer.

| Boundary | Vectors |
| --- | --- |
| Paillier modulus | `SMALL_COMPOSITE_MODULUS`, `EVEN_MODULUS`, `OVERSIZED_MODULUS` |
| Paillier generator | `INVALID_GENERATOR` |
| Ciphertext domain | `ZERO_CIPHERTEXT`, `NON_COPRIME_CIPHERTEXT`, `OUT_OF_RANGE_CIPHERTEXT` |
| Range proof | `TAMPER_RANGE_PROOF` |
| Biprime proof | `TAMPER_BIPRIME_PROOF`, `NON_COPRIME_BIPRIME_W`, `TRUNCATE_BIPRIME_TRANSCRIPT` |
| No-small-factor proof | `TAMPER_NO_SMALL_FACTOR_PROOF` |

#### Threshold ML-DSA transcript

| Vectors | Security property | Expected contract |
| --- | --- | --- |
| `TAMPER_OWN_COMMITMENT`, `TAMPER_COMMITMENT_OPENING` | Commitment and reveal must match. | `SecurityException` |
| `TRUNCATE_COMMITMENT`, `DUPLICATE_ROUND1_PARTY`, `OMIT_ROUND1_PARTY`, `OUT_OF_RANGE_ROUND1_PARTY` | Round-1 encoding and participant set are exact. | `IllegalArgumentException` |
| `TRUNCATE_REVEAL`, `DUPLICATE_ROUND2_PARTY`, `OMIT_ROUND2_PARTY`, `OUT_OF_RANGE_ROUND2_PARTY` | Round-2 encoding and participant set are exact. | `IllegalArgumentException` |
| `REUSE_ROUND1_STATE` | Round-1 state is consumed after advancing. | `IllegalStateException` |
| `REUSE_ROUND2_STATE` | Round-2 state is consumed after producing a response. | Fails closed; the current dependency surfaces `NullPointerException`. |

After every negative FROST and ML-DSA transcript probe, the functional suite
produces and verifies a normal distributed signature with the same stored key.

[RFC 9591]: https://www.rfc-editor.org/rfc/rfc9591.html
[NCC Group Zcash FROST assessment]: https://www.nccgroup.com/media/m1yjijzn/_ncc_group_zcashfoundation_e008263_report_2023-10-20_v11-1.pdf
[BitForge GG18/GG20 finding]: https://www.fireblocks.com/blog/gg18-and-gg20-paillier-key-vulnerability-technical-report?cve=title
[CGGMP21 modulus-proof advisory]: https://github.com/LFDT-Lockness/cggmp21/security/advisories/GHSA-m95p-425x-x889
[CGGMP24 Pi-enc hardening]: https://github.com/LFDT-Lockness/cggmp21/commit/a1b7dc6c1e669789e2bfdff8e1bbfbf12cbe1057
[CGGMP24 aff-g hardening]: https://github.com/LFDT-Lockness/cggmp21/commit/fd81bb8cb70f0b04961c1771cfa31e571847694e
[Anvil Paillier proof regressions]: https://github.com/exploit-org/anvil/tree/main/paillier/src/test/java/org/exploit/crypto/paillier/test
[Efficient Threshold ML-DSA]: https://inria.hal.science/hal-05442192v1/document
[NIST ACVP ML-DSA specification]: https://pages.nist.gov/ACVP/draft-celi-acvp-ml-dsa.html
[ArbitraryAuthorityConfigTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/ArbitraryAuthorityConfigTests.kt
[AuthorityPolicyTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/AuthorityPolicyTests.kt
[CoordinatorDisabledTest]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/CoordinatorDisabledTest.kt
[DryRunTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/DryRunTests.kt
[ECIESTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/ECIESTests.kt
[FailureInjectionTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/FailureInjectionTests.kt
[RecoveryFailureInjectionTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/RecoveryFailureInjectionTests.kt
[FourEyeControlTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/FourEyeControlTests.kt
[InventoryIndexTest]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/InventoryIndexTest.kt
[InMemoryTemporaryMapConcurrencyTest]: https://github.com/tkeeper-org/tkeeper/blob/main/src/test/kotlin/org/exploit/keeper/tests/temporary/InMemoryTemporaryMapConcurrencyTest.kt
[KeyImportTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/KeyImportTests.kt
[KeyLifecycleTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/KeyLifecycleTests.kt
[LegacyStorageMigrationTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/LegacyStorageMigrationTests.kt
[LegacyStorageMixedStateTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/LegacyStorageMixedStateTests.kt
[LegacyStorageUntrustedRootTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/LegacyStorageUntrustedRootTests.kt
[MLDSAStateMachineFuzzTest]: https://github.com/tkeeper-org/tkeeper/blob/main/platform-pqc/src/test/kotlin/org/exploit/keeper/platform/pqc/fuzz/MLDSAStateMachineFuzzTest.kt
[MLDSAStateMachineProperties]: https://github.com/tkeeper-org/tkeeper/blob/main/platform-pqc/src/test/kotlin/org/exploit/keeper/platform/pqc/property/MLDSAStateMachineProperties.kt
[KeeperMLDSAProtocolStateFuzzTest]: https://github.com/tkeeper-org/tkeeper/blob/main/platform-pqc/src/test/kotlin/org/exploit/keeper/platform/pqc/fuzz/KeeperMLDSAProtocolStateFuzzTest.kt
[ProductionTransportSecurityTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/ProductionTransportSecurityTests.kt
[QuorumPromotionTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/QuorumPromotionTests.kt
[ProtocolStateMachineProperties]: https://github.com/tkeeper-org/tkeeper/blob/main/platform-ecc/src/test/kotlin/org/exploit/keeper/platform/ecc/property/ProtocolStateMachineProperties.kt
[KeeperProtocolStateFuzzTest]: https://github.com/tkeeper-org/tkeeper/blob/main/platform-ecc/src/test/kotlin/org/exploit/keeper/platform/ecc/fuzz/KeeperProtocolStateFuzzTest.kt
[SecurityBinaryParserFuzzTest]: https://github.com/tkeeper-org/tkeeper/blob/main/platform-ecc/src/test/kotlin/org/exploit/keeper/platform/ecc/fuzz/SecurityBinaryParserFuzzTest.kt
[SecurityProtocolStateFuzzTest]: https://github.com/tkeeper-org/tkeeper/blob/main/platform-ecc/src/test/kotlin/org/exploit/keeper/platform/ecc/fuzz/SecurityProtocolStateFuzzTest.kt
[SecuritySerializationProperties]: https://github.com/tkeeper-org/tkeeper/blob/main/platform-ecc/src/test/kotlin/org/exploit/keeper/platform/ecc/property/SecuritySerializationProperties.kt
[SignatureTests]: https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/functional/src/test/kotlin/org/exploit/test/functional/SignatureTests.kt


---

Source file: content/docs/security-model/quorum-modes.md
Canonical: https://tkeeper.org/docs/security-model/quorum-modes

# Quorum modes

TKeeper supports two custody modes:

| Mode | Configuration | Compromise boundary |
| --- | --- | --- |
| `mono` | `1-of-1` | one TKeeper host can use the key |
| `threshold` | `t-of-n` | fewer than `t` peers cannot use the key alone |

Choose the mode from the failure you need to survive. Threshold mode is appropriate when compromise of one host, operator, VM, or cluster zone must not grant the identity's full authority.

## Threshold guarantees

Each peer stores one share and validates an operation before participating. Signing or threshold decryption completes only with enough accepted contributions; the private key is not reconstructed by the normal protocol flow.

This protects against:

- compromise or theft of fewer than `threshold` shares
- one malicious or careless operator controlling one peer
- unilateral key use by one TKeeper host
- some Byzantine peer behavior, where the protocol can reject or identify invalid contributions

Policy enforcement also becomes quorum-dependent when honest peers hold matching policy state and independently validate the same request. A compromised minority may approve locally but cannot complete the cryptographic operation alone.

Imposter evidence is protocol-specific. FROST, GG20, and threshold ECIES can identify some invalid contributions. Threshold ML-DSA validates transcripts, but a normal rejection-sampling abort is not evidence of a malicious peer.

## What threshold mode does not guarantee

Threshold mode does not:

- preserve confidentiality or integrity after at least `threshold` peers are compromised
- guarantee availability when too few healthy peers can participate
- make inconsistent or malicious policy accepted by a quorum safe
- remove the need for host, network, seal, backup, and audit controls
- undo exposure that happened while a key previously existed in mono form

The threshold is a security boundary and an availability dependency. Select `t` and `n` together with failure-domain placement and recovery objectives.

## Operational cost

Threshold operations add peer networking, session deadlines, coordinated deployment, per-peer sealing, consistency recovery, and more failure states. ML-DSA adds bounded retries because a healthy signing attempt can abort during rejection sampling.

Plan for:

- loss or sealing of a peer
- internal TLS or trust failures
- partially completed lifecycle operations
- version or feature drift between peers
- session timeout and retry behavior
- per-peer backup and recovery without collapsing shares into one failure domain

## When mono is acceptable

Mono retains authentication, permissions, authorities, four-eye control, time policy, and audit, but one process holds full private key material. A host compromise is therefore a key compromise.

Use mono when that risk is explicitly acceptable: local development, low-impact workloads, or a controlled bootstrap phase. Do not describe mono as threshold custody or protection from a malicious host.

## Promotion to threshold

TKeeper can promote a mono identity with:

```http
POST /v2/keeper/quorum/promote
```

Promotion distributes the existing identity into threshold state; it does not create a history in which the key was never whole. Backups, memory captures, or prior compromise may retain the mono key. Rotate or run a new DKG when prior exposure cannot be accepted.

Target peers must already be initialized and unsealed with matching `threshold` and `total`. See [Quorum Promotion](https://tkeeper.org/docs/cryptographic-identities/quorum-promotion).

## Decision rule

Use threshold when the answer to this question is no:

```text
May one compromised TKeeper host authorize as this identity?
```

If operational constraints force mono for a high-impact identity, document that exception as a security risk rather than presenting policy controls as a substitute for distributed custody.


---

Source file: content/docs/security-model/authentication-authorization.md
Canonical: https://tkeeper.org/docs/security-model/authentication-and-authorization

# Authentication and Authorization

External requests are authenticated before controller logic runs. Authorization is permission-string based and enforced before key material participates in the operation.

## Authentication modes

| Type | Header | Use |
| --- | --- | --- |
| `dev` | `X-DEV-TOKEN` | explicit opt-in deployments |
| `jwt` | `X-JWT-TOKEN` | production deployments |

Developer authentication is never selected implicitly. The feature may be packaged into a production artifact, but enabling it there is outside the recommended production profile. An operator who accepts that risk must protect its token, configuration, and permissions as production credentials.

## Developer authentication

Developer auth loads a separate config file from `keeper.dev.config.location`.
Its implementation lives in the optional `auth-dev` feature. It is excluded from `keeper.features=all` but may be explicitly selected for any deployable artifact.

Example:

```hocon
keeper.dev {
  token = "dev-token"
  permissions = [
    "tkeeper.system.init",
    "tkeeper.system.unseal",
    "tkeeper.dkg.create",
    "tkeeper.key.*.public",
    "tkeeper.key.*.sign",
    "tkeeper.key.*.verify"
  ]
}
```

Build it into an artifact explicitly:

```bash
./gradlew :build -Pkeeper.features=auth-dev -Pkeeper.platforms=ecc
```

Then enable it at runtime:

```bash
-Dkeeper.dev.enabled=true
-Dkeeper.dev.config.location=/etc/tkeeper
```

## JWT authentication

JWT auth verifies token signature and claims against configured issuer metadata.

```hocon
auth {
  type = "jwt"

  jwt {
    jwks-location = "https://issuer.example/.well-known/jwks.json"
    issuer = "https://issuer.example"
    audience = "tkeeper"
    refresh = 15m
    clock-skew = 15s
  }
}
```

Tokens must contain:

- `sub`
- `aud`
- `exp`
- `permissions` as a string list claim

If `auth.jwt.issuer` is configured, `iss` must match it. Configure issuer in production.

`aud` may be a single value or an array. TKeeper checks that it contains `auth.jwt.audience`.

`nbf` is optional. When present, TKeeper rejects the token before that time.

## Permission model

Permissions are explicit strings:

```text
tkeeper.key.{keyId}.sign
tkeeper.key.{keyId}.verify
tkeeper.key.{keyId}.public
```

Wildcards are supported:

```text
tkeeper.key.*.sign
tkeeper.key.prefix-*.sign
tkeeper.key.*.*
```

Wildcards match dot-separated permission segments. `tkeeper.key.*.sign` matches `tkeeper.key.wallet.sign`, but not `tkeeper.key.team.wallet.sign`.

Negative permissions remove access granted by a broader permission:

```text
tkeeper.key.*.sign
-tkeeper.key.hot-wallet.sign
```

This grants signing on all one-segment key ids except `hot-wallet`.

## Permission groups

| Permission | Allows |
| --- | --- |
| `tkeeper.system.init` | initialize keeper |
| `tkeeper.system.unseal` | unseal |
| `tkeeper.system.seal` | seal |
| `tkeeper.dkg.create` | create key identity |
| `tkeeper.dkg.rotate` | rotate key identity |
| `tkeeper.dkg.refresh` | refresh generation |
| `tkeeper.key.{keyId}.public` | read public key |
| `tkeeper.key.{keyId}.sign` | sign |
| `tkeeper.key.{keyId}.verify` | verify |
| `tkeeper.key.{keyId}.encrypt` | ECIES encrypt |
| `tkeeper.key.{keyId}.decrypt` | ECIES decrypt |
| `tkeeper.key.{keyId}.destroy` | destroy key generation |
| `tkeeper.storage.write` | trusted-dealer import |
| `tkeeper.consistency.fix` | run consistency fix |
| `tkeeper.expired.view` | read key-expiration indexes |
| `tkeeper.integrity.rotate` | rotate audit integrity key |
| `tkeeper.audit.log.verify` | verify signed audit log lines |
| `tkeeper.compliance.inventory` | read asset inventory |
| `tkeeper.quorum.promote` | promote mono to threshold |
| `tkeeper.control.system` | read control-plane system state |
| `tkeeper.control.sinks` | read control-plane audit sink state |

Keep destructive and lifecycle permissions narrower than signing permissions.

## Internal peer authentication

Peer-to-peer calls use TKeeper's internal request signing. This is separate from public API authentication.

Each internal request carries:

```text
X-INSTANCE-ID
X-INTENDED-FOR
X-TIMESTAMP
X-NONCE
X-KEY-ID
X-PUBLIC-KEY
X-BOOT-PROOF
X-SIGNATURE
```

The request signature binds the HTTP method, path, canonical query, body hash, intended peer, timestamp, nonce, boot proof, and any forwarded actor token. The nonce must be unique and the timestamp must be fresh. Accepted nonces share the persistent RocksDB replay store with four-eye approvals and are retained for `keeper.approval.ttl`.

The external JWT or dev token is forwarded only on internal operation entrypoints that independently enforce actor permissions, such as signing or DKG initialization, trusted-dealer import, destroy prepare, and consistency mutations. Protocol rounds and marker-bound commit or abort calls use peer authentication and existing session state without repeatedly forwarding the actor credential.

On first contact, a peer proves its integrity key with the shared bootstrap token. After that, the integrity key is pinned for the lifetime of the process. If `keeper.peers[].tls-spki-sha256` is configured, TKeeper first verifies that the mTLS client certificate matches the claimed peer id; this removes network-first bootstrap enrollment from the trust decision.

Authenticated internal responses carry the peer identity, request hash and nonce, response timestamp, body hash, boot proof, and `X-RESPONSE-SIGNATURE`. The caller verifies the raw status, content type, and body before completing the response future used by protocol code. Unsigned, replayed, cross-peer, or request-substituted responses are rejected.

Protected internal routes require TLS outside dev mode. Mutual TLS with per-peer SPKI binding authenticates the transport peer; signed requests and responses separately bind protocol content and session intent.

Protect the bootstrap token as cluster enrollment authority. Without mTLS SPKI binding, an attacker that can reach an unenrolled peer and knows the token may be able to pin an attacker-controlled peer identity before the legitimate peer connects.

Forwarded bearer credentials remain visible to a malicious recipient peer. Use short-lived tokens, restrict protected internal paths, and enable internal mTLS. Sender-constrained external credentials require deployment-specific mTLS, DPoP, or request-signing support and are not inferred from an ordinary bearer token.

## Common failures

### `UNAUTHENTICATED`

The token is missing, invalid, expired, has the wrong issuer or audience, or is signed by a key not present in JWKS.

### `ACCESS_DENIED`

The token is valid, but the `permissions` claim does not allow the operation. Check negative permissions too; a matching negative permission wins over a broad grant.


---

Source file: content/docs/security-model/audit-logging.md
Canonical: https://tkeeper.org/docs/security-model/audit-logging

# Audit Logging

Audit logs are newline-delimited JSON records.

Each line contains:

- `event`: audit event payload
- `signature`: Signature over the encoded `event`

> Signature algorithm depends on the backend
> - If `platform-ecc` is included signature algorithm always would be `Ed25519`
> - If **ONLY** `platform-pqc` is included signature algorithm always would be `ML-DSA-44`
>
> Ed25519 currently has a higher priority because the present threat model does not yet require post-quantum signatures for every audit event, while ML-DSA signatures would significantly increase audit-log storage and network traffic. The priority can be switched to ML-DSA as the quantum threat becomes more immediate.

The signing key is TKeeper's integrity key. `event.integrityKeyVersion` tells the verifier which integrity public key version to use.

Example line, formatted for readability:

```json
{
  "event": {
    "id": "01J9Y3J7F8H4B8N8H5M6Y2K3Q1",
    "peerId": 1,
    "integrityKeyVersion": 3,
    "timestamp": 1760000000000,
    "event": "keeper.sign",
    "auth": {
      "subject": "service:payments-api"
    },
    "context": {
      "sid": "sign-01J9Y3K2H7G9M5N4"
    },
    "request": {
      "method": "POST",
      "path": "/v2/keeper/sign",
      "remoteAddress": "10.0.12.44"
    },
    "crypto": {
      "algo": "ECDSA",
      "kid": "payments-hot",
      "generation": 2
    },
    "digest": {
      "purpose": "audit",
      "hmacKeyVersion": 4,
      "bodyHash": {
        "alg": "HMAC_SHA256",
        "value64": "3Aq9i3sM1c03eK1d8eAH7Q=="
      }
    },
    "outcome": {
      "statusCode": 200
    },
    "approvers": [
      "Jq7P3Zx7T0Jmj5..."
    ],
    "policy": {
      "decision": "ALLOW",
      "matches": [
        {
          "id": "small-payment",
          "effect": "ALLOW"
        }
      ]
    },
    "imposters": [],
    "dead": []
  },
  "signature": "MEUCIQD3n7uN..."
}
```

Some fields depend on the operation. `approvers` appears only for approved operations. `policy` appears when a Verdict authority was evaluated. `imposters` and `dead` appear when a threshold protocol reports bad or unavailable peers.

`auth.subject` is the identity authenticated on the current HTTP hop. For a direct public request it is the external principal. For a protected peer request it is the keeper peer that signed the internal request, while optional `auth.actor` preserves the original external principal. Peer-only protocol rounds omit `actor`.

The policy object is Verdict's `PolicyEvaluation`: `decision`, matched rules, and any `approvalRequirements`. `ALLOW_WITH_REQUIREMENTS` remains visible after proofs satisfy the requirements. A rejected challenge records the redacted groups in `outcome.approvals`.

## Integrity boundary

The signature detects modification of an individual encoded event when the verifier has the correct integrity public key. It does not by itself prove that the log is complete, correctly ordered, retained, or delivered to every configured sink.

For compliance or incident evidence:

- preserve events in an access-controlled external system
- retain integrity public keys for every referenced version
- monitor gaps, duplicate ids, and unexpected time ordering at the collector
- define retention and deletion controls outside TKeeper
- verify records independently instead of trusting only the producer

## File sink

```hocon
keeper.audit {
  enabled = true
  timeout = 1000

  file {
    directory = "/var/lib/tkeeper/audit"
    extension = "ndjson"
    prefix = "audit"
    max-file-size-bytes = 67108864
    roll-every = 1d
    max-files = 10
    retention-days = 30
    gzip = false
    fsync = false
  }
}
```

## Socket sink

```hocon
keeper.audit {
  enabled = true

  socket {
    host = "audit.local"
    port = 443
    tls {
      protocols = ["TLSv1.3", "TLSv1.2"]
      verify-hostname = true
      trust {
        mode = "system"
      }
    }
  }
}
```

The socket sink marks a line accepted after TKeeper writes and flushes it to the local socket stream. This is not a collector durability acknowledgement. If the local write does not complete before `ack-timeout`, TKeeper treats that sink as failed.

## Verification

Verify one signed audit line:

```http
POST /v1/keeper/audit/verify
```

Body:

```json
{
  "event": {
    "id": "01J9Y3J7F8H4B8N8H5M6Y2K3Q1",
    "peerId": 1,
    "integrityKeyVersion": 3,
    "timestamp": 1760000000000,
    "event": "keeper.sign",
    "auth": null,
    "context": null,
    "request": null,
    "crypto": null,
    "digest": null,
    "outcome": null,
    "approvers": null,
    "policy": null,
    "imposters": null,
    "dead": null
  },
  "signature": "..."
}
```

Response:

```json
{ "valid": true }
```

Verify a batch:

```http
POST /v1/keeper/audit/verify/batch
```

Body:

```json
{
  "logs": [
    {
      "event": {
        "id": "01J9Y3J7F8H4B8N8H5M6Y2K3Q1",
        "peerId": 1,
        "integrityKeyVersion": 3,
        "timestamp": 1760000000000,
        "event": "keeper.sign",
        "auth": null,
        "context": null,
        "request": null,
        "crypto": null,
        "digest": null,
        "outcome": null,
        "approvers": null,
        "policy": null,
        "imposters": null,
        "dead": null
      },
      "signature": "..."
    }
  ]
}
```

Batch response is keyed by event id:

```json
{
  "01J9Y3J7F8H4B8N8H5M6Y2K3Q1": {
    "valid": true
  }
}
```

Required permission:

```text
tkeeper.audit.log.verify
```

Rotate the integrity key:

```http
POST /v1/keeper/integrity/rotate
```

Required permission:

```text
tkeeper.integrity.rotate
```

Restart every peer after rotating an integrity key. Peer integrity-key pins are process-local in 2.2.0+; until restart, callers that previously contacted the rotated peer continue to reject its new signed responses.

Rotation keeps historical public keys for log verification and removes the corresponding historical private keys. Replaying only the current-version pointer fails closed when it no longer matches the stored history. A coordinated same-location replay of the pointer and its matching records can still pass local verification, as can a complete database rollback. Detecting that class of rollback requires an independently protected monotonic checkpoint or external audit anchor.

Audit-HMAC keys are encrypted in record-id-bound envelopes and must decode to exactly 32 bytes. TKeeper migrates older unbound HMAC records during unseal and does not become ready until the bound form validates.

Peers expose their current integrity public key on the internal API:

```http
GET /v1/integrity/publicKey
```

Response:

```json
{ "data": "base64-public-key" }
```

## Failure behavior

When audit is enabled, TKeeper checks sink availability before protected operations. At least one configured sink must be available.

When an event is written, the operation continues if at least one configured sink accepts the event before the audit timeout. If all configured sinks fail or miss the timeout, the operation fails with `AUDIT_FAILED`.

Multiple configured sinks are therefore redundant destinations, not an all-sinks durability guarantee. If policy requires delivery to a particular archive, enforce and monitor that requirement at the deployment or collector layer.

## Common problems

### Audit sink is down

If at least one sink is alive, operations continue. If no sink is available, protected operations fail with `AUDIT_NOT_AVAILABLE` before the crypto session starts.

### Verification fails

Check the line encoding, the Ed25519 signature, and `event.integrityKeyVersion`.


---

Source file: content/docs/security-model/four-eye-control.md
Canonical: https://tkeeper.org/docs/security-model/four-eye-control

# Four Eye Control

Four-eye control can be unconditional at the key level or selected by an authority policy for a particular typed signing intent. In both cases, TKeeper requires the configured number of distinct approver signatures before continuing.

## Key policy shape

```json
{
  "fourEye": {
    "mode": "STRICT",
    "m": 2,
    "n": 3,
    "keys": [
      {
        "algorithm": "SECP256K1",
        "publicKey64": "..."
      },
      {
        "algorithm": "P256",
        "publicKey64": "..."
      },
      {
        "algorithm": "ED25519",
        "publicKey64": "..."
      }
    ]
  }
}
```

Rules:

- `mode` is `STRICT` or `LENIENT`; omitted values default to `STRICT`
- `m` must be at least `2`
- `m` cannot be greater than `n`
- `keys.size` must equal `n`
- duplicate approver keys are rejected
- approver public keys must decode under the declared algorithm
- approver algorithms must be present in the runtime artifact; ECC provides `SECP256K1`, `P256`, and `ED25519`, while the optional PQC platform adds `MLDSA44`, `MLDSA65`, and `MLDSA87`

`STRICT` preserves the original behavior: approvals are required for every operation protected by the key policy, including signing, decrypting, rotating, refreshing, and destroying a generation.

`LENIENT` protects key lifecycle changes: `ROTATE`, `REFRESH`, and generation destruction. Signing and decryption proceed without key-bound approvals. Initial `CREATE` has no stored key policy to enforce; a `fourEye` policy supplied during creation protects later operations. Authentication, permissions, authority policies, and audit checks still apply.

## Policy-driven approvals

An authority manifest can require approvals only when a particular allow rule matches. Approver public keys are declared once under `policy.approvers`; each rule selects a threshold and a set of approver ids:

```yaml
policy:
  id: payment-policy
  fallback: DENY
  approvers:
    operator-a:
      algorithm: SECP256K1
      publicKey64: "..."
    operator-b:
      algorithm: P256
      publicKey64: "..."
    compliance:
      algorithm: ED25519
      publicKey64: "..."
  allow:
    - id: approve-payment
      where:
        - "purpose == 'payment'"
      approvals:
        threshold: 2
        approvers: [operator-a, operator-b]
    - id: compliance-review
      where:
        - "purpose == 'payment'"
      approvals:
        threshold: 1
        approvers: [compliance]
```

If both rules match, both approval groups must be satisfied. Requirements from a key-level `fourEye` policy are cumulative with authority-policy requirements; one approval payload can carry proofs for every group.

An `ALLOW_WITH_REQUIREMENTS` fallback uses the same model:

```yaml
policy:
  id: guarded-fallback
  fallback: ALLOW_WITH_REQUIREMENTS
  approvers:
    operator:
      algorithm: ED25519
      publicKey64: "..."
  fallbackApprovals:
    threshold: 1
    approvers: [operator]
```

For an authority-policy challenge, TKeeper returns `APPROVAL_REQUIRED` with the `policyId`, rule or fallback `source`, and `threshold` for every required group. The public keys remain in the authority manifest. Sign the canonical request hash with enough keys from every group, attach all proofs to the same request, and resubmit it unchanged.

Policy-driven approvals apply to typed signing through the selected authority. Arbitrary signing does not evaluate an authority policy. The audit event retains the `ALLOW_WITH_REQUIREMENTS` decision and its requirements after successful approval.

## Approval model

Approvers sign a hash of the exact operation body. TKeeper verifies the submitted proofs before continuing to signing, DKG, destroy, or decrypt.

Approval payload:

```json
{
  "approvals": {
    "keeperId": 1,
    "nonce": "unique-nonce",
    "timestamp": 1760000000000,
    "proofs": [
      {
        "fingerprint": "...",
        "signature64": "..."
      }
    ]
  }
}
```

At the coordinator boundary, the nonce is one-time and is consumed only after enough signatures verify. Consumed nonces are persisted in RocksDB for `keeper.approval.ttl`, so a coordinator restart does not reopen the replay window. The timestamp must not be in the future and must fit the same TTL.

Threshold protocol retries reuse the same approval. Non-coordinator peers therefore verify its signatures, approved request fields, and TTL without independently consuming the nonce. If the coordinator is compromised, it can replay an approval with those same fields only while it remains fresh; see the threat model.

The coordinator peer id in `approvals.keeperId` must match the peer coordinating the operation.

## Signature algorithms

| Approver key | Approval signature |
| --- | --- |
| `SECP256K1` | ECDSA |
| `P256` | ECDSA |
| `ED25519` | EdDSA |
| `MLDSA44` | ML-DSA |
| `MLDSA65` | ML-DSA |
| `MLDSA87` | ML-DSA |

The approver fingerprint is:

```text
base64(sha256(encoded-public-key))
```

## Building `hashForSigning`

### One format for key-bound and policy-bound approvals

Key-bound Four-Eye loads approval keys from the policy stored with the selected key generation. Policy-bound Four-Eye loads approval groups from every matching authority allow rule. The guard merges both sources before verification.

All approvers sign one `hashForSigning`. All proofs travel in one `approvals.proofs` array with one `keeperId`, nonce, and timestamp. Combined enforcement requires every key-bound and policy-bound group; it creates no second request or hash.

Policy-bound groups currently apply to typed `Sign` requests. Key-bound groups apply according to their mode:

| Key policy mode | Protected operations |
| --- | --- |
| `STRICT` | `Sign`, `ROTATE`, `REFRESH`, ECIES `Decrypt`, and `Destroy` |
| `LENIENT` | `ROTATE`, `REFRESH`, and `Destroy` |

Initial `CREATE` has no stored key policy. A `fourEye` policy carried by `CREATE` starts protecting the key after generation 1 becomes active.

A policy-only request with an empty proof list returns `APPROVAL_REQUIRED` and the required `policyId`, rule or fallback `source`, and `threshold`. The caller supplies the approval envelope and calculates the hash.

### Request transformation

Add this field to the exact operation request:

```json
{
  "approvals": {
    "keeperId": 1,
    "nonce": "018f-example-unique-nonce",
    "timestamp": 1760000000000,
    "proofs": []
  }
}
```

Build the hash preimage with this transformation:

1. Copy every operation field except the top-level `approvals` field.
2. Copy `approvals.keeperId`, `approvals.nonce`, and `approvals.timestamp` into the preimage root.
3. Leave `approvals.proofs` outside the preimage.
4. Recursively canonicalize the preimage and hash its UTF-8 bytes with SHA-256.
5. Sign the resulting 32 bytes with enough keys from every required group.
6. Add the proofs to the original request and submit it with the same operation fields, keeper id, nonce, and timestamp.

Each proof contains:

```text
fingerprint = base64(sha256(encoded-public-key))
signature64 = base64(signature-bytes)
```

ECDSA signatures use compact `r || s` bytes with an optional recovery-id byte. Ed25519 uses its 64-byte detached signature. ML-DSA uses the encoded detached signature for the declared parameter set.

The Java SDK models `Sign`, `Generate`, `Decrypt`, and `KeyDestroyReference` implement `Approvable`:

```java
var approvals = Approvals.template(
        coordinatorPeerId,
        UUID.randomUUID().toString(),
        Instant.now().toEpochMilli()
);
var request = Sign.builder(keyId, command)
        .approvals(approvals)
        .build();

byte[] hashForSigning = request.hashForSigning();
byte[] signatureBytes = approvalSigner.signDigest(hashForSigning);

request.addProof(new Approvals.Proof(
        approverFingerprint64,
        Base64.getEncoder().encodeToString(signatureBytes)
));
client.signature().sign(request);
```

`approvalSigner` denotes the application's HSM, wallet, or external approval service.

### Sign

The same request supports key-bound approvals, policy-bound approvals, or both:

```json
{
  "keyId": "payments-key",
  "command": {
    "type": "custom",
    "authorityId": "payments",
    "artifact": {
      "scheme": "ECDSA",
      "hash": "SHA256",
      "typed": {
        "purpose": "payment",
        "amount": "1000"
      }
    }
  },
  "approvals": {
    "keeperId": 1,
    "nonce": "018f-example-unique-nonce",
    "timestamp": 1760000000000,
    "proofs": []
  }
}
```

Hash preimage:

```json
{
  "keyId": "payments-key",
  "command": {
    "type": "custom",
    "authorityId": "payments",
    "artifact": {
      "scheme": "ECDSA",
      "hash": "SHA256",
      "typed": {
        "purpose": "payment",
        "amount": "1000"
      }
    }
  },
  "keeperId": 1,
  "nonce": "018f-example-unique-nonce",
  "timestamp": 1760000000000
}
```

Copy a non-null request `tweak` into the preimage root.

### DKG: `CREATE`, `ROTATE`, and `REFRESH`

`Generate.hashForSigning()` covers all three modes. The `mode` string belongs to the signed data, so a proof for one mode fails for the other two.

Example `ROTATE` request:

```json
{
  "keyId": "lifecycle-key",
  "algorithm": "SECP256K1",
  "authorities": [
    {"id": "arbitrary"}
  ],
  "mode": "ROTATE",
  "approvals": {
    "keeperId": 1,
    "nonce": "rotate-unique-nonce",
    "timestamp": 1760000000000,
    "proofs": []
  }
}
```

Hash preimage:

```json
{
  "keyId": "lifecycle-key",
  "algorithm": "SECP256K1",
  "authorities": [
    {"id": "arbitrary"}
  ],
  "mode": "ROTATE",
  "keeperId": 1,
  "nonce": "rotate-unique-nonce",
  "timestamp": 1760000000000
}
```

Optional request fields `policy` and `assetOwner` enter the preimage recursively. This binds approval to the resulting generation's authorities, policy, and owner.

| Mode | Approval source and result |
| --- | --- |
| `CREATE` | No previous generation supplies a key-bound group. The request's `policy` and `assetOwner` become generation 1 metadata. |
| `ROTATE` | The active generation's policy authorizes replacement. The request's optional `policy` and `assetOwner` become metadata for the new key material. |
| `REFRESH` | The active generation's policy authorizes refreshed shares. The request's optional `policy` and `assetOwner` become metadata for the refreshed generation. |

For Java SDK requests, pass `KeyGenMode.CREATE`, `KeyGenMode.ROTATE`, or `KeyGenMode.REFRESH` to `Generate.builder(...)`, attach `.approvals(approvals)`, and call `hashForSigning()` before adding proofs.

### ECIES decrypt

ECIES encryption uses public key material and carries no approvals. Decrypt uses the key-bound policy stored with the requested generation. An omitted `generation` selects the active generation and leaves `generation` out of the preimage.

Request:

```json
{
  "keyId": "ecies-key",
  "algorithm": "AES_GCM",
  "generation": 3,
  "ciphertext64": "AQIDBA==",
  "tweak": "invoice-42",
  "approvals": {
    "keeperId": 1,
    "nonce": "decrypt-unique-nonce",
    "timestamp": 1760000000000,
    "proofs": []
  }
}
```

Hash preimage:

```json
{
  "keyId": "ecies-key",
  "algorithm": "AES_GCM",
  "generation": 3,
  "ciphertext64": "AQIDBA==",
  "tweak": "invoice-42",
  "keeperId": 1,
  "nonce": "decrypt-unique-nonce",
  "timestamp": 1760000000000
}
```

The server resolves an omitted `algorithm` to `AES_GCM` and includes that value in the preimage. Clients should send `algorithm` explicitly. A non-null `tweak` and an explicit `generation` enter the preimage.

For the Java SDK, build `Decrypt` with `.generation(...)`, `.tweak(...)`, and `.approvals(approvals)`, then call `hashForSigning()`.

### Destroy

Destroy uses the key-bound policy stored with the target generation. Both `STRICT` and `LENIENT` protect this operation.

Request:

```json
{
  "keyId": "lifecycle-key",
  "generation": 1,
  "approvals": {
    "keeperId": 1,
    "nonce": "destroy-unique-nonce",
    "timestamp": 1760000000000,
    "proofs": []
  }
}
```

Hash preimage:

```json
{
  "keyId": "lifecycle-key",
  "generation": 1,
  "keeperId": 1,
  "nonce": "destroy-unique-nonce",
  "timestamp": 1760000000000
}
```

Java SDK clients use `new KeyDestroyReference(keyId, generation, approvals)`, call `hashForSigning()`, add proofs, and pass the request to `client.destroy().destroy(request)`.

### Canonical encoding

Canonicalization rules:

- serialize compact UTF-8 JSON with no insignificant whitespace
- omit fields whose value is `null`
- sort JSON object field names lexicographically at every object level
- sort map entries by key
- preserve array order
- canonicalize objects inside arrays recursively
- preserve scalar types and string bytes, including Base64 text, enum names, nonce, and tweak

```text
hashForSigning = SHA-256(UTF-8(canonical-json))
```

Feed those 32 bytes directly to the approver algorithm. Hexadecimal and Base64 encodings serve display and transport. ECDSA consumes the supplied digest; Ed25519 and ML-DSA consume the 32 bytes as their message.

Canonical Sign vector:

```json
{"command":{"artifact":{"hash":"SHA256","scheme":"ECDSA","typed":{"amount":"1000","purpose":"payment"}},"authorityId":"payments","type":"custom"},"keeperId":1,"keyId":"payments-key","nonce":"018f-example-unique-nonce","timestamp":1760000000000}
```

Expected SHA-256: `ad0a5caa1cf84dcfb4b0012ac5af306ebb8938b1cafe4f0ab0d4e6232b135e9b`.

## Security notes

- Four-eye approvals do not replace TKeeper authentication or permissions.
- Approver keys should be stored separately from TKeeper peers.
- Any change to the approved request body requires new approvals.
- Approval signatures are only as trustworthy as approver key custody.
- Approval tooling should render the canonical operation from the signed fields. A trusted human summary that is not bound to the approval hash can mislead the approver.
- Nonce uniqueness prevents approval reuse inside TKeeper; downstream replay rules are still required for the resulting cryptographic proof.

## Common failures

### Approvals fail after changing the request

Create a new approval for the exact request body.

### Duplicate approver keys fail

`n` is the number of distinct approvers.

### Approval nonce is rejected on second use

Approval nonces are one-time.


---

Source file: content/docs/key-management/README.md
Canonical: https://tkeeper.org/docs/cryptographic-identities

# Cryptographic Identities

In TKeeper, a key is the identity boundary. The key's authorities define what the identity can authorize, and lifecycle operations change the generations through which that identity operates.

Read:

- [Create, Rotate, and Refresh](https://tkeeper.org/docs/cryptographic-identities/key-lifecycle)
- [Trusted Dealer Import](https://tkeeper.org/docs/cryptographic-identities/trusted-dealer-import)
- [Quorum Promotion](https://tkeeper.org/docs/cryptographic-identities/quorum-promotion)
- [Asset Inventory](https://tkeeper.org/docs/cryptographic-identities/asset-inventory)

## Lifecycle choices

| Operation | Use when | Public key |
| --- | --- | --- |
| `CREATE` | creating a new identity | new |
| `ROTATE` | creating new cryptographic material for the same logical identity | changes |
| `REFRESH` | creating a new generation without changing the public identity | same |
| Trusted dealer import | bringing existing key material into TKeeper | imported |
| Quorum promotion | moving a mono identity into threshold custody | same identity |
| Destroy | removing an old generation | active generation stays |

Refresh behavior is algorithm-specific. Threshold ECC replaces the peer shares while preserving the same aggregate key. ML-DSA advances the generation while carrying each peer's existing share and public key forward unchanged; it performs no cryptographic refresh.

## Security notes

- Keep lifecycle permissions narrower than signing permissions.
- Treat trusted-dealer import as a different trust model from DKG.
- Use rotate when key material must change.
- Use asset inventory to review authorities attached to identities.


---

Source file: content/docs/key-management/key-lifecycle.md
Canonical: https://tkeeper.org/docs/cryptographic-identities/key-lifecycle

# Create, Rotate, and Refresh

The key id is the logical identity. A generation is the version of cryptographic material or share state used by that identity.

Create, rotate, and refresh all use the same endpoint:

```http
POST /v2/keeper/dkg
```

Body:

```json
{
  "keyId": "deployment-signing",
  "algorithm": "SECP256K1",
  "mode": "CREATE",
  "assetOwner": "customer-42",
  "authorities": [
    {
      "id": "production-deployment",
      "oci": "oci://registry.example/verdict/authorities/production-deployment@sha256:..."
    }
  ]
}
```

`authorities` is a JSON array of key authorities. These authorities define what the key identity can authorize.

Modes:

| Mode | Meaning |
| --- | --- |
| `CREATE` | new cryptographic identity |
| `ROTATE` | new generation under the same logical id; the public key changes |
| `REFRESH` | new generation with the same public key; material behavior is algorithm-specific |

Required permission is selected from `mode`:

| Mode | Permission |
| --- | --- |
| `CREATE` | `tkeeper.dkg.create` |
| `ROTATE` | `tkeeper.dkg.rotate` |
| `REFRESH` | `tkeeper.dkg.refresh` |

Successful lifecycle requests return `204 No Content`.

## Quorum mode behavior

The endpoint name is the same in both quorum modes, but the work is different.

In `mono` mode, TKeeper manages full key material locally:

- `CREATE` creates a local key pair
- `ROTATE` creates a new local key pair under the same logical id
- `REFRESH` creates a new generation with the same private key and public key

Mono refresh is useful when lifecycle history must move forward without changing the identity. It does not create peer shares because there are no peers in mono mode.

In `threshold` mode, TKeeper coordinates lifecycle changes across peers:

- `CREATE` creates the first shared key generation
- `ROTATE` creates a new shared key generation and changes the public key
- ECC `REFRESH` creates new shares for the same public key
- ML-DSA `REFRESH` carries each peer's existing share and public key into the new generation unchanged; it does not replace shares or refresh cryptographic material

Threshold lifecycle state stores the key share and metadata for each generation. ECC commitments are later used to derive peer public shares for signing, ECIES, consistency checks, and Byzantine detection. ML-DSA stores its aggregate public key as signed side state.

During the first storage upgrade, marker-gated migration relocates legacy AEAD-protected generations into the isolated key-version store without signing or activating new key material. `REFRESH` is the online conversion path that preserves the public key; `ROTATE` converts by creating a new public key. Both write a new signed generation and activate it through the normal pending-generation workflow. The old legacy generation remains physically available for consistency rollback and inventory, but it is inactive and cannot be selected for historical processing after conversion. A storage read never performs an implicit migration.

## Public key

```http
GET /v1/keeper/publicKey?keyId=deployment-signing
GET /v1/keeper/publicKey?keyId=deployment-signing&generation=1
GET /v1/keeper/publicKey?keyId=deployment-signing&tweak=user-42
```

Required permission:

```text
tkeeper.key.{keyId}.public
```

Response:

```json
{ "data64": "..." }
```

`tweak` derives a deterministic tweaked public key. Use the same tweak later when signing, verifying, encrypting, or decrypting data bound to that tweaked key.

## Policy

Key policy:

```json
{
  "apply": {
    "unit": "SECONDS",
    "notAfter": 1893456000
  },
  "process": {
    "unit": "SECONDS",
    "notAfter": 1893459600
  },
  "allowHistoricalProcess": true
}
```

Fields:

| Field | Meaning |
| --- | --- |
| `apply` | deadline for operations that create a new effect |
| `process` | deadline for operations that process existing material |
| `fourEye` | m-of-n approval policy; `STRICT` protects every supported operation, `LENIENT` protects `ROTATE`, `REFRESH`, and generation destruction |
| `allowHistoricalProcess` | allow process operations against historical generations |

`unit` can be `SECONDS` or `MILLISECONDS`.

If both `apply` and `process` are set, `process` must be later than `apply`.

## Destroy

```http
POST /v1/keeper/destroy
```

Body:

```json
{
  "keyId": "deployment-signing",
  "generation": 1,
  "approvals": {
    "keeperId": 1,
    "nonce": "destroy-deployment-signing-1",
    "timestamp": 1760000000000,
    "proofs": []
  }
}
```

Required permission:

```text
tkeeper.key.{keyId}.destroy
```

Destroy works on a specific generation. `generation` must be greater than zero. You cannot sign with an old generation after it is destroyed.

Destroy follows the quorum mode too.

In mono mode, destroy is local-only. Any non-current generation can be destroyed. The current generation cannot be destroyed.

In threshold mode, destroy is coordinated across peers. Commit and abort are bound to the peer that prepared the signed destroy session. The current generation cannot be destroyed, and a generation must be at least two generations behind the active one. This keeps the cluster away from deleting material that may still be needed while a lifecycle operation is settling.

## Consistency fix

```http
POST /v1/keeper/consistency/fix?keyId=deployment-signing
```

Required permission:

```text
tkeeper.consistency.fix
```

Use consistency fix when peers disagree about active key state and the system can safely repair from quorum data.

It is meant for interrupted `CREATE`, `ROTATE`, or `REFRESH` flows. It can sync a pending generation, clean stale pending state, or roll back to a majority-active generation when that is the only safe result. If no safe active generation has quorum support, TKeeper fails closed and does not start another DKG automatically; repair the inconsistency before rotating.

If the repair cannot prove a safe state, it fails.

Consistency fix is for threshold mode. Mono lifecycle operations are local, so there is no peer state to reconcile.

## Expiration index

TKeeper keeps an index for keys that are close to `apply` or `process` expiry.

Endpoints:

```http
GET /v1/keeper/expires?type=apply&windowSec=86400
GET /v1/keeper/expires?type=process&from=1760000000&to=1760086400
GET /v1/keeper/expires/apply?windowSec=86400
GET /v1/keeper/expires/process?windowSec=86400
GET /v1/keeper/expires/expired?type=apply
```

Required permission:

```text
tkeeper.expired.view
```

Response:

```json
{
  "items": [
    {
      "type": "APPLY",
      "logicalId": "deployment-signing",
      "generation": 1,
      "expiresAt": 1893456000
    }
  ],
  "next": null
}
```

`limit` is optional and capped at 2000. `cursor` continues a previous page.

## Common problems

### `KEY_APPLY_OPS_FORBIDDEN` or `KEY_PROCESS_OPS_FORBIDDEN`

The key time policy expired. Check `policy.apply`, `policy.process`, and the operation type.

### `NOT_COORDINATOR`

You called a coordinator-only endpoint on a node with coordinator disabled. Call a coordinator peer.

### `DESTROY_FORBIDDEN`

Destroy requires a concrete generation greater than zero. The current generation cannot be destroyed. In threshold mode, the generation also has to be at least two generations behind the active one.


---

Source file: content/docs/key-management/trusted-dealer-import.md
Canonical: https://tkeeper.org/docs/cryptographic-identities/trusted-dealer-import

# Trusted Dealer Import

Trusted dealer imports an existing private key into the current quorum mode.

In mono mode, TKeeper stores the full key material locally and records the matching public side state.

In threshold mode, TKeeper splits the key into peer shares and distributes them to the cluster.

Endpoint:

```http
POST /v2/keeper/storage/store
```

Body:

```json
{
  "keyId": "imported-secp256k1",
  "algorithm": "SECP256K1",
  "value64": "base64-raw-private-key",
  "authorities": [
    {
      "id": "payments-small",
      "oci": "oci://registry.example/verdict/authorities/payments-small@sha256:..."
    }
  ]
}
```

`authorities` is a JSON array of key authorities.

`value64` is base64 of the raw private key bytes. For Ed25519, import the standard seed bytes.

Required permission:

```text
tkeeper.storage.write
```

Important details:

- the dealer sees the raw private key
- threshold mode splits the raw key into peer shares
- mono mode stores the raw key locally
- algorithm-specific public side state is stored too: ECC commitments or the aggregate ML-DSA public key
- the imported key can sign and verify like a DKG-created key
- for `ED25519`, import the standard seed, not an expanded private scalar

Response:

```http
200 OK
```

Use trusted dealer only for bringing an existing key into TKeeper. For new keys, prefer DKG.

Import does not erase the dealer's copy, backups, or handling history. Threshold custody protects later use by TKeeper peers, but it cannot make the key equivalent to one that was never reconstructed. Rotate after migration when continuity of the imported public key is not required.

## Common problems

### Imported key exists but signing fails

Trusted dealer import must store the algorithm-specific public side state with the imported material. Without ECC commitments or the ML-DSA public key, public key checks and later protocols cannot prove the same key state.

### Wrong algorithm

The raw private key must match the declared algorithm and its expected encoding.


---

Source file: content/docs/key-management/quorum-promotion.md
Canonical: https://tkeeper.org/docs/cryptographic-identities/quorum-promotion

# Quorum Promotion

Quorum promotion turns an existing local key into distributed threshold key state. Use it when a key started in mono mode for adoption or bootstrap and later needs quorum protection.

## What promotion does

Promotion:

- reads the current mono key material
- splits or imports threshold shares for the selected peers
- writes threshold metadata and platform side state
- creates a pending generation
- requires restart before normal operations continue

After promotion, later signing uses threshold mode.

## What promotion does not do

Promotion does not make the original mono period retroactively threshold-secure and cannot erase every backup or captured copy of the full key. If prior exposure is possible, rotate or create a new identity instead of relying on promotion.

## Platform side state

The promotion path must store platform-specific public side state:

- ECC commitments for ECC algorithms
- aggregate ML-DSA public key for ML-DSA algorithms

Without side state, later public-key checks and threshold protocols cannot prove the same key state.

## When to use rotate instead

Use rotate or a new DKG when you need new cryptographic material instead of promoting existing material.

For ML-DSA, refresh carries the existing shares and public key into the new generation unchanged. Use rotate when new ML-DSA material is required.


---

Source file: content/docs/key-management/asset-inventory.md
Canonical: https://tkeeper.org/docs/cryptographic-identities/asset-inventory

# Asset Inventory

Asset inventory is the read model for keys.

Use it when you need to answer:

- what keys exist
- which generation is active
- which authorities are attached
- which asset owner owns the key
- whether a key is destroyed
- whether old generations are included

Endpoint:

```http
GET /v1/keeper/compliance/inventory
```

Query params:

| Param | Meaning |
| --- | --- |
| `logicalId` | filter by key id |
| `assetOwner` | filter by owner |
| `historical` | include old generations |
| `lastSeen` | cursor |
| `limit` | max 200 |

Required permission:

```text
tkeeper.compliance.inventory
```

Example:

```bash
curl \
  -H 'X-DEV-TOKEN: dev-token' \
  'http://localhost:8080/v1/keeper/compliance/inventory?assetOwner=customer-42&historical=true'
```

Response shape:

```json
{
  "inventory": {
    "generatedAt": 1760000000000,
    "peerId": 1,
    "threshold": 2,
    "totalPeers": 3,
    "items": [
      {
        "logicalId": "deployment-signing",
        "status": "ACTIVE",
        "currentGeneration": 1,
        "authorities": [
          {
            "id": "production-deployment",
            "oci": "oci://registry.example/verdict/authorities/production-deployment@sha256:..."
          }
        ],
        "algorithm": "SECP256K1",
        "createdAt": 1760000000000,
        "updatedAt": 1760000000000,
        "policy": null,
        "hasActiveKey": true,
        "lastPendingGeneration": null,
        "assetOwner": "customer-42",
        "tampered": false
      }
    ]
  },
  "nextCursor": null,
  "hasMore": false
}
```

Asset Inventory is exportable from the control-plane UI when `:features:ui` is enabled.

See [Control Plane UI](https://tkeeper.org/docs/deployment/control-plane-ui).

`tampered = true` means local signed metadata failed integrity verification while inventory was being read.

Treat `tampered = true` as a security incident, not a stale-data warning. Stop relying on that node's inventory or key state until the cause is understood.

Inventory is a control-plane read model. It does not replace comparison of peer generation state, audit history, or external asset ownership records during reconciliation.

## Common problems

### Inventory is empty

Check permissions first. Then check whether the key was created on this cluster and whether you are filtering by `assetOwner`, `logicalId`, or cursor.


---

Source file: content/docs/signing-and-authorities/README.md
Canonical: https://tkeeper.org/docs/signing-and-authorities

# Signing and Authorities

TKeeper signs a command after its authority turns it into an intent and policy allows it.

## Sign and compose

- [Signing](https://tkeeper.org/docs/signing-and-authorities/signing): command requests and verification
- [Composer](https://tkeeper.org/docs/signing-and-authorities/composer): protocol results from signed commands

## Define and check policy

- [Authorities](https://tkeeper.org/docs/signing-and-authorities/authorities): attach and govern an identity's capabilities
- [Arbitrary and Typed Authorities](https://tkeeper.org/docs/signing-and-authorities/arbitrary-and-typed): raw and JSON commands
- [CEL Functions](https://tkeeper.org/docs/signing-and-authorities/cel-functions): policy expressions
- [Dry Run Policy Evaluation](https://tkeeper.org/docs/signing-and-authorities/dry-run): preview a decision

## Protocol guides

- [AI and Agentic Payments](https://tkeeper.org/docs/ai): MCP, AP2, MC Intent
- [Digital Asset Authorities](https://tkeeper.org/docs/digital-assets): Bitcoin, EVM, Tron, XRP, Solana
- [PKI Authorities](https://tkeeper.org/docs/pki): X.509 certificates

The consumer must trust the expected identity, verify the signed command, and prevent replay where the action requires freshness.


---

Source file: content/docs/signing-and-authorities/signing.md
Canonical: https://tkeeper.org/docs/signing-and-authorities/signing

# Signing

Signing is where a key identity produces proof. TKeeper signs only after the command is accepted by the key's authorities and any configured policy, audit, lifecycle, and quorum controls.

## Endpoints

```http
POST /v2/keeper/sign
POST /v2/keeper/sign/verify
POST /v2/keeper/compose
```

Required permissions:

```text
tkeeper.key.{keyId}.sign
tkeeper.key.{keyId}.verify
```

## Signing flow

```text
command
-> authority match
-> intent materialization
-> policy and key controls
-> audit gate
-> mono or threshold signing
-> signature proof
```

For `arbitrary`, TKeeper signs bytes after checking the key identity allows `arbitrary`.

For concrete authorities, TKeeper loads the authority document, materializes the command into an intent, extracts effects where supported, and evaluates policy. It signs for `ALLOW`, or after every requirement from `ALLOW_WITH_REQUIREMENTS` is satisfied.

## Quorum modes

| Mode | Behavior |
| --- | --- |
| `mono` | signs locally with full key material after controls pass |
| `threshold` | coordinator starts a threshold protocol; private key is not reconstructed |

Threshold signing protocols:

| Scheme | Threshold protocol |
| --- | --- |
| `ECDSA` | GG20 |
| `EdDSA` | FROST |
| `SCHNORR` | FROST |
| `BIP340` | FROST |
| `TAPROOT` | FROST |
| `MLDSA` | threshold ML-DSA |

## Commands

Arbitrary command:

```json
{
  "keyId": "demo-identity",
  "command": {
    "type": "arbitrary",
    "authorityId": "arbitrary",
    "artifact": {
      "scheme": "ECDSA",
      "hash": "SHA256",
      "data64": "aGVsbG8="
    }
  }
}
```

Typed command:

```json
{
  "keyId": "deployment-signing",
  "command": {
    "type": "custom",
    "authorityId": "production-deployment",
    "artifact": {
      "scheme": "ECDSA",
      "hash": "SHA256",
      "typed": {
        "action": "deploy",
        "service": "billing-api",
        "environment": "production",
        "releaseVersion": "2.5.0",
        "sequence": 10000000000000000001,
        "riskScore": 0.20,
        "roles": ["release-manager", "production"],
        "sourceIp": "10.20.4.17",
        "requestedAt": "2030-01-02T03:04:05Z",
        "expiresAt": "2030-01-02T03:09:05Z",
        "changeProof": "AQIDBA=="
      }
    }
  }
}
```

See the [typed authority example](https://tkeeper.org/docs/signing-and-authorities/authorities#custom-authority-example) for the matching schema, payload, effects, and policy. [Digital assets](https://tkeeper.org/docs/digital-assets), [agentic payments](https://tkeeper.org/docs/ai/agentic-payments), and [X.509](https://tkeeper.org/docs/pki/x509) have their own authority guides.
The exact typed JSON byte encoding and hashing contract is documented in
[Typed JSON signing material](https://tkeeper.org/docs/signing-and-authorities/arbitrary-and-typed#typed-json-signing-material).

## Schemes and algorithms

| Scheme | Mono | Threshold | Algorithms |
| --- | --- | --- | --- |
| `ECDSA` | local ECDSA | GG20 | `SECP256K1`, `P256` |
| `SCHNORR` | not supported | FROST | `SECP256K1`, `P256` |
| `BIP340` | local BIP340 | FROST | `SECP256K1` |
| `TAPROOT` | local Taproot key-path | FROST | `SECP256K1` |
| `EdDSA` | local EdDSA | FROST | `ED25519` |
| `MLDSA` | local ML-DSA | threshold ML-DSA | `MLDSA44`, `MLDSA65`, `MLDSA87` |

Hash methods:

| Hash | Meaning |
| --- | --- |
| `NONE` | sign bytes as-is |
| `SHA256` | hash before signing |
| `SHA512` | hash before signing |
| `KECCAK256` | hash before signing |

The command artifact decides the scheme and hash. The top-level sign request does not carry `hash` or `algorithm`.

## Response

```json
{
  "type": "ECDSA",
  "signature64": "...",
  "generation": 1,
  "imposters": []
}
```

`imposters` is meaningful for threshold protocols. Mono signatures return an empty list.

Use [Composer](https://tkeeper.org/docs/signing-and-authorities/composer) to turn supported signatures into signed transactions or payment credentials. Other command types return the raw signature result.

Verify response:

```json
{ "valid": true }
```

`generation` is optional on verify. If omitted, TKeeper uses the active generation.

Verify is purely cryptographic. Its command contains only `type` and `artifact`; it does not contain `authorityId`, load an authority manifest, or evaluate policy. A caller may verify any supported material type, including `custom` typed data or `arbitrary` bytes, even when that type or payload would not be authorized for signing by the key's current manifest. The type still selects structural material validation and canonical serialization before the signature is checked, and a successful result proves only cryptographic validity, not current policy authorization.

```json
{
  "keyId": "payments-key",
  "generation": 1,
  "command": {
    "type": "custom",
    "artifact": {
      "scheme": "ECDSA",
      "hash": "SHA256",
      "typed": {
        "amount": 100,
        "currency": "USD"
      }
    }
  },
  "signature64": "..."
}
```

## ML-DSA availability

Threshold ML-DSA uses probabilistic rejection sampling. A complete attempt can abort even when peers are healthy. TKeeper retries with fresh session state up to:

```text
keeper.session.mldsa.max-rounds
```

The default is `12`. Exhaustion returns `SESSION_MAX_ROUNDS_EXCEEDED`; treat it as availability first, not proof of corruption.

## Downstream verification

The downstream system should verify that the proof matches the exact command it is about to execute and the identity it intended to trust.

Signature validity alone is insufficient. The acceptance contract should cover:

- expected key identity or public key
- canonical command and every field that changes the effect
- generation and tweak context required by the integration
- environment or domain separation between test and production
- nonce, expiry, sequence, or idempotency where replay matters

If context is enforced outside the signed payload, the verifier must reject mismatches before execution. For `arbitrary`, TKeeper governs only the supplied bytes; it cannot infer omitted business context.

## Common failures

### Verify returns false

Check that the command type and artifact, `tweak`, `generation`, and `signature64` match the signed material. `authorityId` is signing policy context and is not part of Verify. For arbitrary commands, also check `hash` and `scheme` inside the command artifact.

### Authority rejects the command

`AUTHORITY_VIOLATION` means the command selected an authority that is not attached to the key. `INVALID_AUTHORITY_ARTIFACT` means the command shape does not match the selected authority or its feature is absent. `POLICY_VIOLATION` means the materialized intent was understood but policy returned `DENY`.

### No manager for scheme and algorithm

The key algorithm or quorum mode does not support the requested signature scheme.


---

Source file: content/docs/signing-and-authorities/composer.md
Canonical: https://tkeeper.org/docs/signing-and-authorities/composer

# Composer

`POST /v2/keeper/compose` accepts the same `Sign` request as `/v2/keeper/sign` and requires `tkeeper.key.{keyId}.sign`. TKeeper checks the key's authority, policy, approvals, and signing controls, then passes the signature to the composer registered for the command type.

| Command type | Compose result |
| --- | --- |
| `evm.transaction` | `SignedEvmTransaction`: signed hex and transaction hash |
| `tron.transaction` | `SignedTronTransaction`: signed JSON and transaction hash |
| `xrp.transaction` | `SignedXrpTransaction`: signed hex and transaction hash |
| `solana.transaction` | `SignedSolanaTransaction`: signed Base64, `complete`, and hash when complete |
| `ap2.mandate`, `mcintent.mandate` | `PaymentCredential`: signed credential layer |
| Other types, including `bitcoin.transaction` | `ThresholdSignature`: raw signature, as with `/sign` |

The result also carries `generation` and `imposters`. Composition does not broadcast a transaction or submit a credential to a merchant.

## SDK example

Build an unsigned EVM transaction first. Its serialized bytes go in `message64` as Base64:

```java
var command = Command.of(authorityId, new UnsignedEvmTransaction(message64));
var result = client.signature().compose(
        Sign.of(keyId, command), SignedEvmTransaction.class);
String rawTransaction = result.rawTransaction();
String transactionHash = result.transactionHash();
```

Use `signature().compose(request)` for an untyped JSON result, or pass the expected result class. `signature().sign(request)` still returns only the raw signature.

See the [digital asset authorities](https://tkeeper.org/docs/digital-assets) and [agentic payments](https://tkeeper.org/docs/ai/agentic-payments) for input formats, policies, and result limits.


---

Source file: content/docs/signing-and-authorities/authorities.md
Canonical: https://tkeeper.org/docs/signing-and-authorities/authorities

# Authorities

Authorities bind a key identity to the actions it may authorize.

With concrete authorities, TKeeper checks the requested effect before signing starts.

An authority document is security policy and an intent schema. Review changes to either with the same care as changes to signing code.

TKeeper validates the document, intent config, public approver material, and CEL policy before creating or importing a key. The policy compiles against the selected intent's strict root schema.

## Threshold-backed authorization

In threshold mode, authority enforcement is backed by the same `t-of-n` boundary as key use: fewer than `t` compromised peers cannot complete a threshold signature for an action rejected by the honest peers. This makes the governed identity the strongest authorization boundary in the stack.

Treat authority documents accordingly: keep them narrow, review every schema and policy change, and attach them through digest-pinned references. See [Quorum Modes](https://tkeeper.org/docs/security-model/quorum-modes) and the [Threat Model](https://tkeeper.org/docs/security-model/threat-model) for detailed guarantees, assumptions, and residual risks.

## Key authorities

A key stores a list of authorities.

Concrete authorities use OCI references:

```json
[
  {
    "id": "production-deployment",
    "oci": "oci://registry.example/verdict/authorities/production-deployment@sha256:..."
  }
]
```

`arbitrary` is for raw data signing. It is useful for demos and compatibility, but it gives TKeeper no semantic intent:

```json
[
  { "id": "arbitrary" }
]
```

Rules:

- the request field is a JSON array
- every key needs at least one authority
- use `arbitrary` for raw signing
- use concrete authorities for policy-checked commands
- raw `arbitrary` signing requires `keeper.authority.arbitrary.enabled = true`
- `arbitrary` does not use an OCI reference
- OCI authority documents cannot declare `type: arbitrary`
- `arbitrary` cannot be mixed with concrete authorities on the same key
- non-arbitrary authorities require an OCI reference
- concrete authorities must be digest-pinned with `@sha256:...`
- tags are for local development, not production trust anchors
- authority ids must be unique on the same key

### Design advice: one action per authority

Treat one authority document as one logical action. A document can technically contain rules for several unrelated actions, but doing so mixes intent meaning, limits, approvers, and audit interpretation. Multiple rules are useful when they express conditions for the same action, such as automatic and reviewed amount ranges. Keep different actions, destinations, networks, and certificate profiles under separate authority ids and separately reviewed OCI digests.

## Authority document

Concrete authorities are Verdict authority documents. `custom` defines a typed JSON request directly in the document and is the neutral starting point for a new integration.

### Custom authority example

This example defines a custom typed authority and a matching command. Its policy shows standard CEL and at least one function from each helper category: effects, decimal, bigint, lists, network, semver, crypto, and time.

```yaml
schemaVersion: verdict.authority/v1
id: production-deployment
type: custom
version: 1.0.0

metadata:
  title: Reviewed production deployment

config:
  fields:
    action:
      type: string
    service:
      type: string
    environment:
      type: string
    releaseVersion:
      type: string
    sequence:
      type: bigint
    riskScore:
      type: decimal
    roles:
      type: list
      items:
        type: string
    sourceIp:
      type: string
    requestedAt:
      type: time
    expiresAt:
      type: time
    changeProof:
      type: bytes
  effects:
    - type: deployment.release
      fields:
        action: "$action"
        service: "$service"
        environment: "$environment"
        version: "$releaseVersion"
        sequence: "$sequence"

policy:
  id: production-deployment
  fallback: DENY
  variables:
    allowedEnvironments: [production]
    requiredRoles: [release-manager, production]
    allowedCidrs: ["10.20.0.0/16", "fd00:20::/48"]
    minimumReleaseVersion: "2.5.0"
    maximumRiskScore: "0.25"
    minimumSequence: "10000000000000000000"
    maximumWindowSeconds: 300
    expectedProofHash: "9f64a747e1b97f131fabb6b447296c9b6f0201e79fb3c5356e6c77e89b6a806a"
  allow:
    - id: allow-reviewed-deployment
      where:
        - "action == 'deploy' && environment in allowedEnvironments"
        - "roles.exists(role, role == 'release-manager')"
        - "effect.one(effects, 'deployment.release')"
        - "decimal.lte(riskScore, maximumRiskScore)"
        - "bigint.gte(sequence, minimumSequence)"
        - "lists.containsAll(roles, requiredRoles)"
        - "ip.isValid(sourceIp) && cidr.matchesAny(sourceIp, allowedCidrs)"
        - "semver.isValid(releaseVersion) && semver.gte(releaseVersion, minimumReleaseVersion)"
        - "crypto.sha256(changeProof) == expectedProofHash"
        - "time.before(requestedAt, expiresAt) && time.durationSeconds(requestedAt, expiresAt) <= maximumWindowSeconds"
  deny: []
```

Each expression demonstrates a policy surface:

| Category | Expression in the example | Purpose |
| --- | --- | --- |
| Standard CEL | `action == ...`, `in`, `roles.exists(...)` | operators, membership, and macros |
| Effects | `effect.one(...)` | normalized consequence count |
| Decimal | `decimal.lte(...)` | exact risk-score comparison |
| Bigint | `bigint.gte(...)` | integer comparison beyond 64-bit range |
| Lists | `lists.containsAll(...)` | required role set |
| Network | `ip.isValid(...)`, `cidr.matchesAny(...)` | source address validation |
| Semver | `semver.isValid(...)`, `semver.gte(...)` | release version floor |
| Crypto | `crypto.sha256(...)` | digest binding for Base64-decoded bytes |
| Time | `time.before(...)`, `time.durationSeconds(...)` | bounded request window |

The matching command in [Request matching](#request-matching) passes every condition. `changeProof` is Base64 for bytes `01 02 03 04`; its SHA-256 value is the `expectedProofHash` constant above.

### Document fields

| Field | Required | Meaning |
| --- | --- | --- |
| `schemaVersion` | yes | `verdict.authority/v1`. |
| `id` | yes | Stable authority id. Must match the id attached to the key. |
| `type` | yes | Intent type. Must match the command artifact type. |
| `version` | yes | Human release version. Not a trust anchor. |
| `metadata` | no | Labels for humans. TKeeper does not enforce them. |
| `config` | no | Trusted intent config. |
| `policy` | yes | Verdict policy. |

TKeeper rejects the authority when the loaded document id does not match the configured key authority id.

## Request matching

For a concrete authority, the sign command must reference an authority attached to the key:

```json
{
  "keyId": "deployment-signing",
  "command": {
    "type": "custom",
    "authorityId": "production-deployment",
    "artifact": {
      "scheme": "ECDSA",
      "hash": "SHA256",
      "typed": {
        "action": "deploy",
        "service": "billing-api",
        "environment": "production",
        "releaseVersion": "2.5.0",
        "sequence": 10000000000000000001,
        "riskScore": 0.20,
        "roles": ["release-manager", "production"],
        "sourceIp": "10.20.4.17",
        "requestedAt": "2030-01-02T03:04:05Z",
        "expiresAt": "2030-01-02T03:09:05Z",
        "changeProof": "AQIDBA=="
      }
    }
  }
}
```

The command `authorityId` must exist on the key.

The command `type` must match the authority document `type`.

The authority document `id` must match the key authority id.

If policy returns `ALLOW`, TKeeper starts threshold signing. If the policy returns `DENY`, signing does not start.

For `arbitrary`, TKeeper only checks that the key allows `arbitrary` and that the command artifact type is `arbitrary`. No Verdict policy is loaded.

## Intent types

Authority `type` selects the payload format and policy context.

| Authority type | Build feature | Command data | Main policy surface |
| --- | --- | --- | --- |
| [`custom`](https://tkeeper.org/docs/signing-and-authorities/arbitrary-and-typed) | core | typed JSON | declared fields and configured `effects` |
| [`evm.transaction`](https://tkeeper.org/docs/digital-assets/evm) | `evm` | unsigned serialized EVM transaction | transaction fields, decoded call, `effects` |
| [`bitcoin.transaction`](https://tkeeper.org/docs/digital-assets/bitcoin) | `bitcoin` | unsigned tx, previous txs, signing input, sighash | inputs, outputs, fee, sighash, `effects` |
| [`tron.transaction`](https://tkeeper.org/docs/digital-assets/tron) | `tron` | unsigned transaction JSON | contracts, fee limit, `effects` |
| [`xrp.transaction`](https://tkeeper.org/docs/digital-assets/xrp) | `xrp` | unsigned transaction hex | payment, fee, ledger limit, `effects` |
| [`solana.transaction`](https://tkeeper.org/docs/digital-assets/solana) | `solana` | unsigned transaction Base64 | instructions, signers, `effects` |
| [`ap2.mandate`](https://tkeeper.org/docs/ai/agentic-payments/ap2) | `ap2` | JWS signing input and SD-JWT disclosures | payment, checkout, request total |
| [`mcintent.mandate`](https://tkeeper.org/docs/ai/agentic-payments/mastercard-verifiable-intent) | `mc-vi` | JWS signing input and SD-JWT disclosures | payment, checkout, request total |
| [`x509.tbs-certificate`](https://tkeeper.org/docs/pki/x509) | `authority-x509` | DER-encoded TBS certificate | subject, issuer, validity, extensions |
| `arbitrary` | core | raw bytes | no Verdict policy |

If a feature module is missing, TKeeper cannot process that command type and returns `INVALID_AUTHORITY_ARTIFACT`.

Build example:

```bash
./gradlew shadowJar -Pkeeper.features=agentic-payments,digital-assets,authority-x509 -Pkeeper.platforms=ecc
```

## Effects

Effects are normalized consequences exposed to CEL as `effects`.

Raw request fields explain the input. Effects describe what the input does.

Example effect:

```json
{
  "type": "deployment.release",
  "action": "deploy",
  "service": "billing-api",
  "environment": "production",
  "version": "2.5.0",
  "sequence": 10000000000000000001
}
```

Common CEL pattern:

```cel
effect.onlyTypes(effects, ['deployment.release']) &&
effect.one(effects, 'deployment.release') &&
effect.any(effects, 'deployment.release', {
  'service': expectedService,
  'environment': 'production'
})
```

Native intent modules fail closed when they cannot describe a consequence.

Examples:

- EVM call to an unknown contract.
- EVM whitelisted function without an effect mapping.
- Bitcoin output script that cannot be classified.
- Bitcoin input without the previous transaction.

Typed JSON authorities produce only the effects declared in authority config.

## Policy format

Authority policies use `allow`, `deny`, and `fallback`:

```yaml
policy:
  id: policy-id
  fallback: DENY
  variables:
    expectedService: billing-api
  allow:
    - id: allow-example
      where:
        - "effect.one(effects, 'deployment.release')"
      unless:
        - "time.after(time.now(), expiresAt)"
  deny:
    - id: deny-example
      where:
        - "action == 'delete'"
```

Rules:

- policy id must be non-blank
- rule ids must be unique across `allow` and `deny`
- a rule matches when every `where` expression is `true`
- a rule does not match when any `unless` expression is `true`
- empty `where` and empty `unless` match unconditionally
- policy variables are available as root CEL variables
- policy variables must not collide with declared intent roots
- deny matches override allow matches
- if no rule matches, `fallback` is returned

Decision order:

1. Evaluate every deny and allow rule.
2. Return `DENY` when at least one deny rule matches.
3. Collect approval groups from every matching allow rule that declares `approvals`.
4. Return `ALLOW_WITH_REQUIREMENTS` when the collected list is non-empty; otherwise return `ALLOW`.
5. Apply `fallback` when no allow rule matches.

`ALLOW` starts signing. `ALLOW_WITH_REQUIREMENTS` starts signing after every collected group passes. `DENY` stops the operation before threshold signing.

### Policy-driven approval groups

Declare named approver keys once and reference their names from allow rules:

```yaml
policy:
  id: payment-policy
  fallback: DENY
  approvers:
    operator-a:
      algorithm: SECP256K1
      publicKey64: "..."
    operator-b:
      algorithm: P256
      publicKey64: "..."
    compliance:
      algorithm: ED25519
      publicKey64: "..."
  allow:
    - id: approve-payment
      where: ["purpose == 'payment'"]
      approvals:
        threshold: 2
        approvers: [operator-a, operator-b]
    - id: compliance-review
      where: ["purpose == 'payment'"]
      approvals:
        threshold: 1
        approvers: [compliance]
```

Both groups apply when both rules match. Every group receives the same signed request hash and all proofs travel in one `approvals.proofs` array.

Conditional fallback:

```yaml
policy:
  id: guarded-fallback
  fallback: ALLOW_WITH_REQUIREMENTS
  approvers:
    operator:
      algorithm: ED25519
      publicKey64: "..."
  fallbackApprovals:
    threshold: 1
    approvers: [operator]
```

Approval validation:

- approvals are valid on allow rules
- `fallbackApprovals` requires `fallback: ALLOW_WITH_REQUIREMENTS`
- every selected approver name must exist in `policy.approvers`
- names within a group must be unique
- `threshold` must be between `1` and the number of selected approvers
- one group may select at most 256 approvers
- declared algorithms must exist in the runtime artifact
- public keys must decode under their declared algorithms
- public key material must be distinct across named approvers

Key-bound and policy-bound groups share the same request format, hash, proof array, timestamp, and nonce. Combined enforcement requires every group from both sources. See [Four Eye Control](https://tkeeper.org/docs/security-model/four-eye-control) for the signed request format and replay rules.

### Strict CEL roots

TKeeper compiles each authority policy with the root schema produced by its intent type and trusted config.

- intent roots and their CEL types come from the selected authority type
- `policy.variables` add constant root values
- policy variables cannot reuse an intent root name
- any external root missing from the intent schema rejects the authority during key creation or import
- compilation errors identify the policy, rule, and expression location
- nested key access must follow the type exposed by the intent schema

For example, `purpoze == 'payment'` fails authority creation when the custom schema declares `purpose`. Native schemas similarly reject misspelled roots such as `chainID` when the EVM intent exposes `chainId`.

See [CEL functions](https://tkeeper.org/docs/signing-and-authorities/cel-functions) for the standard macros and installed `effect`, decimal, bigint, list, network, semver, crypto, and time helpers.

The audit event stores the policy decision and matched rules.

For a policy-checked sign request, the audit event always carries the Verdict policy evaluation.

## OCI artifacts

An authority OCI artifact contains one authority document:

- `authority.json`
- `authority.yaml`
- `authority.yml`

Use digest-pinned references:

```text
oci://registry.example/verdict/authorities/production-deployment@sha256:...
```

Tags are mutable. They are fine for local development, but not as a production trust anchor.

Allow every registry explicitly. The match includes the port, and an empty list denies OCI pulls:

```hocon
oras {
  allowed-registries = ["registry.example"]
}
```

For a local HTTP registry, allow that registry and enable insecure ORAS access:

```hocon
oras {
  allowed-registries = ["registry:5000"]
  insecure = true
}
```

## Custom typed authorities

Use `custom` when the request is JSON and no native intent exists. The [custom authority example](#custom-authority-example) above includes a configured authority and matching command.

Only declared fields become CEL variables. Unknown JSON fields are rejected before signing. `effects` is reserved.

This has an important integration consequence: a backend must not act on unknown fields that were invisible to policy. Reject extra fields before calling TKeeper, or construct the executed action exclusively from declared, governed fields.

Schema evolution should be explicit. Changing field meaning, effect mapping, or policy requires a new reviewed artifact digest; the human-readable `version` field is not a trust anchor.

Supported custom field types and validation rules are documented in [Arbitrary and Typed Authorities](https://tkeeper.org/docs/signing-and-authorities/arbitrary-and-typed). CEL helpers are documented in [CEL functions](https://tkeeper.org/docs/signing-and-authorities/cel-functions).

## Common problems

### Key with `arbitrary` plus another authority is rejected

`arbitrary` means raw signing. Mixing it with concrete authorities makes the key ambiguous.

### `INVALID_AUTHORITY`

The authority list is invalid, the OCI reference is malformed, the authority id is duplicated, the loaded document id does not match the configured id, or the authority policy is invalid.

### `INVALID_AUTHORITY_ARTIFACT`

The command artifact type does not match the authority type, or the feature module for that intent is missing.

### `AUTHORITY_VIOLATION`

The command selected an authority id that is not attached to the key identity. This also applies when an `arbitrary` command is sent to a key that does not allow `arbitrary`.

### `INVALID_INTENT`

The command payload could not be decoded into the authority intent. Common causes are malformed transactions, missing previous Bitcoin transactions, unknown EVM contracts, or invalid typed JSON.

### `POLICY_VIOLATION`

The Verdict policy evaluated to `DENY`.

### OCI pull fails with TLS errors

Check that the exact host and port are present in `oras.allowed-registries`. A local HTTP registry also needs `oras.insecure = true`.


---

Source file: content/docs/signing-and-authorities/dry-run.md
Canonical: https://tkeeper.org/docs/signing-and-authorities/dry-run

# Dry Run Policy Evaluation

The optional `dry-run` module evaluates an `AuthorityCommand` against the policy attached to a key's current authority without executing the command.

Use it to preview whether the same command would be allowed, denied, or require authority-policy approvals before submitting it for signing. Dry run does not create a signature, verify or consume approvals, or mutate key state.

## Enable the module

Dry run is an explicit build feature. It is excluded from `keeper.features=all` and must be selected by name:

```bash
./gradlew :build \
  -Pkeeper.features=dry-run \
  -Pkeeper.platforms=ecc
```

For a Docker image:

```bash
./gradlew dockerBuild \
  -Pkeeper.docker.features=dry-run \
  -Pkeeper.docker.platforms=ecc
```

See [Build and Features](https://tkeeper.org/docs/deployment/build-and-features) for feature and platform selection.

## Permission

The endpoint requires an authenticated caller with:

```text
tkeeper.emulate
```

This permission is not scoped to a key id. A successful response can expose policy matches, approver public keys, and approver metadata for any requested key known to the caller. Grant it only to services that are allowed to inspect authority-policy decisions.

## Java SDK

The Java SDK exposes dry-run evaluation through `TKeeperClient.dryRun()`.

```java
import org.exploit.tkeeper.sdk.model.Emulate;
import org.exploit.tkeeper.sdk.model.HashMethod;
import org.exploit.tkeeper.sdk.model.PolicyVerdict;
import org.exploit.tkeeper.sdk.model.SignatureSchemes;
import org.exploit.tkeeper.sdk.model.command.AuthorityCommand;
import org.exploit.tkeeper.sdk.model.command.artifact.TypedData;
import org.exploit.tkeeper.sdk.util.TKeeperJackson;

var payload = TKeeperJackson.signingNode()
        .put("purpose", "payments")
        .put("amount", 500)
        .put("currency", "USD");

var command = AuthorityCommand.of(
        "payments-small",
        new TypedData(SignatureSchemes.ECDSA, HashMethod.SHA256, payload)
);

var evaluation = keeper.dryRun().emulate(
        Emulate.of("payments-key", command)
);

if (evaluation.decision() == PolicyVerdict.DENY) {
    throw new IllegalStateException("authority policy denied the command");
}
```

The request uses the same `AuthorityCommand` shape as signing. The command's `authorityId` must be assigned to the key, and its artifact type must match that authority.

## Decisions

Dry run returns HTTP `200` for every completed policy evaluation, including denials.

| `decision` | Meaning |
| --- | --- |
| `ALLOW` | The authority policy currently allows the command without policy-driven approvals. |
| `ALLOW_WITH_REQUIREMENTS` | The authority policy allows the command after every returned approval group is satisfied. |
| `DENY` | The authority policy rejects the command. A real signing request would fail with `POLICY_VIOLATION`. |

`matches` contains the rules that contributed to the decision. `approvalRequirements` contains every authority-policy approval group required by the decision.

Example response requiring four-eye approval:

```json
{
  "decision": "ALLOW_WITH_REQUIREMENTS",
  "matches": [
    {
      "id": "review-large-payment",
      "effect": "ALLOW_WITH_REQUIREMENTS"
    }
  ],
  "approvalRequirements": [
    {
      "policyId": "payments-policy",
      "source": "review-large-payment",
      "threshold": 1,
      "approvers": {
        "operator": {
          "algorithm": "ED25519",
          "publicKey64": "...",
          "metadata": {
            "team": "payments"
          }
        }
      }
    }
  ]
}
```

Each approval requirement identifies the policy and matching rule or fallback in `source`, the required threshold, and the eligible approvers. See [Four-Eye Control](https://tkeeper.org/docs/security-model/four-eye-control) for constructing approvals for the later signing request.

## HTTP API

The SDK calls:

```text
POST /v1/keeper/emulate
```

Request body:

```json
{
  "keyId": "payments-key",
  "command": {
    "type": "custom",
    "authorityId": "payments-small",
    "artifact": {
      "scheme": "ECDSA",
      "hash": "SHA256",
      "typed": {
        "purpose": "payments",
        "amount": 500,
        "currency": "USD"
      }
    }
  }
}
```

The complete wire contract is in [OpenAPI reference](https://tkeeper.org/docs/api-reference/openapi).


---

Source file: content/docs/signing-and-authorities/cel-functions.md
Canonical: https://tkeeper.org/docs/signing-and-authorities/cel-functions

# CEL Functions

TKeeper installs these functions for every authority policy. Intent-specific root variables are listed on the `custom`, EVM, Bitcoin, and X.509 authority pages. The [typed authority example](https://tkeeper.org/docs/signing-and-authorities/authorities#custom-authority-example) shows at least one function from each category below and includes a matching command.

## Standard CEL

Standard CEL operators and macros are available:

```cel
roles.exists(role, role == 'admin')
roles.all(role, role != 'banned')
has(resource.ownerId)
```

The Google CEL list extension is enabled:

```cel
[1, 2, 3].reverse()[0] == 3
[1, 2, 3, 4].slice(1, 3) == [2, 3]
```

Missing roots fail strict policy compilation. Missing nested values fail evaluation unless the expression guards their presence:

```cel
'plan' in subject.account && subject.account.plan == 'pro'
```

## Effect helpers

Inputs are lists of maps. Every effect contains a `type` field.

```cel
effect.onlyTypes(effects, ['deployment.release']) &&
effect.one(effects, 'deployment.release') &&
effect.any(effects, 'deployment.release', {
  'service': expectedService,
  'environment': 'production'
}) &&
bigint.gte(effect.sum(effects, 'deployment.release', 'sequence'), minimumSequence)
```

| Function | Result |
| --- | --- |
| `effect.types(effects)` | effect type strings in input order |
| `effect.has(effects, type)` | at least one effect has `type` |
| `effect.none(effects, type)` | no effect has `type` |
| `effect.one(effects, type)` | exactly one effect has `type` |
| `effect.count(effects, type)` | number of effects with `type` |
| `effect.of(effects, type)` | effects with `type` |
| `effect.onlyTypes(effects, allowedTypes)` | every effect type belongs to `allowedTypes` |
| `effect.any(effects, type, criteria)` | at least one matching effect contains every field in `criteria` |
| `effect.all(effects, type, criteria)` | at least one effect has `type` and all effects of that type match `criteria` |
| `effect.values(effects, type, field)` | values of `field`; missing fields are skipped |
| `effect.amount(effects, type)` | sum of `amount` for matching effects as `BigInteger` |
| `effect.sum(effects, type, field)` | sum of a numeric field as `BigInteger` |

`effect.amount` and `effect.sum` fail evaluation when a matching effect lacks the summed field.

## Decimal helpers

Accepted inputs: `BigDecimal`, `BigInteger`, finite Java numbers, CEL `int` and `double`, and decimal strings.

```cel
decimal.eq(decimal.add(balance, fee), '0.30')
decimal.between(amount, '10.00', '100.00')
decimal.round(amount, 2)
```

| Function | Result |
| --- | --- |
| `decimal.from(value)` | convert to `BigDecimal` |
| `decimal.compare(left, right)` | `-1`, `0`, or `1` |
| `decimal.eq(left, right)` | equal while ignoring scale |
| `decimal.ne(left, right)` | unequal |
| `decimal.gt(left, right)` | `left > right` |
| `decimal.gte(left, right)` | `left >= right` |
| `decimal.lt(left, right)` | `left < right` |
| `decimal.lte(left, right)` | `left <= right` |
| `decimal.between(value, min, max)` | inclusive range |
| `decimal.add(left, right)` | addition |
| `decimal.sub(left, right)` | subtraction |
| `decimal.mul(left, right)` | multiplication |
| `decimal.div(left, right)` | `DECIMAL128` division; zero divisor fails evaluation |
| `decimal.round(value, scale)` | `HALF_UP` rounding |
| `decimal.abs(value)` | absolute value |

## Bigint helpers

Accepted inputs: `BigInteger`, exact integral `BigDecimal`, integral Java numbers, CEL `int`, and integer strings.

```cel
bigint.gt(counter, '9223372036854775808')
bigint.eq(bigint.mod(counter, 10), 9)
```

| Function | Result |
| --- | --- |
| `bigint.from(value)` | convert to `BigInteger`; fractional values fail evaluation |
| `bigint.compare(left, right)` | `-1`, `0`, or `1` |
| `bigint.eq(left, right)` | equality |
| `bigint.ne(left, right)` | inequality |
| `bigint.gt(left, right)` | `left > right` |
| `bigint.gte(left, right)` | `left >= right` |
| `bigint.lt(left, right)` | `left < right` |
| `bigint.lte(left, right)` | `left <= right` |
| `bigint.between(value, min, max)` | inclusive range |
| `bigint.add(left, right)` | addition |
| `bigint.sub(left, right)` | subtraction |
| `bigint.mul(left, right)` | multiplication |
| `bigint.mod(left, right)` | remainder; zero divisor fails evaluation |
| `bigint.abs(value)` | absolute value |

## List helpers

Inputs may be Java collections, Java arrays, or CEL lists. Membership compares numeric values independently of their Java wrapper types.

```cel
lists.containsAny(subject.account.scopes, ['admin', 'billing'])
lists.hasOnly(roles, allowedRoles)
lists.nonEmpty(lists.without(roles, ['viewer']))
```

| Function | Result |
| --- | --- |
| `lists.isEmpty(value)` | list has no elements |
| `lists.nonEmpty(value)` | list has at least one element |
| `lists.size(value)` | element count |
| `lists.first(value)` | first element; empty lists fail evaluation |
| `lists.last(value)` | last element; empty lists fail evaluation |
| `lists.contains(values, candidate)` | any element equals `candidate` |
| `lists.containsAny(values, candidates)` | at least one candidate exists in `values` |
| `lists.containsAll(values, candidates)` | every candidate exists in `values` |
| `lists.containsNone(values, candidates)` | no candidate exists in `values` |
| `lists.hasOnly(values, allowed)` | every value exists in `allowed` |
| `lists.concat(left, right)` | concatenated list |
| `lists.without(values, excluded)` | values absent from `excluded` |
| `lists.distinct(values)` | first occurrence of each value |

## Network helpers

`ip.*` accepts IP strings, `InetAddress`, and raw bytes. Invalid IP values return `false`.

```cel
ip.isPrivate(request.ip)
cidr.matches(request.ip, '10.0.0.0/8')
cidr.matchesAny(request.ip, allowedCidrs)
```

| Function | Result |
| --- | --- |
| `ip.isValid(value)` | valid IPv4 or IPv6 |
| `ip.isV4(value)` | IPv4 |
| `ip.isV6(value)` | IPv6 |
| `ip.isPrivate(value)` | RFC1918 IPv4 or unique-local IPv6 |
| `ip.isPublic(value)` | public address excluding invalid, private, loopback, link-local, reserved, multicast, and documentation ranges |
| `ip.isLoopback(value)` | loopback address |
| `ip.isLinkLocal(value)` | IPv4 `169.254.0.0/16` or IPv6 `fe80::/10` |
| `cidr.matches(ip, cidr)` | IP belongs to CIDR |
| `cidr.matchesAny(ip, cidrs)` | IP belongs to any CIDR; accepts a list, array, string, or comma-separated string |
| `cidr.contains(cidr, ipOrCidr)` | first CIDR contains an IP or another CIDR |

Invalid CIDR inputs return `false`.

## Semver helpers

Semver parsing supports prerelease ordering, ignored build metadata, an optional `v` prefix, and missing minor or patch components as zero.

```cel
semver.gte(app.version, '1.4.0')
semver.lt(app.version, '2.0.0-beta')
semver.between(app.version, '1.4.0', '1.5.0')
```

| Function | Result |
| --- | --- |
| `semver.isValid(value)` | accepted Semver string |
| `semver.compare(left, right)` | `-1`, `0`, or `1` |
| `semver.eq(left, right)` | equality |
| `semver.ne(left, right)` | inequality |
| `semver.gt(left, right)` | `left > right` |
| `semver.gte(left, right)` | `left >= right` |
| `semver.lt(left, right)` | `left < right` |
| `semver.lte(left, right)` | `left <= right` |
| `semver.between(value, min, max)` | inclusive range |

## Crypto helpers

Byte inputs may be `byte[]`, `ByteBuffer`, `UUID`, collections, or arrays of byte values. String inputs use UTF-8 unless a function documents normalization.

```cel
crypto.sha256(subject.email) == expectedHash
crypto.digest(payload, 'sha-512') == expectedDigest
crypto.uuidEq(request.id, expectedRequestId)
```

| Function | Result |
| --- | --- |
| `crypto.digest(value, algorithm)` | hexadecimal digest; accepts normalized algorithm spellings such as `sha_256`, `sha-256`, and `SHA-256` |
| `crypto.sha256(value)` | SHA-256 hexadecimal digest |
| `crypto.sha512(value)` | SHA-512 hexadecimal digest |
| `crypto.md5(value)` | MD5 hexadecimal digest |
| `crypto.hex(value)` | hexadecimal bytes; existing hex strings normalize to lowercase |
| `crypto.isHex(value)` | valid even-length hexadecimal string after normalization |
| `crypto.uuid(value)` | canonical lowercase UUID |
| `crypto.isUuid(value)` | UUID parse succeeds |
| `crypto.uuidEq(left, right)` | normalized UUID equality; invalid values return `false` |

## Time helpers

Inputs may be `Instant`, `Date`, `Calendar`, `OffsetDateTime`, `ZonedDateTime`, UTC `LocalDateTime`, UTC-start-of-day `LocalDate`, ISO/RFC-1123 strings, epoch seconds, or epoch milliseconds.

```cel
time.after(time.now(), subject.createdAt)
time.before(request.createdAt, request.expiresAt)
time.ageMinutes(subject.createdAt, time.now()) < 30
```

| Function | Result |
| --- | --- |
| `time.now()` | current UTC `Instant` |
| `time.parse(value)` | convert to `Instant` |
| `time.before(left, right)` | `left < right` |
| `time.after(left, right)` | `left > right` |
| `time.between(value, start, end)` | inclusive range |
| `time.durationSeconds(start, end)` | whole seconds from start to end; may be negative |
| `time.ageSeconds(timestamp, now)` | whole seconds from timestamp to now |
| `time.ageMinutes(timestamp, now)` | whole minutes from timestamp to now |
| `time.ageHours(timestamp, now)` | whole hours from timestamp to now |
| `time.ageDays(timestamp, now)` | whole days from timestamp to now |

Numeric epochs with absolute values below `10_000_000_000` are seconds. Larger values are milliseconds.


---

Source file: content/docs/signing-and-authorities/arbitrary-and-typed.md
Canonical: https://tkeeper.org/docs/signing-and-authorities/arbitrary-and-typed

# Arbitrary and Typed Authorities

## `arbitrary`

`arbitrary` is raw signing. TKeeper checks that the key identity allows `arbitrary`, then signs the bytes from the command.
It is disabled by default and requires `keeper.authority.arbitrary.enabled = true` on every keeper
in the cluster.

Use it for:

- local demos
- compatibility with systems that already govern the payload elsewhere
- narrow raw-signing cases accepted by policy and security review

Do not use it when TKeeper is expected to understand the business effect. Raw bytes do not tell TKeeper whether the action moves funds, changes production, issues a certificate, or approves a tool call.

## `custom`

`custom` is for typed JSON commands. The authority document defines the expected command shape and the effects exposed to policy.

Start with the [typed authority example and matching command](https://tkeeper.org/docs/signing-and-authorities/authorities#custom-authority-example). It shows the field shapes and each CEL helper category in context.

Use it for:

- AI-agent tool or action intents
- internal service commands
- business-specific approvals
- workflows where a backend verifies TKeeper proof before execution

Only declared fields are available to policy. The executing backend must not derive additional effects from undeclared fields in the submitted JSON.

## Typed JSON signing material

For a `custom` command, TKeeper signs the submitted `artifact.typed` JSON object. It does not sign
the authority document, the materialized CEL intent, or the generated `effects` object.

TKeeper converts `artifact.typed` into signing material as follows:

1. Sort every JSON object's field names lexicographically.
2. Apply the same rule recursively to nested objects, including objects inside arrays.
3. Preserve array element order.
4. Preserve scalar JSON values and types, including strings, booleans, numbers, and explicit
   `null` values.
5. Serialize the resulting tree as compact UTF-8 JSON with no insignificant whitespace.
6. Apply the command artifact's `hash` method to those bytes. `NONE` leaves the bytes unchanged;
   `SHA256`, `SHA512`, and `KECCAK256` produce the corresponding digest.
7. Pass that byte sequence to the selected signature scheme.

In compact notation:

```text
canonical = UTF-8(compact-json(sort-object-fields-recursively(artifact.typed)))
signingMaterial = hash.process(canonical)
signature = scheme.sign(signingMaterial)
```

For example, this submitted object:

```json
{
  "sequence": 7,
  "deployment": {
    "version": "2.5.0",
    "environment": "production"
  },
  "roles": [
    { "name": "operator", "priority": 1 },
    { "priority": 2, "name": "auditor" }
  ]
}
```

is serialized for signing as:

```json
{"deployment":{"environment":"production","version":"2.5.0"},"roles":[{"name":"operator","priority":1},{"name":"auditor","priority":2}],"sequence":7}
```

With `hash: SHA256`, the expected signing material is:

```text
a55960ff8a2b7af9338df51db5074e4bf5cd012dafe07cb113bda0d6642c72e1
```

Object field order and insignificant input whitespace therefore do not change the signing
material. Array order does. Missing fields remain missing, while an explicitly submitted `null`
remains present. Authority defaults may affect the materialized policy intent, but they are not
inserted into the submitted JSON before signing.

### Relationship to RFC 8785/JCS

This encoding is TKeeper-specific and is not
[RFC 8785 JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785).

Both encodings remove insignificant whitespace, recursively sort object names by their UTF-16
code-unit values, preserve array order, emit UTF-8, and preserve Unicode strings without applying
Unicode normalization. Their primitive-value contracts differ:

- JCS restricts input to I-JSON, represents JSON numbers as IEEE-754 double-precision values, and
  uses ECMAScript's exact primitive serialization rules.
- TKeeper canonicalizes a parsed Jackson `JsonNode` tree and uses Jackson's compact serialization.
  It serializes the numeric node type and value produced by Jackson instead of applying JCS's
  required ECMAScript number-normalization algorithm. Integral values may use arbitrary-precision
  nodes; floating-point representation depends on the parsed node type.
- JCS defines validation requirements for duplicate object names and invalid Unicode. TKeeper's
  canonicalizer receives an already-parsed object tree, so parsing has already resolved the input
  token stream before canonicalization.

A generic JCS implementation is therefore not a compatible replacement for TKeeper's encoder.
Producers and verifiers should use the SDK command types and must not independently rewrite numeric
values or representations before verification.

The outer `keyId`, `authorityId`, command `type`, `scheme`, `hash`, generation, policy decision, and
effects are not concatenated into the JSON message. They select the key, authorization path, and
cryptographic processing. Verification must use the same typed payload, scheme, hash method, key,
generation, and tweak context that the integration expects.

## Custom authority config

```yaml
type: custom
config:
  fields:
    amount:
      type: bigint
    currency:
      type: string
    customer:
      type: object
      fields:
        id:
          type: string
        country:
          type: string
          required: false
          default: UNKNOWN
  effects:
    - type: payment.transfer
      fields:
        asset: "$currency"
        amount: "$amount"
        customerId: "$customer.id"
```

Field rules:

- JSON root must be an object
- unknown fields are rejected before policy evaluation
- `effects` is reserved
- `required` defaults to `true`
- `nullable` defaults to `false`
- optional missing fields without a default become CEL `null`
- config typos and invalid defaults reject the authority

Supported types:

| Type | CEL/runtime value |
| --- | --- |
| `string` | string |
| `bool` | boolean |
| `int` | signed 32-bit integer |
| `bigint` | arbitrary-precision integer |
| `decimal` | arbitrary-precision decimal |
| `time` | instant |
| `bytes` | bytes decoded from Base64 |
| `object` | nested object with declared `fields` |
| `list` | list whose element schema is declared in `items` |

`bigint` requires an integral JSON number and `decimal` requires a JSON number. Preserve large values when constructing JSON; do not pass them through an IEEE-754 `double`. `bytes` accepts Base64 strings. `time` accepts ISO-8601 instant strings such as `2030-01-02T03:04:05Z`.

Effect mapping strings beginning with `$` resolve declared field paths. `$$value` produces the literal string `$value`.

The declared fields and `effects` become strict CEL roots. See [Authorities](https://tkeeper.org/docs/signing-and-authorities/authorities#strict-cel-roots) and [CEL Functions](https://tkeeper.org/docs/signing-and-authorities/cel-functions).

## Decision rule

| Need | Use |
| --- | --- |
| Sign bytes with no semantic policy in TKeeper | `arbitrary` |
| Govern a typed business action | `custom` |
| Govern an EVM transaction | `evm.transaction` |
| Govern a Bitcoin transaction | `bitcoin.transaction` |
| Govern a Tron, XRP, or Solana transaction | `tron.transaction`, `xrp.transaction`, or `solana.transaction` |
| Govern an AP2 or MC VI mandate | `ap2.mandate` or `mcintent.mandate` |
| Govern certificate issuance | `x509.tbs-certificate` |

`arbitrary` cannot be mixed with concrete authorities on the same key identity.

Do not describe an `arbitrary` integration as governed intent unless another trusted layer defines, validates, and binds the meaning of the signed bytes.


---

Source file: content/docs/ai/README.md
Canonical: https://tkeeper.org/docs/ai

# AI and Agentic Payments

| Task | Guide |
| --- | --- |
| Connect an agent host, discover tools, govern MCP calls | [MCP connection and tools](https://tkeeper.org/docs/ai/mcp) |
| Sign AP2 payment credentials | [AP2 authority](https://tkeeper.org/docs/ai/agentic-payments/ap2) |
| Sign MC Intent payment credentials | [MC Intent authority](https://tkeeper.org/docs/ai/agentic-payments/mastercard-verifiable-intent) |
| Apply Keeper to an AI workflow | [For AI](https://tkeeper.org/docs/use-cases/for-ai) |

MCP tools and payment commands use the same [authority and policy checks](https://tkeeper.org/docs/signing-and-authorities/authorities) as the HTTP API. See [Agentic Payment Authorities](https://tkeeper.org/docs/ai/agentic-payments) for the shared payment contract.


---

Source file: content/docs/ai/mcp.md
Canonical: https://tkeeper.org/docs/ai/mcp

# MCP

The `mcp` feature adds `POST /mcp` to TKeeper's public HTTP server. An agent host can discover permitted identities and authorities, read command schemas, verify signatures, and request governed signing or composition. The host holds the Keeper token and sends it with each request.

## What the tools provide

| Tools | Result | Permission |
| --- | --- | --- |
| `identity.list`, `identity.describe_authority` | Active signing identities, authority descriptions, command JSON Schemas | `tkeeper.key.<keyId>.sign` |
| `utility.keeper_status` | Sealed and ready state | `tkeeper.system.status` |
| `utility.get_public_key` | Public key for an identity | `tkeeper.key.<keyId>.public` |
| `utility.verify_signature` | Cryptographic command verification | `tkeeper.key.<keyId>.verify` |
| `action.sign`, `action.compose` | Signature or [composed result](https://tkeeper.org/docs/signing-and-authorities/composer) | `tkeeper.key.<keyId>.sign` |

`tools/list` shows tools available to the caller. Each tool call checks permission again for its target key. `identity.describe_authority` exposes the input schema; the attached authority and policy decide whether a submitted command may be signed.

## Connect an agent host

1. Build Keeper with `mcp` and the authority features the host needs. `mcp` is an explicit build selector, including when `keeper.features=all` is selected:

   ```sh
   ./gradlew :build -Pkeeper.features=mcp,digital-assets,agentic-payments -Pkeeper.platforms=ecc
   ```

   For local developer authentication, also select `auth-dev` and follow the [single-node setup](https://tkeeper.org/docs/getting-started/local-single-node).

2. Run Keeper with its normal [public server and authentication configuration](https://tkeeper.org/docs/security-model/authentication-and-authorization). Create a key with an authority before expecting it in `identity.list`. MCP uses the same public port; there is no separate MCP process.

3. Configure the agent host for Streamable HTTP at `https://<keeper-public-host>/mcp`. It must support MCP version `2026-07-28`, the `server/discover`, `tools/list`, and `tools/call` methods, and custom request headers:

   | Authentication mode | Request header |
   | --- | --- |
   | Production JWT | `X-JWT-TOKEN: <raw-jwt>` |
   | Local `auth-dev` | `X-DEV-TOKEN: <dev-token>` |

   Send the token from the host on every request. Browser requests with an `Origin` header are rejected, so connect from a server-side host. Keeper accepts HTTP POST and returns JSON.

## Check the connection

With a JWT for the agent host, list the available tools:

```sh
curl -sS 'https://<keeper-public-host>/mcp' \
  -H 'X-JWT-TOKEN: <raw-jwt>' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/list' \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
```

For `tools/call`, set `Mcp-Method: tools/call`, set `Mcp-Name` to the tool name, and supply the same name plus `arguments` in `params`. The response places a tool's data in `result.structuredContent`; tool failures set `result.isError: true`. Discovery and tool lists have `ttlMs: 0` and `cacheScope: private`, so refresh them after permission or authority changes.

## Govern signing

Attach a digest-pinned authority to each signing key and grant the host `tkeeper.key.<keyId>.sign` only for the keys it may use. `action.sign` and `action.compose` run the regular policy, approval, audit, and signing pipeline. The system that performs the action must verify the returned proof and enforce freshness or replay rules.

[For AI](https://tkeeper.org/docs/use-cases/for-ai#govern-an-mcp-action) has a complete governed action and `tools/call` request. See [agentic payment authorities](https://tkeeper.org/docs/ai/agentic-payments) and [digital asset authorities](https://tkeeper.org/docs/digital-assets) for command examples. The [feature reference](https://github.com/tkeeper-org/tkeeper/blob/main/features/mcp/README.md) lists each tool.


---

Source file: content/docs/ai/agentic-payments/README.md
Canonical: https://tkeeper.org/docs/ai/agentic-payments

# Agentic Payment Authorities

AP2 and Mastercard Verifiable Intent (MC VI) sign ES256 JWS inputs with a P-256 key. Both use Keeper's authority, approval, and signing pipeline.

| Protocol | Authority type | Build selector | Guide |
| --- | --- | --- | --- |
| AP2 | `ap2.mandate` | `ap2` | [AP2](https://tkeeper.org/docs/ai/agentic-payments/ap2) |
| MC VI | `mcintent.mandate` | `mc-vi` | [MC Intent](https://tkeeper.org/docs/ai/agentic-payments/mastercard-verifiable-intent) |

Build both with `-Pkeeper.features=agentic-payments -Pkeeper.platforms=ecc`, or select one feature from the table. Each guide contains a complete authority YAML and a compose request.

`signingInput` is the exact unpadded base64url JWS header and payload joined by `.`. Supply every disclosure referenced by the mandate. Keeper checks their commitments, evaluates every action against the authority policy, and signs the unchanged input. `/v2/keeper/sign` returns the raw signature; [Composer](https://tkeeper.org/docs/signing-and-authorities/composer) returns a serialized `PaymentCredential` layer.

The caller supplies issuer, audience, nonce, time, and parent-binding claims. The recipient verifies the credential chain, merchant identity, audience, expiry, and replay rules. Keeper checks the outgoing layer's content commitments; parent credentials and merchant signatures need verification by the recipient. See the [module contract](https://github.com/tkeeper-org/tkeeper/blob/main/features/agentic-payments/README.md) for supported profiles and disclosure rules.


---

Source file: content/docs/ai/agentic-payments/ap2.md
Canonical: https://tkeeper.org/docs/ai/agentic-payments/ap2

# AP2 Authority

Use `type: ap2.mandate` for an AP2 payment mandate. The [tested authority](https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/src/testFixtures/resources/authorities/payments/ap2-purchases.yaml) permits the configured shop and card, at most USD 100 per purchase and USD 150 across the request:

```yaml
schemaVersion: verdict.authority/v1
id: test:ap2/purchases
type: ap2.mandate
version: 2.0.0

config:
  merchants:
    officeShop:
      website: https://office.example
  methods:
    companyCard:
      id: card-1
      type: card

policy:
  id: office-purchases
  fallback: DENY
  variables:
    maxPerPurchase: "100.00"
    maxPerRequest: "150.00"

  allow:
    - id: office-purchase
      where:
        - "action.kind == 'purchase'"
        - "checkout.merchantIs(merchants.officeShop)"
        - "payment.payeeIs(merchants.officeShop)"
        - "payment.methodIs(methods.companyCard)"
        - "payment.amountAtMost(maxPerPurchase, 'USD')"

  deny:
    - id: request-total
      where:
        - "!request.totalAtMost(maxPerRequest, 'USD')"
```

`config.mode` defaults to `PAIRED`. For a standalone stage, set it to `PAYMENTS` or `CHECKOUTS`, write the policy for that stage, and use the same mode in the artifact. This example uses `PAIRED`.

```java
var artifact = new Ap2Mandates(PaymentRequestMode.PAIRED, signingInput, disclosures);
var command = Command.of("test:ap2/purchases", artifact);
var result = client.signature().compose(
        Sign.of(p256KeyId, command), PaymentCredential.class);
String credential = result.credential();
```

`signingInput` contains the AP2 protected header and payload encoded as an unsigned JWS; `disclosures` contains every referenced SD-JWT disclosure. The result is the signed credential layer. AP2 supports trusted-provider SD-JWT and delegated KB-SD-JWT; for a delegated credential, present the parent credential bound by `sd_hash`. See the [shared signing contract](https://tkeeper.org/docs/ai/agentic-payments) and [module contract](https://github.com/tkeeper-org/tkeeper/blob/main/features/agentic-payments/README.md).


---

Source file: content/docs/ai/agentic-payments/mcintent.md
Canonical: https://tkeeper.org/docs/ai/agentic-payments/mastercard-verifiable-intent

# MC Intent Authority

Use `type: mcintent.mandate` for a Mastercard Verifiable Intent mandate. The [tested authority](https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/src/testFixtures/resources/authorities/payments/mcintent-purchases.yaml) permits the configured shop and card, at most USD 100 per purchase and USD 150 across the request:

```yaml
schemaVersion: verdict.authority/v1
id: test:mcintent/purchases
type: mcintent.mandate
version: 2.0.0

config:
  merchants:
    officeShop:
      website: https://office.example
  methods:
    companyCard:
      id: card-1
      type: card

policy:
  id: office-purchases
  fallback: DENY
  variables:
    maxPerPurchase: "100.00"
    maxPerRequest: "150.00"

  allow:
    - id: office-purchase
      where:
        - "action.kind == 'purchase'"
        - "checkout.merchantIs(merchants.officeShop)"
        - "payment.payeeIs(merchants.officeShop)"
        - "payment.methodIs(methods.companyCard)"
        - "payment.amountAtMost(maxPerPurchase, 'USD')"

  deny:
    - id: request-total
      where:
        - "!request.totalAtMost(maxPerRequest, 'USD')"
```

`config.mode` defaults to `PAIRED`. For a terminal L3 stage, set it to `PAYMENTS` or `CHECKOUTS`, write the policy for that stage, and use the same mode in the artifact. This example uses `PAIRED`.

```java
var artifact = new McMandates(PaymentRequestMode.PAIRED, signingInput, disclosures);
var command = Command.of("test:mcintent/purchases", artifact);
var result = client.signature().compose(
        Sign.of(p256KeyId, command), PaymentCredential.class);
String credential = result.credential();
```

`signingInput` contains the MC VI protected header and payload encoded as an unsigned JWS; `disclosures` contains every referenced SD-JWT disclosure. The result is the signed credential layer. MC VI supports L2 Immediate and Autonomous credentials and terminal L3 credentials. The protected header's `typ` identifies the profile; L3 requires `kid` and a validity interval of at most one hour. See the [shared signing contract](https://tkeeper.org/docs/ai/agentic-payments) and [module contract](https://github.com/tkeeper-org/tkeeper/blob/main/features/agentic-payments/README.md).


---

Source file: content/docs/ai/agentic-payments/payments.md
Canonical: https://tkeeper.org/docs/ai/agentic-payments/payment-policy

# AP2 and Mastercard: write a payment policy

Start with one sentence: **“Allow office purchases from this shop, using this card,
up to USD 100 per purchase and USD 150 for the complete request.”** This page turns
that sentence into a policy. The same conditions work for Google AP2 and Mastercard VI.

## What is being checked?

Your integration receives the protocol content, builds an intent, asks Verdict for a
decision, then signs the evaluated content if authorized. Signing, SD-JWT assembly
and cryptographic verification remain in the integration.

A request can contain several actions. **You write the rule for one action; Verdict
runs it for every action.** If one action is denied, the entire request is denied.

| Action kind  | What you are authorizing                                   | Fields to check       |
|--------------|------------------------------------------------------------|-----------------------|
| `purchase`   | A concrete checkout and its payment together               | `checkout`, `payment` |
| `payment`    | A concrete payment at a payment-only signing stage         | `payment`             |
| `checkout`   | A concrete checkout at a checkout-only signing stage       | `checkout`            |
| `delegation` | Permission to make future purchases within proposed bounds | `delegation`          |

A **checkout** is the order/cart description. A **payment** specifies who receives
money, the method and the amount. A **delegation** grants future authority: a maximum
on a delegation bounds future spending. Start each allow rule with the permitted
action kind.

## The data seen by one purchase rule

Relevant fields produced by the mandate decoder:

```json
{
  "action": {"kind": "purchase", "index": 0},
  "checkout": {
    "vct": "mandate.checkout.1",
    "content": {"merchant": {"id": "merchant-1", "website": "https://office.example"}}
  },
  "payment": {
    "vct": "mandate.payment.1",
    "payee": {"id": "merchant-1", "website": "https://office.example"},
    "payment_instrument": {"id": "card-1", "type": "card"},
    "payment_amount": {"currency": "USD", "amount": 7500}
  },
  "request": {"actionCount": 2, "allAmountsKnown": true, "totals": {"USD": 15000}}
}
```

This action spends USD 75.00. Another action also spends USD 75.00, so the request
total is USD 150.00. `payment` changes when Verdict evaluates the next action;
`request` describes the same complete request each time.

## Copy this authority

```yaml
schemaVersion: verdict.authority/v1
id: com.acme.office.ap2
type: ap2.mandate
version: 2.0.0

config:
  merchants:
    officeShop:
      website: https://office.example
  methods:
    companyCard:
      id: card-1
      type: card

policy:
  id: office-purchases
  fallback: DENY
  variables:
    maxPerPurchase: "100.00"
    maxPerRequest: "150.00"

  allow:
    - id: office-purchase
      where:
        - "action.kind == 'purchase'"
        - "checkout.merchantIs(merchants.officeShop)"
        - "payment.payeeIs(merchants.officeShop)"
        - "payment.methodIs(methods.companyCard)"
        - "payment.amountAtMost(maxPerPurchase, 'USD')"

  deny:
    - id: request-total
      where:
        - "!request.totalAtMost(maxPerRequest, 'USD')"
```

Download [AP2 authority](https://github.com/exploit-org/verdict/blob/main/docs/authority/examples/ap2-payment.yaml) or
[Mastercard VI authority](https://github.com/exploit-org/verdict/blob/main/docs/authority/examples/mcintent-payment.yaml).
For VI, the authority uses `type: mcintent.mandate`; the policy expressions are the same.

## Read each part in plain language

| Part                                                   | Meaning                                                                   |
|--------------------------------------------------------|---------------------------------------------------------------------------|
| `config.merchants.officeShop`                          | A name for an exact merchant identity                                     |
| `config.methods.companyCard`                           | A name for one specific card/method                                       |
| `maxPerPurchase: "100.00"`                             | A policy constant, in major units (USD dollars here)                      |
| `action.kind == 'purchase'`                            | Only approve concrete checkout/payment pairs                              |
| `checkout.merchantIs(merchants.officeShop)`            | The shop in the checkout must match this configured shop                  |
| `payment.payeeIs(merchants.officeShop)`                | The recipient of the money must match this configured shop                |
| `payment.methodIs(methods.companyCard)`                | Use this particular configured card                                       |
| `payment.amountAtMost(maxPerPurchase, 'USD')`          | Current payment is in USD and no more than USD 100                        |
| `!request.totalAtMost(maxPerRequest, 'USD')` in `deny` | Deny if the complete request is not priced entirely in USD within USD 150 |

The shop and payee are separate checks because a checkout's seller can differ from
the payment recipient, for example with an intermediary. Configure both identities
if your payment flow needs that. A merchant entry may use `id`, `website`, or both;
all supplied criteria must match. A method needs `id` and may also specify `type`.
A display name is never sufficient identity.

The module exposes the authority's identity catalogs as `merchants` and `methods`.
Pass a catalog entry to each helper, for example `payment.methodIs(methods.companyCard)`.
Accessing a missing catalog key raises an evaluation error.

## Try these cases

All purchases below use the configured shop, payee and method unless stated otherwise.

| Request                                        | Decision | Reason                                            |
|------------------------------------------------|----------|---------------------------------------------------|
| One purchase of USD 100                        | ALLOW    | Meets both limits                                 |
| Two purchases of USD 75                        | ALLOW    | Each <= 100; together <= 150                      |
| Two purchases of USD 80                        | DENY     | Total 160                                         |
| One purchase of USD 100.01                     | DENY     | Individual limit exceeded                         |
| USD 10 plus EUR 10                             | DENY     | Request is not entirely in USD                    |
| One valid purchase plus one using another card | DENY     | Every action must pass                            |
| One valid purchase plus a delegation           | DENY     | Delegation is not permitted by this rule          |
| Payment-only action                            | DENY     | Different signing stage; `kind` is not `purchase` |

The total check returns false for unpriced actions or mixed currencies.
It sums payments within the current request. Daily or monthly limits require
spending history supplied by the application.

## Common changes

These snippets replace or add conditions inside the same allow rule. Keep the other
checks when adding a new condition.

### Permit two shops

Add `backupShop` to `config.merchants`, then replace each shop condition as needed:

```cel
checkout.merchantIs(merchants.officeShop) || checkout.merchantIs(merchants.backupShop)
payment.payeeIs(merchants.officeShop) || payment.payeeIs(merchants.backupShop)
```

These conditions allow the seller and payee to match different catalog entries.
To require the same shop for both, use:

```cel
(checkout.merchantIs(merchants.officeShop) && payment.payeeIs(merchants.officeShop)) ||
(checkout.merchantIs(merchants.backupShop) && payment.payeeIs(merchants.backupShop))
```

### Bound a range or require an exact amount

```cel
payment.amountAtLeast('1.00', 'USD')
payment.amountAtMost('100.00', 'USD')
```

Use both conditions for USD 1–100, inclusive. For an exact charge use
`payment.amountEquals('49.99', 'USD')`. The protocol amount stays in integer minor
units; the helpers convert the quoted limit. Never write `7500` when you mean a
USD 75.00 helper limit: write `'75.00'` and `'USD'`.

### Restrict cart items

For the AP2 UCP checkout shape, add:

```cel
size(checkout.content.line_items) > 0
checkout.content.line_items.all(line, line.item.id in ['paper-a4', 'pens-blue'])
checkout.content.line_items.all(line, bigint.lte(line.quantity, '10'))
```

This permits the listed SKUs with at most ten units per line. Repeated SKUs are
checked separately on each line. Product restrictions come from these conditions;
`officeShop` identifies the merchant.
VI checkout JSON is integration-defined: use these paths only if your VI adapter
supplies this shape. The common merchant helper expects `content.merchant`.

### Require approval

Add an `approvals` block to the allow rule and define its named approvers under
`policy.approvers`, as shown in [the approval guide](https://github.com/exploit-org/verdict/blob/main/docs/approvals.md). Keep the amount,
shop and method checks. For a request with several actions, every applicable approval
requirement must be satisfied before the integration signs it.

### Approve a payment-only stage

Use `action.kind == 'payment'`, retain the payment identity/method/amount checks, and
omit the checkout condition because no checkout is present. The integration must
select `PaymentRequestMode.PAYMENTS` as part of the signing operation's configuration.
An incomplete paired request must fail validation.

## Grant future spending authority separately

A delegation policy checks the proposed permissions for future purchases.
The application enforces those permissions when later payments occur.
For open checkout/payment pairs, use a rule such as:

```yaml
allow:
  - id: bounded-delegation
    where:
      - "action.kind == 'delegation'"
      - "delegation.merchantsLimitedTo([merchants.officeShop])"
      - "delegation.payeesLimitedTo([merchants.officeShop])"
      - "delegation.methodsLimitedTo([methods.companyCard])"
      - "delegation.maxAmountAtMost('100.00', 'USD')"
      - >-
        delegation.hasOnlyConstraints([
          'checkout.allowed_merchants', 'checkout.line_items',
          'payment.reference', 'payment.allowed_payees',
          'payment.allowed_payment_instruments', 'payment.amount_range'
        ])
```

This is a **replacement policy fragment for a delegation-specific authority**. Keep
`fallback: DENY` and the identity catalogs. Remove the purchase example's
`!request.totalAtMost(...)` deny rule: delegations are unpriced and fail that check.

| Function                           | Question it answers                                                           |
|------------------------------------|-------------------------------------------------------------------------------|
| `merchantsLimitedTo([...])`        | Is there an explicit merchant restriction, with every candidate allowed here? |
| `payeesLimitedTo([...])`           | Are all fixed/allowed payment recipients within these definitions?            |
| `methodsLimitedTo([...])`          | Are all fixed/allowed payment methods within these definitions?               |
| `maxAmountAtMost('100.00', 'USD')` | Is there an explicit fixed amount or maximum no greater than USD 100?         |
| `hasOnlyConstraints([...])`        | Are all constraint types in this list?                                        |

Missing bounds return false. The lists must be nonempty. `hasOnlyConstraints`
checks the types of constraints present; use bound helpers to require specific limits.
To restrict products, also check the values inside `checkout.line_items`.
This fragment limits identities and per-payment amounts. The application handles
cumulative spending, recurrence, fulfillment, replay and delegation-chain verification.

## Java integration

Create an evaluator with the payment functions, compile the authority policy, and
build an intent from all mandate content belonging to the signing request:

```java
import java.util.List;
import org.exploit.verdict.PolicyEvaluator;
import org.exploit.verdict.intent.ap2.Ap2Intent;
import org.exploit.verdict.intent.payment.PaymentRequest;
import org.exploit.verdict.intent.payment.config.PaymentIntentConfig;
import org.exploit.verdict.intent.payment.function.PaymentFunctions;
import org.exploit.verdict.intent.payment.model.MandateInput;

var functions = new PaymentFunctions();
var evaluator = PolicyEvaluator.builder().library(functions, functions).build();
var config = PaymentIntentConfig.fromMap(authority.config());
var policy = evaluator.compileStrict(authority.policy(), PaymentRequest.policySchema());
var request = Ap2Intent.request(List.of(
        MandateInput.checkout(checkoutMandateJson, decodedCheckoutJson),
        MandateInput.of(paymentMandateJson)), config);
var result = evaluator.evaluate(policy, request);
```

`authority` is a loaded [authority document](https://github.com/exploit-org/verdict/blob/main/docs/authority/README.md).
`checkoutMandateJson`, `paymentMandateJson`, and `decodedCheckoutJson` are Jackson
`JsonNode` objects. Pass the trees from your application's JSON decoder directly.
`decodedCheckoutJson` contains the checkout associated with the checkout mandate.
The adapter establishes that association and handles all cryptography.
For Mastercard VI, use `McIntent.request` from
`org.exploit.verdict.intent.mcintent` with the same input and config types.

The default mode expects checkout/payment pairs. Include every mandate being signed;
Verdict groups pairs by protocol references and rejects orphaned or duplicate members.
For an open pair, use `MandateInput.openCheckout(openCheckoutJson, checkoutDisclosureHash)`
plus `MandateInput.of(openPaymentJson)`. The adapter supplies the actual disclosure
hash; Verdict compares the supplied references without computing or verifying hashes.

For a payment-only signing operation, select the mode explicitly:

```java
var request = Ap2Intent.request(
        List.of(MandateInput.of(paymentMandateJson)),
        PaymentRequestMode.PAYMENTS, config);
```

Import `org.exploit.verdict.intent.payment.constant.PaymentRequestMode`.
`CHECKOUTS` similarly accepts closed checkout mandates only. Standalone open mandates
are not supported by these modes.

`MandateInput` also works with Jackson deserialization. For example, a payment input
for `PAYMENTS` mode has this JSON representation:

```json
{
  "json": {
    "vct": "mandate.payment.1",
    "transaction_id": "order-1",
    "payee": {"id": "merchant-1", "name": "Office shop", "website": "https://office.example"},
    "payment_instrument": {"id": "card-1", "type": "card"},
    "payment_amount": {"currency": "USD", "amount": 7500}
  },
  "checkoutJson": null,
  "disclosureHash": null
}
```

Use `mapper.treeToValue(inputNode, MandateInput.class)` to construct the input.
For a closed checkout mandate, `checkoutJson` contains the decoded checkout object.
Both JSON fields are copied on construction and access, so changes to a caller's
tree do not change retained inputs or the evaluated request.

Enable `StreamReadFeature.STRICT_DUPLICATE_DETECTION` and
`DeserializationFeature.FAIL_ON_TRAILING_TOKENS` on the application's JSON decoder.
Enable `DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS` to preserve decimal values.
Duplicate keys and numeric precision already lost during decoding cannot be recovered
from a `JsonNode`.

Reuse the evaluator across authorities and compile once per authority revision.
Pass trusted additional data through `request.withContext(context)`; it is exposed
under the `context` root. Evaluate the returned request through the same API.
For multiple actions, rule matches and approval sources include an `action[i]/`
prefix. `request.actions().get(i).mandateIndices()` identifies their original inputs.
Retain `request.inputs()` and sign exactly the evaluated content after satisfying
any [approval requirements](https://github.com/exploit-org/verdict/blob/main/docs/approvals.md).

## Where to go next

- [Core API](https://github.com/exploit-org/verdict/blob/main/docs/core-policy-engine.md): compilation, evaluation and custom functions.
- [AP2 input format](https://tkeeper.org/docs/ai/agentic-payments/ap2) or [VI input format](https://tkeeper.org/docs/ai/agentic-payments/mastercard-verifiable-intent): protocol-specific reader behavior.
- [Troubleshooting](https://github.com/exploit-org/verdict/blob/main/docs/troubleshooting.md): missing catalog keys, library registration and failed conditions.


---

Source file: content/docs/digital-assets/README.md
Canonical: https://tkeeper.org/docs/digital-assets

# Digital Asset Authorities

Build all chains with `-Pkeeper.features=digital-assets -Pkeeper.platforms=ecc`, or select one chain by name. Each authority parses an unsigned transaction, evaluates its effects, and signs only after policy and approval checks pass.

| Chain | Authority | Key | Result from `/compose` | Guide |
| --- | --- | --- | --- | --- |
| Bitcoin | `bitcoin.transaction` | secp256k1 | Raw signature | [Bitcoin](https://tkeeper.org/docs/digital-assets/bitcoin) |
| EVM | `evm.transaction` | secp256k1 | Signed hex and hash | [EVM](https://tkeeper.org/docs/digital-assets/evm) |
| Tron | `tron.transaction` | secp256k1 | Signed JSON and hash | [Tron](https://tkeeper.org/docs/digital-assets/tron) |
| XRP | `xrp.transaction` | secp256k1 or Ed25519 | Signed hex and hash | [XRP](https://tkeeper.org/docs/digital-assets/xrp) |
| Solana | `solana.transaction` | Ed25519 | Signed Base64, completeness, and optional hash | [Solana](https://tkeeper.org/docs/digital-assets/solana) |

The caller constructs the unsigned transaction and handles fees, submission, and confirmation. For Bitcoin, Tron, XRP, or Solana signing with a `tweak`, construct the transaction from `central().getPublicKey(keyId, tweak)`; TKeeper checks that derived key against the transaction's owner or required signer. See [Composer](https://tkeeper.org/docs/signing-and-authorities/composer) for result behavior.


---

Source file: content/docs/digital-assets/bitcoin.md
Canonical: https://tkeeper.org/docs/digital-assets/bitcoin

# Bitcoin Authorities

Bitcoin authority support lives in the `digital-assets:bitcoin` module and currently requires the `ecc` platform.

Build example:

```bash
./gradlew shadowJar -Pkeeper.features=bitcoin -Pkeeper.platforms=ecc
```

A Bitcoin authority lets TKeeper parse unsigned transaction data, previous transactions, signing input, sighash settings, and policy effects before signing.

```java
var input = new UtxoInput(unsignedTransaction64, previousTransactions64, inputIndex);
var command = Command.of("btc-cold-storage-sweep", input);
var signature = client.signature().sign(Sign.of(bitcoinKeyId, command));
```

`unsignedTransaction64` and each previous transaction are Base64-encoded transaction bytes. The wallet attaches the returned signature to the selected input. Bitcoin has no registered composer; `/v2/keeper/compose` returns the raw `ThresholdSignature`.

Use Bitcoin authorities for:

- governed UTXO spending
- treasury withdrawals
- fee and output policy
- external risk verdicts before signing

## Enforcement boundary

Policy evaluation depends on the unsigned transaction, the selected input and sighash mode, and the previous transactions needed to understand input values. Missing or unclassifiable data must not be treated as a harmless unknown effect.

TKeeper does not select coins, construct change, choose fees, broadcast transactions, or track confirmations. The wallet or custody service must broadcast the exact approved transaction and ensure that its sighash choice covers the fields the policy assumes are fixed.

## Authority config

```yaml
type: bitcoin.transaction
config:
  network: MAINNET
```

Verdict 0.2 supports Bitcoin networks `MAINNET`, `TESTNET`, `REGTEST`, and `SIGNET`. TKeeper signs the selected artifact input with `SIGHASH_ALL`.

Effects:

- `utxo.spend`
- `utxo.output`
- `utxo.data`
- `utxo.fee`

Strict CEL roots:

- `protocol`, `network`, `asset`, `assetDecimals`
- `txId`, `wtxId`, `version`, `lockTime`
- `sighash`, `signing`
- `inputs`, `outputs`, `previousTransactions`
- `totalInput`, `totalOutput`, `fee`, `effects`

Intent validation rejects malformed or signed transactions, missing or duplicate previous transactions, missing outputs, coinbase inputs, unknown scripts, negative fees, invalid signing inputs, and unsafe or unknown sighash modes.

## Authority example: cold-storage sweep

This authority represents one logical action: sweep BTC into one cold-storage address. It allows sweeps up to 0.25 BTC directly and requires one treasury approval above 0.25 and up to 1 BTC. The fee is capped at 100,000 satoshis. A transaction with change, an additional output, another destination, or another sighash mode is denied.

```yaml
schemaVersion: verdict.authority/v1
id: btc-cold-storage-sweep
type: bitcoin.transaction
version: 1.0.0

metadata:
  title: BTC sweep to cold storage

config:
  network: MAINNET

policy:
  id: btc-cold-storage-sweep
  fallback: DENY
  approvers:
    treasury-reviewer:
      algorithm: ED25519
      publicKey64: "BASE64_ENCODED_ED25519_PUBLIC_KEY"
  variables:
    coldStorageAddress: "bc1qreplacewiththeapprovedaddress"
    automaticLimit: "25000000"
    reviewedLimit: "100000000"
    maximumFee: "100000"
  allow:
    - id: automatic-sweep
      where:
        - "protocol == 'BTC'"
        - "sighash.all && !sighash.anyoneCanPay"
        - "effect.onlyTypes(effects, ['utxo.spend', 'utxo.output', 'utxo.fee'])"
        - "effect.one(effects, 'utxo.output')"
        - "effect.one(effects, 'utxo.fee')"
        - "effect.any(effects, 'utxo.output', {'address': coldStorageAddress})"
        - "bigint.lte(effect.amount(effects, 'utxo.output'), automaticLimit)"
        - "bigint.lte(effect.amount(effects, 'utxo.fee'), maximumFee)"
    - id: reviewed-sweep
      where:
        - "protocol == 'BTC'"
        - "sighash.all && !sighash.anyoneCanPay"
        - "effect.onlyTypes(effects, ['utxo.spend', 'utxo.output', 'utxo.fee'])"
        - "effect.one(effects, 'utxo.output')"
        - "effect.one(effects, 'utxo.fee')"
        - "effect.any(effects, 'utxo.output', {'address': coldStorageAddress})"
        - "bigint.gt(effect.amount(effects, 'utxo.output'), automaticLimit)"
        - "bigint.lte(effect.amount(effects, 'utxo.output'), reviewedLimit)"
        - "bigint.lte(effect.amount(effects, 'utxo.fee'), maximumFee)"
      approvals:
        threshold: 1
        approvers: [treasury-reviewer]
  deny: []
```

Replace the address, limits, and `publicKey64` with trusted production values. A payout, consolidation with change, or sweep to another vault should use another authority id and document.

See [Authorities](https://tkeeper.org/docs/signing-and-authorities/authorities) for the document and policy schema, [CEL Functions](https://tkeeper.org/docs/signing-and-authorities/cel-functions) for policy helpers, and [Composer](https://tkeeper.org/docs/signing-and-authorities/composer) for response behavior.


---

Source file: content/docs/digital-assets/evm.md
Canonical: https://tkeeper.org/docs/digital-assets/evm

# EVM Authorities

EVM authority support lives in the `digital-assets:evm` module and currently requires the `ecc` platform.

Build example:

```bash
./gradlew shadowJar -Pkeeper.features=evm -Pkeeper.platforms=ecc
```

An EVM authority lets TKeeper parse an unsigned serialized transaction, decode configured contract calls, expose normalized effects to policy, and sign after the resulting allow decision and any approval requirements are satisfied.

Use EVM authorities for:

- treasury transactions
- spender approvals
- contract-specific governed actions
- policy inputs from AML, KYT, fraud, or business systems

## Enforcement boundary

The authority should pin the intended chain and describe every contract call the policy is allowed to approve. Native intent handling fails closed when TKeeper cannot map a call to a known effect.

TKeeper signs the approved transaction. The surrounding wallet or custody service remains responsible for transaction construction, nonce and gas strategy, broadcast, replacement, receipt tracking, and settlement state. It must broadcast exactly the transaction that policy approved.

External risk verdicts must be bound to the same transaction intent; a verdict for an address or amount outside the signed transaction is only advisory metadata.

## Compose a signed transaction

Pass the serialized unsigned transaction bytes as Base64 in `UnsignedEvmTransaction.message64` to `/v2/keeper/compose`. The EVM composer applies the Keeper signature to that transaction and returns `SignedEvmTransaction` with `rawTransaction` (hex for `eth_sendRawTransaction`), `transactionHash`, `generation`, and `imposters`. The regular sign endpoint still returns the signature alone.

```java
var command = Command.of(authorityId, new UnsignedEvmTransaction(message64));
var signed = client.signature().compose(Sign.of(keyId, command), SignedEvmTransaction.class);
var rawTransaction = signed.rawTransaction();
```

The result covers legacy and typed transactions 1–4. For type 3 blob transactions, `rawTransaction` contains the signed transaction envelope; broadcasting also requires the blob sidecar.

## Authority config

```yaml
type: evm.transaction
config:
  chainId: 1
  contracts:
    - standard: erc20
      address: "0x1111111111111111111111111111111111111111"
```

Custom contract calls declare their ABI signature, argument names, and effects:

```yaml
config:
  chainId: 1
  contracts:
    - name: vault
      address: "0x4444444444444444444444444444444444444444"
      functions:
        - signature: "withdraw(address,uint256)"
          arguments: [to, amount]
          effects:
            - type: vault.withdraw
              fields:
                vault: "$transaction.to"
                to: "$to"
                amount: "$amount"
```

`chainId` comes from trusted config. Typed transactions must match it; legacy unsigned transactions carry no chain id.

Built-in effects:

- `native.transfer`
- `erc20.transfer`
- `erc20.approval`
- `erc20.transferFrom`

Strict CEL roots:

- `type`, `chainId`, `nonce`
- `gasPrice`, `gasLimit`, `maxPriorityFeePerGas`, `maxFeePerGas`
- `to`, `value`, `data`, `selector`
- `transaction`, `call`, `effects`

Intent validation rejects unsigned-data violations, chain mismatch, unlisted contracts or functions, calldata decoding failures, and effects that the trusted config cannot describe.

## Authority example: treasury USDC transfer

This authority represents one logical action: transfer mainnet USDC from the treasury to one operating wallet. It allows transfers up to 100 USDC directly and requires one treasury approval above 100 and up to 1,000 USDC. Other recipients, effects, contracts, chains, and larger amounts fall through to `DENY`.

```yaml
schemaVersion: verdict.authority/v1
id: evm-usdc-operating-transfer
type: evm.transaction
version: 1.0.0

metadata:
  title: Treasury USDC transfer to operating wallet

config:
  chainId: 1
  contracts:
    - standard: erc20
      address: "0x1111111111111111111111111111111111111111"

policy:
  id: evm-usdc-operating-transfer
  fallback: DENY
  approvers:
    treasury-reviewer:
      algorithm: ED25519
      publicKey64: "BASE64_ENCODED_ED25519_PUBLIC_KEY"
  variables:
    recipientAddress: "0x2222222222222222222222222222222222222222"
    automaticLimit: "100000000"
    reviewedLimit: "1000000000"
  allow:
    - id: automatic-transfer
      where:
        - "chainId == 1"
        - "effect.onlyTypes(effects, ['erc20.transfer'])"
        - "effect.one(effects, 'erc20.transfer')"
        - "effect.any(effects, 'erc20.transfer', {'to': recipientAddress})"
        - "bigint.lte(effect.amount(effects, 'erc20.transfer'), automaticLimit)"
    - id: reviewed-transfer
      where:
        - "chainId == 1"
        - "effect.onlyTypes(effects, ['erc20.transfer'])"
        - "effect.one(effects, 'erc20.transfer')"
        - "effect.any(effects, 'erc20.transfer', {'to': recipientAddress})"
        - "bigint.gt(effect.amount(effects, 'erc20.transfer'), automaticLimit)"
        - "bigint.lte(effect.amount(effects, 'erc20.transfer'), reviewedLimit)"
      approvals:
        threshold: 1
        approvers: [treasury-reviewer]
  deny: []
```

Replace the token contract, recipient, limits, and `publicKey64` with trusted production values. A transfer to another wallet should use another authority id and document.

See [Authorities](https://tkeeper.org/docs/signing-and-authorities/authorities) for the document and policy schema, [CEL Functions](https://tkeeper.org/docs/signing-and-authorities/cel-functions) for policy helpers, and [Composer](https://tkeeper.org/docs/signing-and-authorities/composer) for response behavior.


---

Source file: content/docs/digital-assets/tron.md
Canonical: https://tkeeper.org/docs/digital-assets/tron

# Tron Authorities

`tron.transaction` accepts unsigned transaction JSON. It requires a secp256k1 key and checks that the transaction owner matches the signing key. Build with `-Pkeeper.features=tron -Pkeeper.platforms=ecc`.

## Example: one recipient, at most 100 TRX

Tron amounts and `feeLimit` use [sun](https://developers.tron.network/docs/token-standards-trx); 100 TRX is 100,000,000 sun. This [authority](https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/src/testFixtures/resources/authorities/tron/native-transfer.yaml) limits the recipient, amount, fee limit, and effect type:

```yaml
schemaVersion: verdict.authority/v1
id: test:tron/native-transfer
type: tron.transaction
version: 1.0.0
config: {}
policy:
  id: tron-native-transfer
  fallback: DENY
  variables:
    recipient: "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb"
  allow:
    - id: small-transfer
      where:
        - "feeLimit <= 30000000"
        - "effect.onlyTypes(effects, ['native.transfer'])"
        - "effect.one(effects, 'native.transfer')"
        - "effect.all(effects, 'native.transfer', {'asset': 'trx', 'to': recipient})"
        - "bigint.gt(effect.amount(effects, 'native.transfer'), '0')"
        - "bigint.lte(effect.amount(effects, 'native.transfer'), '100000000')"
```

Build the unsigned JSON with the key's owner address, then compose:

```java
var command = Command.of("test:tron/native-transfer",
        new UnsignedTronTransaction(unsignedTronJson));
var signed = client.signature().compose(
        Sign.of(tronKeyId, command), SignedTronTransaction.class);
String transactionJson = signed.rawTransaction();
String transactionHash = signed.transactionHash();
```

The composer keeps the approved transaction body and adds the signature. The broadcaster submits `transactionJson`. With a `tweak`, derive the owner address from `central().getPublicKey(tronKeyId, tweak)`. [Composer](https://tkeeper.org/docs/signing-and-authorities/composer) describes the common response contract.


---

Source file: content/docs/digital-assets/xrp.md
Canonical: https://tkeeper.org/docs/digital-assets/xrp

# XRP Authorities

`xrp.transaction` accepts an unsigned transaction in hex. It supports secp256k1 and Ed25519 keys. TKeeper checks both `Account` and `SigningPubKey` against the signing key. Build with `-Pkeeper.features=xrp -Pkeeper.platforms=ecc`.

## Example: bounded native payment

This [authority](https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/src/testFixtures/resources/authorities/xrp/native-payment.yaml) permits one recipient, at most 100 [drops](https://xrpl.org/docs/introduction/transactions-and-requests), a fee of at most 12 drops, and a ledger expiry:

```yaml
schemaVersion: verdict.authority/v1
id: test:xrp/native-payment
type: xrp.transaction
version: 1.0.0
config: {}
policy:
  id: xrp-native-payment
  fallback: DENY
  variables:
    recipient: "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"
  allow:
    - id: small-payment
      where:
        - "type == 'payment'"
        - "lastLedgerSequence != null"
        - "bigint.lte(fee, '12')"
        - "effect.onlyTypes(effects, ['native.transfer'])"
        - "effect.one(effects, 'native.transfer')"
        - "effect.all(effects, 'native.transfer', {'asset': 'xrp', 'to': recipient, 'destinationTag': 0})"
        - "bigint.gt(effect.amount(effects, 'native.transfer'), '0')"
        - "bigint.lte(effect.amount(effects, 'native.transfer'), '100')"
```

Set `Account` and `SigningPubKey` from the TKeeper public key before serializing the unsigned transaction:

```java
var command = Command.of("test:xrp/native-payment",
        new UnsignedXrpTransaction(unsignedXrpHex));
var signed = client.signature().compose(
        Sign.of(xrpKeyId, command), SignedXrpTransaction.class);
String transactionHex = signed.rawTransaction();
String transactionHash = signed.transactionHash();
```

With a `tweak`, derive those fields from `central().getPublicKey(xrpKeyId, tweak)`. The caller submits the signed hex and tracks its ledger result. [Composer](https://tkeeper.org/docs/signing-and-authorities/composer) describes the common response contract.


---

Source file: content/docs/digital-assets/solana.md
Canonical: https://tkeeper.org/docs/digital-assets/solana

# Solana Authorities

`solana.transaction` accepts an unsigned transaction in Base64 and requires an Ed25519 key. TKeeper checks that the signing key is a required signer of the message. Build with `-Pkeeper.features=solana -Pkeeper.platforms=ecc`.

## Example: one native transfer

This [authority](https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/src/testFixtures/resources/authorities/solana/native-transfer.yaml) permits one required signer and a transfer of at most 100 [lamports](https://solana.com/docs/references/terminology) to one recipient:

```yaml
schemaVersion: verdict.authority/v1
id: test:solana/native-transfer
type: solana.transaction
version: 1.0.0
config: {}
policy:
  id: solana-native-transfer
  fallback: DENY
  variables:
    recipient: "8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR"
  allow:
    - id: small-transfer
      where:
        - "requiredSignatures == 1"
        - "effect.onlyTypes(effects, ['native.transfer'])"
        - "effect.one(effects, 'native.transfer')"
        - "effect.all(effects, 'native.transfer', {'asset': 'sol', 'to': recipient})"
        - "bigint.gt(effect.amount(effects, 'native.transfer'), '0')"
        - "bigint.lte(effect.amount(effects, 'native.transfer'), '100')"
```

Build the message with the TKeeper key as a required signer, then compose:

```java
var command = Command.of("test:solana/native-transfer",
        new UnsignedSolanaTransaction(unsignedSolana64));
var signed = client.signature().compose(
        Sign.of(solanaKeyId, command), SignedSolanaTransaction.class);
if (!signed.complete()) throw new IllegalStateException("Missing signatures");
String transaction64 = signed.rawTransaction();
String transactionHash = signed.transactionHash();
```

For policies that allow multiple signers, the composer can return a partial transaction with `complete: false` and no hash. With a `tweak`, build the message using `central().getPublicKey(solanaKeyId, tweak)`. The caller submits the complete transaction. [Composer](https://tkeeper.org/docs/signing-and-authorities/composer) describes the common response contract.


---

Source file: content/docs/pki/README.md
Canonical: https://tkeeper.org/docs/pki

# PKI Authorities

Use [X.509 authorities](https://tkeeper.org/docs/pki/x509) to govern certificate fields and signing. [For PKI](https://tkeeper.org/docs/use-cases/for-pki) shows the issuance flow; [Signing and Authorities](https://tkeeper.org/docs/signing-and-authorities) explains the shared policy and approval checks.


---

Source file: content/docs/pki/x509.md
Canonical: https://tkeeper.org/docs/pki/x509

# X.509 Authorities

X.509 authority support lives in the `authority-x509` feature and currently requires the `ecc` platform.

Build example:

```bash
./gradlew shadowJar -Pkeeper.features=authority-x509 -Pkeeper.platforms=ecc
```

An X.509 authority lets TKeeper parse a DER-encoded TBS certificate, expose certificate fields to policy, and sign only approved certificate requests.

Use X.509 authorities for:

- internal CA operations
- workload identity certificates
- service or agent certificates
- certificate issuance requiring four-eye approval

## Enforcement boundary

TKeeper signs the DER-encoded TBS certificate after policy evaluation. It does not replace the rest of the CA:

- requester authentication and enrollment authorization
- proof-of-possession checks
- serial-number allocation
- final certificate assembly and publication
- certificate transparency, revocation, renewal, or inventory

The CA pipeline must submit and publish the same certificate content that policy approved. Relying parties must still validate the completed certificate chain and constraints.

Strict CEL roots:

- `encodedDerHex`, `encodedSha256`
- `version`, `serialNumber`, `signature`
- `issuer`, `subject`, `validity`
- `subjectPublicKeyInfo`
- `issuerUniqueID`, `subjectUniqueID`
- `extensions`

The intent accepts a DER-encoded RFC 5280 `TBSCertificate`. Malformed DER fails intent validation before policy evaluation.

## Authority example: workload server certificate

This authority represents one logical action: issue a server leaf certificate for `api.internal.example`. It is intended for attachment to a P-256 issuing key, limits validity to 24 hours, and requires one PKI operator approval.

```yaml
schemaVersion: verdict.authority/v1
id: api-internal-workload-certificate
type: x509.tbs-certificate
version: 1.0.0

metadata:
  title: API workload server certificate

policy:
  id: api-internal-workload-certificate
  fallback: DENY
  approvers:
    pki-operator:
      algorithm: ED25519
      publicKey64: "BASE64_ENCODED_ED25519_PUBLIC_KEY"
  variables:
    expectedIssuer: "CN=Internal Issuing CA,O=Example Corp"
    permittedDnsName: "api.internal.example"
    maximumValiditySeconds: 86400
  allow:
    - id: reviewed-workload-leaf
      where:
        - "version == 3"
        - "signature.oid == '1.2.840.10045.4.3.2'"
        - "issuer.rfc2253 == expectedIssuer"
        - "subject.commonName == permittedDnsName"
        - "time.before(validity.notBefore, validity.notAfter)"
        - "time.durationSeconds(validity.notBefore, validity.notAfter) <= maximumValiditySeconds"
        - "extensions.basicConstraints.present && extensions.basicConstraints.critical"
        - "!extensions.basicConstraints.cA"
        - "extensions.keyUsage.present && extensions.keyUsage.critical"
        - "extensions.keyUsage.digitalSignature"
        - "lists.hasOnly(extensions.keyUsage.names, ['digitalSignature'])"
        - "extensions.extKeyUsage.present && extensions.extKeyUsage.serverAuth"
        - "lists.hasOnly(extensions.extKeyUsage.names, ['serverAuth'])"
        - "extensions.subjectAltName.present"
        - "lists.size(extensions.subjectAltName.all) == 1"
        - "lists.contains(extensions.subjectAltName.dnsNames, permittedDnsName)"
      approvals:
        threshold: 1
        approvers: [pki-operator]
  deny: []
```

Replace the issuer, DNS name, and `publicKey64` with trusted production values. Another DNS identity or certificate profile should use another authority id and document. The rule governs the TBS certificate fields and approval requirement. The CA pipeline must still authenticate the requester, authorize control of the requested name, validate the submitted public key, and verify proof of possession before calling TKeeper.

See [Authorities](https://tkeeper.org/docs/signing-and-authorities/authorities) for the document and policy schema and [CEL Functions](https://tkeeper.org/docs/signing-and-authorities/cel-functions) for policy helpers.


---

Source file: content/docs/crypto-platforms/README.md
Canonical: https://tkeeper.org/docs/crypto-platforms

# Crypto Platforms

Platforms are build-time modules. They provide key algorithms and protocol implementations. Features provide product surfaces that may depend on those platforms.

Read:

- [Platforms](https://tkeeper.org/docs/crypto-platforms/platforms)
- [ECC](https://tkeeper.org/docs/crypto-platforms/ecc)
- [PQC ML-DSA](https://tkeeper.org/docs/crypto-platforms/pqc-mldsa)
- [ECIES](https://tkeeper.org/docs/crypto-platforms/ecies)

## Quick choice

| Need | Platform |
| --- | --- |
| EVM, Bitcoin, X.509, ECIES, ECDSA, FROST | `ecc` |
| ML-DSA-44/65/87 identities | `pqc` |
| Everything | `all` |

A deployable artifact needs at least one platform.


---

Source file: content/docs/crypto-platforms/platforms.md
Canonical: https://tkeeper.org/docs/crypto-platforms/platforms

# Platforms

Platforms provide algorithm implementations. They are selected separately from features.

| Platform value | Module | Provides |
| --- | --- | --- |
| `ecc` | `:platform-ecc` | `SECP256K1`, `P256`, `ED25519`, ECDSA, FROST, BIP-340/Taproot support, ECIES side support |
| `pqc` | `:platform-pqc` | `MLDSA44`, `MLDSA65`, `MLDSA87`, ML-DSA DKG, signing, import, and promotion support |

## Build examples

All platforms:

```bash
./gradlew shadowJar -Pkeeper.platforms=all
```

ECC only:

```bash
./gradlew shadowJar -Pkeeper.platforms=ecc
```

PQC only:

```bash
./gradlew shadowJar -Pkeeper.platforms=pqc
```

## Feature dependencies

Some features require a platform:

| Feature | Required platform |
| --- | --- |
| `evm` | `ecc` |
| `bitcoin` | `ecc` |
| `tron` | `ecc` |
| `xrp` | `ecc` |
| `solana` | `ecc` |
| `authority-x509` | `ecc` |
| `ecies` | `ecc` |

If a feature needs a platform, include both. The build should fail early when the graph is incomplete.

## Operational note

Platform selection is not runtime configuration. If an algorithm provider is missing, rebuild the artifact.


---

Source file: content/docs/crypto-platforms/ecc.md
Canonical: https://tkeeper.org/docs/crypto-platforms/ecc

# ECC

The `ecc` platform provides:

- `SECP256K1`
- `P256`
- `ED25519`
- ECDSA signing for Secp256k1, P-256
- FROST signing for all curves
- Schnorr, BIP-340, and Taproot signing for Secp256k1
- deterministic ECC key derivation
- ECIES support for compatible curves

Features that currently require `ecc`:

- Bitcoin, EVM, Tron, XRP, and Solana (`digital-assets`)
- AP2 and MC VI (`agentic-payments`)
- `authority-x509`
- `ecies`

Build example:

```bash
./gradlew shadowJar -Pkeeper.features=evm,ecies -Pkeeper.platforms=ecc
```


---

Source file: content/docs/crypto-platforms/pqc-mldsa.md
Canonical: https://tkeeper.org/docs/crypto-platforms/pqc-mldsa

# PQC ML-DSA

The `pqc` platform provides:

- `MLDSA44`
- `MLDSA65`
- `MLDSA87`
- mono ML-DSA key generation and signing
- threshold ML-DSA DKG
- threshold ML-DSA signing
- trusted-dealer import
- quorum promotion

Build example:

```bash
./gradlew shadowJar -Pkeeper.platforms=pqc
```

## Authority model

ML-DSA changes the signature algorithm, not the authority model.

The same identity rules apply:

- attach authorities to the key identity
- materialize the command into an understood intent
- evaluate policy
- produce proof only after approval

Use `MLDSA` as the signature scheme for ML-DSA algorithms.

## Threshold protocol

Threshold signing follows the three-round construction in
[Efficient Threshold ML-DSA](https://inria.hal.science/hal-05442192v1/document):
parties commit to their sampled value, reveal it after all commitments are
fixed, and produce per-party rejection-sampled responses for a challenge bound
to the aggregate commitment and message. The combiner applies the ML-DSA norm
and hint bounds and verifies the final standard ML-DSA signature before release.

TKeeper additionally binds each attempt to an exact configured signer set, the
stored aggregate public key, materialized message, and one-shot session state.
The adversarial regressions and their limits are listed in
[Security Assurance](https://tkeeper.org/docs/security-model/security-assurance).

## Signing availability

Threshold ML-DSA signing can abort during rejection sampling even when peers are healthy. TKeeper retries with fresh session state up to:

```text
keeper.session.mldsa.max-rounds
```

The default is `12`.

If the cap is exhausted, TKeeper returns:

```text
SESSION_MAX_ROUNDS_EXCEEDED
```

Treat this as an availability outcome first. It is not automatic evidence that a peer is corrupt.

## Latency planning

Increasing `keeper.session.mldsa.max-rounds` increases the chance of success but also increases worst-case latency and resource use.

For production:

- keep the cap bounded
- set end-to-end request deadlines
- monitor retry counts and latency
- alert on repeated exhaustion

## Refresh and rotate

ML-DSA refresh advances the generation while carrying each peer's existing share and public key forward unchanged. It does not replace shares or refresh cryptographic material.

Use rotate or a new DKG when new ML-DSA material is required.

## Import and promotion

Trusted-dealer import and quorum promotion must store the aggregate ML-DSA public key as platform side state. Without that side state, later public-key checks and threshold protocols cannot prove the same key identity state.


---

Source file: content/docs/crypto-platforms/ecies.md
Canonical: https://tkeeper.org/docs/crypto-platforms/ecies

# ECIES

ECIES lives in `:features:ecies`.

It uses an ElGamal-style KEM over the key curve plus an AEAD payload cipher.

Encryption uses the public key, so it does not need peer participation in either quorum mode.

In `mono` mode, decrypt is local. TKeeper reads the active private key material, applies the optional tweak, unwraps the KEM secret, and decrypts the payload.

In `threshold` mode, decrypt needs a quorum. Each peer returns a partial decrypt with a DLEQ proof. The coordinator verifies each proof against the ciphertext point, the peer public share, and the partial decrypt before combining the plaintext.

In threshold mode the private key is never reconstructed.

Build with it:

```bash
./gradlew shadowJar -Pkeeper.features=ecies -Pkeeper.platforms=ecc
```

Required permissions:

```text
tkeeper.key.{keyId}.encrypt
tkeeper.key.{keyId}.decrypt
```

Encrypt:

```http
POST /v1/keeper/ecies/encrypt
```

Body:

```json
{
  "keyId": "ecies-key",
  "algorithm": "AES_GCM",
  "plaintext64": "aGVsbG8=",
  "tweak": "optional"
}
```

Response:

```json
{
  "ciphertext64": "...",
  "generation": 1
}
```

Decrypt:

```http
POST /v1/keeper/ecies/decrypt
```

Body:

```json
{
  "keyId": "ecies-key",
  "algorithm": "AES_GCM",
  "generation": 1,
  "ciphertext64": "...",
  "tweak": "optional",
  "approvals": {
    "keeperId": 1,
    "nonce": "unique-nonce",
    "timestamp": 1760000000000,
    "proofs": []
  }
}
```

Response:

```json
{
  "plaintext64": "aGVsbG8=",
  "imposters": []
}
```

Algorithms:

```text
AES_GCM
CHACHA20_POLY1305
```

Supported curves:

```text
SECP256K1
P256
```

Decrypt requests can carry four eye approvals. The approval hash binds the decrypt request fields, including key id, algorithm, ciphertext, generation, tweak, nonce, and timestamp.

`imposters` contains peers that returned invalid partial decrypt proofs. It is only meaningful in threshold mode. Mono decrypt returns an empty list.

If quorum is still honest, threshold decrypt can succeed and report the bad peers.

## Common problems

### ECIES endpoints are missing

Rebuild with `:features:ecies`.

### `INVALID_CIPHERTEXT`

The ciphertext is malformed, from another key, or from another tweak/generation.

### `NOT_ENOUGH_HONEST_CLIENTS`

Too many peers were unavailable or returned invalid partial decrypt proofs.


---

Source file: content/docs/api-reference/README.md
Canonical: https://tkeeper.org/docs/api-reference

# API Reference

Read:

- [OpenAPI](https://tkeeper.org/docs/api-reference/openapi)
- [Java SDK](https://tkeeper.org/docs/api-reference/java-sdk)
- [Dry Run Policy Evaluation](https://tkeeper.org/docs/signing-and-authorities/dry-run)
- [Errors](https://tkeeper.org/docs/api-reference/errors)
- [Permissions](https://tkeeper.org/docs/api-reference/permissions)

The source of truth for request and response models is [OpenAPI reference](https://tkeeper.org/docs/api-reference/openapi).


---

Source file: content/docs/api-reference/openapi.md
Canonical: https://tkeeper.org/docs/api-reference/openapi

# OpenAPI

The OpenAPI file is the source of truth:

- [OpenAPI specification](https://tkeeper.org/openapi.yaml)

It describes:

- routes
- request bodies
- response bodies
- error responses
- authentication headers
- permission templates
- algorithm and scheme enums

If generated SDK helpers disagree with OpenAPI, treat OpenAPI as the current contract and update the SDK.


---

Source file: content/docs/api-reference/sdk.md
Canonical: https://tkeeper.org/docs/api-reference/java-sdk

# Java SDK

The Java SDK provides typed modules for the public TKeeper API:

- [SDK README](https://github.com/tkeeper-org/tkeeper/blob/main/sdk/README.md)
- Maven coordinate: `org.exploit:tkeeper-sdk:2.5.0`
- Java toolchain: 17 or newer

The SDK follows the same module boundaries as the API: system, DKG, signing, storage/import, quorum promotion, ECIES, dry run, compliance, expiration, audit, integrity, consistency, destroy, and control-plane reads.

The wire contract remains [OpenAPI reference](https://tkeeper.org/docs/api-reference/openapi). Treat generated or handwritten SDK helpers as convenience code, not a competing source of truth.

## Integration rules

- Use `JwtTokenAuth` in production and scope its permissions to the required identities and operations.
- Close `TKeeperClient` to release its HTTP resources.
- Pass `KeySetAuthorities` explicitly when creating governed identities; convenience constructors without authorities select `arbitrary` raw signing.
- Build signing requests with `AuthorityCommand`. Verify converts it to a `VerificationCommand`, which retains only the material `type` and `artifact`. Verification may use any supported material type, including typed or arbitrary material, without requiring that type or payload to belong to the key's current authority manifest.
- Use `dryRun().emulate(...)` to preview an authority-policy decision; see [Dry Run Policy Evaluation](https://tkeeper.org/docs/signing-and-authorities/dry-run).
- Catch `TKeeperException` and branch on `ErrorType`, not diagnostic detail text.
- Do not retry authorization or policy denials as availability failures.

## Compose results

`signature().compose(Sign.of(keyId, command), ResultType.class)` runs the same authority, policy, approval, and signing checks as `signature().sign(...)`. It then assembles a protocol result when the command type has a composer. See [Composer](https://tkeeper.org/docs/signing-and-authorities/composer) for the endpoint and fallback behavior.

| Command type | Result type |
| --- | --- |
| `ap2.mandate`, `mcintent.mandate` | `PaymentCredential` |
| `evm.transaction` | `SignedEvmTransaction` |
| `tron.transaction` | `SignedTronTransaction` |
| `xrp.transaction` | `SignedXrpTransaction` |
| `solana.transaction` | `SignedSolanaTransaction` |

For other types, compose returns `ThresholdSignature`, the same raw signature result as sign. To inspect an unknown result, use `signature().compose(request)` for JSON. The signed Solana result can be partial; check `complete` before broadcast. See [agentic payments](https://tkeeper.org/docs/ai/agentic-payments) and [digital assets](https://tkeeper.org/docs/digital-assets) for examples.


---

Source file: content/docs/api-reference/errors.md
Canonical: https://tkeeper.org/docs/api-reference/errors

# Errors

TKeeper fails closed. If a protected operation cannot pass auth, policy, audit, lifecycle, or quorum checks, no proof is produced.

## Error response shape

Error responses include:

```json
{
  "error": "ACCESS_DENIED",
  "details": "...",
  "imposters": [],
  "dead": [],
  "approvals": []
}
```

Fields:

| Field | Meaning |
| --- | --- |
| `error` | stable error enum |
| `details` | optional diagnostic detail |
| `imposters` | peers identified as bad where the protocol can identify them |
| `dead` | peers that were unavailable or failed to respond where tracked |
| `approvals` | approval groups required to resubmit an operation; omitted for ordinary errors |

`imposters` is not guaranteed for every failure. Threshold ML-DSA can abort normally during rejection sampling without identifying an imposter.

## Common outcomes

| Error | Usually means | First check |
| --- | --- | --- |
| `UNAUTHENTICATED` | no accepted client identity | token/header/JWKS |
| `ACCESS_DENIED` | client lacks required permission | permission claim and negative grants |
| `APPROVAL_REQUIRED` | an authority decision requires external proofs | required `approvals` groups, then [construct a fresh approval envelope and hash](https://tkeeper.org/docs/security-model/four-eye-control#building-hashforsigning) |
| `KEEPER_SEALED` | node is sealed | unseal state |
| `NOT_COORDINATOR` | request sent to non-coordinator | coordinator config |
| `INVALID_AUTHORITY` | invalid authority list, id, OCI reference, document, or policy | key authority configuration and artifact digest |
| `AUTHORITY_VIOLATION` | command authority is not attached to the key | key authorities and command `authorityId` |
| `INVALID_AUTHORITY_ARTIFACT` | command cannot be decoded or feature missing | authority type and build features |
| `POLICY_VIOLATION` | policy denied the intent | materialized intent and matched rules |
| `AUDIT_NOT_AVAILABLE` | no audit sink is usable | audit sink config |
| `AUDIT_FAILED` | audit write failed | sink health and timeout |
| `SESSION_MAX_ROUNDS_EXCEEDED` | retry cap exhausted | peer health, request deadline, ML-DSA retries |

For the complete enum, see [OpenAPI reference](https://tkeeper.org/docs/api-reference/openapi).

## Handling guidance

- Do not retry permission or policy denials blindly.
- Retry availability errors only with deadlines.
- Treat `imposters` as security telemetry.
- Treat empty `imposters` as inconclusive, not proof that no peer misbehaved.
- Log `details`, but do not build business logic on unstable detail text.


---

Source file: content/docs/api-reference/permissions.md
Canonical: https://tkeeper.org/docs/api-reference/permissions

# Permissions

Permissions are explicit strings. Most key permissions are scoped by key id.

## Key permissions

```text
tkeeper.key.{keyId}.public
tkeeper.key.{keyId}.sign
tkeeper.key.{keyId}.verify
tkeeper.key.{keyId}.encrypt
tkeeper.key.{keyId}.decrypt
tkeeper.key.{keyId}.destroy
```

Wildcards are supported:

```text
tkeeper.key.*.sign
tkeeper.key.*.*
```

Negative permissions remove broad grants:

```text
tkeeper.key.*.sign
-tkeeper.key.hot-wallet.sign
```

## System and lifecycle permissions

| Permission | Allows |
| --- | --- |
| `tkeeper.system.init` | initialize keeper |
| `tkeeper.system.unseal` | unseal keeper |
| `tkeeper.system.seal` | seal keeper |
| `tkeeper.dkg.create` | create key identity |
| `tkeeper.dkg.rotate` | rotate key identity |
| `tkeeper.dkg.refresh` | refresh key identity |
| `tkeeper.storage.write` | trusted-dealer import |
| `tkeeper.quorum.promote` | promote mono to threshold |
| `tkeeper.consistency.fix` | repair threshold state |
| `tkeeper.recover` | run share-recovery protocols |
| `tkeeper.expired.view` | read key-expiration indexes |
| `tkeeper.compliance.inventory` | read asset inventory |
| `tkeeper.integrity.rotate` | rotate audit integrity key |
| `tkeeper.audit.log.verify` | verify signed audit log lines |
| `tkeeper.emulate` | evaluate an authority policy without executing its command |
| `tkeeper.control.system` | read control-plane system state |
| `tkeeper.control.sinks` | read control-plane audit sink state |

## Assignment guidance

- Give signing services only the key identities they need.
- Keep lifecycle permissions out of normal signing clients.
- Keep import permissions highly restricted.
- Restrict consistency repair, share recovery, and quorum promotion to recovery operators.
- Treat control-plane read permissions as sensitive operational metadata.
- Treat dry-run access as sensitive policy metadata; `tkeeper.emulate` is not scoped to a key id.
- Keep destroy permissions separate from rotate and refresh.
- Use negative permissions to carve high-risk identities out of broad grants.
- Review wildcard grants before production.

See [Authentication and Authorization](https://tkeeper.org/docs/security-model/authentication-and-authorization) for JWT claims and matching behavior.


---

Source file: content/docs/operations/README.md
Canonical: https://tkeeper.org/docs/operations

# Operations

Read:

- [Monitoring](https://tkeeper.org/docs/operations/monitoring)
- [Troubleshooting](https://tkeeper.org/docs/operations/troubleshooting)
- [Integration Tests](https://tkeeper.org/docs/operations/integration-tests)
- [Failure Injection](https://tkeeper.org/docs/operations/failure-injection)
- [Error Tracking](https://tkeeper.org/docs/operations/error-tracking)

Operationally, watch the authority path:

```text
auth -> permission -> authority -> policy -> audit -> quorum/session -> proof
```

If any stage fails, TKeeper should fail closed: no proof is produced.

Operational dashboards should distinguish an intentional denial from loss of service. See [Monitoring](https://tkeeper.org/docs/operations/monitoring) for the signals and [Troubleshooting](https://tkeeper.org/docs/operations/troubleshooting) for stage-by-stage diagnosis.


---

Source file: content/docs/operations/monitoring.md
Canonical: https://tkeeper.org/docs/operations/monitoring

# Monitoring

Monitor the authority path, not only process uptime. A ready node can still be unable to authorize an action because policy, audit, platform, or quorum state is failing.

Use service logs, API outcomes, signed audit events, and infrastructure telemetry together. Do not treat audit logs as a high-volume metrics transport or ordinary logs as compliance evidence.

## Core signals

| Area | Measure | Why it matters |
| --- | --- | --- |
| Node | liveness, readiness, sealed state, restart-required state | distinguishes process health from operational availability |
| Public API | request rate, latency, status, stable error enum | shows caller-visible health |
| Identity use | sign/decrypt outcomes by key id, authority, scheme, algorithm, and mode | finds failures isolated to an identity or crypto path |
| Authority | invalid authority/artifact/intent and policy decisions | detects schema drift, missing features, and denied actions |
| Quorum | peer availability, session latency, timeouts, `dead`, `imposters` | shows loss of threshold capacity or Byzantine evidence |
| Lifecycle | create, import, promote, rotate, refresh, destroy, consistency repair | these operations change custody or key state |
| Audit | sink availability, write latency, rejected events, verification failures | audit enforcement may block protected operations |

Avoid high-cardinality labels for raw payloads, signatures, nonces, tokens, or arbitrary error details. Key ids and authority ids may also be sensitive inventory; expose them only to the monitoring boundary that needs them.

## Separate denial from outage

Do not combine all non-success outcomes into one availability rate.

- `UNAUTHENTICATED`, `ACCESS_DENIED`, and `POLICY_VIOLATION` are security decisions. Spikes may indicate attack, client drift, or a policy rollout problem.
- `KEEPER_SEALED`, audit failures, peer loss, timeouts, and exhausted session attempts are availability outcomes.
- `INVALID_AUTHORITY_ARTIFACT` or a missing algorithm provider often indicates artifact/configuration drift.
- `imposters` is security evidence; an empty list is inconclusive.

Define signing availability over well-formed, authenticated, authorized requests that policy would allow. Track denied requests separately so a successful security control is not reported as downtime.

## Quorum capacity

Alert before the cluster loses quorum. Track healthy and unsealed peers against the configured threshold, not just against total node count.

Useful views:

- remaining peer failures before quorum loss
- session success and latency by protocol
- peer-specific timeout or imposter frequency
- generation consistency and repair events
- version, feature, and platform drift between peers

A single unhealthy peer may not break a `t-of-n` operation, but it removes fault tolerance and should not remain invisible until the next peer fails.

## ML-DSA

Threshold ML-DSA requires separate retry telemetry:

- attempts per completed signature
- end-to-end signing latency
- `SESSION_MAX_ROUNDS_EXCEEDED` rate
- request deadline exhaustion
- correlation with peer or generation changes

An isolated rejection-sampling abort is expected. A sustained shift in the attempt distribution or repeated exhaustion is an availability incident and may justify protocol-level investigation. Raising `keeper.session.mldsa.max-rounds` increases worst-case latency and work; it is not a substitute for diagnosis.

## Audit

Monitor both delivery and integrity:

- at least one required sink accepts events
- write latency remains below the configured timeout
- verification of stored audit records succeeds
- sink backpressure and reconnects do not accumulate
- integrity-key rotation is expected and recorded

If audit enforcement is enabled, alert before sink failure consumes the entire operation timeout budget.

## Page immediately

- a production node unexpectedly starts with developer authentication
- an integration artifact or failure-injection surface appears in production
- the internal API becomes reachable outside the peer network
- the cluster is at or below quorum capacity for a high-impact identity
- required audit sinks cannot accept events
- audit verification fails
- an unexpected import, promotion, destroy, integrity-key rotation, or consistency repair occurs
- `imposters` is non-empty
- policy or authority changes unexpectedly enable raw `arbitrary` signing

Route expected business denials to security analytics unless their volume or source crosses an incident threshold; do not page on every rejected request.


---

Source file: content/docs/operations/troubleshooting.md
Canonical: https://tkeeper.org/docs/operations/troubleshooting

# Troubleshooting

## Start with the failed stage

Most failures map to one authority-path stage:

| Stage | Common symptom |
| --- | --- |
| Auth | `UNAUTHENTICATED` |
| Permission | `ACCESS_DENIED` |
| Seal state | `KEEPER_SEALED` |
| Feature/platform | endpoint missing or provider missing |
| Authority | `INVALID_AUTHORITY`, `AUTHORITY_VIOLATION`, `INVALID_AUTHORITY_ARTIFACT` |
| Policy | `POLICY_VIOLATION` |
| Audit | `AUDIT_NOT_AVAILABLE`, `AUDIT_FAILED` |
| Quorum/session | timeout, dead peers, max rounds |

## Endpoint returns 404

The feature is probably missing from the artifact.

Rebuild with the required feature and platform. See [Build and Features](https://tkeeper.org/docs/deployment/build-and-features).

## No algorithm provider

The platform is missing from the artifact.

Examples:

- EVM, Bitcoin, X.509, and ECIES require `ecc`
- ML-DSA requires `pqc`

## `KEEPER_SEALED`

Unseal the node before protected operations. See [Initialization and Unseal](https://tkeeper.org/docs/deployment/initialization-and-unseal).

## `ACCESS_DENIED`

The authenticated client does not have the required permission.

Check:

- token identity
- permission string
- wildcard scope
- key id in the permission template

See [Permissions](https://tkeeper.org/docs/api-reference/permissions).

## `AUTHORITY_VIOLATION`

The command authority does not match the authority attached to the key identity.

Check:

- command `authorityId`
- command `type`
- key authorities
- authority document `id`
- authority document `type`

See [Authorities](https://tkeeper.org/docs/signing-and-authorities/authorities).

## `INVALID_AUTHORITY`

The key's authority configuration is invalid. Check for an empty list, duplicate ids, mixing `arbitrary` with concrete authorities, missing or mutable OCI references, a loaded document id mismatch, or invalid policy syntax.

## `POLICY_VIOLATION`

The authority policy denied the intent. This is a normal fail-closed result.

Check the materialized intent and effects. If an external verdict is used, verify the verdict input is bound to the same action that will be signed.

## `SESSION_MAX_ROUNDS_EXCEEDED`

The session retry cap was exhausted.

For threshold ML-DSA, this can happen from normal rejection sampling. Treat it as availability first, not automatic corruption.

Check:

- retry count
- peer health
- request deadline
- `keeper.session.mldsa.max-rounds`
- signing latency distribution

## Threshold operation hangs or fails

Check:

- every required peer is reachable
- every required peer is unsealed
- internal API TLS/trust is correct
- quorum configuration matches
- the key generation exists on enough peers
- the operation is sent to a coordinator
- consistency repair is not required

## Audit blocks operations

If audit enforcement is enabled, TKeeper can deny protected operations when no audit sink accepts the event.

Check:

- audit device config
- network path to socket sink
- file sink directory permissions
- audit timeout
- TLS and SPKI pinning for socket sink


---

Source file: content/docs/operations/integration-tests.md
Canonical: https://tkeeper.org/docs/operations/integration-tests

# Integration Tests

Run the complete release verification with:

```bash
./gradlew releaseGate
```

This runs all root and module unit tests, verifies production/test artifact isolation, builds both test images, and executes the functional integration suite. Performance benchmarks remain separate.

Build both test images from the repository root:

```bash
./gradlew buildTestContainers
```

This creates `exploit/tkeeper:dev` and `exploit/tkeeper:production-it`. Run the complete functional suite with:

```bash
./gradlew :integration-tests:functional:test
```

Functional tests reuse the current images and do not rebuild them. Re-run `buildTestContainers` after changing application code, dependencies, or Dockerfiles. The test task always executes when requested instead of treating the external container state as `UP-TO-DATE`.

Run one class with:

```bash
./gradlew :integration-tests:functional:test \
  --tests 'org.exploit.test.functional.ProductionTransportSecurityTests'
```

Run only the audit-inspired malicious protocol-input probes with:

```bash
./gradlew :integration-tests:functional:test \
  --tests 'org.exploit.test.functional.FailureInjectionTests.frostRejectsInvalidSigningPackage' \
  --tests 'org.exploit.test.functional.FailureInjectionTests.validFrostSigningTranscriptPasses' \
  --tests 'org.exploit.test.functional.FailureInjectionTests.frostSigningTranscriptRejectsMaliciousInput' \
  --tests 'org.exploit.test.functional.FailureInjectionTests.gg20RejectsInvalidSigningPackage' \
  --tests 'org.exploit.test.functional.FailureInjectionTests.validGg20MtATranscriptsPassOnSupportedCurves' \
  --tests 'org.exploit.test.functional.FailureInjectionTests.mldsaRejectsInvalidSigningPackage' \
  --tests 'org.exploit.test.functional.FailureInjectionTests.validMLDSASigningTranscriptPasses' \
  --tests 'org.exploit.test.functional.FailureInjectionTests.mldsaSigningTranscriptRejectsMaliciousInput' \
  --tests 'org.exploit.test.functional.FailureInjectionTests.gg20MtARejectsMaliciousInput' \
  --tests 'org.exploit.test.functional.FailureInjectionTests.maliciousCoordinatorCannotInjectInvalidEciesParticipantSet'
```

Run the in-flight FROST, GG20, and ML-DSA crash-recovery checkpoints with:

```bash
./gradlew buildTestContainers
./gradlew :integration-tests:functional:test \
  --tests 'org.exploit.test.functional.FailureInjectionTests.inFlightProtocolStateDoesNotSurviveKeeperRestart*'
```

The development topology uses compose volumes for keeper-1 through keeper-3 so
RocksDB and the keeper-2 SoftHSM token survive a container restart. Signing
sessions remain process-local and must not survive it.

Run the keeper protocol-order, replay, and concurrent-transition cases with:

```bash
./gradlew :integration-tests:functional:test \
  --tests 'org.exploit.test.functional.FailureInjectionTests.keeperProtocolStateRejectsReorderedReplayAndConcurrentTransitions*'
```

This sends invalid transitions through the internal peer transport for ECC DKG, PQC DKG, FROST,
GG20, and threshold ML-DSA signing. Replay and eight-way race cases require exactly one accepted
transition. A normal distributed signature follows every case.

Run the 3-of-5 share-recovery scenario with:

```bash
./gradlew :integration-tests:functional:test \
  --tests 'org.exploit.test.functional.RecoveryFailureInjectionTests'
```

The scenario starts a production-TLS cluster, creates ECC and ML-DSA histories, damages two peers in
different ways, restarts all five keepers in recovery mode, repairs both peers from three explicitly
selected healthy helpers, restarts in normal mode, and verifies state and signing.

Run the deterministic property and fuzz-seed regressions for security-sensitive
binary formats with:

```bash
./gradlew :platform-ecc:test \
  --tests 'org.exploit.keeper.platform.ecc.property.SecuritySerializationProperties' \
  --tests 'org.exploit.keeper.platform.ecc.fuzz.SecurityBinaryParserFuzzTest'
```

Run the coverage-guided parser fuzzer for 30 seconds with:

```bash
./gradlew :platform-ecc:fuzzSecurityParsers
```

Run the deterministic protocol-state properties and fuzz seeds with:

```bash
./gradlew :platform-ecc:test \
  --tests 'org.exploit.keeper.platform.ecc.property.ProtocolStateMachineProperties' \
  --tests 'org.exploit.keeper.platform.ecc.fuzz.SecurityProtocolStateFuzzTest' \
  --tests 'org.exploit.keeper.platform.ecc.fuzz.KeeperProtocolStateFuzzTest'
./gradlew :platform-pqc:test \
  --tests 'org.exploit.keeper.platform.pqc.property.MLDSAStateMachineProperties' \
  --tests 'org.exploit.keeper.platform.pqc.fuzz.MLDSAStateMachineFuzzTest' \
  --tests 'org.exploit.keeper.platform.pqc.fuzz.KeeperMLDSAProtocolStateFuzzTest'
./gradlew :test \
  --tests 'org.exploit.keeper.tests.temporary.InMemoryTemporaryMapConcurrencyTest'
```

Run all coverage-guided security campaigns with:

```bash
./gradlew securityFuzz
```

Override the time budget when running a longer local or scheduled campaign:

```bash
./gradlew :platform-ecc:fuzzSecurityParsers -Pkeeper.fuzz.duration=5m
./gradlew :platform-ecc:fuzzProtocolStateMachines -Pkeeper.fuzz.duration=5m
./gradlew :platform-ecc:fuzzKeeperProtocolStateMachines -Pkeeper.fuzz.duration=5m
./gradlew :platform-pqc:fuzzMLDSAStateMachine -Pkeeper.fuzz.duration=5m
./gradlew :platform-pqc:fuzzKeeperMLDSAProtocols -Pkeeper.fuzz.duration=5m
./gradlew :features:recovery:ecc:fuzzRecoveryProtocol -Pkeeper.fuzz.duration=5m
./gradlew :features:recovery:pqc:fuzzRecoveryPayloads -Pkeeper.fuzz.duration=5m
```

The generated `.cifuzz-corpus/` is local build state and is ignored. Minimize
any finding and retain it as an explicit seed or property regression before
merging the fix.

Do not pass `keeper.features` or `keeper.platforms` to `buildTestContainers`. The development integration artifact uses its own classpath and includes:

- every default production feature
- the explicit recovery feature and both recovery platform modules
- development authentication
- dry-run policy evaluation
- every platform
- the test-only failure-injection module

The production transport test image uses the production UBI Dockerfile and excludes development
authentication, dry run, recovery, and failure injection. Regular `shadowJar` and `dockerBuild`
exclude failure injection; dry run and recovery are included only when selected.

See [integration test setup](https://github.com/tkeeper-org/tkeeper/blob/main/integration-tests/README.md) for local requirements and Testcontainers setup.

See [Security Assurance](https://tkeeper.org/docs/security-model/security-assurance) for the security vectors exercised by the functional suite and the limits of that evidence.


---

Source file: content/docs/operations/failure-injection.md
Canonical: https://tkeeper.org/docs/operations/failure-injection

# Failure Injection

Failure injection is test-only. It is wired into the integration artifact and must not be deployed in production.

It is used for scenarios such as:

- metadata tampering
- authority mutation
- policy mutation
- key material corruption
- pending generation deletion
- peer demotion and promotion recovery
- PQC share corruption and consistency repair

Build the test images with:

```bash
./gradlew buildTestContainers
```

Functional tests reuse the built images and do not rebuild them automatically.

Production builds use `shadowJar` or `dockerBuild` and do not include failure injection.


---

Source file: content/docs/operations/error-tracking.md
Canonical: https://tkeeper.org/docs/operations/error-tracking

# Error Tracking (Sentry)

Sentry is initialized at startup if it is enabled.

Config fields:

```hocon
sentry {
  enabled = true
  dsn = "https://public@example.sentry.io/1"
  environment = "prod"
  release = "2.5.0"
}
```

Environment fallbacks:

```text
SENTRY_APPLICATION_ENVIRONMENT
SENTRY_APPLICATION_RELEASE
```

## Common problems

### Sentry stays disabled

Check that `sentry.enabled` is `true` and that the DSN is valid.

### Bad DSN

The DSN must be `http` or `https`, include user info, include a host, and end with a numeric project id.

---

Source: openapi.yaml
Canonical: https://tkeeper.org/openapi.yaml

```yaml
openapi: 3.0.3
info:
  title: TKeeper API
  version: 2.5.0
  description: |
    TKeeper public HTTP API.
    JSON request bodies reject unknown properties and are limited to 10 MiB by the HTTP server.
    Digital asset, agentic payment, and PKI commands use the shared signing API.
    Enabled features add their command types and, where available, composed results.

servers:
  - url: /

tags:
  - name: System
  - name: Keys
  - name: DKG
  - name: Signing
    description: Shared signing and verification for AI, digital assets, PKI, and custom authorities.
  - name: Composition
    description: Signed digital asset transactions and agentic payment credentials; other types return a raw signature.
  - name: MCP
    description: AI agent tools over the optional MCP feature.
  - name: TrustedDealer
  - name: Quorum
  - name: DryRun
  - name: ECIES
  - name: Audit
  - name: Inventory
  - name: ControlPlane
  - name: Consistency
  - name: Recovery
  - name: Expiration
  - name: Integrity

security:
  - DevToken: []
  - JwtToken: []

paths:
  /v1/keeper/system/status:
    get:
      tags: [System]
      operationId: getSystemStatus
      summary: Get keeper status
      responses:
        '200':
          description: Keeper status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/system/init:
    post:
      tags: [System]
      operationId: initializeKeeper
      summary: Initialize keeper
      description: |
        Writes keeper identity and quorum settings into the sealed store.
        `threshold=1,total=1` initializes mono mode. `threshold>1` initializes threshold/MPC mode.
      x-permissions:
        required: [tkeeper.system.init]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KeeperInitData'
      responses:
        '200':
          description: Provider initialization data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShamirInitData'
        '204':
          description: Initialized without provider payload
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/system/health:
    get:
      tags: [System]
      operationId: health
      summary: Liveness probe
      security: []
      responses:
        '204':
          description: Service is up

  /v1/keeper/system/ready:
    get:
      tags: [System]
      operationId: ready
      summary: Readiness probe
      security: []
      responses:
        '204':
          description: Keeper is unsealed
        '503':
          description: Keeper is not ready

  /v1/keeper/system/unseal:
    post:
      tags: [System]
      operationId: submitUnsealShares
      summary: Submit unseal shares
      x-permissions:
        required: [tkeeper.system.unseal]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Unseal'
      responses:
        '200':
          description: Unseal progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Progress'
        default:
          $ref: '#/components/responses/ErrorResponse'
    get:
      tags: [System]
      operationId: autoUnseal
      summary: Unseal with the configured automatic seal provider
      x-permissions:
        required: [tkeeper.system.unseal]
      responses:
        '200':
          description: Unseal progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Progress'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/system/seal:
    post:
      tags: [System]
      operationId: sealKeeper
      summary: Seal keeper
      x-permissions:
        required: [tkeeper.system.seal]
      responses:
        '204':
          description: Keeper sealed
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/ping:
    get:
      tags: [System]
      operationId: ping
      summary: Lightweight readiness status
      security: []
      responses:
        '200':
          description: Readiness flag
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShortStatus'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/peerId:
    get:
      tags: [System]
      operationId: peerId
      summary: Get local peer id
      responses:
        '200':
          description: Peer id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntValue'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/publicKey:
    get:
      tags: [Keys]
      operationId: publicKey
      summary: Get public key
      description: Read-only key operation that remains available in recovery mode for generation comparison.
      x-permissions:
        template: tkeeper.key.{keyId}.public
      parameters:
        - $ref: '#/components/parameters/KeyIdQuery'
        - name: generation
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/Generation'
        - name: tweak
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/Tweak'
      responses:
        '200':
          description: Public key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicKeyDto'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v2/keeper/dkg:
    post:
      tags: [DKG]
      operationId: generateKey
      summary: Create, rotate, or refresh a key generation
      description: |
        Runs the key lifecycle operation in the current quorum mode.
        Mono mode manages full key material locally. Threshold mode runs distributed key generation across peers.
      x-permissions:
        byField:
          field: mode
          CREATE: tkeeper.dkg.create
          ROTATE: tkeeper.dkg.rotate
          REFRESH: tkeeper.dkg.refresh
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Generate'
      responses:
        '204':
          description: Lifecycle operation completed
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/destroy:
    post:
      tags: [Keys]
      operationId: destroyKeyGeneration
      summary: Destroy one key generation
      description: |
        Destroys a historical key generation.
        Mono mode is local-only and allows any non-current generation.
        Threshold mode coordinates across peers and requires the generation to be at least two generations behind the active one.
      x-permissions:
        template: tkeeper.key.{keyId}.destroy
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KeyDestroyReference'
      responses:
        '204':
          description: Generation destroyed
        '299':
          description: Threshold destroy committed by quorum, but not all peers
          headers:
            Warning:
              schema:
                type: string
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v2/keeper/storage/store:
    post:
      tags: [TrustedDealer]
      operationId: trustedDealerStore
      summary: Import key material through trusted dealer flow
      description: |
        Imports existing private key material into the current quorum mode.
        Mono mode stores the key locally. Threshold mode splits the key into peer shares and stores commitments.
      x-permissions:
        required: [tkeeper.storage.write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Store'
      responses:
        '200':
          description: Key material stored
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v2/keeper/quorum/promote:
    post:
      tags: [Quorum]
      operationId: promoteQuorum
      summary: Promote mono keeper into threshold quorum
      description: |
        Promotes a mono keeper into a threshold quorum.
        Target peers must already be initialized, unsealed, and configured with the requested threshold and total.
        The promoted keeper becomes peer 1 and must be restarted before normal operations continue.
      x-permissions:
        required: [tkeeper.quorum.promote]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuorumPromotion'
      responses:
        '200':
          description: Promotion completed; restart the keeper before normal operations
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuorumPromotionResult'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v2/keeper/sign:
    post:
      tags: [Signing]
      operationId: sign
      summary: Sign in the current quorum mode
      description: |
        Materializes the command, evaluates key controls, and signs using the current quorum mode.
        Mono mode signs locally. Threshold mode uses FROST or GG20.
      x-permissions:
        template: tkeeper.key.{keyId}.sign
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Sign'
      responses:
        '200':
          description: Signature result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CalculatedSignature'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v2/keeper/compose:
    post:
      tags: [Composition]
      operationId: compose
      summary: Sign and compose an artifact
      description: |
        Uses the same request, permission, authority, policy, approval, and signing checks as `/v2/keeper/sign`.
        EVM, TRON, XRP, and Solana commands return signed transactions. AP2 and MC Intent
        commands return payment credentials. Types without a composer, including Bitcoin and X.509,
        return `CalculatedSignature`. Composition does not broadcast or submit the result.
      x-permissions:
        template: tkeeper.key.{keyId}.sign
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Sign'
      responses:
        '200':
          description: Composed artifact or raw signature fallback
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/SignedTransaction'
                  - $ref: '#/components/schemas/SignedSolanaTransaction'
                  - $ref: '#/components/schemas/PaymentCredential'
                  - $ref: '#/components/schemas/CalculatedSignature'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v2/keeper/sign/verify:
    post:
      tags: [Signing]
      operationId: verifySignature
      summary: Verify signature
      x-permissions:
        template: tkeeper.key.{keyId}.verify
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Verify'
      responses:
        '200':
          description: Verification result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResult'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /mcp:
    post:
      tags: [MCP]
      operationId: mcp
      summary: Discover and call agent tools
      description: |
        Optional `mcp` feature. Supports `server/discover`, `tools/list`, and `tools/call`
        over HTTP POST with JSON-RPC 2.0. The host authenticates with the same Keeper token
        used by the public API. Tool discovery is permission filtered; calls check permission
        again. Requests with an `Origin` header are rejected.
      parameters:
        - name: MCP-Protocol-Version
          in: header
          required: true
          schema:
            type: string
            enum: ['2026-07-28']
        - name: Mcp-Method
          in: header
          required: true
          description: Must match the JSON-RPC `method`.
          schema:
            type: string
            enum: [server/discover, tools/list, tools/call]
        - name: Mcp-Name
          in: header
          required: false
          description: Required for `tools/call`; must match `params.name`.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/McpRequest'
      responses:
        '200':
          description: JSON-RPC result, including tool errors and some protocol errors
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/McpResponse'
        '400':
          description: Invalid JSON-RPC request, headers, or protocol version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/McpErrorResponse'
        '403':
          description: Browser origin rejected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/McpErrorResponse'
        '404':
          description: Unknown MCP method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/McpErrorResponse'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/emulate:
    post:
      tags: [DryRun]
      operationId: emulateAuthorityCommand
      summary: Evaluate an authority command without executing it
      description: |
        Requires the optional `dry-run` feature. Resolves the key authority and evaluates its policy
        without signing, consuming approvals, or mutating key state. `ALLOW_WITH_REQUIREMENTS`
        includes every four-eye approval requirement needed by the authority policy.
      x-permissions:
        required: [tkeeper.emulate]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Emulate'
      responses:
        '200':
          description: Authority policy evaluation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyEvaluation'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/ecies/encrypt:
    post:
      tags: [ECIES]
      operationId: eciesEncrypt
      summary: Encrypt with ECIES public key
      description: Encryption uses the public key and does not require peer participation.
      x-permissions:
        template: tkeeper.key.{keyId}.encrypt
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Encrypt'
      responses:
        '200':
          description: Ciphertext
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Encrypted'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/ecies/decrypt:
    post:
      tags: [ECIES]
      operationId: eciesDecrypt
      summary: Decrypt ECIES ciphertext
      description: |
        Decrypts in the current quorum mode.
        Mono mode decrypts locally. Threshold mode collects peer partial decrypts and verifies DLEQ proofs.
      x-permissions:
        template: tkeeper.key.{keyId}.decrypt
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Decrypt'
      responses:
        '200':
          description: Plaintext
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Decrypted'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/compliance/inventory:
    get:
      tags: [Inventory]
      operationId: assetInventory
      summary: List asset inventory
      x-permissions:
        required: [tkeeper.compliance.inventory]
      parameters:
        - name: logicalId
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/KeyId'
        - name: historical
          in: query
          required: false
          schema:
            type: boolean
            default: false
        - name: lastSeen
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/Cursor'
        - name: assetOwner
          in: query
          required: false
          schema:
            $ref: '#/components/schemas/AssetOwner'
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            format: int32
            default: 200
            minimum: 1
            maximum: 200
      responses:
        '200':
          description: Asset inventory page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssetInventoryPage'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/audit/verify:
    post:
      tags: [Audit]
      operationId: verifyAuditLine
      summary: Verify one signed audit line
      x-permissions:
        required: [tkeeper.audit.log.verify]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignedLine'
      responses:
        '200':
          description: Verification result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResult'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/audit/verify/batch:
    post:
      tags: [Audit]
      operationId: verifyAuditBatch
      summary: Verify a batch of signed audit lines
      x-permissions:
        required: [tkeeper.audit.log.verify]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Logs'
      responses:
        '200':
          description: Verification results by event id
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  $ref: '#/components/schemas/VerifyResult'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/integrity/rotate:
    post:
      tags: [Integrity]
      operationId: rotateIntegrityKey
      summary: Rotate audit integrity key
      description: |
        Rotates this peer's audit and internal-response integrity key.
        Restart every peer after rotation so process-local peer key pins are re-enrolled.
      x-permissions:
        required: [tkeeper.integrity.rotate]
      responses:
        '204':
          description: Integrity key rotated
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/consistency/fix:
    post:
      tags: [Consistency]
      operationId: consistencyFix
      summary: Try to repair key consistency
      x-permissions:
        required: [tkeeper.consistency.fix]
      parameters:
        - $ref: '#/components/parameters/KeyIdQuery'
      responses:
        '200':
          description: Consistency verdict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsistencyCheck'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/recovery/recover:
    post:
      tags: [Recovery]
      operationId: recover
      summary: Rebuild key-scoped state on this peer
      description: |
        Call this endpoint on the damaged keeper. The keeper asks exactly `threshold` remote helpers
        for the key description, requires them to agree on the algorithm and current generation, and
        selects the matching recovery flow locally. Available only when the artifact includes the
        corresponding recovery feature and every participating keeper runs with
        `keeper.recovery=true`. Supports any configured quorum with `1 < threshold < total`.
        All non-destroyed generations must belong to one crypto platform; mixed ECC/PQC rotation
        history fails closed without installing a partial recovery.
        After all operator-selected helpers agree, the target discards and atomically rebuilds the
        logical key's shares, metadata, policies, destroy state, public side-state, and derived indexes.
      x-permissions:
        required: [tkeeper.recover]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecoverRequest'
      responses:
        '200':
          description: All non-destroyed generations are present and verified on the target peer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecoverResult'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/expires:
    get:
      tags: [Expiration]
      operationId: listExpires
      summary: List expiring key policy entries
      description: |
        Supply either a positive `windowSec`, or `to` with optional `from`.
        `windowSec` cannot be combined with `from` or `to`, and `from` cannot exceed `to`.
      x-permissions:
        required: [tkeeper.expired.view]
      parameters:
        - $ref: '#/components/parameters/ExpireTypeQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - $ref: '#/components/parameters/CursorQuery'
        - name: windowSec
          in: query
          required: false
          schema:
            type: integer
            format: int64
            minimum: 1
        - name: from
          in: query
          required: false
          schema:
            type: integer
            format: int64
            minimum: 0
        - name: to
          in: query
          required: false
          schema:
            type: integer
            format: int64
            minimum: 0
      responses:
        '200':
          description: Expiration page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExpirePage'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/expires/apply:
    get:
      tags: [Expiration]
      operationId: listApplyExpires
      summary: List keys with apply policy expiring in a window
      x-permissions:
        required: [tkeeper.expired.view]
      parameters:
        - $ref: '#/components/parameters/LimitQuery'
        - $ref: '#/components/parameters/CursorQuery'
        - name: windowSec
          in: query
          required: true
          schema:
            type: integer
            format: int64
            minimum: 1
      responses:
        '200':
          description: Expiration page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExpirePage'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/expires/process:
    get:
      tags: [Expiration]
      operationId: listProcessExpires
      summary: List keys with process policy expiring in a window
      x-permissions:
        required: [tkeeper.expired.view]
      parameters:
        - $ref: '#/components/parameters/LimitQuery'
        - $ref: '#/components/parameters/CursorQuery'
        - name: windowSec
          in: query
          required: true
          schema:
            type: integer
            format: int64
            minimum: 1
      responses:
        '200':
          description: Expiration page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExpirePage'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/expires/expired:
    get:
      tags: [Expiration]
      operationId: listExpired
      summary: List expired key policy entries
      x-permissions:
        required: [tkeeper.expired.view]
      parameters:
        - $ref: '#/components/parameters/ExpireTypeQuery'
        - $ref: '#/components/parameters/LimitQuery'
        - $ref: '#/components/parameters/CursorQuery'
      responses:
        '200':
          description: Expiration page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExpirePage'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/control/auth/config:
    get:
      tags: [ControlPlane]
      operationId: controlPlaneAuthConfig
      summary: Get UI auth config
      security: []
      responses:
        '200':
          description: UI auth config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ControlPlaneAuthConfig'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/control/system:
    get:
      tags: [ControlPlane]
      operationId: controlPlaneSystem
      summary: Get cluster status for UI
      x-permissions:
        required: [tkeeper.control.system]
      responses:
        '200':
          description: Cluster status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemInfo'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/control/capabilities:
    get:
      tags: [ControlPlane]
      operationId: controlPlaneCapabilities
      summary: Get capabilities exposed by installed cryptographic platforms
      responses:
        '200':
          description: Runtime control-plane capabilities
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ControlPlaneCapabilities'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/control/audit/sinks:
    get:
      tags: [ControlPlane]
      operationId: controlPlaneAuditSinks
      summary: Get audit sink status
      x-permissions:
        required: [tkeeper.control.sinks]
      responses:
        '200':
          description: Audit sink status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditInfo'
        default:
          $ref: '#/components/responses/ErrorResponse'

  /v1/keeper/control/me:
    get:
      tags: [ControlPlane]
      operationId: controlPlaneMe
      summary: Get authenticated subject
      responses:
        '200':
          description: Authenticated subject
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimpleAuthData'
        default:
          $ref: '#/components/responses/ErrorResponse'

components:
  securitySchemes:
    DevToken:
      type: apiKey
      in: header
      name: X-DEV-TOKEN
    JwtToken:
      type: apiKey
      in: header
      name: X-JWT-TOKEN

  parameters:
    KeyIdQuery:
      name: keyId
      in: query
      required: true
      schema:
        $ref: '#/components/schemas/KeyId'
    ExpireTypeQuery:
      name: type
      in: query
      required: true
      schema:
        type: string
        enum: [apply, process]
    LimitQuery:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        format: int32
        default: 100
        minimum: 1
        maximum: 2000
    CursorQuery:
      name: cursor
      in: query
      required: false
      schema:
        $ref: '#/components/schemas/Cursor'

  responses:
    ErrorResponse:
      description: Error response
      headers:
        WWW-Authenticate:
          description: Authentication challenge included on `401 Unauthorized` responses.
          schema:
            type: string
            example: TKeeper realm="tkeeper"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorMessage'

  schemas:
    ErrorMessage:
      type: object
      required: [error, imposters, dead]
      properties:
        error:
          $ref: '#/components/schemas/ErrorType'
        details:
          type: string
          nullable: true
        imposters:
          type: array
          items:
            type: string
        dead:
          type: array
          items:
            type: string
        approvals:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/RequiredApprovalDetails'

    ErrorType:
      type: string
      enum:
        - KEEPER_ALREADY_INITIALIZED
        - KEEPER_ALREADY_UNSEALED
        - NOT_SUPPORTED_FOR_AUTO_PROVIDER
        - NOT_SUPPORTED_FOR_MANUAL_PROVIDER
        - KEEPER_NOT_INITIALIZED
        - KEEPER_SEALED
        - RESTART_REQUIRED
        - RECOVERY_MODE_ACTIVE
        - RECOVERY_NOT_ENABLED
        - RECOVERY_NOT_POSSIBLE
        - RECOVERY_FAILED
        - KEY_NOT_FOUND
        - SESSION_MANAGER_NOT_FOUND
        - SESSION_NOT_FOUND
        - UNSUPPORTED_KEY_ALGORITHM
        - SIGNATURE_VERIFIER_NOT_FOUND
        - SESSION_ALREADY_EXISTS
        - NOT_ENOUGH_HEALTHY_CLIENTS
        - NOT_ENOUGH_CONFIGURED_CLIENTS
        - NOT_ENOUGH_HONEST_CLIENTS
        - INVALID_CONFIG
        - INVALID_SIGNATURE
        - KEY_ALGORITHM_MISMATCH
        - INVALID_BASE64
        - INVALID_THRESHOLD
        - INVALID_TOTAL
        - INVALID_PEER_ID
        - INVALID_CIPHERTEXT
        - INVALID_SESSION_ID
        - INVALID_REQUEST_BODY
        - INVALID_QUERY_PARAMETER
        - INVALID_SHARE
        - INVALID_KEY_SET_POLICY
        - INVALID_KEY_GENERATION
        - INVALID_CURSOR
        - INVALID_EXPIRATION_TIME
        - INVALID_HASH_METHOD
        - INVALID_PAYLOAD
        - INVALID_SIG_SCHEME
        - INVALID_KEY_ID
        - INVALID_ASSET_OWNER
        - INVALID_EXPIRE_TYPE
        - INVALID_TWEAK
        - INVALID_AUTHORITY_ARTIFACT
        - INVALID_AUTHORITY_TYPE
        - INVALID_AUTHORITY
        - INVALID_INTENT
        - NOT_PARTICIPANT
        - SHARE_NOT_FOUND
        - IMPOSTER_FOUND
        - CORRUPTED_KEY_STATE
        - KEY_ALREADY_EXISTS
        - KEY_APPLY_OPS_FORBIDDEN
        - KEY_PROCESS_OPS_FORBIDDEN
        - AUDIT_FAILED
        - AUDIT_NOT_AVAILABLE
        - INTEGRITY_KEY_NOT_FOUND
        - HMAC_KEY_NOT_FOUND
        - MISSING_KEY_ID
        - MISSING_SESSION_ID
        - MISSING_KEY_VERSION
        - MISSING_WINDOW
        - MISSING_EXPIRE_TYPE
        - IDENTIFIABLE_ABORT
        - DEV_MODE_VIOLATION
        - APPROVAL_REQUIRED
        - ACCESS_DENIED
        - UNAUTHENTICATED
        - DESTROY_FORBIDDEN
        - POLICY_VIOLATION
        - AUTHORITY_VIOLATION
        - NOT_THRESHOLD
        - NOT_MONO
        - WARNING
        - CONSISTENCY_CHECK_FAILED
        - ROTATE_NEEDED
        - INCONSISTENT_KEEPER
        - TAMPERED_KEEPER
        - SESSION_MAX_ROUNDS_EXCEEDED
        - NOT_COORDINATOR
        - HSM_ERROR
        - INTERNAL_ERROR

    KeeperInitData:
      type: object
      additionalProperties: false
      description: |
        Keeper init data. `threshold=1,total=1` selects mono mode.
        `threshold>1,total>=threshold` selects threshold/MPC mode.
      required: [peerId, threshold, total]
      properties:
        peerId:
          type: integer
          format: int32
          minimum: 1
          maximum: 255
          description: Local peer id. Must not exceed `total`. Mono mode requires `1`.
        threshold:
          type: integer
          format: int32
          minimum: 1
          maximum: 255
          description: Required quorum size. Use `1` only with `total=1`.
        total:
          type: integer
          format: int32
          minimum: 1
          maximum: 255
          description: Total peers in the quorum. Must be `1` for mono mode and cannot exceed 255.

    ShamirInitData:
      type: object
      required: [threshold, total, shares64]
      properties:
        threshold:
          type: integer
          format: int32
        total:
          type: integer
          format: int32
        shares64:
          type: array
          minItems: 1
          maxItems: 255
          items:
            $ref: '#/components/schemas/NonEmptyBase64'

    Unseal:
      type: object
      additionalProperties: false
      anyOf:
        - required: [payload64]
        - required: [payloads64]
      properties:
        payload64:
          $ref: '#/components/schemas/NonEmptyBase64'
        payloads64:
          type: array
          minItems: 1
          maxItems: 255
          nullable: true
          items:
            $ref: '#/components/schemas/NonEmptyBase64'
        reset:
          type: boolean
          default: false
          nullable: true

    StoreState:
      type: string
      enum: [UNINITIALIZED, SEALED, UNSEALED]

    Progress:
      oneOf:
        - $ref: '#/components/schemas/ReadyProgress'
        - $ref: '#/components/schemas/ShamirUnsealProgress'

    ReadyProgress:
      type: object
      required: [ready]
      properties:
        ready:
          type: boolean

    ShamirUnsealProgress:
      type: object
      required: [threshold, total, progress, ready]
      properties:
        threshold:
          type: integer
          format: int32
        total:
          type: integer
          format: int32
        progress:
          type: integer
          format: int32
        ready:
          type: boolean

    StatusResponse:
      type: object
      required: [state, progress]
      properties:
        sealedBy:
          type: string
          nullable: true
        state:
          $ref: '#/components/schemas/StoreState'
        progress:
          $ref: '#/components/schemas/Progress'

    ShortStatus:
      type: object
      required: [ready]
      properties:
        ready:
          type: boolean
        peerId:
          type: integer
          format: int32
          nullable: true
        threshold:
          type: integer
          format: int32
          nullable: true
        total:
          type: integer
          format: int32
          nullable: true

    IntValue:
      type: object
      required: [serviceId]
      properties:
        serviceId:
          type: integer
          format: int32
        result:
          type: integer
          format: int32
          nullable: true

    PublicKeyDto:
      type: object
      required: [data64]
      properties:
        data64:
          $ref: '#/components/schemas/NonEmptyBase64'

    KeyId:
      type: string
      minLength: 1
      maxLength: 255
      pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*(?::[a-z0-9]+(?:-[a-z0-9]+)*)?$

    AssetOwner:
      type: string
      minLength: 1
      maxLength: 255
      pattern: ^[a-z0-9]+(?:[-_:/][a-z0-9]+)*$

    AuthorityId:
      type: string
      minLength: 1
      maxLength: 255
      pattern: ^[a-z0-9]+(?:[-_:/][a-z0-9]+)*$

    Tweak:
      type: string
      minLength: 1
      maxLength: 255
      pattern: ^[a-z0-9:_\-/]+$

    Generation:
      type: integer
      format: int32
      minimum: 1

    Cursor:
      type: string
      minLength: 1

    Base64:
      type: string
      format: byte

    NonEmptyBase64:
      type: string
      format: byte
      minLength: 1

    KeyAlgorithm:
      type: string
      minLength: 1
      maxLength: 64
      pattern: ^[A-Za-z0-9][A-Za-z0-9._-]*$
      description: Key algorithm exposed by an installed platform. Built-in values are SECP256K1, ED25519, P256, MLDSA44, MLDSA65, and MLDSA87.

    KeyGenMode:
      type: string
      enum: [CREATE, ROTATE, REFRESH]
      description: |
        CREATE starts a new cryptographic identity. ROTATE replaces its key material.
        REFRESH keeps the aggregate key unchanged: threshold ECC replaces peer shares,
        while ML-DSA carries the existing shares forward unchanged.

    Generate:
      type: object
      additionalProperties: false
      description: |
        Key lifecycle request. Mono mode creates or updates local full key material.
        Threshold mode runs distributed key generation and stores peer shares plus commitments.
      required: [keyId, algorithm, authorities, mode]
      properties:
        keyId:
          $ref: '#/components/schemas/KeyId'
        algorithm:
          $ref: '#/components/schemas/KeyAlgorithm'
        authorities:
          type: array
          minItems: 1
          maxItems: 255
          items:
            $ref: '#/components/schemas/KeySetAuthority'
        mode:
          $ref: '#/components/schemas/KeyGenMode'
        policy:
          allOf:
            - $ref: '#/components/schemas/KeySetPolicy'
          nullable: true
        assetOwner:
          allOf:
            - $ref: '#/components/schemas/AssetOwner'
          nullable: true
        approvals:
          $ref: '#/components/schemas/Approvals'

    Store:
      type: object
      additionalProperties: false
      description: |
        Trusted dealer import request. Mono mode stores the imported key locally.
        Threshold mode splits the imported key into peer shares.
      required: [keyId, algorithm, authorities, value64]
      properties:
        keyId:
          $ref: '#/components/schemas/KeyId'
        algorithm:
          $ref: '#/components/schemas/KeyAlgorithm'
        authorities:
          type: array
          minItems: 1
          maxItems: 255
          items:
            $ref: '#/components/schemas/KeySetAuthority'
        value64:
          allOf:
            - $ref: '#/components/schemas/NonEmptyBase64'
          description: Base64 raw private key bytes. For Ed25519, use the standard seed bytes.
        policy:
          allOf:
            - $ref: '#/components/schemas/KeySetPolicy'
          nullable: true
        assetOwner:
          allOf:
            - $ref: '#/components/schemas/AssetOwner'
          nullable: true

    QuorumPromotion:
      type: object
      additionalProperties: false
      description: Target threshold/MPC quorum for mono promotion.
      required: [threshold, total]
      properties:
        threshold:
          type: integer
          format: int32
          minimum: 2
          maximum: 255
          description: Target threshold. Must be greater than 1.
        total:
          type: integer
          format: int32
          minimum: 2
          maximum: 255
          description: Target total peers. Must be at least `threshold`. The promoted keeper becomes peer 1.

    QuorumPromotionResult:
      type: object
      description: Result of mono to threshold promotion.
      required: [peerId, threshold, total, promotedKeys, restartRequired]
      properties:
        peerId:
          type: integer
          format: int32
          description: New peer id of the promoted keeper. It is always 1.
        threshold:
          type: integer
          format: int32
        total:
          type: integer
          format: int32
        promotedKeys:
          type: integer
          format: int32
        restartRequired:
          type: boolean
          description: True when the promoted keeper must be restarted before normal operations.

    KeySetAuthority:
      type: object
      additionalProperties: false
      required: [id]
      properties:
        id:
          $ref: '#/components/schemas/AuthorityId'
        oci:
          type: string
          minLength: 1
          nullable: true

    KeySetPolicy:
      type: object
      additionalProperties: false
      properties:
        apply:
          allOf:
            - $ref: '#/components/schemas/NotAfter'
          nullable: true
        process:
          allOf:
            - $ref: '#/components/schemas/NotAfter'
          nullable: true
        fourEye:
          allOf:
            - $ref: '#/components/schemas/FourEyeControlPolicy'
          nullable: true
        allowHistoricalProcess:
          type: boolean
          default: true

    TimestampUnit:
      type: string
      enum: [MILLISECONDS, SECONDS]

    NotAfter:
      type: object
      additionalProperties: false
      required: [unit]
      properties:
        unit:
          $ref: '#/components/schemas/TimestampUnit'
        notAfter:
          type: integer
          format: int64
          nullable: true

    FourEyeControlPolicy:
      type: object
      additionalProperties: false
      required: [m, n, keys]
      properties:
        mode:
          $ref: '#/components/schemas/FourEyeControlMode'
        m:
          type: integer
          format: int32
          minimum: 2
          maximum: 256
        n:
          type: integer
          format: int32
          minimum: 2
          maximum: 256
        keys:
          type: array
          minItems: 2
          maxItems: 256
          uniqueItems: true
          items:
            $ref: '#/components/schemas/ApproverPublicKey'

    FourEyeControlMode:
      type: string
      enum: [STRICT, LENIENT]
      default: STRICT
      description: STRICT protects every supported operation. LENIENT protects ROTATE, REFRESH, and generation destruction.

    ApproverPublicKey:
      type: object
      additionalProperties: false
      required: [algorithm, publicKey64]
      properties:
        algorithm:
          $ref: '#/components/schemas/KeyAlgorithm'
        publicKey64:
          $ref: '#/components/schemas/NonEmptyBase64'

    Approvals:
      type: object
      additionalProperties: false
      properties:
        keeperId:
          type: integer
          format: int32
          default: 0
          minimum: 0
          maximum: 255
        nonce:
          type: string
          default: ''
          maxLength: 255
        timestamp:
          type: integer
          format: int64
        proofs:
          type: array
          maxItems: 256
          items:
            $ref: '#/components/schemas/Proof'
          default: []

    Proof:
      type: object
      additionalProperties: false
      required: [fingerprint, signature64]
      properties:
        fingerprint:
          $ref: '#/components/schemas/NonEmptyBase64'
        signature64:
          $ref: '#/components/schemas/NonEmptyBase64'

    KeyDestroyReference:
      type: object
      additionalProperties: false
      description: Key generation destroy request.
      required: [keyId, generation]
      properties:
        keyId:
          $ref: '#/components/schemas/KeyId'
        generation:
          allOf:
            - $ref: '#/components/schemas/Generation'
          description: Generation to destroy. Current generation cannot be destroyed.
        approvals:
          $ref: '#/components/schemas/Approvals'

    Sign:
      type: object
      additionalProperties: false
      description: Sign request. The command carries the signature scheme and hash.
      required: [keyId, command]
      properties:
        keyId:
          $ref: '#/components/schemas/KeyId'
        command:
          $ref: '#/components/schemas/AuthorityCommand'
        tweak:
          allOf:
            - $ref: '#/components/schemas/Tweak'
          nullable: true
        approvals:
          $ref: '#/components/schemas/Approvals'

    Emulate:
      type: object
      additionalProperties: false
      description: Authority-policy evaluation request for the optional dry-run feature.
      required: [keyId, command]
      properties:
        keyId:
          $ref: '#/components/schemas/KeyId'
        command:
          $ref: '#/components/schemas/AuthorityCommand'

    Verify:
      type: object
      additionalProperties: false
      description: |
        Pure cryptographic verification request. If generation is omitted, the active generation is used.
        The command type selects material validation and serialization; no authority or policy is resolved.
        Any supported material type, including typed or arbitrary material, may be verified independently
        of the key's current authority manifest. A valid result proves cryptographic validity only.
      required: [keyId, command, signature64]
      properties:
        keyId:
          $ref: '#/components/schemas/KeyId'
        generation:
          allOf:
            - $ref: '#/components/schemas/Generation'
          nullable: true
        command:
          $ref: '#/components/schemas/VerificationCommand'
        signature64:
          $ref: '#/components/schemas/NonEmptyBase64'
        tweak:
          allOf:
            - $ref: '#/components/schemas/Tweak'
          nullable: true

    AuthorityCommand:
      type: object
      additionalProperties: false
      required: [type, authorityId, artifact]
      properties:
        type:
          type: string
          minLength: 1
          maxLength: 64
          pattern: ^[a-z0-9]+(?:[._-][a-z0-9]+)*$
          description: Extensible command type registered by the core or an enabled feature.
          example: arbitrary
        authorityId:
          $ref: '#/components/schemas/AuthorityId'
        artifact:
          $ref: '#/components/schemas/Artifact'

    VerificationCommand:
      type: object
      additionalProperties: false
      required: [type, artifact]
      properties:
        type:
          type: string
          minLength: 1
          maxLength: 64
          pattern: ^[a-z0-9]+(?:[._-][a-z0-9]+)*$
          description: Material type registered by the core or an enabled feature.
          example: arbitrary
        artifact:
          $ref: '#/components/schemas/Artifact'

    Artifact:
      description: The command type selects the artifact format and its authority checks.
      oneOf:
        - $ref: '#/components/schemas/ArbitraryArtifact'
        - $ref: '#/components/schemas/TypedArtifact'
        - $ref: '#/components/schemas/EvmTransactionArtifact'
        - $ref: '#/components/schemas/TransactionArtifact'
        - $ref: '#/components/schemas/PaymentMandatesArtifact'
        - $ref: '#/components/schemas/BitcoinTransactionArtifact'
        - $ref: '#/components/schemas/X509TbsCertificateArtifact'

    SignatureScheme:
      type: string
      minLength: 1
      maxLength: 64
      pattern: ^[A-Za-z0-9][A-Za-z0-9._-]*$
      description: Signature scheme exposed by an installed platform. Built-in values are ECDSA, EdDSA, SCHNORR, BIP340, TAPROOT, and MLDSA.

    HashMethod:
      type: string
      enum: [NONE, SHA256, SHA512, KECCAK256]

    ArbitraryArtifact:
      type: object
      additionalProperties: false
      required: [scheme, data64]
      properties:
        scheme:
          $ref: '#/components/schemas/SignatureScheme'
        hash:
          $ref: '#/components/schemas/HashMethod'
        data64:
          $ref: '#/components/schemas/Base64'

    TypedArtifact:
      type: object
      additionalProperties: false
      required: [scheme, typed]
      properties:
        scheme:
          $ref: '#/components/schemas/SignatureScheme'
        hash:
          $ref: '#/components/schemas/HashMethod'
        typed:
          type: object
          additionalProperties: true

    EvmTransactionArtifact:
      type: object
      additionalProperties: false
      required: [message64]
      properties:
        message64:
          $ref: '#/components/schemas/NonEmptyBase64'

    TransactionArtifact:
      type: object
      additionalProperties: false
      description: |
        For `tron.transaction`, `transaction` is unsigned transaction JSON.
        For `xrp.transaction`, it is unsigned transaction hex.
        For `solana.transaction`, it is an unsigned transaction encoded as Base64.
      required: [transaction]
      properties:
        transaction:
          type: string
          minLength: 1

    PaymentMandatesArtifact:
      type: object
      additionalProperties: false
      description: Artifact for `ap2.mandate` and `mcintent.mandate`.
      required: [mode, signingInput, disclosures]
      properties:
        mode:
          type: string
          enum: [PAIRED, PAYMENTS, CHECKOUTS]
        signingInput:
          type: string
          minLength: 1
          description: Unsigned JWS protected header and payload.
        disclosures:
          type: array
          minItems: 1
          items:
            type: string
            minLength: 1

    BitcoinTransactionArtifact:
      type: object
      additionalProperties: false
      required: [transaction64, previousTransactions64, input]
      properties:
        transaction64:
          $ref: '#/components/schemas/NonEmptyBase64'
        previousTransactions64:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/NonEmptyBase64'
        input:
          type: integer
          format: int32
          minimum: 0
        redeemScript64:
          allOf:
            - $ref: '#/components/schemas/NonEmptyBase64'
          nullable: true
        witnessScript64:
          allOf:
            - $ref: '#/components/schemas/NonEmptyBase64'
          nullable: true
        merkleRoot64:
          allOf:
            - $ref: '#/components/schemas/NonEmptyBase64'
          nullable: true

    X509TbsCertificateArtifact:
      type: object
      additionalProperties: false
      required: [der64]
      properties:
        der64:
          $ref: '#/components/schemas/NonEmptyBase64'

    CalculatedSignature:
      type: object
      additionalProperties: false
      required: [type, generation, imposters]
      properties:
        type:
          $ref: '#/components/schemas/SignatureScheme'
        signature64:
          allOf:
            - $ref: '#/components/schemas/NonEmptyBase64'
          nullable: true
        generation:
          type: integer
          format: int32
        imposters:
          type: array
          description: Peers identified as bad during threshold protocols. Empty in mono mode.
          items:
            type: string

    SignedTransaction:
      type: object
      additionalProperties: false
      description: EVM, TRON, or XRP composed transaction.
      required: [type, rawTransaction, transactionHash, generation, imposters]
      properties:
        type:
          type: string
          enum: [evm.transaction, tron.transaction, xrp.transaction]
        rawTransaction:
          type: string
          description: Signed hex for EVM and XRP; signed JSON for TRON.
        transactionHash:
          type: string
        generation:
          $ref: '#/components/schemas/Generation'
        imposters:
          type: array
          items:
            type: string

    SignedSolanaTransaction:
      type: object
      additionalProperties: false
      required: [type, rawTransaction, transactionHash, complete, generation, imposters]
      properties:
        type:
          type: string
          enum: [solana.transaction]
        rawTransaction:
          type: string
          description: Signed or partially signed transaction encoded as Base64.
        transactionHash:
          type: string
          nullable: true
          description: Available when the transaction is complete.
        complete:
          type: boolean
        generation:
          $ref: '#/components/schemas/Generation'
        imposters:
          type: array
          items:
            type: string

    PaymentCredential:
      type: object
      additionalProperties: false
      required: [type, credential, generation, imposters]
      properties:
        type:
          type: string
          enum: [ap2.mandate, mcintent.mandate]
        credential:
          type: string
          description: Signed JWS with the referenced SD-JWT disclosures.
        generation:
          $ref: '#/components/schemas/Generation'
        imposters:
          type: array
          items:
            type: string

    McpRequest:
      type: object
      required: [jsonrpc, id, method, params]
      properties:
        jsonrpc:
          type: string
          enum: ['2.0']
        id:
          oneOf:
            - type: string
            - type: integer
          description: JSON-RPC request id; notifications are not supported.
        method:
          type: string
          enum: [server/discover, tools/list, tools/call]
        params:
          type: object
          required: [_meta]
          description: '`tools/call` also requires `name` and an object `arguments`.'
          properties:
            name:
              type: string
            arguments:
              type: object
            _meta:
              type: object
              required: [io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities]
              properties:
                io.modelcontextprotocol/protocolVersion:
                  type: string
                  enum: ['2026-07-28']
                io.modelcontextprotocol/clientCapabilities:
                  type: object
      example:
        jsonrpc: '2.0'
        id: 1
        method: tools/list
        params:
          _meta:
            io.modelcontextprotocol/protocolVersion: '2026-07-28'
            io.modelcontextprotocol/clientCapabilities: {}

    McpResponse:
      oneOf:
        - $ref: '#/components/schemas/McpSuccessResponse'
        - $ref: '#/components/schemas/McpErrorResponse'

    McpSuccessResponse:
      type: object
      required: [jsonrpc, id, result]
      properties:
        jsonrpc:
          type: string
          enum: ['2.0']
        id:
          description: Echoes the string or integer request id.
        result:
          type: object
          required: [resultType, _meta]
          description: |
            Discovery returns `supportedVersions`, `capabilities`, `ttlMs`, and `cacheScope`.
            Tool listing returns `tools`, `ttlMs`, and `cacheScope`. Tool calls return `content`,
            `structuredContent`, and `isError`; tool failures set `isError` to `true`.
          properties:
            resultType:
              type: string
              enum: [complete]
            _meta:
              type: object
              description: Includes `io.modelcontextprotocol/serverInfo`.
            supportedVersions:
              type: array
              items:
                type: string
            capabilities:
              type: object
            tools:
              type: array
              items:
                type: object
            ttlMs:
              type: integer
              enum: [0]
            cacheScope:
              type: string
              enum: [private]
            content:
              type: array
              items:
                type: object
                required: [type, text]
                properties:
                  type:
                    type: string
                    enum: [text]
                  text:
                    type: string
            structuredContent:
              description: Tool-specific JSON result or error object.
            isError:
              type: boolean

    McpErrorResponse:
      type: object
      required: [jsonrpc, id, error]
      properties:
        jsonrpc:
          type: string
          enum: ['2.0']
        id:
          description: Request id, or null when the request could not be parsed.
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: integer
            message:
              type: string
            data:
              type: object

    VerifyResult:
      type: object
      required: [valid]
      properties:
        valid:
          type: boolean

    CipherType:
      type: string
      enum: [AES_GCM, CHACHA20_POLY1305]

    Encrypt:
      type: object
      additionalProperties: false
      description: ECIES encrypt request. Encryption uses public key material.
      required: [keyId, plaintext64]
      properties:
        keyId:
          $ref: '#/components/schemas/KeyId'
        algorithm:
          $ref: '#/components/schemas/CipherType'
        plaintext64:
          $ref: '#/components/schemas/Base64'
        tweak:
          allOf:
            - $ref: '#/components/schemas/Tweak'
          nullable: true

    Encrypted:
      type: object
      required: [ciphertext64, generation]
      properties:
        ciphertext64:
          $ref: '#/components/schemas/NonEmptyBase64'
        generation:
          $ref: '#/components/schemas/Generation'

    Decrypt:
      type: object
      additionalProperties: false
      description: ECIES decrypt request. Mono mode decrypts locally; threshold mode uses peer partial decrypts.
      required: [keyId, ciphertext64]
      properties:
        keyId:
          $ref: '#/components/schemas/KeyId'
        algorithm:
          $ref: '#/components/schemas/CipherType'
        generation:
          allOf:
            - $ref: '#/components/schemas/Generation'
          nullable: true
        ciphertext64:
          $ref: '#/components/schemas/NonEmptyBase64'
        tweak:
          allOf:
            - $ref: '#/components/schemas/Tweak'
          nullable: true
        approvals:
          $ref: '#/components/schemas/Approvals'

    Decrypted:
      type: object
      required: [plaintext64, imposters]
      properties:
        plaintext64:
          $ref: '#/components/schemas/Base64'
        imposters:
          type: array
          description: Peers that returned invalid partial decrypt proofs. Empty in mono mode.
          items:
            type: string

    AssetInventoryPage:
      type: object
      required: [inventory, hasMore]
      properties:
        inventory:
          $ref: '#/components/schemas/AssetInventory'
        nextCursor:
          type: string
          nullable: true
        hasMore:
          type: boolean

    AssetInventory:
      type: object
      required: [generatedAt, peerId, threshold, totalPeers, items]
      properties:
        generatedAt:
          type: integer
          format: int64
        peerId:
          type: integer
          format: int32
        threshold:
          type: integer
          format: int32
        totalPeers:
          type: integer
          format: int32
        items:
          type: array
          items:
            $ref: '#/components/schemas/AssetInventoryItem'

    AssetInventoryItem:
      type: object
      required:
        - logicalId
        - status
        - currentGeneration
        - authorities
        - createdAt
        - updatedAt
        - hasActiveKey
        - tampered
      properties:
        logicalId:
          type: string
        status:
          $ref: '#/components/schemas/KeyStatus'
        currentGeneration:
          type: integer
          format: int32
        authorities:
          type: array
          items:
            $ref: '#/components/schemas/KeySetAuthority'
        algorithm:
          allOf:
            - $ref: '#/components/schemas/KeyAlgorithm'
          nullable: true
        createdAt:
          type: integer
          format: int64
        updatedAt:
          type: integer
          format: int64
        policy:
          allOf:
            - $ref: '#/components/schemas/KeySetPolicy'
          nullable: true
        hasActiveKey:
          type: boolean
        lastPendingGeneration:
          type: integer
          format: int32
          nullable: true
        assetOwner:
          type: string
          nullable: true
        tampered:
          type: boolean

    KeyStatus:
      type: string
      description: 'Shows key current status, where UNKNOWN means key is tampered'
      enum: [ACTIVE, DISABLED, APPLY_EXPIRED, EXPIRED, DESTROYED, UNKNOWN]

    SignedLine:
      type: object
      additionalProperties: false
      required: [event, signature]
      properties:
        event:
          $ref: '#/components/schemas/AuditEvent'
        signature:
          $ref: '#/components/schemas/NonEmptyBase64'

    Logs:
      type: object
      additionalProperties: false
      required: [logs]
      properties:
        logs:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            $ref: '#/components/schemas/SignedLine'
          default: []

    AuditEvent:
      type: object
      additionalProperties: false
      required: [id, peerId, integrityKeyVersion, timestamp, event]
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 255
        peerId:
          type: integer
          format: int32
          minimum: 1
          maximum: 255
        integrityKeyVersion:
          type: integer
          format: int32
          minimum: 1
        timestamp:
          type: integer
          format: int64
          minimum: 0
        event:
          type: string
          minLength: 1
          maxLength: 255
        auth:
          allOf:
            - $ref: '#/components/schemas/Auth'
          nullable: true
        context:
          allOf:
            - $ref: '#/components/schemas/Ctx'
          nullable: true
        request:
          allOf:
            - $ref: '#/components/schemas/Http'
          nullable: true
        crypto:
          allOf:
            - $ref: '#/components/schemas/Crypto'
          nullable: true
        digest:
          allOf:
            - $ref: '#/components/schemas/Dig'
          nullable: true
        outcome:
          allOf:
            - $ref: '#/components/schemas/Outcome'
          nullable: true
        approvers:
          type: array
          nullable: true
          items:
            type: string
        policy:
          allOf:
            - $ref: '#/components/schemas/PolicyEvaluation'
          nullable: true
        imposters:
          type: array
          nullable: true
          items:
            type: string
        dead:
          type: array
          nullable: true
          items:
            type: string

    Auth:
      type: object
      additionalProperties: false
      required: [subject]
      properties:
        subject:
          type: string
          minLength: 1
        actor:
          type: string
          minLength: 1
          nullable: true
          description: Original external actor when subject is an authenticated keeper peer

    Ctx:
      type: object
      additionalProperties: false
      properties:
        sid:
          type: string
          nullable: true

    Http:
      type: object
      additionalProperties: false
      properties:
        method:
          type: string
          nullable: true
        path:
          type: string
          nullable: true
        remoteAddress:
          type: string
          nullable: true

    Crypto:
      type: object
      additionalProperties: false
      properties:
        algo:
          type: string
          nullable: true
        kid:
          type: string
          nullable: true
        generation:
          allOf:
            - $ref: '#/components/schemas/Generation'
          nullable: true

    Dig:
      type: object
      additionalProperties: false
      properties:
        purpose:
          type: string
          nullable: true
        hmacKeyVersion:
          type: integer
          format: int32
          minimum: 1
          nullable: true
        bodyHash:
          allOf:
            - $ref: '#/components/schemas/Digest'
          nullable: true

    Digest:
      type: object
      additionalProperties: false
      required: [alg, value64]
      properties:
        alg:
          type: string
          minLength: 1
        value64:
          $ref: '#/components/schemas/NonEmptyBase64'

    Outcome:
      type: object
      additionalProperties: false
      properties:
        statusCode:
          type: integer
          format: int32
          minimum: 100
          maximum: 599
          nullable: true
        error:
          allOf:
            - $ref: '#/components/schemas/ErrorType'
          nullable: true
        details:
          type: string
          nullable: true
        approvals:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/RequiredApprovalDetails'

    RequiredApprovalDetails:
      type: object
      additionalProperties: false
      required: [policyId, source, threshold]
      properties:
        policyId:
          type: string
          minLength: 1
        source:
          type: string
          minLength: 1
        threshold:
          type: integer
          format: int32
          minimum: 1

    PolicyEvaluation:
      type: object
      additionalProperties: false
      required: [decision, matches, approvalRequirements]
      properties:
        decision:
          type: string
          enum: [ALLOW, ALLOW_WITH_REQUIREMENTS, DENY]
        matches:
          type: array
          items:
            $ref: '#/components/schemas/RuleMatch'
        approvalRequirements:
          type: array
          items:
            $ref: '#/components/schemas/ApprovalRequirement'

    ApprovalRequirement:
      type: object
      additionalProperties: false
      required: [policyId, source, threshold, approvers]
      properties:
        policyId:
          type: string
          minLength: 1
        source:
          type: string
          minLength: 1
        threshold:
          type: integer
          format: int32
          minimum: 1
        approvers:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/PolicyApprover'

    PolicyApprover:
      type: object
      additionalProperties: false
      required: [algorithm, publicKey64, metadata]
      properties:
        algorithm:
          $ref: '#/components/schemas/KeyAlgorithm'
        publicKey64:
          $ref: '#/components/schemas/NonEmptyBase64'
        metadata:
          type: object
          additionalProperties:
            type: string

    RuleMatch:
      type: object
      additionalProperties: false
      required: [id, effect]
      properties:
        id:
          type: string
          minLength: 1
        effect:
          type: string
          enum: [ALLOW, ALLOW_WITH_REQUIREMENTS, DENY]

    ConsistencyCheck:
      type: object
      required: [verdict, versions]
      properties:
        verdict:
          $ref: '#/components/schemas/ConsistencyVerdict'
        targetGeneration:
          type: integer
          format: int32
          nullable: true
        versions:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/KeyVersion'

    ConsistencyVerdict:
      type: string
      enum: [OK, SYNC_NEEDED, ROTATE_NEEDED, RECOVER_NEEDED, MISSING]

    RecoverRequest:
      type: object
      additionalProperties: false
      required: [keyId, helperIds]
      properties:
        keyId:
          $ref: '#/components/schemas/KeyId'
        helperIds:
          type: array
          description: Operator-selected canonical list of exactly `threshold` remote peer IDs.
          minItems: 1
          items:
            type: integer
            format: int32
            minimum: 1

    RecoverResult:
      type: object
      required: [keyId, algorithm, targetPeerId, currentGeneration, recoveredGenerations, helpers]
      properties:
        keyId:
          $ref: '#/components/schemas/KeyId'
        algorithm:
          type: string
          description: Algorithm selected from the helper consensus.
        targetPeerId:
          type: integer
          format: int32
        currentGeneration:
          $ref: '#/components/schemas/Generation'
        recoveredGenerations:
          type: array
          items:
            $ref: '#/components/schemas/Generation'
        helpers:
          type: array
          items:
            type: integer
            format: int32
            minimum: 1

    KeyVersion:
      type: object
      properties:
        activeGen:
          type: integer
          format: int32
          nullable: true
        pendingGen:
          type: integer
          format: int32
          nullable: true

    ExpirePage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ExpireItem'
        next:
          type: string
          nullable: true

    ExpireItem:
      type: object
      required: [type, logicalId, generation, expiresAt]
      properties:
        type:
          type: string
          enum: [APPLY, PROCESS]
        logicalId:
          type: string
        generation:
          type: integer
          format: int32
        expiresAt:
          type: integer
          format: int64

    ControlPlaneAuthConfig:
      oneOf:
        - $ref: '#/components/schemas/HeaderTokenAuthConfig'
        - $ref: '#/components/schemas/OIDCAuthConfig'
      discriminator:
        propertyName: id
        mapping:
          TOKEN: '#/components/schemas/HeaderTokenAuthConfig'
          OIDC: '#/components/schemas/OIDCAuthConfig'

    ControlPlaneCapabilities:
      type: object
      required: [algorithms]
      properties:
        algorithms:
          type: array
          uniqueItems: true
          description: Key algorithms registered by the platforms included in this build.
          items:
            $ref: '#/components/schemas/KeyAlgorithm'

    HeaderTokenAuthConfig:
      type: object
      required: [id, header]
      properties:
        id:
          type: string
          enum: [TOKEN]
        header:
          type: string

    OIDCAuthConfig:
      type: object
      required: [id, header, clientId, audience, discoveryUrl, callbackUrl]
      properties:
        id:
          type: string
          enum: [OIDC]
        header:
          type: string
        clientId:
          type: string
        audience:
          type: string
        discoveryUrl:
          type: string
        callbackUrl:
          type: string

    SystemInfo:
      type: object
      required: [id, state, threshold, totalPeers, peers]
      properties:
        id:
          type: string
        state:
          $ref: '#/components/schemas/PeerState'
        threshold:
          type: integer
          format: int32
        totalPeers:
          type: integer
          format: int32
        peers:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/PeerState'

    PeerState:
      type: string
      enum: [READY, NOT_READY, UNAVAILABLE]

    AuditInfo:
      type: object
      required: [enabled, sinks]
      properties:
        enabled:
          type: boolean
        sinks:
          type: array
          items:
            $ref: '#/components/schemas/SinkInfo'

    SinkInfo:
      type: object
      required: [id, available]
      properties:
        id:
          type: string
        available:
          type: boolean

    SimpleAuthData:
      type: object
      required: [subject, actor, permissions]
      properties:
        subject:
          type: string
        actor:
          type: string
        permissions:
          type: array
          items:
            type: string
```
