atproto user agency toolkit for individuals and groups
1/**
2 * Challenge verifier: validate a response against the original challenge.
3 *
4 * Pure verification:
5 * - For MST proofs: calls verifyMstProof for each proof.
6 * - For block samples: checks availability and verifies prefix against local copy.
7 */
8
9import type { BlockStore } from "../../ipfs.js";
10import type {
11 StorageChallenge,
12 StorageChallengeResponse,
13 StorageChallengeResult,
14 MstProofResult,
15 BlockCheckResult,
16} from "./types.js";
17import { verifyMstProof } from "../mst-proof.js";
18
19/**
20 * Verify a challenge response.
21 *
22 * Checks:
23 * 1. Challenge not expired
24 * 2. Response challenge ID matches
25 * 3. MST proofs are valid (if applicable)
26 * 4. Block prefixes match local copies (if applicable)
27 */
28export async function verifyResponse(
29 challenge: StorageChallenge,
30 response: StorageChallengeResponse,
31 blockStore: BlockStore,
32 now?: Date,
33): Promise<StorageChallengeResult> {
34 const start = Date.now();
35 const currentTime = now ?? new Date();
36
37 // Check expiration
38 if (currentTime > new Date(challenge.expiresAt)) {
39 return {
40 challengeId: challenge.id,
41 passed: false,
42 verifiedAt: currentTime.toISOString(),
43 durationMs: Date.now() - start,
44 };
45 }
46
47 // Check challenge ID match
48 if (response.challengeId !== challenge.id) {
49 return {
50 challengeId: challenge.id,
51 passed: false,
52 verifiedAt: currentTime.toISOString(),
53 durationMs: Date.now() - start,
54 };
55 }
56
57 let mstResults: MstProofResult[] | undefined;
58 let blockResults: BlockCheckResult[] | undefined;
59 let allPassed = true;
60
61 // Verify MST proofs
62 if (
63 challenge.challengeType === "mst-proof" ||
64 challenge.challengeType === "combined"
65 ) {
66 mstResults = [];
67
68 if (
69 !response.mstProofs ||
70 response.mstProofs.length !== challenge.recordPaths.length
71 ) {
72 allPassed = false;
73 } else {
74 for (let i = 0; i < challenge.recordPaths.length; i++) {
75 const recordPath = challenge.recordPaths[i]!;
76 const proof = response.mstProofs[i]!;
77
78 const verification = await verifyMstProof(
79 proof,
80 challenge.commitCid,
81 recordPath,
82 );
83
84 mstResults.push({
85 recordPath,
86 valid: verification.valid,
87 found: verification.found,
88 error: verification.error,
89 });
90
91 if (!verification.valid) {
92 allPassed = false;
93 }
94 }
95 }
96 }
97
98 // Verify block samples
99 if (
100 challenge.challengeType === "block-sample" ||
101 challenge.challengeType === "combined"
102 ) {
103 blockResults = [];
104
105 if (challenge.blockCids) {
106 if (
107 !response.blockResults ||
108 response.blockResults.length !== challenge.blockCids.length
109 ) {
110 allPassed = false;
111 } else {
112 for (let i = 0; i < challenge.blockCids.length; i++) {
113 const expectedCid = challenge.blockCids[i]!;
114 const result = response.blockResults[i]!;
115
116 if (result.cid !== expectedCid) {
117 blockResults.push({
118 cid: expectedCid,
119 available: false,
120 prefixValid: false,
121 });
122 allPassed = false;
123 continue;
124 }
125
126 if (!result.available || !result.prefix) {
127 blockResults.push({
128 cid: expectedCid,
129 available: false,
130 prefixValid: false,
131 });
132 allPassed = false;
133 continue;
134 }
135
136 // Verify prefix against our local copy
137 const localBytes = await blockStore.getBlock(expectedCid);
138 let prefixValid = false;
139
140 if (localBytes) {
141 const localPrefix = localBytes.slice(
142 0,
143 result.prefix.length,
144 );
145 prefixValid = Buffer.from(result.prefix).equals(
146 Buffer.from(localPrefix),
147 );
148 } else {
149 // We don't have the block locally — accept any non-empty prefix
150 // (we're checking that *they* have it)
151 prefixValid = result.prefix.length > 0;
152 }
153
154 blockResults.push({
155 cid: expectedCid,
156 available: result.available,
157 prefixValid,
158 });
159
160 if (!prefixValid) {
161 allPassed = false;
162 }
163 }
164 }
165 }
166 }
167
168 return {
169 challengeId: challenge.id,
170 passed: allPassed,
171 mstResults,
172 blockResults,
173 verifiedAt: currentTime.toISOString(),
174 durationMs: Date.now() - start,
175 };
176}