Skip to content

Commit

Permalink
Support Git signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
nlewo committed Jan 22, 2025
1 parent 773f614 commit 4e4aa04
Show file tree
Hide file tree
Showing 15 changed files with 259 additions and 46 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ require (
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.3.6 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
Expand All @@ -19,6 +21,8 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
Expand Down
14 changes: 8 additions & 6 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package config

import (
"github.com/nlewo/comin/internal/types"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"os"
"path/filepath"
"strings"

"github.com/nlewo/comin/internal/types"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)

func Read(path string) (config types.Configuration, err error) {
Expand Down Expand Up @@ -57,8 +58,9 @@ func Read(path string) (config types.Configuration, err error) {

func MkGitConfig(config types.Configuration) types.GitConfig {
return types.GitConfig{
Path: filepath.Join(config.StateDir, "repository"),
Dir: config.FlakeSubdirectory,
Remotes: config.Remotes,
Path: filepath.Join(config.StateDir, "repository"),
Dir: config.FlakeSubdirectory,
Remotes: config.Remotes,
GpgPublicKeyPaths: config.GpgPublicKeyPaths,
}
}
8 changes: 6 additions & 2 deletions internal/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,12 @@ func (m *Manager) FetchAndBuild() {
for {
select {
case rs := <-m.Fetcher.RepositoryStatusCh:
logrus.Infof("manager: a generation is evaluating for commit %s", rs.SelectedCommitId)
m.builder.Eval(rs)
if !rs.SelectedCommitShouldBeSigned || rs.SelectedCommitSigned {
logrus.Infof("manager: a generation is evaluating for commit %s", rs.SelectedCommitId)
m.builder.Eval(rs)
} else {
logrus.Infof("manager: the commit %s is not evaluated because it is not signed", rs.SelectedCommitId)
}
case generation := <-m.builder.EvaluationDone:
if generation.EvalErr != nil {
continue
Expand Down
13 changes: 13 additions & 0 deletions internal/repository/fail.public
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEZ4oDaRYJKwYBBAHaRw8BAQdA91zbRSdMphKMs7wP+3/mOpDkxEfeWrfblS5t
uf5xw1O0F2ZhaWwgPGZhaWxAY29taW4uc3BhY2U+iJQEExYKADwWIQSNo3AzK05c
jADI4rwfTCYbHTKLkgUCZ4oDaQIbAwUJBaOagAQLCQgHBBUKCQgFFgIDAQACHgUC
F4AACgkQH0wmGx0yi5IEyAD/ck8A4aPUK8+g7EzMLRnl+twUccwmS7wIthLsA7Sm
s0sA/2RMyImXOK82hesQi8VqV/XNsu/n5Lg6bAfkTHQR1CwLuDgEZ4oDaRIKKwYB
BAGXVQEFAQEHQLr2P/jpdMyluCmFv1mmtHxNy4rOAstT61B+Zsq+8/wtAwEIB4h+
BBgWCgAmFiEEjaNwMytOXIwAyOK8H0wmGx0yi5IFAmeKA2kCGwwFCQWjmoAACgkQ
H0wmGx0yi5IXxwD6AwMQTzw4uXuMJiNC3lsaX5+L9vJDy4tSu/bufc4EKPoA/iiu
kbksGGr4c6gTHOovFhEklvJhjPcEcwdvdEnioWgL
=zjq4
-----END PGP PUBLIC KEY BLOCK-----
28 changes: 9 additions & 19 deletions internal/repository/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package repository
import (
"context"
"fmt"
"io/ioutil"
"time"

"github.com/ProtonMail/go-crypto/openpgp"
"github.com/go-git/go-git/v5"
gitConfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
Expand Down Expand Up @@ -222,31 +222,21 @@ func manageRemote(r *git.Repository, remote types.Remote) error {
return nil
}

func verifyHead(r *git.Repository, config types.GitConfig) error {
func headSignedBy(r *git.Repository, publicKeys []string) (signedBy *openpgp.Entity, err error) {
head, err := r.Head()
if head == nil {
return fmt.Errorf("Repository HEAD should not be nil")
return nil, fmt.Errorf("Repository HEAD should not be nil")
}
logrus.Debugf("Repository HEAD is %s", head.Strings()[1])

commit, err := r.CommitObject(head.Hash())
if err != nil {
return err
return nil, err
}

for _, keyPath := range config.GpgPublicKeyPaths {
key, err := ioutil.ReadFile(keyPath)
if err != nil {
return err
}
entity, err := commit.Verify(string(key))
if err != nil {
logrus.Debug(err)
} else {
for _, k := range publicKeys {
entity, err := commit.Verify(k)
if err == nil {
logrus.Debugf("Commit %s signed by %s", head.Hash(), entity.PrimaryIdentity().Name)
return nil
return entity, nil
}

}
return fmt.Errorf("Commit %s is not signed", head.Hash())
return nil, fmt.Errorf("Commit %s is not signed", head.Hash())
}
42 changes: 37 additions & 5 deletions internal/repository/git_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package repository

import (
"os"
"path/filepath"
"testing"
"time"

"github.com/ProtonMail/go-crypto/openpgp"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert"
"io/ioutil"
"path/filepath"
"testing"
"time"
)

func commitFile(remoteRepository *git.Repository, dir, branch, content string) (commitId string, err error) {
return commitFileAndSign(remoteRepository, dir, branch, content, nil)
}

func commitFileAndSign(remoteRepository *git.Repository, dir, branch, content string, signKey *openpgp.Entity) (commitId string, err error) {
w, err := remoteRepository.Worktree()
if err != nil {
return
Expand All @@ -22,7 +28,7 @@ func commitFile(remoteRepository *git.Repository, dir, branch, content string) (
})

filename := filepath.Join(dir, content)
err = ioutil.WriteFile(filename, []byte(content), 0644)
err = os.WriteFile(filename, []byte(content), 0644)
if err != nil {
return
}
Expand All @@ -36,6 +42,7 @@ func commitFile(remoteRepository *git.Repository, dir, branch, content string) (
Email: "[email protected]",
When: time.Unix(0, 0),
},
SignKey: signKey,
})
if err != nil {
return
Expand Down Expand Up @@ -119,3 +126,28 @@ func TestIsAncestor(t *testing.T) {

//time.Sleep(100*time.Second)
}

func TestHeadSignedBy(t *testing.T) {
dir := t.TempDir()
remoteRepository, _ := git.PlainInit(dir, false)

r, err := os.Open("./test.private")
entityList, _ := openpgp.ReadArmoredKeyRing(r)
commitFileAndSign(remoteRepository, dir, "main", "file-1", entityList[0])

failPublic, _ := os.ReadFile("./fail.public")
testPublic, _ := os.ReadFile("./test.public")
signedBy, err := headSignedBy(remoteRepository, []string{string(failPublic), string(testPublic)})
assert.Nil(t, err)
assert.Equal(t, "test <[email protected]>", signedBy.PrimaryIdentity().Name)

signedBy, err = headSignedBy(remoteRepository, []string{string(failPublic)})
assert.ErrorContains(t, err, "is not signed")
assert.Nil(t, signedBy)

commitFileAndSign(remoteRepository, dir, "main", "file-2", nil)
signedBy, err = headSignedBy(remoteRepository, []string{string(failPublic), string(testPublic)})
assert.ErrorContains(t, err, "is not signed")
assert.Nil(t, signedBy)

}
1 change: 1 addition & 0 deletions internal/repository/invalid.public
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a valid armored GPG pub key
41 changes: 40 additions & 1 deletion internal/repository/repository.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package repository

import (
"bytes"
"context"
"fmt"
"os"
"slices"
"time"

"github.com/ProtonMail/go-crypto/openpgp"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/nlewo/comin/internal/prometheus"
Expand All @@ -17,6 +21,7 @@ type repository struct {
GitConfig types.GitConfig
RepositoryStatus RepositoryStatus
prometheus prometheus.Prometheus
gpgPubliKeys []string
}

type Repository interface {
Expand All @@ -25,9 +30,24 @@ type Repository interface {

// repositoryStatus is the last saved repositoryStatus
func New(config types.GitConfig, mainCommitId string, prometheus prometheus.Prometheus) (r *repository, err error) {
gpgPublicKeys := make([]string, len(config.GpgPublicKeyPaths))
for i, path := range config.GpgPublicKeyPaths {
k, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("Failed to open the GPG public key file %s: %w", path, err)
}
_, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(k))
if err != nil {
return nil, fmt.Errorf("Failed to read the GPG public key %s: %w", path, err)
}
gpgPublicKeys[i] = string(k)
}

r = &repository{
prometheus: prometheus,
prometheus: prometheus,
gpgPubliKeys: gpgPublicKeys,
}

r.GitConfig = config
r.Repository, err = repositoryOpen(config)
if err != nil {
Expand All @@ -38,6 +58,7 @@ func New(config types.GitConfig, mainCommitId string, prometheus prometheus.Prom
return
}
r.RepositoryStatus = NewRepositoryStatus(config, mainCommitId)

return
}

Expand Down Expand Up @@ -177,5 +198,23 @@ func (r *repository) Update() error {
r.RepositoryStatus.ErrorMsg = err.Error()
return err
}

if len(r.gpgPubliKeys) > 0 {
r.RepositoryStatus.SelectedCommitShouldBeSigned = true
signedBy, err := headSignedBy(r.Repository, r.gpgPubliKeys)
if err != nil {
r.RepositoryStatus.Error = err
r.RepositoryStatus.ErrorMsg = err.Error()
}
if signedBy == nil {
r.RepositoryStatus.SelectedCommitSigned = false
r.RepositoryStatus.SelectedCommitSignedBy = ""
} else {
r.RepositoryStatus.SelectedCommitSigned = true
r.RepositoryStatus.SelectedCommitSignedBy = signedBy.PrimaryIdentity().Name
}
} else {
r.RepositoryStatus.SelectedCommitShouldBeSigned = false
}
return nil
}
26 changes: 15 additions & 11 deletions internal/repository/repository_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,21 @@ type Remote struct {
type RepositoryStatus struct {
// This is the deployed Main commit ID. It is used to ensure
// fast forward
SelectedCommitId string `json:"selected_commit_id"`
SelectedCommitMsg string `json:"selected_commit_msg"`
SelectedRemoteName string `json:"selected_remote_name"`
SelectedBranchName string `json:"selected_branch_name"`
SelectedBranchIsTesting bool `json:"selected_branch_is_testing"`
MainCommitId string `json:"main_commit_id"`
MainRemoteName string `json:"main_remote_name"`
MainBranchName string `json:"main_branch_name"`
Remotes []*Remote `json:"remotes"`
Error error `json:"-"`
ErrorMsg string `json:"error_msg"`
SelectedCommitId string `json:"selected_commit_id"`
SelectedCommitMsg string `json:"selected_commit_msg"`
SelectedRemoteName string `json:"selected_remote_name"`
SelectedBranchName string `json:"selected_branch_name"`
SelectedBranchIsTesting bool `json:"selected_branch_is_testing"`
SelectedCommitSigned bool `json:"selected_commit_signed"`
SelectedCommitSignedBy string `json:"selected_commit_signed_by"`
// True if public keys were available when the commit has been checked out
SelectedCommitShouldBeSigned bool `json:"selected_commit_should_be_signed"`
MainCommitId string `json:"main_commit_id"`
MainRemoteName string `json:"main_remote_name"`
MainBranchName string `json:"main_branch_name"`
Remotes []*Remote `json:"remotes"`
Error error `json:"-"`
ErrorMsg string `json:"error_msg"`
}

func NewRepositoryStatus(config types.GitConfig, mainCommitId string) RepositoryStatus {
Expand Down
Loading

0 comments on commit 4e4aa04

Please sign in to comment.