1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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()}
}