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  }