~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
186 lines · 5.5 KB Raw History
package repo

import (
	"bytes"
	"context"
	"fmt"

	comatproto "github.com/bluesky-social/indigo/api/atproto"
	"github.com/bluesky-social/indigo/atproto/atcrypto"
	"github.com/bluesky-social/indigo/atproto/identity"
	indigorepo "github.com/bluesky-social/indigo/atproto/repo"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/ipfs/go-cid"
)

func VerifyCommitSignature(commit *Commit, pub atcrypto.PublicKey) error {
	if commit == nil {
		return fmt.Errorf("%w: missing commit", ErrInvalidRequest)
	}
	if pub == nil {
		return fmt.Errorf("%w: missing public key", ErrInvalidRequest)
	}
	if err := commit.VerifyStructure(); err != nil {
		return fmt.Errorf("%w: %v", ErrUnsupportedRepo, err)
	}
	if err := commit.VerifySignature(pub); err != nil {
		return fmt.Errorf("%w: %v", ErrCorruptRepo, err)
	}
	return nil
}

func VerifyCommitCAR(ctx context.Context, carBytes []byte, pub atcrypto.PublicKey) (*Commit, *indigorepo.Repo, error) {
	if len(carBytes) == 0 {
		return nil, nil, fmt.Errorf("%w: empty car", ErrInvalidRequest)
	}
	root, err := VerifyCAR(ctx, carBytes)
	if err != nil {
		return nil, nil, err
	}
	commit, atr, err := indigorepo.LoadRepoFromCAR(ctx, bytes.NewReader(carBytes))
	if err != nil {
		return nil, nil, fmt.Errorf("loading repo car: %w", err)
	}
	if commit.Data == cid.Undef || !root.Defined() {
		return nil, nil, fmt.Errorf("%w: car root undefined", ErrCorruptRepo)
	}
	if err := VerifyCommitSignature(commit, pub); err != nil {
		return nil, nil, err
	}
	return commit, atr, nil
}

func VerifyCommitMessage(ctx context.Context, dir identity.Directory, msg *comatproto.SyncSubscribeRepos_Commit) (*Commit, *indigorepo.Repo, error) {
	if msg == nil {
		return nil, nil, fmt.Errorf("%w: missing commit message", ErrInvalidRequest)
	}
	if msg.TooBig {
		return nil, nil, fmt.Errorf("%w: tooBig commit has no blocks", ErrInvalidRequest)
	}
	if len(msg.Blocks) == 0 {
		return nil, nil, fmt.Errorf("%w: empty commit blocks", ErrInvalidRequest)
	}
	root, err := VerifyCAR(ctx, []byte(msg.Blocks))
	if err != nil {
		return nil, nil, err
	}
	if !msg.Commit.Defined() {
		return nil, nil, fmt.Errorf("%w: missing commit cid", ErrInvalidRequest)
	}
	if cid.Cid(msg.Commit) != root {
		return nil, nil, fmt.Errorf("%w: commit cid does not match car root", ErrCorruptRepo)
	}
	atr, err := indigorepo.VerifyCommitMessage(ctx, msg)
	if err != nil {
		return nil, nil, fmt.Errorf("verifying commit message: %w", err)
	}
	commit, _, err := indigorepo.LoadCommitFromCAR(ctx, bytes.NewReader(msg.Blocks))
	if err != nil {
		return nil, nil, fmt.Errorf("loading commit from car: %w", err)
	}
	did, err := syntax.ParseDID(commit.DID)
	if err != nil {
		return nil, nil, fmt.Errorf("%w: commit did: %v", ErrCorruptRepo, err)
	}
	ident, err := dir.LookupDID(ctx, did)
	if err != nil {
		return nil, nil, fmt.Errorf("resolving commit key: %w", err)
	}
	pub, err := ident.PublicKey()
	if err != nil {
		return nil, nil, fmt.Errorf("reading commit key: %w", err)
	}
	if err := VerifyCommitSignature(commit, pub); err != nil {
		return nil, nil, err
	}
	return commit, atr, nil
}

func VerifySyncMessage(ctx context.Context, dir identity.Directory, msg *comatproto.SyncSubscribeRepos_Sync) (*Commit, error) {
	if msg == nil {
		return nil, fmt.Errorf("%w: missing sync message", ErrInvalidRequest)
	}
	if len(msg.Blocks) == 0 {
		return nil, fmt.Errorf("%w: empty sync blocks", ErrInvalidRequest)
	}
	if _, err := VerifyCAR(ctx, []byte(msg.Blocks)); err != nil {
		return nil, err
	}
	commit, err := indigorepo.VerifySyncMessage(ctx, dir, msg)
	if err != nil {
		return nil, fmt.Errorf("verifying sync message: %w", err)
	}
	return commit, nil
}

func (s *SQLiteStore) verifyRepo(ctx context.Context, did syntax.DID, pub atcrypto.PublicKey) error {
	s.writeMu.Lock()
	defer s.writeMu.Unlock()
	row, err := s.loadRepoRow(ctx, did)
	if err != nil {
		return err
	}
	if pub == nil {
		pub, err = atcrypto.ParsePublicMultibase(row.pubkey)
		if err != nil {
			return fmt.Errorf("%w: stored public key: %v", ErrCorruptRepo, err)
		}
	}
	cur := row.commit
	first := true
	seen := make(map[cid.Cid]bool)
	childRev := ""
	for {
		if seen[cur] {
			return fmt.Errorf("%w: commit chain cycle at %s", ErrCorruptRepo, cur)
		}
		seen[cur] = true
		cr, err := s.loadCommitByCID(ctx, s.db, did.String(), cur)
		if err != nil {
			return err
		}
		if first {
			if cr.Rev != row.rev || cr.Data != row.root {
				return fmt.Errorf("%w: head mismatch", ErrCorruptRepo)
			}
			first = false
		}
		commit := cr.Signed
		if commit.DID != did.String() {
			return fmt.Errorf("%w: commit did mismatch", ErrCorruptRepo)
		}
		if err := VerifyCommitSignature(commit, pub); err != nil {
			return err
		}
		if childRev != "" && cr.Rev >= syntax.TID(childRev) {
			return fmt.Errorf("%w: commit rev is not increasing", ErrCorruptRepo)
		}
		tree, err := loadFullTree(ctx, s.dbReader(did), commit.Data)
		if err != nil {
			return err
		}
		if err := tree.Walk(func(key []byte, val cid.Cid) error {
			_, err := readBlock(ctx, s.db, did.String(), val)
			return err
		}); err != nil {
			return fmt.Errorf("%w: reading records: %v", ErrCorruptRepo, err)
		}
		if commit.Prev == nil {
			break
		}
		childRev = cr.Rev.String()
		cur = *commit.Prev
	}
	var count int
	if err := s.db.QueryRowContext(ctx, "SELECT count(*) FROM atrepo_commits WHERE did = ?", did.String()).Scan(&count); err != nil {
		return err
	}
	if count != len(seen) {
		return fmt.Errorf("%w: %d unreachable commits", ErrCorruptRepo, count-len(seen))
	}
	return nil
}

func (s *SQLiteStore) dbReader(did syntax.DID) *sqlBlockReader {
	return &sqlBlockReader{db: s.db, did: did.String()}
}