1package validate
2
3// Signatures validation.
4
5import (
6 "crypto"
7 "fmt"
8 "io/ioutil"
9 "sort"
10 "strings"
11 "time"
12
13 "github.com/MichaelMure/git-bug/cache"
14 "github.com/MichaelMure/git-bug/identity"
15 "github.com/MichaelMure/git-bug/util/git"
16 "github.com/go-git/go-git/v5/plumbing"
17 "github.com/go-git/go-git/v5/plumbing/object"
18 "github.com/pkg/errors"
19 "golang.org/x/crypto/openpgp"
20 "golang.org/x/crypto/openpgp/armor"
21 "golang.org/x/crypto/openpgp/packet"
22)
23
24type Validator struct {
25 backend *cache.RepoCache
26
27 // FirstKey is the key used to sign the first commit.
28 FirstKey *identity.Key
29
30 // versions holds all the Identity Versions ordered by lamport time.
31 versions []*versionInfo
32 // keyring holds all the current and past keys along with their expire time.
33 keyring openpgp.EntityList
34 // keyCommit maps the key id to the commit which introduced that key.
35 keyCommit map[uint64]*object.Commit
36 // checkedCommits holds the valid already-checked commits.
37 checkedCommits map[git.Hash]bool
38}
39
40
41// versionInfo contains details about a Version of an Identity, including
42// the added and removed keys, if any.
43type versionInfo struct {
44 Version *identity.Version
45 Identity *identity.Identity
46 KeysAdded []*identity.Key
47 KeysRemoved []*identity.Key
48 Commit *object.Commit
49}
50
51type ByLamportTime []*versionInfo
52
53func (a ByLamportTime) Len() int { return len(a) }
54func (a ByLamportTime) Less(i, j int) bool { return a[i].Version.Time() < a[j].Version.Time() }
55func (a ByLamportTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
56
57
58// NewValidator creates a validator for the current identities snapshot.
59// If identities are changed a new Validator instance should be used.
60//
61// The returned instance can be used to verify multiple git refs
62// from the main repository against the keychain built from the
63// identities snapshot loaded initially.
64func NewValidator(backend *cache.RepoCache) (*Validator, error) {
65 var err error
66
67 v := &Validator{
68 backend: backend,
69 keyring: make(openpgp.EntityList, 0),
70 keyCommit: make(map[uint64]*object.Commit),
71 checkedCommits: make(map[git.Hash]bool),
72 }
73
74 v.versions, err = v.readVersionsInfo()
75 if err != nil {
76 return nil, errors.Wrap(err, "failed to read identity versions")
77 }
78
79 sort.Sort(ByLamportTime(v.versions))
80
81 if len(v.versions) > 0 {
82 lastInfo := v.versions[0]
83 for _, info := range v.versions[1:] {
84 if len(info.KeysAdded) + len(info.KeysRemoved) > 0 && info.Version.Time() == lastInfo.Version.Time() {
85 return nil, fmt.Errorf("multiple versions with the same lamport time: %d in commits %s %s", lastInfo.Version.Time(), lastInfo.Version.CommitHash(), info.Version.CommitHash())
86 }
87 lastInfo = info
88 }
89 }
90
91 v.FirstKey, err = v.validateIdentities()
92 if err != nil {
93 return nil, errors.Wrap(err, "failed to validate identities")
94 }
95
96 return v, nil
97}
98
99// KeyCommitHash reports the hash of the commit associated with the Identity
100// Version introducing the key using the specified keyId, if any.
101func (v *Validator) KeyCommitHash(keyId uint64) string {
102 commit := v.keyCommit[keyId]
103 if commit == nil {
104 return ""
105 }
106 return commit.Hash.String()
107}
108
109// readVersionsInfo stores all the operations ever done on each identity.
110// Checks the keys introduced by the versions to be unique.
111func (v *Validator) readVersionsInfo() ([]*versionInfo, error) {
112 versions := make([]*versionInfo, 0)
113 for _, id := range v.backend.AllIdentityIds() {
114 identityCache, err := v.backend.ResolveIdentity(id)
115 if err != nil {
116 return nil, errors.Wrapf(err, "failed to resolve identity %s", id)
117 }
118
119 lastVersionKeys := make(map[uint64]*identity.Key)
120 for _, version := range identityCache.Identity.Versions() {
121 // Load the commit.
122 hash := version.CommitHash()
123 commit, err := v.backend.ResolveCommit(hash)
124 if err != nil {
125 return nil, errors.Wrapf(err, "failed to read commit %s for identity %s", hash, identityCache.Id())
126 }
127
128 versionKeys := make(map[uint64]*identity.Key)
129
130 // Iterate the keys to see which one has been added in this version.
131 keysAdded := make([]*identity.Key, 0)
132 for _, key := range version.Keys() {
133 pubkey, err := key.GetPublicKey()
134 if err != nil {
135 return nil, err
136 }
137 if _, present := lastVersionKeys[pubkey.KeyId]; present {
138 // The key was already present in the previous version.
139 delete(lastVersionKeys, pubkey.KeyId)
140 } else {
141 // The key was introduced in this version.
142 keysAdded = append(keysAdded, key)
143 if otherCommit, present := v.keyCommit[key.PublicKey.KeyId]; present {
144 // It's simpler to require keyIds to be unique than
145 // to support non-unique keys.
146 return nil, fmt.Errorf("keys with identical keyId introduced in commits %s and %s", otherCommit.Hash, commit.Hash)
147 }
148 v.keyCommit[key.PublicKey.KeyId] = commit
149 }
150 versionKeys[pubkey.KeyId] = key
151 }
152
153 // The remaining keys have been removed.
154 keysRemoved := make([]*identity.Key, 0, len(lastVersionKeys))
155 for _, key := range lastVersionKeys {
156 keysRemoved = append(keysRemoved, key)
157 }
158
159 versions = append(versions, &versionInfo{version, identityCache.Identity, keysAdded, keysRemoved, commit})
160
161 lastVersionKeys = versionKeys
162 }
163 }
164 return versions, nil
165}
166
167// validateIdentities checks the identity operations have been properly signed.
168// Sets the key used to sign the first commit.
169func (v *Validator) validateIdentities() (*identity.Key, error) {
170 var firstKey *identity.Key
171
172 // Iterate the ordered versions to check each of them.
173 for _, info := range v.versions {
174 if firstKey == nil {
175 // For the first commit we update the keyring beforehand,
176 // as it should be signed with the key it introduces.
177 v.updateKeyring(info)
178 }
179
180 signingKey, err := v.ValidateRef(info.Version.CommitHash())
181 if err != nil {
182 return nil, errors.Wrapf(err, "invalid identity %s (%s)", info.Identity.Id(), info.Identity.Email())
183 }
184
185 if firstKey == nil {
186 for _, key := range info.Version.Keys() {
187 if key.PublicKey.KeyId == signingKey.KeyId {
188 firstKey = key
189 }
190 }
191 } else {
192 v.updateKeyring(info)
193 }
194 }
195
196 return firstKey, nil
197}
198
199func (v *Validator) updateKeyring(info *versionInfo) {
200 for _, key := range info.KeysRemoved {
201 for _, entity := range v.keyring {
202 if entity.PrimaryKey.KeyId == key.PublicKey.KeyId {
203 lifetime := info.Commit.Committer.When.Sub(v.keyCommit[key.PublicKey.KeyId].Committer.When)
204 lifetimeSecs := uint32(lifetime.Seconds())
205 // It's only one.
206 for _, i := range entity.Identities {
207 i.SelfSignature.KeyLifetimeSecs = &lifetimeSecs
208 }
209 break
210 }
211 }
212 }
213 for _, key := range info.KeysAdded {
214 e := &openpgp.Entity{
215 PrimaryKey: key.PublicKey,
216 Identities: make(map[string]*openpgp.Identity),
217 }
218
219 creationTime := info.Commit.Committer.When
220 uid := packet.NewUserId(info.Identity.Name(), "", info.Identity.Email())
221 isPrimaryId := true
222 e.Identities[uid.Id] = &openpgp.Identity{
223 Name: uid.Id,
224 UserId: uid,
225 SelfSignature: &packet.Signature{
226 CreationTime: creationTime,
227 SigType: packet.SigTypePositiveCert,
228 PubKeyAlgo: packet.PubKeyAlgoRSA,
229 Hash: crypto.SHA256,
230 KeyLifetimeSecs: nil,
231 IsPrimaryId: &isPrimaryId,
232 FlagsValid: true,
233 FlagSign: true,
234 FlagCertify: true,
235 IssuerKeyId: &e.PrimaryKey.KeyId,
236 },
237 }
238
239 v.keyring = append(v.keyring, e)
240 }
241}
242
243func (v *Validator) ValidateRef(hash git.Hash) (*packet.PublicKey, error) {
244 if v.checkedCommits[hash] {
245 return nil, nil
246 }
247
248 commit, err := v.backend.ResolveCommit(hash)
249 if err != nil {
250 return nil, err
251 }
252
253 for _, h := range commit.ParentHashes {
254 _, err = v.ValidateRef(git.Hash(h.String()))
255 if err != nil {
256 return nil, err
257 }
258 }
259
260 signingKey, err := v.verifyCommitSignature(commit)
261 if err != nil {
262 return nil, errors.Wrapf(err, "invalid signature for commit %s", hash)
263 }
264
265 v.checkedCommits[hash] = true
266 return signingKey, nil
267}
268
269// verifyCommitSignature returns which public key was able to verify the commit
270// or an error.
271func (v *Validator) verifyCommitSignature(commit *object.Commit) (*packet.PublicKey, error) {
272 if commit.PGPSignature == "" {
273 return nil, errors.New("commit is not signed")
274 }
275
276 signature, err := dearmorSignature(commit.PGPSignature)
277 if err != nil {
278 return nil, errors.Wrap(err, "failed to dearmor PGP signature")
279 }
280
281 if signature.IssuerKeyId == nil {
282 // We require this because otherwise it would be expensive to
283 // iterate the keys to check which one can verify the signature.
284 // openpgp.CheckDetachedSignature has the same expectation.
285 return nil, errors.New("signature doesn't have an issuer")
286 }
287
288 // Encode commit components excluding the signature.
289 // This is the content to be signed.
290 encoded := &plumbing.MemoryObject{}
291 if err := commit.EncodeWithoutSignature(encoded); err != nil {
292 return nil, err
293 }
294 er, err := encoded.Reader()
295 if err != nil {
296 return nil, err
297 }
298 body, err := ioutil.ReadAll(er)
299
300 key, err := v.searchKey(signature, body)
301 if err != nil {
302 return nil, err
303 }
304
305 // Check the committer email of the git commit matches
306 // the email of the git-bug identity.
307 var identity_ *openpgp.Identity
308 emails := make([]string, len(key.Entity.Identities))
309 i := 0
310 for _, ei := range key.Entity.Identities {
311 if ei.UserId.Email == commit.Committer.Email {
312 identity_ = ei
313 break
314 }
315 emails[i] = ei.UserId.Email
316 i++
317 }
318 if identity_ == nil {
319 return nil, fmt.Errorf("git commit committer-email does not match the identity-email: %s vs %s",
320 commit.Committer.Email, strings.Join(emails, ","))
321 }
322
323 start := identity_.SelfSignature.CreationTime
324 if start.After(commit.Committer.When) {
325 return nil, fmt.Errorf("key used to sign commit was created after the commit %s", commit.Hash)
326 }
327 if identity_.SelfSignature.KeyLifetimeSecs != nil {
328 expiry := start.Add(time.Duration(*identity_.SelfSignature.KeyLifetimeSecs))
329 if expiry.Before(commit.Committer.When) {
330 return nil, fmt.Errorf("key used to sign commit %s on %s expired on %s",
331 commit.Hash, commit.Committer.When.Format(time.Stamp), expiry.Format(time.Stamp))
332 }
333 }
334
335 return key.PublicKey, nil
336}
337
338// searchKey searches for a key which can verify the signature.
339// It does not check the expire time.
340func (v *Validator) searchKey(signature *packet.Signature, body []byte) (*openpgp.Key, error) {
341 for _, key := range v.keyring.KeysById(*signature.IssuerKeyId) {
342 signed := signature.Hash.New()
343 _, err := signed.Write(body)
344 if err != nil {
345 return nil, err
346 }
347 err = key.PublicKey.VerifySignature(signed, signature)
348 if err == nil {
349 return &key, nil
350 }
351 }
352
353 return nil, errors.New("no key can verify the signature")
354}
355
356// dearmorSignature decodes an armored signature.
357func dearmorSignature(armoredSignature string) (*packet.Signature, error) {
358 block, err := armor.Decode(strings.NewReader(armoredSignature))
359 if err != nil {
360 return nil, errors.Wrap(err, "failed to dearmor signature")
361 }
362 reader := packet.NewReader(block.Body)
363 p, err := reader.Next()
364 if err != nil {
365 return nil, errors.Wrap(err, "failed to read signature packet")
366 }
367 sig, ok := p.(*packet.Signature)
368 if !ok {
369 // https://tools.ietf.org/html/rfc4880#section-5.2.3
370 return nil, errors.New("failed to parse signature as Version 4 Signature Packet Format")
371 }
372 if sig == nil {
373 // The optional "Issuer" field "(8-octet Key ID)" is missing.
374 // https://tools.ietf.org/html/rfc4880#section-5.2.3.5
375 return nil, fmt.Errorf("missing Issuer Key ID")
376 }
377 return sig, nil
378}