validator.go

  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}