# 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.
- 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.
- 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
- 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: docs/README.md
Canonical: https://tkeeper.org/docs

# TKeeper Docs

TKeeper gives machines, agents, services, and workflows a cryptographic identity whose use is constrained by explicit authorities and policy.

## Read first

- [Overview](overview/README.md)
- [Use Cases](use-cases/README.md)
- [Getting Started](getting-started/README.md)
- [Deployment](deployment/README.md)
- [Security Model](security-model/README.md)
- [Cryptographic Identities](key-management/README.md)
- [Signing and Authorities](signing-and-authorities/README.md)
- [Crypto Platforms](crypto-platforms/README.md)
- [API Reference](api-reference/README.md)
- [Operations](operations/README.md)

## Common paths

| Goal | Start here |
| --- | --- |
| Understand the product | [What is TKeeper?](overview/what-is-tkeeper.md) |
| Map it to your use case | [Use Cases](use-cases/README.md) |
| Run it locally | [Local Single Node](getting-started/local-single-node.md) |
| Build an artifact | [Build and Features](deployment/build-and-features.md) |
| Design backup and recovery | [Backup and Recovery](deployment/backup-and-recovery.md) |
| Choose mono or threshold | [Quorum Modes](security-model/quorum-modes.md) |
| Create and govern identities | [Create, Rotate, and Refresh](key-management/key-lifecycle.md) |
| Govern signing | [Authorities](signing-and-authorities/authorities.md) |
| Select cryptographic platforms | [Platforms](crypto-platforms/platforms.md) |
| Use the HTTP API | [OpenAPI](api-reference/openapi.md) |
| Run integration tests | [Integration Tests](operations/integration-tests.md) |
| Review executable security evidence | [Security Assurance](security-model/security-assurance.md) |

## By audience

| Reader | Recommended path |
| --- | --- |
| Application integrator | [Getting Started](getting-started/README.md) -> [Signing](signing-and-authorities/signing.md) -> [Java SDK](api-reference/sdk.md) or [OpenAPI](api-reference/openapi.md) |
| Security reviewer | [Security Model](security-model/README.md) -> [Threat Model](security-model/threat-model.md) -> [Security Assurance](security-model/security-assurance.md) -> [Status and Limitations](overview/status-and-limitations.md) |
| Platform operator | [Deployment](deployment/README.md) -> [Production Checklist](deployment/production-checklist.md) -> [Operations](operations/README.md) |
| Cryptography reviewer | [Crypto Platforms](crypto-platforms/README.md) -> [Quorum Modes](security-model/quorum-modes.md) -> [Anvil threat model](https://github.com/exploit-org/anvil/blob/main/THREAT_MODEL.md) |


---

Source: 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?](what-is-tkeeper.md)
- [Governed Cryptographic Identity](governed-cryptographic-identity.md)
- [Use Cases](use-cases.md)
- [Architecture](architecture.md)
- [Status and Limitations](status-and-limitations.md)
- [Glossary](glossary.md)

## 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](../getting-started/README.md)
- choose a deployment shape in [Quorum Modes](../security-model/quorum-modes.md)
- select build modules in [Build and Features](../deployment/build-and-features.md)
- review guarantees and non-goals in [Threat Model](../security-model/threat-model.md)


---

Source: 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: 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: 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 agent | typed tool call, payment, or production action | tool backend or workflow engine | model security, sandboxing, or risk detection |
| Crypto assets | exact EVM or Bitcoin transaction | chain client, broadcaster, or custody backend | transaction construction, broadcast, or settlement monitoring |
| Certificates | DER-encoded TBS certificate | relying party or CA pipeline | enrollment, serial allocation, revocation, or certificate publication |
| Internal systems | typed privileged command | service that performs the command | business logic or host authorization |

Detailed integration guidance:

- [For AI Agents](../use-cases/for-ai-agents.md)
- [For Crypto Assets](../use-cases/for-crypto-assets.md)
- [For Certificates](../use-cases/for-certificates.md)
- [For Internal Systems](../use-cases/for-internal-systems.md)

## 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: 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 |
| --- | --- |
| `authority-evm` | EVM transaction intent support |
| `authority-bitcoin` | Bitcoin transaction intent support |
| `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. For example, EVM, Bitcoin, X.509, and ECIES currently 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: 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

## 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](../deployment/build-and-features.md)

## 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 production feature, every platform, and the test-only failure-injection module. Regular production builds do not include failure injection.

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: 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: `authority-evm`, `authority-bitcoin`, `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: docs/use-cases/README.md
Canonical: https://tkeeper.org/docs/use-cases

# Use Cases

Every supported use case has the same enforcement shape:

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

Read:

- [For AI Agents](for-ai-agents.md)
- [For Crypto Assets](for-crypto-assets.md)
- [For Certificates](for-certificates.md)
- [For Internal Systems](for-internal-systems.md)

The integration is a good fit when:

- the action has real consequences
- the action can be represented as an intent
- the downstream system can verify proof before execution
- bypassing the governed identity is not allowed

If the action can happen without the proof, TKeeper is not enforcing that path. If the goal is only secret storage, use a secrets manager.


---

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

# For AI Agents

Use TKeeper when an AI agent or autonomous workflow can request actions with real consequences and the executing backend can require cryptographic authorization.

The agent should not receive a raw signing key and should not get arbitrary signing for whatever JSON it emits. The expected tool or action shape should be declared as an authority document attached to the key identity.

## Model

```text
agent proposes typed action
-> TKeeper materializes intent
-> policy and external verdicts approve or deny
-> TKeeper signs exact approved intent
-> backend verifies proof before executing
```

Good candidates:

- tool calls that change production state
- spending or payment actions
- sensitive internal operations
- signed agent decisions
- actions requiring human approval

## What TKeeper enforces

TKeeper does not decide whether every AI output is safe. External systems can detect prompt injection, classify risk, or require human approval.

TKeeper enforces the final boundary: the agent identity cannot produce proof unless the typed action is accepted.

## Authority shape

Use `custom` typed authorities for agent-specific actions. The authority document should define:

- accepted fields
- effect extraction
- policy variables
- allow and deny rules

Avoid `arbitrary` for agent tool calls unless raw signing is explicitly accepted by the security model.

Treat the authority document as the agent identity's capability manifest. A change to the tool schema, effect mapping, or policy is a security change and should produce a new digest-pinned artifact.

## Integration contract

The tool backend should:

- accept only the expected agent identity or public key
- reconstruct the same typed action that TKeeper approved
- reject action fields that were not covered by the authority schema
- bind the action to its target environment and resource
- enforce expiry, nonce, or idempotency rules for repeatable tool calls
- execute only after proof verification succeeds

A signature does not prove that the model was safe, that its reasoning was correct, or that prompt injection did not occur. It proves authorization by the governed identity for the signed action.

## Bypass condition

```text
agent can perform the same action directly without the governed identity
```

In that topology, TKeeper can record or advise but cannot prevent the direct action.


---

Source: docs/use-cases/for-crypto-assets.md
Canonical: https://tkeeper.org/docs/use-cases/crypto-assets

# For Crypto Assets

Use TKeeper when transaction signing must depend on an understood transaction, policy, and optionally external risk or human verdicts.

Flow:

```text
transaction intent
-> authority policy
-> optional external verdicts
-> quorum signing
-> transaction signature
```

Good candidates:

- EVM transaction signing
- Bitcoin transaction signing
- treasury operations
- spender approvals
- AML, KYT, fraud, or business verdicts before signing
- threshold custody where one peer must not be enough

## Integration contract

The transaction builder remains responsible for nonce or UTXO selection, fee strategy, and constructing the final unsigned transaction. TKeeper parses the transaction supported by the selected authority, exposes normalized effects to policy, and returns a signature after approval.

The broadcaster or custody backend should:

- verify the signature against the expected key identity
- broadcast exactly the transaction that was approved
- prevent unsigned metadata from changing the transaction's meaning
- bind external AML, KYT, fraud, or business verdicts to the same transaction intent
- handle replacement, confirmation, and settlement state outside TKeeper

TKeeper does not broadcast transactions or prove that a confirmed transaction achieved the intended business outcome.

Relevant docs:

- [EVM Authorities](../signing-and-authorities/evm.md)
- [Bitcoin Authorities](../signing-and-authorities/bitcoin.md)
- [Quorum Modes](../security-model/quorum-modes.md)


---

Source: docs/use-cases/for-certificates.md
Canonical: https://tkeeper.org/docs/use-cases/certificates

# For Certificates

Use TKeeper when use of a CA key must depend on an understood TBS certificate and explicit issuance policy.

Flow:

```text
certificate request
-> X.509 authority policy
-> optional four-eye approval
-> CA key signature
-> certificate accepted by downstream systems
```

Good candidates:

- internal CA operations
- workload identity certificates
- service certificates
- agent certificates
- high-risk certificate requests requiring approval

The boundary is the CA key identity. If issuing a certificate requires TKeeper, certificate creation can be tied to permissions, authority policy, quorum mode, and audit.

## Integration contract

The CA or enrollment service constructs the DER-encoded TBS certificate. TKeeper exposes supported certificate fields to policy and returns the CA signature after approval.

The surrounding PKI remains responsible for:

- authenticating and authorizing the enrollment request
- proof-of-possession checks where required
- serial-number allocation and uniqueness
- assembling and publishing the final certificate
- certificate transparency, revocation, renewal, and inventory

Relying systems must trust the expected CA identity and validate the finished certificate. A TKeeper signature does not make an otherwise invalid certificate acceptable.

Relevant docs:

- [X.509 Authorities](../signing-and-authorities/x509.md)
- [Four Eye Control](../security-model/four-eye-control.md)
- [Audit Logging](../security-model/audit-logging.md)


---

Source: docs/use-cases/for-internal-systems.md
Canonical: https://tkeeper.org/docs/use-cases/internal-systems

# For Internal Systems

Use TKeeper when an internal system should execute a sensitive action only after verifying authorization by a specific machine or workflow identity.

Flow:

```text
internal command
-> typed authority
-> policy
-> signature proof
-> backend verifies proof and executes
```

Good candidates:

- production maintenance actions
- typed service-to-service commands
- protected data export approval
- break-glass workflows
- privileged automation
- custom business approvals

Usually this uses `custom` typed authorities. The authority document defines the command shape and effects exposed to policy.

## Integration contract

The executing service should define a canonical command that contains every field that changes the effect. Typical context includes the operation, target resource, environment, requester, expiry, and nonce.

The service must then:

- accept only the expected TKeeper identity
- verify proof over the exact command it will execute
- reject or ignore no security-relevant unsigned fields
- enforce replay and idempotency rules
- keep any alternate administrative path at least as strongly controlled

For `custom` authorities, only declared fields become policy inputs. Do not let the backend act on extra JSON fields that TKeeper did not govern.

Relevant docs:

- [Arbitrary and Typed Authorities](../signing-and-authorities/arbitrary-and-typed.md)
- [Authorities](../signing-and-authorities/authorities.md)
- [Permissions](../api-reference/permissions.md)


---

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

# Getting Started

Start with:

- [Local Single Node](local-single-node.md)

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](../signing-and-authorities/authorities.md)
- [Quorum Modes](../security-model/quorum-modes.md)
- [Build and Features](../deployment/build-and-features.md)
- [Authentication and Authorization](../security-model/authentication-authorization.md)


---

Source: 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.3.1.jar
```

## Create local config

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

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

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

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

  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.3.1.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](../signing-and-authorities/authorities.md) 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](../security-model/quorum-modes.md) before creating high-risk keys.
- Use [Build and Features](../deployment/build-and-features.md) to select only the features and platforms your deployment needs.
- Use [Authentication and Authorization](../security-model/authentication-authorization.md) before leaving local development.


---

Source: 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](build-and-features.md)
- [Configuration](configuration.md)
- [Initialization and Unseal](initialization-and-unseal.md)
- [Sealing and Unsealing](sealing-unsealing.md)
- [Clustering](clustering.md)
- [Backup and Recovery](backup-and-recovery.md)
- [Control Plane UI](control-plane-ui.md)
- [Production Checklist](production-checklist.md)

## 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 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: 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 all 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.3.1.jar
```

TKeeper requires Java 25.

## Build a smaller artifact

Example: EVM signing, ECIES, and the UI:

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

Example: ML-DSA only:

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

Feature names match child project names. The module `:features:authority-evm` is selected with `authority-evm`.

## Feature and platform matrix

| Need | Feature selector | Platform selector |
| --- | --- | --- |
| EVM transaction authority | `authority-evm` | `ecc` |
| Bitcoin transaction authority | `authority-bitcoin` | `ecc` |
| X.509 certificate authority | `authority-x509` | `ecc` |
| ECIES | `ecies` | `ecc` |
| 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 |
| 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.

`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
```

## 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 production module in that category.

## Docker

Build the production Docker image:

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

Production image tags:

```text
exploit/tkeeper:2.3.1
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.3.1
```

## 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 production feature, `auth-dev`, 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.

### No provider for algorithm

The platform was not included in the artifact.

Rebuild with the required platform, for example:

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

### Native access warning

Add:

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

The Docker image already does this.


---

Source: 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.3.1.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.approval.ttl` | Four-eye approval lifetime and persistent nonce replay-retention window |
| `keeper.session.*` | DKG, FROST, GG20, ML-DSA, ECIES, destroy session limits |

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.

## 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](../security-model/audit-logging.md).

## 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](control-plane-ui.md).

## 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: 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](../key-management/quorum-promotion.md).

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: 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: 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](../operations/troubleshooting.md) 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: 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.

## 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: 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.3.1.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: 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](backup-and-recovery.md) procedure before go-live.

## Authorities

- [ ] Use structured authorities for governed identities.
- [ ] 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: 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](threat-model.md)
- [Security Assurance](security-assurance.md)
- [Quorum Modes](quorum-modes.md)
- [Authentication and Authorization](authentication-authorization.md)
- [Audit Logging](audit-logging.md)
- [Four Eye Control](four-eye-control.md)

## 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: 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.md](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: docs/security-model/security-assurance.md
Canonical: https://tkeeper.org/docs/security-model/security-assurance

# Security Assurance

TKeeper security assurance currently comprises **346 automated scenarios**,
including **81 failure-injection scenarios**, executed against multi-node
Keeper deployments.

The Testcontainers topology runs a 2-of-3 quorum with peer
communication, storage, SoftHSM, restarts, and malicious protocol injection. A
three-node production topology exercises TLS, mTLS, JWT/JWKS, peer
authentication, and SPKI pinning with per-run PKI.

Every pull request targeting `main` and every commit pushed to `main` runs the
Release Gate. A passing revision completes all 346 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, concurrent duplicate delivery, crash-safe
> session cleanup, audit persistence, recovery after rejected contributions,
> 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 and
  failure-injection capabilities are verified absent from production artifacts.

## 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, and ECIES flows. | 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 and ML-DSA transcript-mutation case is followed by a distributed signature and verification. | Confirms that the stored key and cluster remain usable after rejection. |
| 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 coverage-guide participant validation, FROST share, GG20 MtA, and ML-DSA round-store transitions. | Compares protocol state containers against explicit legal-transition, uniqueness, operation-isolation, order-independence, single-winner consumption, and destroy-state models. |
| Concurrency and crash recovery | Eight simultaneous signing-package deliveries race for one session id on each threshold protocol. Container-restart cases stop a keeper after FROST nonce generation, GG20 ephemeral initialization, or ML-DSA round 1. | Proves exactly one session creator wins, stale round state cannot resume after restart, durable key state survives, the same session id can be safely recreated, and a signature still verifies. |
| Release isolation | The release gate checks module tests, functional behavior, container builds, and separation of integration-only code from production artifacts. | Prevents the security test harness from becoming a production attack surface. |

## Assurance by domain

| Security domain | Demonstrated assurance | Detailed evidence |
| --- | --- | --- |
| Threshold protocol integrity | Malformed signer sets, sequential or concurrent session replay, invalid proofs, transcript mutations, bad partial contributions, and consumed-state reuse fail closed across 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 quorum shares cannot produce a signature. | [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.

## 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](../operations/integration-tests.md).

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'
./gradlew :platform-pqc:test \
  --tests 'org.exploit.keeper.platform.pqc.property.MLDSAStateMachineProperties' \
  --tests 'org.exploit.keeper.platform.pqc.fuzz.MLDSAStateMachineFuzzTest'
./gradlew :test \
  --tests 'org.exploit.keeper.tests.temporary.InMemoryTemporaryMapConcurrencyTest'
./gradlew securityFuzz
```

## 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](threat-model.md) 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](threat-model.md). 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] |
| 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` |

### 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. Empty, duplicate, malformed, missing-reference, reference/id-mismatched, unsupported, command-unattached, and artifact-type-mismatched authorities fail closed. | [AuthorityPolicyTests]: `rejectsAuthorityThatIsNotAllowedForKey`, `rejectsArtifactTypeThatDoesNotMatchAuthorityType`, `rejectsGenerateWithArbitraryMixedWithOciAuthority`, `rejectsDuplicateAuthorityIdsOnGenerate`, `rejectsGenerateWithEmptyAuthorities`, `rejectsGenerateWithInvalidAuthorityId`, `rejectsOciAuthorityWithoutReference`, `rejectsOciReferenceWhoseAuthorityIdDoesNotMatch`, `rejectsUnsupportedAuthorityTypeBeforeCreatingKey`, `verifyRejectsMalformedEvmMaterialWithoutResolvingAuthority` |
| 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` |
| 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` |
| Failure-injection isolation | Development authentication and the failure-injection module 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
[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
[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
[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
[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
[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: 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](../key-management/quorum-promotion.md).

## 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: 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: 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: 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: 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](key-lifecycle.md)
- [Trusted Dealer Import](trusted-dealer-import.md)
- [Quorum Promotion](quorum-promotion.md)
- [Asset Inventory](asset-inventory.md)

## 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: 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: 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: 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: 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](../deployment/control-plane-ui.md).

`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: docs/signing-and-authorities/README.md
Canonical: https://tkeeper.org/docs/signing-and-authorities

# Signing and Authorities

Signing is an identity action. TKeeper produces a signature only after the requested action is understood through an authority and allowed by policy.

Read:

- [Signing](signing.md)
- [Authorities](authorities.md)
- [CEL Functions](cel-functions.md)
- [Arbitrary and Typed Authorities](arbitrary-and-typed.md)
- [EVM Authorities](evm.md)
- [Bitcoin Authorities](bitcoin.md)
- [X.509 Authorities](x509.md)

## Core rule

```text
command -> authority -> intent -> policy -> proof
```

Use `arbitrary` only when raw signing is intentional. Use concrete authorities when TKeeper must understand and govern the action.

The consumer remains part of the security boundary: it must trust the expected identity, verify the exact intent, and prevent replay where the action requires freshness.


---

Source: 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
```

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.3.1",
        "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](authorities.md#custom-authority-example) for the matching schema, payload, effects, and policy. EVM, Bitcoin, and X.509 commands are documented on their intent-specific pages.

## 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.

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: 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](../security-model/quorum-modes.md) and the [Threat Model](../security-model/threat-model.md) 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
- `arbitrary` does not use an OCI reference
- `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.3.1"
    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.3.1",
        "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`](arbitrary-and-typed.md) | core | typed JSON | declared fields and configured `effects` |
| [`evm.transaction`](evm.md) | `authority-evm` | unsigned serialized EVM transaction | transaction fields, decoded call, `effects` |
| [`bitcoin.transaction`](bitcoin.md) | `authority-bitcoin` | unsigned tx, previous txs, signing input, sighash | inputs, outputs, fee, sighash, `effects` |
| [`x509.tbs-certificate`](x509.md) | `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=authority-evm,authority-bitcoin,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.3.1",
  "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](../security-model/four-eye-control.md) 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](cel-functions.md) 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](arbitrary-and-typed.md). CEL helpers are documented in [CEL functions](cel-functions.md).

## 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: 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](authorities.md#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: 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.

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](authorities.md#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.

## 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](authorities.md#strict-cel-roots) and [CEL Functions](cel-functions.md).

## 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 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: docs/signing-and-authorities/evm.md
Canonical: https://tkeeper.org/docs/signing-and-authorities/evm

# EVM Authorities

EVM authority support lives in the `authority-evm` feature and currently requires the `ecc` platform.

Build example:

```bash
./gradlew shadowJar -Pkeeper.features=authority-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.

## 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](authorities.md) for the document and policy schema and [CEL Functions](cel-functions.md) for policy helpers.


---

Source: docs/signing-and-authorities/bitcoin.md
Canonical: https://tkeeper.org/docs/signing-and-authorities/bitcoin

# Bitcoin Authorities

Bitcoin authority support lives in the `authority-bitcoin` feature and currently requires the `ecc` platform.

Build example:

```bash
./gradlew shadowJar -Pkeeper.features=authority-bitcoin -Pkeeper.platforms=ecc
```

A Bitcoin authority lets TKeeper parse unsigned transaction data, previous transactions, signing input, sighash settings, and policy effects before signing.

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:
  protocol: BTC
```

Known protocols are `BTC`, `LTC`, `DASH`, and `BCH`. Custom protocols pin their asset symbol, address versions, Bech32 prefix, and decimals in the authority config.

Effects:

- `utxo.spend`
- `utxo.output`
- `utxo.data`
- `utxo.fee`

Strict CEL roots:

- `protocol`, `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:
  protocol: BTC

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](authorities.md) for the document and policy schema and [CEL Functions](cel-functions.md) for policy helpers.


---

Source: docs/signing-and-authorities/x509.md
Canonical: https://tkeeper.org/docs/signing-and-authorities/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](authorities.md) for the document and policy schema and [CEL Functions](cel-functions.md) for policy helpers.


---

Source: 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](platforms.md)
- [ECC](ecc.md)
- [PQC ML-DSA](pqc-mldsa.md)
- [ECIES](ecies.md)

## 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: 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 |
| --- | --- |
| `authority-evm` | `ecc` |
| `authority-bitcoin` | `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: 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`:

- `authority-evm`
- `authority-bitcoin`
- `authority-x509`
- `ecies`

Build example:

```bash
./gradlew shadowJar -Pkeeper.features=authority-evm,ecies -Pkeeper.platforms=ecc
```


---

Source: 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](../security-model/security-assurance.md).

## 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: 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: docs/api-reference/README.md
Canonical: https://tkeeper.org/docs/api-reference

# API Reference

Read:

- [OpenAPI](openapi.md)
- [Java SDK](sdk.md)
- [Errors](errors.md)
- [Permissions](permissions.md)

The source of truth for request and response models is [`../../openapi.yaml`](../../openapi.yaml).


---

Source: docs/api-reference/openapi.md
Canonical: https://tkeeper.org/docs/api-reference/openapi

# OpenAPI

The OpenAPI file is the source of truth:

- [`../../openapi.yaml`](../../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: 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](../../sdk/README.md)
- Maven coordinate: `org.exploit:tkeeper-sdk:2.3.1`
- Java toolchain: 17 or newer

The SDK follows the same module boundaries as the API: system, DKG, signing, storage/import, quorum promotion, ECIES, compliance, expiration, audit, integrity, consistency, destroy, and control-plane reads.

The wire contract remains [`../../openapi.yaml`](../../openapi.yaml). 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.
- Catch `TKeeperException` and branch on `ErrorType`, not diagnostic detail text.
- Do not retry authorization or policy denials as availability failures.


---

Source: 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](../security-model/four-eye-control.md#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.yaml`](../../openapi.yaml).

## 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: 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.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.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 and quorum promotion to recovery operators.
- Treat control-plane read permissions as sensitive operational metadata.
- 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](../security-model/authentication-authorization.md) for JWT claims and matching behavior.


---

Source: docs/operations/README.md
Canonical: https://tkeeper.org/docs/operations

# Operations

Read:

- [Monitoring](monitoring.md)
- [Troubleshooting](troubleshooting.md)
- [Integration Tests](integration-tests.md)
- [Failure Injection](failure-injection.md)
- [Error Tracking](error-tracking.md)

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](monitoring.md) for the signals and [Troubleshooting](troubleshooting.md) for stage-by-stage diagnosis.


---

Source: 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: 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](../deployment/build-and-features.md).

## 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](../deployment/initialization-and-unseal.md).

## `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](../api-reference/permissions.md).

## `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](../signing-and-authorities/authorities.md).

## `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: 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 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'
./gradlew :platform-pqc:test \
  --tests 'org.exploit.keeper.platform.pqc.property.MLDSAStateMachineProperties' \
  --tests 'org.exploit.keeper.platform.pqc.fuzz.MLDSAStateMachineFuzzTest'
./gradlew :test \
  --tests 'org.exploit.keeper.tests.temporary.InMemoryTemporaryMapConcurrencyTest'
```

Run the coverage-guided FROST/GG20 and ML-DSA state-machine 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-pqc:fuzzMLDSAStateMachine -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 production feature
- development authentication
- every platform
- the test-only failure-injection module

The production transport test image uses the production UBI Dockerfile and excludes development authentication and failure injection. Regular `shadowJar` and `dockerBuild` also exclude failure injection.

See [`../../integration-tests/README.md`](../../integration-tests/README.md) for local requirements and Testcontainers setup.

See [Security Assurance](../security-model/security-assurance.md) for the security vectors exercised by the functional suite and the limits of that evidence.


---

Source: 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: 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.3.1"
}
```

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.3.0
  description: |
    TKeeper public HTTP API.
    JSON request bodies reject unknown properties and are limited to 10 MiB by the HTTP server.

servers:
  - url: /

tags:
  - name: System
  - name: Keys
  - name: DKG
  - name: Signing
  - name: TrustedDealer
  - name: Quorum
  - name: ECIES
  - name: Audit
  - name: Inventory
  - name: ControlPlane
  - name: Consistency
  - 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
      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/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'

  /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/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
      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
        - 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'

    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:
      oneOf:
        - $ref: '#/components/schemas/ArbitraryArtifact'
        - $ref: '#/components/schemas/TypedArtifact'
        - $ref: '#/components/schemas/EvmTransactionArtifact'
        - $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'

    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
      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

    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/ApproverPublicKey'

    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, MISSING]

    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
```
