UNPKG

@atproto/repo

Version:

atproto repo and MST implementation

204 lines (194 loc) 5.78 kB
import { CID } from 'multiformats/cid' import { BlockMap } from '../block-map' import { readCarWithRoot } from '../car' import { DataDiff } from '../data-diff' import { MST } from '../mst' import { ReadableRepo } from '../readable-repo' import { MemoryBlockstore, ReadableBlockstore, SyncStorage } from '../storage' import { RecordCidClaim, RecordClaim, VerifiedDiff, VerifiedRepo, def, } from '../types' import * as util from '../util' export const verifyRepoCar = async ( carBytes: Uint8Array, did?: string, signingKey?: string, ): Promise<VerifiedRepo> => { const car = await readCarWithRoot(carBytes) return verifyRepo(car.blocks, car.root, did, signingKey) } export const verifyRepo = async ( blocks: BlockMap, head: CID, did?: string, signingKey?: string, opts?: { ensureLeaves?: boolean }, ): Promise<VerifiedRepo> => { const diff = await verifyDiff(null, blocks, head, did, signingKey, opts) const creates = util.ensureCreates(diff.writes) return { creates, commit: diff.commit, } } export const verifyDiffCar = async ( repo: ReadableRepo | null, carBytes: Uint8Array, did?: string, signingKey?: string, opts?: { ensureLeaves?: boolean }, ): Promise<VerifiedDiff> => { const car = await readCarWithRoot(carBytes) return verifyDiff(repo, car.blocks, car.root, did, signingKey, opts) } export const verifyDiff = async ( repo: ReadableRepo | null, updateBlocks: BlockMap, updateRoot: CID, did?: string, signingKey?: string, opts?: { ensureLeaves?: boolean }, ): Promise<VerifiedDiff> => { const { ensureLeaves = true } = opts ?? {} const stagedStorage = new MemoryBlockstore(updateBlocks) const updateStorage = repo ? new SyncStorage(stagedStorage, repo.storage) : stagedStorage const updated = await verifyRepoRoot( updateStorage, updateRoot, did, signingKey, ) const diff = await DataDiff.of(updated.data, repo?.data ?? null) const writes = await util.diffToWriteDescripts(diff) const newBlocks = diff.newMstBlocks const leaves = updateBlocks.getMany(diff.newLeafCids.toList()) if (leaves.missing.length > 0 && ensureLeaves) { throw new Error(`missing leaf blocks: ${leaves.missing}`) } newBlocks.addMap(leaves.blocks) const removedCids = diff.removedCids const commitCid = await newBlocks.add(updated.commit) // ensure the commit cid actually changed if (repo) { if (commitCid.equals(repo.cid)) { newBlocks.delete(commitCid) } else { removedCids.add(repo.cid) } } return { writes, commit: { cid: updated.cid, rev: updated.commit.rev, prev: repo?.cid ?? null, since: repo?.commit.rev ?? null, newBlocks, relevantBlocks: newBlocks, removedCids, }, } } // @NOTE only verifies the root, not the repo contents const verifyRepoRoot = async ( storage: ReadableBlockstore, head: CID, did?: string, signingKey?: string, ): Promise<ReadableRepo> => { const repo = await ReadableRepo.load(storage, head) if (did !== undefined && repo.did !== did) { throw new RepoVerificationError(`Invalid repo did: ${repo.did}`) } if (signingKey !== undefined) { const validSig = await util.verifyCommitSig(repo.commit, signingKey) if (!validSig) { throw new RepoVerificationError( `Invalid signature on commit: ${repo.cid.toString()}`, ) } } return repo } export const verifyProofs = async ( proofs: Uint8Array, claims: RecordCidClaim[], did: string, didKey: string, ): Promise<{ verified: RecordCidClaim[]; unverified: RecordCidClaim[] }> => { const car = await readCarWithRoot(proofs) const blockstore = new MemoryBlockstore(car.blocks) const commit = await blockstore.readObj(car.root, def.commit) if (commit.did !== did) { throw new RepoVerificationError(`Invalid repo did: ${commit.did}`) } const validSig = await util.verifyCommitSig(commit, didKey) if (!validSig) { throw new RepoVerificationError( `Invalid signature on commit: ${car.root.toString()}`, ) } const mst = MST.load(blockstore, commit.data) const verified: RecordCidClaim[] = [] const unverified: RecordCidClaim[] = [] for (const claim of claims) { const found = await mst.get( util.formatDataKey(claim.collection, claim.rkey), ) const record = found ? await blockstore.readObj(found, def.map) : null if (claim.cid === null) { if (record === null) { verified.push(claim) } else { unverified.push(claim) } } else { if (claim.cid.equals(found)) { verified.push(claim) } else { unverified.push(claim) } } } return { verified, unverified } } export const verifyRecords = async ( proofs: Uint8Array, did: string, signingKey: string, ): Promise<RecordClaim[]> => { const car = await readCarWithRoot(proofs) const blockstore = new MemoryBlockstore(car.blocks) const commit = await blockstore.readObj(car.root, def.commit) if (commit.did !== did) { throw new RepoVerificationError(`Invalid repo did: ${commit.did}`) } const validSig = await util.verifyCommitSig(commit, signingKey) if (!validSig) { throw new RepoVerificationError( `Invalid signature on commit: ${car.root.toString()}`, ) } const mst = MST.load(blockstore, commit.data) const records: RecordClaim[] = [] const leaves = await mst.reachableLeaves() for (const leaf of leaves) { const { collection, rkey } = util.parseDataKey(leaf.key) const record = await blockstore.attemptReadRecord(leaf.value) if (record) { records.push({ collection, rkey, record, }) } } return records } export class RepoVerificationError extends Error {}