github.com/twelho/conform@v0.0.0-20231016230407-c25e9238598a/internal/policy/commit/check_gpg_identity.go (about) 1 // This Source Code Form is subject to the terms of the Mozilla Public 2 // License, v. 2.0. If a copy of the MPL was not distributed with this 3 // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 package commit 6 7 import ( 8 "context" 9 "fmt" 10 "io" 11 "net/http" 12 "sync" 13 14 "github.com/google/go-github/v41/github" 15 "golang.org/x/sync/errgroup" 16 17 "github.com/twelho/conform/internal/git" 18 "github.com/twelho/conform/internal/policy" 19 ) 20 21 // GPGIdentityCheck ensures that the commit is cryptographically signed using known identity. 22 // 23 //nolint:govet 24 type GPGIdentityCheck struct { 25 errors []error 26 identity string 27 } 28 29 // Name returns the name of the check. 30 func (g GPGIdentityCheck) Name() string { 31 return "GPG Identity" 32 } 33 34 // Message returns to check message. 35 func (g GPGIdentityCheck) Message() string { 36 if len(g.errors) != 0 { 37 return g.errors[0].Error() 38 } 39 40 return fmt.Sprintf("Signed by %q", g.identity) 41 } 42 43 // Errors returns any violations of the check. 44 func (g GPGIdentityCheck) Errors() []error { 45 return g.errors 46 } 47 48 // ValidateGPGIdentity checks the commit GPG signature for a known identity. 49 func (c Commit) ValidateGPGIdentity(g *git.Git) policy.Check { //nolint:ireturn 50 check := &GPGIdentityCheck{} 51 52 switch { 53 case c.GPG.Identity.GitHubOrganization != "": 54 githubClient := github.NewClient(nil) 55 56 list, _, err := githubClient.Organizations.ListMembers(context.Background(), c.GPG.Identity.GitHubOrganization, &github.ListMembersOptions{}) 57 if err != nil { 58 check.errors = append(check.errors, err) 59 60 return check 61 } 62 63 members := make([]string, len(list)) 64 65 for i := range list { 66 members[i] = list[i].GetLogin() 67 } 68 69 keyrings, err := getKeyring(context.Background(), members) 70 if err != nil { 71 check.errors = append(check.errors, err) 72 73 return check 74 } 75 76 entity, err := g.VerifyPGPSignature(keyrings) 77 if err != nil { 78 check.errors = append(check.errors, err) 79 80 return check 81 } 82 83 for identity := range entity.Identities { 84 check.identity = identity 85 86 break 87 } 88 default: 89 check.errors = append(check.errors, fmt.Errorf("no signature identity configuration found")) 90 } 91 92 return check 93 } 94 95 func getKeyring(ctx context.Context, members []string) ([]string, error) { 96 var ( 97 result []string 98 mu sync.Mutex 99 ) 100 101 eg, ctx := errgroup.WithContext(ctx) 102 103 for _, member := range members { 104 member := member 105 106 eg.Go(func() error { 107 key, err := getKey(ctx, member) 108 109 mu.Lock() 110 result = append(result, key) 111 mu.Unlock() 112 113 return err 114 }) 115 } 116 117 err := eg.Wait() 118 119 return result, err 120 } 121 122 func getKey(ctx context.Context, login string) (string, error) { 123 // GitHub client doesn't have a method to fetch a key unauthenticated 124 req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://github.com/%s.gpg", login), nil) 125 if err != nil { 126 return "", err 127 } 128 129 resp, err := http.DefaultClient.Do(req) 130 if err != nil { 131 return "", err 132 } 133 134 defer resp.Body.Close() //nolint:errcheck 135 136 buf, err := io.ReadAll(resp.Body) 137 138 return string(buf), err 139 }