Repository navigation
Key Management and Rotation
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
This document explains how SigvaraIdentity manages cryptographic keys and rotates them safely, focusing on Ed25519 public key storage, challenge-response authentication, operator-controlled rotation, and agent control transfers. It also provides security guidance, best practices, and operational procedures for key management.
The system uses:
- An Ethereum address as the identity anchor.
- A deterministic DID derived from chain ID and agent address.
- A raw 32-byte Ed25519 public key stored on-chain for off-chain challenge-response verification.
- Operator-controlled functions to rotate keys and transfer control between addresses.
- SDK utilities for generating, encoding, signing, and verifying Ed25519 challenges.
Key files involved in this documentation:
- Solidity registry contract that stores identities and enforces authorization.
- TypeScript SDK modules for Ed25519 key handling and challenge-response flows.
- Test suite validating rotation, status transitions, and operator transfer logic.
graph TB
subgraph "On-chain"
Identity["SigvaraIdentity.sol"]
end
subgraph "Off-chain SDK"
Keys["keys.ts"]
Challenge["challenge.ts"]
Agent["agent.ts"]
end
subgraph "Tests"
IdentityTests["SigvaraIdentity.t.sol"]
end
Agent --> Challenge
Agent --> Keys
Identity --> IdentityTests
Challenge --> Keys
Diagram sources
Section sources
- AgentIdentity: Stores operator address, agent address, Ed25519 public key, status, and registration timestamp.
- Operator roles: Only the recorded operator can rotate keys or manage status; slashing is controlled by a staking core role.
- DID derivation: Deterministic hash from chain ID and agent address, enabling trustless off-chain computation.
- Challenge-response: Off-chain Ed25519 signatures over structured payloads with nonce, timestamp, prover DID, and audience binding.
Key responsibilities:
- Registration requires an EIP-191 signed digest bound to chain, registry, agent, operator, and Ed25519 key.
- Rotation updates only the Ed25519 public key and emits an event.
- Operator transfer is two-phase: offer then accept, with collateral checks and slash-pending guards.
Section sources
- SigvaraIdentity.sol:58-64
- SigvaraIdentity.sol:240-253
- SigvaraIdentity.sol:295-321
- SigvaraIdentity.sol:328-336
- SigvaraIdentity.sol:410-468
The architecture separates on-chain identity anchoring from off-chain cryptographic operations:
sequenceDiagram
participant Client as "Client App"
participant SDK as "SigvaraAgent (agent.ts)"
participant Challenge as "challenge.ts"
participant Keys as "keys.ts"
participant Registry as "SigvaraIdentity.sol"
Client->>SDK : Generate agent + private key
SDK->>Keys : seedToKeyPair()
Client->>Registry : registerAgent(agentAddress, ed25519PubKey, signature), sent by the operator through the registerAgent() helper
Registry->>Registry : verifyRegistration()
Registry-->>Client : didHash emitted
Client->>SDK : issueChallenge(peerDid)
SDK->>Challenge : generateChallenge(proverDid, audience, ttl)
Note over SDK,Challenge : Payload includes nonce, timestamp, DID, audience
Client->>SDK : signChallengeFor(payload, audience)
SDK->>Challenge : signChallenge(payload, secretKey)
Challenge->>Keys : base58Encode(signature)
Challenge-->>Client : base58 signature
Verifier->>Registry : getIdentity(didHash)
Verifier->>Challenge : verifyChallenge(payload, signature, publicKey)
Diagram sources
- agent.ts:15-124
- challenge.ts:49-93
- keys.ts:89-125
- SigvaraIdentity.sol:258-271
- SigvaraIdentity.sol:295-321
- The registry stores a raw 32-byte Ed25519 public key per agent identity.
- Registration binds the public key into the EIP-191 digest, preventing substitution attacks.
- The SDK validates input formats and encodes keys for both on-chain bytes32 and W3C multibase DID documents.
Validation rules:
- Zero public keys are rejected during registration and rotation.
- Public keys must be exactly 32 bytes.
- Multibase encoding adds a multicodec header required by standard resolvers.
Security implications:
- Binding the public key in the registration digest prevents an attacker from intercepting registration and swapping the key.
- Raw 32-byte storage avoids ambiguity; clients must ensure correct encoding when interacting with DIDs.
Section sources
Purpose:
- Allows the operator to replace the stored Ed25519 public key for an agent.
Authorization and validation:
- Caller must be the current operator.
- Slashed agents cannot rotate keys.
- New public key must not be zero.
- No additional signature requirement is enforced by the contract; the operator’s Ethereum signature proves control.
Behavior:
- Updates the stored public key and emits PublicKeyRotated.
- Rotation is allowed even while Suspended, enabling recovery after compromise.
flowchart TD
Start([rotatePublicKey Entry]) --> RequireRegistered["Require registered didHash"]
RequireRegistered --> RequireOperator["Require caller is operator"]
RequireOperator --> CheckSlashed{"Status == Slashed?"}
CheckSlashed --> |Yes| RevertSlashed["Revert SlashedAgentImmutable"]
CheckSlashed --> |No| ValidateNewKey["Validate newEd25519PubKey != 0"]
ValidateNewKey --> UpdateKey["Update stored public key"]
UpdateKey --> EmitEvent["Emit PublicKeyRotated"]
EmitEvent --> End([Exit])
RevertSlashed --> End
Diagram sources
Section sources
These functions implement a two-phase operator transfer mechanism:
-
offerOperatorTransfer:
- Requires operator authorization.
- Prevents transfers if slashed or slash-suspended.
- Validates new operator address is non-zero and different from current operator.
- Emits OperatorTransferOffered without changing ownership yet.
-
cancelOperatorTransfer:
- Either the current operator or the offered recipient can cancel.
- Clears pendingOperator mapping.
-
acceptOperatorTransfer:
- Only the offered recipient can accept.
- Prevents acceptance if slashed or slash-suspended.
- Requires minimum stake via stakeView oracle.
- Transfers operatorship, updates indexes, records change timestamp and transfer count. The change timestamp restarts the score’s maturity in
SigvaraReputation, so the new operator re-earns the right to spend it.
sequenceDiagram
participant OldOp as "Old Operator"
participant NewOp as "New Operator"
participant Registry as "SigvaraIdentity.sol"
OldOp->>Registry : offerOperatorTransfer(didHash, newOperator)
Registry->>Registry : requireOperator(), checkSlashState()
Registry-->>OldOp : emit OperatorTransferOffered
NewOp->>Registry : acceptOperatorTransfer(didHash)
Registry->>Registry : requireOfferedRecipient(), checkSlashState()
Registry->>Registry : stakeView.hasMinimumStake(didHash)
Registry->>Registry : update operator, indexes, timestamps
Registry-->>NewOp : emit OperatorTransferred
Diagram sources
Section sources
The SDK implements a secure challenge-response protocol:
-
generateChallenge:
- Produces a structured payload including version header, prover DID, audience, nonce, and timestamp.
- Audience binding prevents replay across verifiers.
- TTL controls expiration.
-
signChallenge (the exported function, which takes a secret key):
- Signs the UTF-8 payload with Ed25519 using tweetnacl.
- Returns base58-encoded signature.
- Signs whatever it is handed, with no check on who the challenge is addressed to.
- The agent's
signChallengemethod called it with no check and is removed in 1.0.0-alpha.12; the agent signs withsignChallengeFor.
-
signChallengeFor (new in 1.0.0-alpha.10):
- Signs only a v2 challenge that names this agent as the prover and names exactly the audience the caller passes, the verifier it means to prove itself to. Anything else throws and nothing is signed.
- Closes the signer’s half of the audience binding. Without it a verifier could hand the agent a payload naming a different verifier, collect the signature and present it there as the agent.
-
verifyChallenge:
- Decodes base58 signature and verifies against the payload and public key.
- Rejects invalid lengths and mismatches.
-
isChallengeExpired:
- Parses payload and compares timestamp against current time and max age.
- Also refuses a payload dated more than 60 seconds ahead of the verifier’s clock (
MAX_FUTURE_SKEW_SECONDS). A future timestamp used to count as fresh for as long as it stayed in the future.
flowchart TD
Start([Generate Challenge]) --> ValidateAudience["Validate audience and prover DID"]
ValidateAudience --> CreatePayload["Create newline-delimited payload"]
CreatePayload --> ReturnChallenge["Return payload, nonce, timestamp, audience, expiresAt"]
SignStart([Sign Challenge]) --> EncodeMessage["Encode payload to UTF-8 bytes"]
EncodeMessage --> SignWithNaCl["nacl.sign.detached(message, secretKey)"]
SignWithNaCl --> Base58Encode["base58Encode(signature)"]
Base58Encode --> ReturnSignature["Return base58 signature"]
VerifyStart([Verify Challenge]) --> DecodeSig["base58Decode(signatureBase58)"]
DecodeSig --> CheckLength{"Signature length == 64?"}
CheckLength --> |No| ReturnFalse["Return false"]
CheckLength --> |Yes| NaClVerify["nacl.sign.detached.verify(message, sig, publicKey)"]
NaClVerify --> ReturnResult["Return verification result"]
Diagram sources
Section sources
- Registration signature binds chain ID, registry address, agent address, operator, and Ed25519 public key, preventing replay and key substitution.
- Operator control is enforced by storing the operator address and checking msg.sender.
- Slash state is terminal for mutations like key rotation and operator transfer.
- Slash suspension lock prevents operators from reactivating mid-slash.
- Collateral checks prevent under-collateralized activations and transfers.
- Challenge-response binds audience to prevent cross-verifier impersonation. The verifier checks it (
expectedAudience, required since 1.0.0-alpha.11), and the agent checks it before signing (signChallengeFor, 1.0.0-alpha.10 and later, and the only signing method on the agent from 1.0.0-alpha.12). -
SigvaraAgentkeeps its secret key in a JavaScript private field, soJSON.stringify(agent), a logged agent and an error reporter’s snapshot do not carry it. On alpha.9 the key is an ordinary property, so an agent from that version or earlier that was ever logged, serialized or sent to an error reporter has exposed its key: treat it as compromised and rotate it as described above.
Best practices:
- Store private keys securely offline or in hardware wallets.
- Rotate keys promptly upon suspected compromise.
- Use short TTLs for challenges to limit replay windows.
- Ensure stake is at least
minimumStakebefore reactivation or transfer. - Monitor slash events and suspend agents proactively if needed.
Section sources
- SigvaraIdentity.sol:240-253
- SigvaraIdentity.sol:328-336
- SigvaraIdentity.sol:410-468
- challenge.ts:27-47
The components interact as follows:
graph LR
AgentTS["agent.ts"] --> ChallengeTS["challenge.ts"]
AgentTS --> KeysTS["keys.ts"]
ChallengeTS --> KeysTS
Registry["SigvaraIdentity.sol"] --> StakeView["IStakeView interface"]
Tests["SigvaraIdentity.t.sol"] --> Registry
Diagram sources
Section sources
- Operator index removal uses swap-and-pop with recorded positions for constant-time removal where possible, falling back to linear scan for legacy entries.
- Paged queries allow efficient retrieval of large operator agent lists.
- Challenge parsing rejects malformed payloads early to avoid unnecessary computation.
- Base58 encoding/decoding is implemented efficiently but should be used judiciously for large payloads.
Common issues and resolutions:
-
Registration fails due to bad signature:
- Ensure the agent address signs the exact registrationDigest returned by the contract.
- Verify operator and public key match the intended values.
-
Rotation fails:
- Confirm caller is the current operator.
- Check that the agent is not slashed.
- Ensure new public key is non-zero and exactly 32 bytes.
-
Operator transfer fails:
- Verify slash state is not suspended or slashed.
- Ensure stake view reports sufficient collateral.
- Confirm the recipient is the offered address.
-
Challenge verification fails:
- Check payload format and audience binding.
- Ensure signature is base58-encoded and decodes to 64 bytes.
- Validate timestamp is within TTL, and not more than 60 seconds ahead of the verifier’s clock (main).
Section sources
- SigvaraIdentity.sol:258-271
- SigvaraIdentity.sol:328-336
- SigvaraIdentity.sol:410-468
- challenge.ts:84-93
SigvaraIdentity provides a robust framework for managing AI agent identities with strong cryptographic guarantees. Ed25519 public keys are securely stored and validated through operator-controlled rotation and challenge-response authentication. The two-phase operator transfer mechanism ensures safe handover of control with collateral and slash-state checks. Following the documented best practices and troubleshooting steps will help maintain secure and reliable key management operations.
- Agent Registration and Lifecycle
- Contract Overview and Architecture
- Economic Security Model and Incentives
- Evidence Verification and Merkle Proofs
- Key Management and Rotation
- Maturity Mechanism and Score Release
- Proposal Workflow and Challenge System
- Score Storage and Data Model
- SigvaraIdentity Contract
- SigvaraOracleBond Contract
- SigvaraReputation Contract
- SigvaraStaking Contract
- Slashing Lifecycle and Dispute Resolution
- Stake Management Functions
- Status Management and Control
- Token and Fee Contracts
- Unbonding Period and Withdrawal Security
- Anti-Manipulation and Security Measures
- Community Flag Processing
- Evidence Sources and Verification
- External Trust Integration
- Merkle Proof System
- Oracle Score Computation
- Payment-Backed Attestations
- Scoring Model and Formulas
- Agent Creation and Key Management
- Agent Operations
- Agent Registration Workflow
- Challenge-Response Authentication
- Gate Access Control
- Integration Patterns and Examples
- SDK Overview and Setup
- Verifier Functions