# 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. ### 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. - 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. - 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 the Threat Model and Status and Limitations pages when they affect the answer. - 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 - 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) | ## 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) -> [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 deploy dev authentication in production 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 all production features and platforms: ```bash ./gradlew :build -Pkeeper.features=all -Pkeeper.platforms=all ``` The jar is: ```text build/libs/tkeeper-2.2.0.jar ``` ## Create local config Create `/tmp/tkeeper/application.conf`: ```hocon auth { type = "dev" } boot { token = "local-boot-token" } keeper { database { path = "/tmp/tkeeper/db" } 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" ] } ``` Developer authentication is for local development only. Do not use it in production. ## 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.2.0.jar ``` ## Initialize the node ```bash curl -s \ -H 'X-DEV-TOKEN: dev-token' \ -H 'Content-Type: application/json' \ -d '{"peerId":1,"threshold":1,"total":1}' \ http://localhost:8080/v1/keeper/system/init ``` With the Shamir provider, the response contains `shares64`. Save the share outside the node. You need it to unseal TKeeper after initialization or restart. ## Unseal Replace `share-from-init` with one value from the `shares64` response. ```bash curl -s \ -H 'X-DEV-TOKEN: dev-token' \ -H 'Content-Type: application/json' \ -d '{"payload64":"share-from-init"}' \ http://localhost:8080/v1/keeper/system/unseal ``` ## Create a demo key identity This creates a local `SECP256K1` identity that can authorize `arbitrary` signing requests. ```bash curl -s \ -H 'X-DEV-TOKEN: dev-token' \ -H 'Content-Type: application/json' \ -d '{ "keyId": "demo-identity", "algorithm": "SECP256K1", "mode": "CREATE", "authorities": [ { "id": "arbitrary" } ] }' \ http://localhost:8080/v2/keeper/dkg ``` For production-like flows, attach a real authority document to the key, such as `custom`, `evm.transaction`, `bitcoin.transaction`, or `x509.tbs-certificate`. Those authorities let TKeeper parse the request into an understood intent and evaluate policy before signing. ## Sign This signs `hello`, base64-encoded as `aGVsbG8=`. ```bash curl -s \ -H 'X-DEV-TOKEN: dev-token' \ -H 'Content-Type: application/json' \ -d '{ "keyId": "demo-identity", "command": { "type": "arbitrary", "authorityId": "arbitrary", "artifact": { "scheme": "ECDSA", "hash": "SHA256", "data64": "aGVsbG8=" } } }' \ http://localhost:8080/v2/keeper/sign ``` The response contains the signature proof: ```json { "signature64": "...", "type": "ECDSA", "generation": 1, "imposters": [] } ``` ## Verify Replace `signature-from-sign-response` with `signature64` from the sign response. ```bash curl -s \ -H 'X-DEV-TOKEN: dev-token' \ -H 'Content-Type: application/json' \ -d '{ "keyId": "demo-identity", "command": { "type": "arbitrary", "authorityId": "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 ``` 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.2.0.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 | | ML-DSA identities | none | `pqc` | | Everything production | `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. ## 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.2.0 exploit/tkeeper:latest ``` The Dockerfile adds the JVM flag required by the FFI Java API: ```text --enable-native-access=ALL-UNNAMED ``` Run the image: ```bash docker run --rm \ -p 8080:8080 \ -p 9090:9090 \ -v "$PWD/config:/etc/tkeeper:ro" \ -v "$PWD/data:/var/lib/tkeeper" \ -e KEEPER_CONFIG_LOCATION=/etc/tkeeper \ exploit/tkeeper:2.2.0 ``` ## Integration image Build the integration image with: ```bash ./gradlew dockerBuildIntegration ``` Do not pass `keeper.features` or `keeper.platforms` to this task. `dockerBuildIntegration` uses a dedicated classpath containing every production feature, every platform, and the test-only failure-injection module. Never deploy the integration 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:...` - `https://...` - `http://...` only when `-Dkeeper.dev.enabled=true` 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. Enable it with: ```bash -Dkeeper.dev.enabled=true -Dkeeper.dev.config.location=/etc/tkeeper ``` 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.2.0.jar ``` Minimal node config: ```hocon auth { type = "dev" } boot { token = "change-me" } keeper { database { path = "/var/lib/tkeeper/db" } providers { selected = "shamir" shamir { total = 5 threshold = 3 } } server { public { host = "0.0.0.0" port = 8080 } internal { host = "0.0.0.0" port = 9090 } } peers = [ { id = 2, internal-url = "http://keeper-2:9090" }, { id = 3, internal-url = "http://keeper-3:9090" } ] } ``` Common fields: | Field | Meaning | | --- | --- | | `keeper.database.path` | RocksDB path | | `keeper.server.public` | API users call this | | `keeper.server.internal` | Peers call this | | `keeper.peers` | Other peers in the cluster; self is omitted | | `keeper.providers.selected` | Seal provider id | | `keeper.client.tls` | TLS for peer clients | | `keeper.approval.ttl` | Four eye approval lifetime | | `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" } ``` Public and internal servers have separate TLS blocks: ```hocon keeper.server.public.tls { enabled = true } keeper.server.internal.tls { enabled = true } ``` 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. Outside dev mode, `jwks-location` must not use plain HTTP. ## 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.2.0.jar ``` ## Authentication The UI uses the same external auth mode as the API. With dev auth, the browser sends: ```text X-DEV-TOKEN ``` With JWT auth, the browser sends: ```text X-JWT-TOKEN ``` If `auth.jwt.oidc` is configured, the UI reads OIDC settings from: ```http GET /v1/keeper/control/auth/config ``` After authentication, the UI discovers the key algorithms included in the running build from: ```http GET /v1/keeper/control/capabilities ``` Key generation, trusted-dealer import, and four-eye approver forms use this runtime list instead of assuming a fixed platform set. ## Content Security Policy The UI feature adds security headers under `/ui`. Default CSP: ```hocon keeper.csp { default-src = ["'self'"] base-uri = ["'self'"] object-src = ["'none'"] frame-ancestors = ["'none'"] script-src = ["'self'"] style-src = ["'self'"] img-src = ["'self'", "data:"] font-src = ["'self'", "data:"] connect-src = ["'self'"] form-action = ["'self'"] } ``` Report-only mode: ```hocon keeper.csp { report-only = true } ``` Extra origins: ```hocon keeper.csp { connect-extra = [ "https://issuer.example" ] form-action-extra = [ "https://issuer.example" ] img-extra = [ "https://assets.example" ] } ``` OIDC origins are added to `connect-src` automatically when: ```hocon keeper.csp { oidc-auto-connect = true } ``` Set it to `false` when `connect-src` is managed manually. ## Headers The UI also sends: ```text Referrer-Policy: no-referrer X-Content-Type-Options: nosniff X-Frame-Options: DENY Permissions-Policy: geolocation=(), microphone=(), camera=() Cross-Origin-Opener-Policy: same-origin Cross-Origin-Resource-Policy: same-origin ``` When public TLS is enabled: ```text Strict-Transport-Security: max-age=31536000; includeSubDomains ``` ## Common problems ### `/ui/` returns 404 Rebuild with `:features:ui`. ### OIDC login cannot reach the issuer Check `keeper.csp.connect-extra` or keep `keeper.csp.oidc-auto-connect = true`. --- Source: 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`. - [ ] 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 - [ ] Disable developer authentication. - [ ] Configure production authentication. - [ ] 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. - [ ] 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) - [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? - 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. 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, and advancing the storage marker are one atomic transaction; mixed legacy and bound state is rejected. 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. Legacy key generations remain readable for compatibility and migrate only through an explicit lifecycle operation; after migration they cannot be selected for historical processing. 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..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. - `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 and request-field binding but do not independently consume its nonce or re-check its age. A compromised coordinator that bypasses its own nonce and TTL checks can therefore replay a previously valid 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 validate and bind only the legacy plaintext currently present; it cannot prove that an unbound ciphertext was not substituted before that migration. 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 writes a new signed generation and never overwrites 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. - Keep dev token mode out of production. - 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/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` | local development only | | `jwt` | `X-JWT-TOKEN` | production deployments | Do not use developer authentication in production. ## Developer authentication Developer auth loads a separate config file from `keeper.dev.config.location`. 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" ] } ``` Enable it only in local or test environments: ```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. 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` plus matched rules. ## 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 is a key policy. It requires `m` distinct approver signatures from `n` configured approver keys before TKeeper continues. ## 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` requires approvals only for `ROTATE` and `REFRESH`. Signing, decrypting, destroying, and other operations do not use this four-eye policy. Authentication, permissions, authority policies, and audit checks still apply. ## 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. The timestamp must not be in the future and must fit `keeper.approval.ttl`. Threshold protocol retries reuse the same approval. Non-coordinator peers therefore verify its signatures and approved request fields without independently consuming the nonce or re-checking its age. If the coordinator is compromised, it can replay a previously valid approval with those same fields; 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)) ``` ## Canonical approval hash TKeeper uses canonical JSON for approval hashes. Canonicalization rules for SDKs and non-Java clients: - 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 JSON array element order exactly as supplied - apply the same object-field sorting to objects inside arrays - keep string values byte-exact, including base64 strings, enum names, nonce, and tweak Do not sort arrays globally. Arrays are ordered data. The approval hash is: ```text sha256(canonical-json-bytes) ``` Approvers sign that 32-byte hash. `approvals.proofs` is not part of the hash. ## Signed fields | Operation | Fields | | --- | --- | | DKG | `keeperId`, `keyId`, `algorithm`, `authorities`, `mode`, optional `policy`, optional `assetOwner`, `nonce`, `timestamp` | | Sign | `keeperId`, `keyId`, `command`, optional `tweak`, `nonce`, `timestamp` | | ECIES decrypt | `keeperId`, `keyId`, optional `generation`, `algorithm`, `ciphertext64`, optional `tweak`, `nonce`, `timestamp` | | Destroy | `keeperId`, `keyId`, `generation`, `nonce`, `timestamp` | The table describes logical fields, not serialization order. Serialization order is defined by canonicalization. ## 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": "eth-cold-storage", "algorithm": "SECP256K1", "mode": "CREATE", "assetOwner": "customer-42", "authorities": [ { "id": "evm-mainnet-usdc", "oci": "oci://registry.example/verdict/authorities/evm-mainnet-usdc@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. `REFRESH` is also the online migration path for legacy key generations. TKeeper reads legacy AEAD-protected material, writes the refreshed generation and its metadata to the signed, location-bound stores, and activates it through the normal pending-generation workflow. The old legacy material remains physically available for consistency rollback and inventory, but it is inactive and cannot be selected for historical processing after migration. A storage read never performs an implicit migration. ## Public key ```http GET /v1/keeper/publicKey?keyId=eth-cold-storage GET /v1/keeper/publicKey?keyId=eth-cold-storage&generation=1 GET /v1/keeper/publicKey?keyId=eth-cold-storage&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` only `ROTATE` and `REFRESH` | | `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": "eth-cold-storage", "generation": 1, "approvals": { "keeperId": 1, "nonce": "destroy-eth-cold-storage-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=eth-cold-storage ``` 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": "eth-cold-storage", "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": "eth-cold-storage", "status": "ACTIVE", "currentGeneration": 1, "authorities": [ { "id": "evm-mainnet-usdc", "oci": "oci://registry.example/verdict/authorities/evm-mainnet-usdc@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) - [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, evaluates policy, and signs only when the final decision is `ALLOW`. ## 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": "payments-key", "command": { "type": "custom", "authorityId": "payments-small", "artifact": { "scheme": "ECDSA", "hash": "SHA256", "typed": { "amount": 100, "currency": "USD" } } } } ``` EVM command: ```json { "keyId": "evm-key", "command": { "type": "evm.transaction", "authorityId": "evm-mainnet-erc20-usdc", "artifact": { "message64": "..." } } } ``` Bitcoin and X.509 commands follow the authority type selected by their feature modules. ## 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. ## 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 - authority, 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 `command`, `tweak`, `generation`, and `signature64` match the original sign request. 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. Full Verdict syntax lives here: [github.com/exploit-org/verdict](https://github.com/exploit-org/verdict) ## Key authorities A key stores a list of authorities. Concrete authorities use OCI references: ```json [ { "id": "payments-small", "oci": "oci://registry.example/verdict/authorities/payments-small@sha256:..." }, { "id": "evm-mainnet-usdc", "oci": "oci://registry.example/verdict/authorities/evm-mainnet-usdc@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 ## Authority document Concrete authorities are Verdict authority documents. ```yaml schemaVersion: verdict.authority/v1 id: evm-mainnet-usdc type: evm.transaction version: 1.0.0 metadata: title: Mainnet USDC policy config: chainId: 1 contracts: - standard: erc20 address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" policy: id: usdc-transfer fallback: DENY allow: - id: allow-small-usdc-transfer where: - "effect.one(effects, 'erc20.transfer')" - "effect.any(effects, 'erc20.transfer', {'token': tokenAddress})" - "bigint.lte(effect.amount(effects, 'erc20.transfer'), maxAmount)" deny: [] variables: tokenAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" maxAmount: "100000000" ``` 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": "evm-treasury", "command": { "type": "evm.transaction", "authorityId": "evm-mainnet-usdc", "artifact": { "message64": "..." } } } ``` 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` | core | typed JSON | declared fields and configured `effects` | | `evm.transaction` | `authority-evm` | unsigned serialized EVM transaction | transaction fields, decoded call, `effects` | | `bitcoin.transaction` | `authority-bitcoin` | unsigned tx, previous txs, signing input, sighash | inputs, outputs, fee, sighash, `effects` | | `x509.tbs-certificate` | `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": "erc20.transfer", "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "to": "0x2222222222222222222222222222222222222222", "amount": "1000000" } ``` Common CEL pattern: ```cel effect.onlyTypes(effects, ['erc20.transfer']) && effect.one(effects, 'erc20.transfer') && effect.any(effects, 'erc20.transfer', { 'token': tokenAddress, 'to': recipientAddress }) && bigint.lte(effect.amount(effects, 'erc20.transfer'), maxAmount) ``` 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 Verdict policies use `allow`, `deny`, and `fallback`: ```yaml policy: id: policy-id fallback: DENY variables: maxAmount: "1000000" allow: - id: allow-example where: - "effect.one(effects, 'erc20.transfer')" unless: - "time.after(time.now(), expiresAt)" deny: - id: deny-example where: - "effect.has(effects, 'erc20.approval')" ``` 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 override runtime variables with the same name - deny matches override allow matches - if no rule matches, `fallback` is returned TKeeper allows signing only when the final decision is `ALLOW`. 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/evm-mainnet-usdc@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 authority Use `custom` when the request is JSON and no native intent exists. Authority: ```yaml schemaVersion: verdict.authority/v1 id: payments-small type: custom version: 1.0.0 config: fields: amount: type: bigint currency: type: string customer: type: object fields: id: type: string effects: - type: payment.transfer fields: asset: "$currency" amount: "$amount" customerId: "$customer.id" policy: id: payments fallback: DENY allow: - id: small-usd-payment where: - "currency == 'USD'" - "effect.one(effects, 'payment.transfer')" - "bigint.lte(effect.amount(effects, 'payment.transfer'), '10000')" deny: [] ``` Command: ```json { "type": "custom", "authorityId": "payments-small", "artifact": { "scheme": "ECDSA", "hash": "SHA256", "typed": { "amount": 5000, "currency": "USD", "customer": { "id": "customer-42" } } } } ``` Only declared fields become CEL variables. Unknown JSON fields are ignored. `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. For all supported field types, effect mapping rules, and CEL helpers, use the Verdict docs: [github.com/exploit-org/verdict](https://github.com/exploit-org/verdict) ## 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/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. 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. ## 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 only when the final decision is `ALLOW`. 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. See [Authorities](authorities.md) for authority documents and policy format. --- 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. See [Authorities](authorities.md) for authority documents and policy format. --- 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. See [Authorities](authorities.md) for authority documents and policy format. --- 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. ## 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.2.0` - 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. - Reuse the exact `Command` when verifying a returned signature. - 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": [] } ``` 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 | `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 | | `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 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 The integration image is built with: ```bash ./gradlew dockerBuildIntegration ``` Do not pass `keeper.features` or `keeper.platforms` to this task. The integration artifact uses its own classpath and includes: - every production feature - every platform - the test-only failure-injection module Regular `shadowJar` and `dockerBuild` do not include failure injection. See [`../../integration-tests/README.md`](../../integration-tests/README.md) for local requirements and Testcontainers setup. --- 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 integration image with: ```bash ./gradlew dockerBuildIntegration ``` 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.2.0" } ``` Environment fallbacks: ```text SENTRY_APPLICATION_ENVIRONMENT SENTRY_APPLICATION_RELEASE ``` ## Common problems ### Sentry stays disabled Check that `sentry.enabled` is `true` and that the DSN is valid. ### Bad DSN The DSN must be `http` or `https`, include user info, include a host, and end with a numeric project id. --- Source: openapi.yaml Canonical: https://tkeeper.org/openapi.yaml ```yaml openapi: 3.0.3 info: title: TKeeper API version: 2.2.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 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 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 - 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: 255 n: type: integer format: int32 minimum: 2 maximum: 255 keys: type: array minItems: 2 maxItems: 255 uniqueItems: true items: $ref: '#/components/schemas/ApproverPublicKey' FourEyeControlMode: type: string enum: [STRICT, LENIENT] default: STRICT 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: 255 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/Command' tweak: allOf: - $ref: '#/components/schemas/Tweak' nullable: true approvals: $ref: '#/components/schemas/Approvals' Verify: type: object additionalProperties: false description: Verify request. If generation is omitted, the active generation is used. required: [keyId, command, signature64] properties: keyId: $ref: '#/components/schemas/KeyId' generation: allOf: - $ref: '#/components/schemas/Generation' nullable: true command: $ref: '#/components/schemas/Command' signature64: $ref: '#/components/schemas/NonEmptyBase64' tweak: allOf: - $ref: '#/components/schemas/Tweak' nullable: true Command: 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' 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 enum: [ACTIVE, DISABLED, APPLY_EXPIRED, EXPIRED, DESTROYED] 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 PolicyEvaluation: type: object additionalProperties: false required: [decision, matches] properties: decision: type: string enum: [ALLOW, DENY] matches: type: array items: $ref: '#/components/schemas/RuleMatch' RuleMatch: type: object additionalProperties: false required: [id, effect] properties: id: type: string minLength: 1 effect: type: string enum: [ALLOW, 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 ```