Repository navigation
Challenge Response Authentication
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
This document explains the challenge-response authentication mechanism used by Sigvara for secure peer-to-peer agent identity verification. The system uses cryptographic challenges signed with Ed25519 keys, time-bound validity through a TTL, and dual-binding to prevent relay attacks. It covers how challenges are generated, how responses are signed, how signatures are verified, and how both prover and audience identities are bound to protect against replay and relay threats. Practical bidirectional flows between two agents are included, along with security considerations, integration patterns, and error handling guidance.
This page describes the SDK in 1.0.0-alpha.12, published to npm on 29 October 2026. New since 1.0.0-alpha.11: the agent's signChallenge is removed, so a call does not compile in TypeScript and throws in JavaScript, and signChallengeFor is the way to sign. New since 1.0.0-alpha.10: verifySignature requires the expected audience (see the changelog). These changes are new since 1.0.0-alpha.9 (listed in the changelog): SigvaraAgent.signChallengeFor, the refusal of a challenge dated more than 60 seconds ahead, and the agent keeping its secret key in a private field.
The challenge-response authentication is implemented in the SDK under the packages directory:
- Core challenge generation, signing, parsing, and expiration logic lives in the challenge module.
- The agent class exposes high-level methods for issuing and responding to challenges.
- Tests validate nonce uniqueness, signature correctness, payload parsing, TTL behavior, and relay attack prevention.
- Documentation shows an end-to-end example of agent-to-agent authentication.
graph TB
subgraph "SDK"
A["agent.ts<br/>High-level agent API"]
B["challenge.ts<br/>Challenge generation, signing, parsing, TTL"]
T["challenge.test.ts<br/>Behavioral tests"]
end
D["quickstart.md<br/>End-to-end usage example"]
A --> B
T --> B
D --> A
D --> B
Diagram sources
Section sources
- SigvaraAgent: Provides a high-level interface for agents to issue challenges and sign challenge payloads. It encapsulates key management, DID derivation, and default TTL configuration.
- Challenge Module: Implements challenge payload construction (v2), Ed25519 signing and verification, payload parsing, and TTL-based expiration checks. It also enforces audience binding to prevent relay attacks.
Key responsibilities:
- Generate time-bound challenges with unique nonces and explicit audience binding.
- Sign challenge payloads using the agent’s private key, and (with
signChallengeFor) only when the challenge is addressed to the verifier the agent means to prove itself to. - Verify signatures and parse payloads safely.
- Enforce expiration and reject malformed, unparseable or far-future-dated payloads.
Section sources
The authentication flow is a standard challenge-response protocol between two agents:
- Verifier issues a challenge targeting the prover’s DID and binding itself as the audience.
- Prover signs the challenge payload with its private key.
- Verifier verifies the signature against the prover’s public key and ensures the audience matches the verifier’s DID.
- Optional reputation checks can be performed after successful signature verification.
sequenceDiagram
participant V as "Verifier Agent"
participant P as "Prover Agent"
participant C as "Challenge Module"
V->>C : generateChallenge(proverDid, verifierDid, ttlSeconds)
C-->>V : Challenge {payload, nonce, timestamp, expiresAt}
V->>P : Send payload + nonce
P->>C : signChallenge(payload, privateKey)
C-->>P : base58-encoded signature
P->>V : Return signature
V->>C : verifyChallenge(payload, signature, publicKey)
C-->>V : boolean result
Note over V,P : Audience binding prevents relay attacks
Diagram sources
The challenge module constructs v2 payloads that bind both the prover’s DID and the audience (the verifier). This dual-binding prevents relay attacks where a valid response intended for one verifier could be reused against another.
-
Parameters:
- proverDid: The DID of the agent being authenticated.
- audience: The DID or stable identifier of the verifier; required and validated.
- ttlSeconds: Time-to-live for the challenge; defaults to a safe value if not provided.
-
Output:
- payload: Newline-delimited fields including header, did, aud, nonce, ts.
- nonce: Unique random hex string per challenge.
- timestamp: Current Unix seconds.
- expiresAt: Computed expiration based on timestamp plus ttlSeconds.
Security properties:
- Audience binding ensures a signature created for one verifier cannot authenticate to another, provided the signer checks the audience before it signs. The agent's
signChallengesigned whatever it was handed, so a verifier could give an agent a payload naming a different verifier, collect the signature, and present it there as the agent. It is removed in 1.0.0-alpha.12.signChallengeFormakes the check (alpha.9 has onlysignChallenge, so check the payload's audience yourself). - Nonce uniqueness prevents replay within the same session context.
- Strict field validation rejects newline injection and empty audiences.
flowchart TD
Start(["generateChallenge"]) --> ValidateAudience["Validate audience is present and newline-free"]
ValidateAudience --> ValidateProver["Validate prover DID is newline-free"]
ValidateProver --> CreateNonce["Generate unique nonce"]
CreateNonce --> CreateTimestamp["Create current timestamp"]
CreateTimestamp --> BuildPayload["Build v2 payload with did, aud, nonce, ts"]
BuildPayload --> ComputeExpiry["Compute expiresAt = timestamp + ttlSeconds"]
ComputeExpiry --> ReturnChallenge["Return Challenge object"]
Diagram sources
Section sources
Agents sign challenge payloads using their Ed25519 private keys. The resulting signature is base58-encoded for transport.
-
Input:
- payload: The exact bytes to be signed (UTF-8 encoded).
- secretKey: The agent’s private key bytes.
-
Output:
- base58-encoded signature suitable for transmission back to the verifier.
Verification requires the corresponding public key and the original payload.
The agent's signChallenge, removed in 1.0.0-alpha.12, did not look at what it signed. SigvaraAgent.signChallengeFor(payload, audience) parses the payload first and signs only a v2 challenge that names this agent as the prover and names exactly the audience the caller passes (the origin it connected to, or the peer's DID). Any other payload makes it throw and sign nothing.
flowchart TD
Start(["signChallenge"]) --> EncodePayload["Encode payload to UTF-8 bytes"]
EncodePayload --> SignBytes["Sign bytes with Ed25519 private key"]
SignBytes --> Base58Encode["Base58 encode signature"]
Base58Encode --> ReturnSignature["Return base58-encoded signature"]
Diagram sources
Section sources
Verification decodes the base58 signature, validates length, and checks it against the payload and public key. Payload parsing supports both legacy v1 and current v2 formats, ensuring backward compatibility while enforcing strict structure for v2. verifySignature itself refuses a v1 payload, because it names no audience to check.
-
Verification:
- Decodes base58 signature to raw bytes.
- Validates signature length.
- Verifies Ed25519 signature over the payload bytes.
-
Parsing:
- Detects v2 vs v1 headers.
- For v2, parses newline-delimited fields and validates presence and types.
- For v1, extracts DID, nonce, and timestamp from colon-delimited segments.
flowchart TD
Start(["verifyChallenge / parseChallengePayload"]) --> DecodeSig["Decode base58 signature"]
DecodeSig --> CheckLength{"Signature length == 64?"}
CheckLength --> |No| Reject["Reject invalid signature"]
CheckLength --> |Yes| VerifyEd25519["Verify Ed25519 signature"]
VerifyEd25519 --> Result{"Valid?"}
Result --> |No| Reject
Result --> |Yes| Accept["Accept signature"]
ParseHeader["Parse header to detect v1/v2"] --> V2Check{"Starts with v2 header?"}
V2Check --> |Yes| ParseV2["Parse newline-delimited fields"]
V2Check --> |No| ParseV1["Parse colon-delimited legacy fields"]
ParseV2 --> ValidateFields["Validate required fields"]
ParseV1 --> ValidateLegacy["Validate legacy fields"]
Diagram sources
Section sources
Challenges include a timestamp and TTL. Expired or malformed payloads are treated conservatively as expired to avoid false positives.
-
Expiration check:
- Parses payload to extract timestamp.
- Compares current time minus timestamp against maxAgeSeconds.
- Treats a payload dated more than 60 seconds (
MAX_FUTURE_SKEW_SECONDS) ahead of the verifier's clock as expired. Before 1.0.0-alpha.10 a payload dated ahead counted as fresh for as long as its date stayed ahead, so a signature over one dated ten years out worked for ten years. - Treats unparseable payloads as expired.
-
Replay prevention:
- Unique nonce per challenge reduces replay risk.
- Audience binding prevents reuse across verifiers.
- TTL limits the window for replay.
flowchart TD
Start(["isChallengeExpired"]) --> TryParse["Try parse payload"]
TryParse --> ParseOk{"Parsed successfully?"}
ParseOk --> |No| MarkExpired["Mark as expired"]
ParseOk --> |Yes| ExtractTs["Extract timestamp"]
ExtractTs --> Compare["Expired if now - ts > maxAgeSeconds, or if ts is more than 60 s ahead of now"]
Compare --> Expired{"Expired?"}
Expired --> |Yes| ReturnTrue["Return true"]
Expired --> |No| ReturnFalse["Return false"]
Diagram sources
Section sources
The agent class wraps challenge functionality:
- issueChallenge(peerDid, ttlSeconds?): Generates a challenge targeting the peer’s DID and binding this agent’s DID as the audience. Uses default TTL if not specified.
- signChallengeFor(payload, audience): Signs the payload only if it is a v2 challenge naming this agent as the prover and exactly
audienceas the verifier, and otherwise throws and signs nothing. It is the only signing method on the agent from 1.0.0-alpha.12, whensignChallenge(payload), which signed whatever it was handed, was removed. Code that built the payload itself can use the exported functionsignChallenge(payload, secretKey).
These methods provide a simple interface for real-time authentication scenarios. The agent holds its secret key in a true private field, so JSON.stringify(agent) and a logged agent do not carry it (on alpha.9 the key is an ordinary property and they do).
classDiagram
class SigvaraAgent {
-keyPair
-_agentAddress
-_chainId
-_challengeTtl
+issueChallenge(peerDid, ttlSeconds) Challenge
+signChallengeFor(payload, audience) string
}
class ChallengeModule {
+generateChallenge(proverDid, audience, ttlSeconds) Challenge
+signChallenge(payload, secretKey) string
+verifyChallenge(payload, signature, publicKey) boolean
+parseChallengePayload(payload) ParsedChallenge
+isChallengeExpired(payload, maxAgeSeconds) boolean
}
SigvaraAgent --> ChallengeModule : "uses"
Diagram sources
Section sources
Two agents can authenticate each other by exchanging challenges and signatures:
- Agent A issues a challenge to Agent B, binding A as the audience.
- Agent B signs the challenge and returns the signature to A. B should sign with
signChallengeFor(payload, A.did), which refuses a payload not addressed to A. - A verifies B’s signature, passing its own DID as the expected audience, and optionally checks reputation thresholds.
- Then Agent B issues a challenge to Agent A, binding B as the audience.
- Agent A signs and returns the signature to B.
- B verifies A’s signature and optionally checks reputation thresholds.
This mutual exchange establishes trust in both directions without a central authority.
sequenceDiagram
participant A as "Agent A"
participant B as "Agent B"
A->>B : Issue challenge to B (audience=A.did)
B-->>A : Signed challenge response
A->>A : Verify B's signature + optional reputation check
B->>A : Issue challenge to A (audience=B.did)
A-->>B : Signed challenge response
B->>B : Verify A's signature + optional reputation check
Diagram sources
Section sources
The challenge-response system has clear dependencies:
- SigvaraAgent depends on the challenge module for generating and signing challenges.
- The challenge module depends on cryptographic primitives (tweetnacl) and utility functions for base58 encoding/decoding.
- Tests validate core behaviors and edge cases, including audience binding and expiration.
graph LR
Agent["agent.ts"] --> Challenge["challenge.ts"]
Challenge --> NaCl["tweetnacl"]
Challenge --> Keys["keys.ts"]
Tests["challenge.test.ts"] --> Challenge
Docs["quickstart.md"] --> Agent
Docs --> Challenge
Diagram sources
Section sources
- Challenge generation is lightweight: random nonce creation, timestamp capture, and string assembly.
- Signing and verification use Ed25519 operations which are efficient for real-time authentication.
- TTL checks involve simple arithmetic and timestamp comparisons.
- Avoid unnecessary re-parsing of payloads; cache parsed results when processing multiple related messages.
- Use appropriate TTL values balancing security and latency; shorter TTLs reduce replay windows but require tighter clock synchronization.
Common issues and resolutions:
- Expired challenges: Ensure clients synchronize clocks and respect TTL. If payloads are malformed, they are treated as expired.
- Invalid signatures: Verify that the correct public key is used and that the payload was not altered during transit.
- Relay attack failures: Confirm that the audience in the challenge matches the verifier’s DID; mismatched audiences will cause verification to fail.
-
signChallengeForthrows: the payload is not a v2 Sigvara challenge, names a different prover, or is addressed to a different audience than the one you passed. Nothing was signed. - A challenge is refused as expired although it is fresh: check the clocks. A payload dated more than 60 seconds ahead of the verifier's clock is refused.
- Nonce collisions: Each challenge should have a unique nonce; ensure proper randomness sources are available.
Error handling patterns:
- Throwing errors for invalid inputs (e.g., missing audience, newline injection).
- Returning false for failed signature verification.
- Treating unparseable payloads as expired to err on the side of caution.
Section sources
Sigvara’s challenge-response authentication provides a robust, decentralized method for peer-to-peer agent identity verification. By combining time-bound challenges, Ed25519 signatures, and dual-binding of prover and audience identities, it mitigates replay and relay attacks while enabling efficient real-time authentication flows. Proper implementation of TTL, nonce management, and audience validation ensures strong security guarantees. Integration patterns demonstrated in the quickstart guide show how agents can mutually authenticate without relying on a central authority.
- 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