diff --git a/cmd/status.go b/cmd/status.go index 908853c..69c3564 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -58,13 +58,24 @@ var statusCmd = &cobra.Command{ } fmt.Printf(" Need to reboot: %s\n", needToReboot) fmt.Printf(" Fetcher\n") + if status.Fetcher.RepositoryStatus.SelectedCommitShouldBeSigned { + if status.Fetcher.RepositoryStatus.SelectedCommitSigned { + fmt.Printf(" Commit %s signed by %s\n", status.Fetcher.RepositoryStatus.SelectedCommitId, status.Fetcher.RepositoryStatus.SelectedCommitSignedBy) + } else { + fmt.Printf(" Commit %s is not signed while it should be\n", status.Fetcher.RepositoryStatus.SelectedCommitId) + } + } for _, r := range status.Fetcher.RepositoryStatus.Remotes { fmt.Printf(" Remote %s %s fetched %s\n", r.Name, r.Url, humanize.Time(r.FetchedAt), ) } fmt.Printf(" Builder\n") - builder.GenerationShow(*status.Builder.Generation) + if status.Builder.Generation != nil { + builder.GenerationShow(*status.Builder.Generation) + } else { + fmt.Printf(" No build available\n") + } status.Deployer.Show(" ") }, } diff --git a/docs/generated-module-options.md b/docs/generated-module-options.md index 7ebf749..2d9cf66 100644 --- a/docs/generated-module-options.md +++ b/docs/generated-module-options.md @@ -140,6 +140,24 @@ string +## services\.comin\.gpgPublicKeyPaths + + + +A list of GPG public key file paths\. Each of this file should contains an armored GPG key\. + + + +*Type:* +list of Concatenated string + + + +*Default:* +` [ ] ` + + + ## services\.comin\.hostname diff --git a/docs/howtos.md b/docs/howtos.md index 9f6e601..29f269e 100644 --- a/docs/howtos.md +++ b/docs/howtos.md @@ -61,3 +61,11 @@ the `/etc/machine-id` file), comin won't deploy the configuration. So, to migrate to another machine, you have to update this option in the `testing-` branch in order to only deploy this configuration to the new machine. + +## Check Git commit signatures + +The option `services.comin.gpgPublicKeyPaths` allows to declare a list +of GPG public keys. If `services.comin.gpgPublicKeyPaths != []`, comin **only** evaluates commits signed +by one of these GPG keys. Note only the last commit needs to be signed. + +The file containing a GPG public key has to be created with `gpg --armor --export alice@cyb.org`. diff --git a/go.mod b/go.mod index 777e632..1c3ffdc 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/nlewo/comin go 1.22 require ( + github.com/ProtonMail/go-crypto v1.1.5 github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df github.com/dustin/go-humanize v1.0.1 github.com/go-co-op/gocron/v2 v2.11.0 @@ -18,10 +19,9 @@ 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/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 diff --git a/go.sum b/go.sum index 0950a46..6b888f2 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 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= @@ -13,12 +13,10 @@ github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLo github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -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= @@ -104,74 +102,37 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/config/config.go b/internal/config/config.go index 8c5e270..8c847f0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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) { @@ -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, } } diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 0036433..37b0600 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -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 diff --git a/internal/repository/fail.public b/internal/repository/fail.public new file mode 100644 index 0000000..f797786 --- /dev/null +++ b/internal/repository/fail.public @@ -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----- diff --git a/internal/repository/git.go b/internal/repository/git.go index 6bed7f4..8148ede 100644 --- a/internal/repository/git.go +++ b/internal/repository/git.go @@ -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" @@ -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()) } diff --git a/internal/repository/git_test.go b/internal/repository/git_test.go index 4e462c9..1655bb2 100644 --- a/internal/repository/git_test.go +++ b/internal/repository/git_test.go @@ -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 @@ -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 } @@ -36,6 +42,7 @@ func commitFile(remoteRepository *git.Repository, dir, branch, content string) ( Email: "john@doe.org", When: time.Unix(0, 0), }, + SignKey: signKey, }) if err != nil { return @@ -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 ", 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) + +} diff --git a/internal/repository/invalid.public b/internal/repository/invalid.public new file mode 100644 index 0000000..78cf8e3 --- /dev/null +++ b/internal/repository/invalid.public @@ -0,0 +1 @@ +Not a valid armored GPG pub key \ No newline at end of file diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 9529fd4..a18c993 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -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" @@ -17,6 +21,7 @@ type repository struct { GitConfig types.GitConfig RepositoryStatus RepositoryStatus prometheus prometheus.Prometheus + gpgPubliKeys []string } type Repository interface { @@ -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 { @@ -38,6 +58,7 @@ func New(config types.GitConfig, mainCommitId string, prometheus prometheus.Prom return } r.RepositoryStatus = NewRepositoryStatus(config, mainCommitId) + return } @@ -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 } diff --git a/internal/repository/repository_status.go b/internal/repository/repository_status.go index a323d78..cf9af17 100644 --- a/internal/repository/repository_status.go +++ b/internal/repository/repository_status.go @@ -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 { diff --git a/internal/repository/repository_test.go b/internal/repository/repository_test.go index 0b4dc63..938cf83 100644 --- a/internal/repository/repository_test.go +++ b/internal/repository/repository_test.go @@ -1,8 +1,10 @@ package repository import ( + "os" "testing" + "github.com/ProtonMail/go-crypto/openpgp" "github.com/go-git/go-git/v5/plumbing" "github.com/nlewo/comin/internal/prometheus" "github.com/nlewo/comin/internal/types" @@ -37,6 +39,22 @@ func TestNew(t *testing.T) { assert.Equal(t, "r1", r.RepositoryStatus.Remotes[0].Name) } +func TestNewGpg(t *testing.T) { + const invalidPublic = "not an armored public key" + gitConfig := types.GitConfig{ + GpgPublicKeyPaths: []string{"./fail.public", "./test.public"}, + } + r, err := New(gitConfig, "", prometheus.New()) + assert.Nil(t, err) + assert.Equal(t, 2, len(r.gpgPubliKeys)) + + gitConfig = types.GitConfig{ + GpgPublicKeyPaths: []string{"./fail.public", "./test.public", "./invalid.public"}, + } + r, err = New(gitConfig, "", prometheus.New()) + assert.ErrorContains(t, err, "Failed to read the GPG public key") +} + func TestPreferMain(t *testing.T) { var err error r1Dir := t.TempDir() @@ -697,3 +715,75 @@ func TestTestingHardReset(t *testing.T) { assert.Equal(t, "main", r.RepositoryStatus.SelectedBranchName) assert.Equal(t, "r1", r.RepositoryStatus.SelectedRemoteName) } + +func TestUpdateGpg(t *testing.T) { + dir := t.TempDir() + cominRepositoryDir := t.TempDir() + r1, err := initRemoteRepostiory(dir, true) + + f, _ := os.Open("./test.private") + entityList, _ := openpgp.ReadArmoredKeyRing(f) + entity := entityList[0] + commitFileAndSign(r1, dir, "main", "file-1", entity) + cMain := HeadCommitId(r1) + + gitConfig := types.GitConfig{ + Path: cominRepositoryDir, + GpgPublicKeyPaths: []string{"./test.public", "./fail.public"}, + Remotes: []types.Remote{ + { + Name: "r1", + URL: dir, + Branches: types.Branches{ + Main: types.Branch{ + Name: "main", + }, + }, + Timeout: 30, + }, + }, + } + r, err := New(gitConfig, "", prometheus.New()) + assert.Nil(t, err) + r.Fetch([]string{"r1"}) + err = r.Update() + assert.Nil(t, err) + assert.Equal(t, cMain, r.RepositoryStatus.SelectedCommitId) + assert.True(t, r.RepositoryStatus.SelectedCommitSigned) + assert.Equal(t, "test ", r.RepositoryStatus.SelectedCommitSignedBy) + assert.True(t, r.RepositoryStatus.SelectedCommitShouldBeSigned) + + commitFile(r1, dir, "main", "file-2") + r.Fetch([]string{"r1"}) + err = r.Update() + assert.Nil(t, err) + assert.Equal(t, HeadCommitId(r1), r.RepositoryStatus.SelectedCommitId) + assert.False(t, r.RepositoryStatus.SelectedCommitSigned) + assert.Equal(t, "", r.RepositoryStatus.SelectedCommitSignedBy) + assert.True(t, r.RepositoryStatus.SelectedCommitShouldBeSigned) + + // No GPG keys available so commits don't need to be signed + gitConfig = types.GitConfig{ + Path: cominRepositoryDir, + Remotes: []types.Remote{ + { + Name: "r1", + URL: dir, + Branches: types.Branches{ + Main: types.Branch{ + Name: "main", + }, + }, + Timeout: 30, + }, + }, + } + r, err = New(gitConfig, "", prometheus.New()) + assert.Nil(t, err) + r.Fetch([]string{"r1"}) + err = r.Update() + assert.Nil(t, err) + assert.False(t, r.RepositoryStatus.SelectedCommitSigned) + assert.Equal(t, "", r.RepositoryStatus.SelectedCommitSignedBy) + assert.False(t, r.RepositoryStatus.SelectedCommitShouldBeSigned) +} diff --git a/internal/repository/test.private b/internal/repository/test.private new file mode 100644 index 0000000..2688005 --- /dev/null +++ b/internal/repository/test.private @@ -0,0 +1,15 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lFgEZ4lpqhYJKwYBBAHaRw8BAQdAhG8F35gOiGpGbTXXLePu9CuX7yRc1wRkSlMV +bEQggncAAQDpEvcmClRNLm2igJ33lzGpv+oz8+SpxK/C/x1n1amRyxJ0tBd0ZXN0 +IDx0ZXN0QGNvbWluLnNwYWNlPoiUBBMWCgA8FiEEwp3AlleagwWbiZq2sPzqz40E +VgQFAmeJaaoCGwMFCQWjmoAECwkIBwQVCgkIBRYCAwEAAh4FAheAAAoJELD86s+N +BFYEBjAA/3pcckmyDp37KRTOwLVQYKuQpGIhfyvR34D3P/qCayTDAP4jpiHOJ4VG +s6gF2KoRPdjcCAFAQqA6EpALpDWQE9ljBpxdBGeJaaoSCisGAQQBl1UBBQEBB0De +q04TspciL1YqF1sUG+Nd4yS5oQIhLV23RADZTNxfbQMBCAcAAP9kxG36krkVc+OX +wninZe9ERgnhFl/7Fvrhk6CkClydGBHXiH4EGBYKACYWIQTCncCWV5qDBZuJmraw +/OrPjQRWBAUCZ4lpqgIbDAUJBaOagAAKCRCw/OrPjQRWBOP/AP9xreDJhTW+QlU+ +LoxCwUTzE4yCQgu1FK9ccD1Cf1tBMwD/ZiqPFVqW+WidXa7IsR8zT9Sd0Q/CzfNk +/tvn0pux+QU= +=vPHb +-----END PGP PRIVATE KEY BLOCK----- diff --git a/internal/repository/test.public b/internal/repository/test.public new file mode 100644 index 0000000..ecfeae8 --- /dev/null +++ b/internal/repository/test.public @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZ4lpqhYJKwYBBAHaRw8BAQdAhG8F35gOiGpGbTXXLePu9CuX7yRc1wRkSlMV +bEQggne0F3Rlc3QgPHRlc3RAY29taW4uc3BhY2U+iJQEExYKADwWIQTCncCWV5qD +BZuJmraw/OrPjQRWBAUCZ4lpqgIbAwUJBaOagAQLCQgHBBUKCQgFFgIDAQACHgUC +F4AACgkQsPzqz40EVgQGMAD/elxySbIOnfspFM7AtVBgq5CkYiF/K9HfgPc/+oJr +JMMA/iOmIc4nhUazqAXYqhE92NwIAUBCoDoSkAukNZAT2WMGuDgEZ4lpqhIKKwYB +BAGXVQEFAQEHQN6rThOylyIvVioXWxQb413jJLmhAiEtXbdEANlM3F9tAwEIB4h+ +BBgWCgAmFiEEwp3AlleagwWbiZq2sPzqz40EVgQFAmeJaaoCGwwFCQWjmoAACgkQ +sPzqz40EVgTj/wD/ca3gyYU1vkJVPi6MQsFE8xOMgkILtRSvXHA9Qn9bQTMA/2Yq +jxValvlonV2uyLEfM0/UndEPws3zZP7b59KbsfkF +=YhvJ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/internal/types/types.go b/internal/types/types.go index 3a71e4f..0c52255 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -52,4 +52,5 @@ type Configuration struct { Remotes []Remote `yaml:"remotes"` ApiServer HttpServer `yaml:"api_server"` Exporter HttpServer `yaml:"exporter"` + GpgPublicKeyPaths []string `yaml:"gpg_public_key_paths"` } diff --git a/nix/module-options.nix b/nix/module-options.nix index ed33e96..e440cd4 100644 --- a/nix/module-options.nix +++ b/nix/module-options.nix @@ -169,6 +169,11 @@ Note it is only used by comin at evaluation. ''; }; + gpgPublicKeyPaths = mkOption { + description = "A list of GPG public key file paths. Each of this file should contains an armored GPG key."; + type = listOf string; + default = []; + }; }; }; } diff --git a/nix/module.nix b/nix/module.nix index c100e48..3281c60 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -11,6 +11,7 @@ let listen_address = cfg.services.comin.exporter.listen_address; port = cfg.services.comin.exporter.port; }; + gpg_public_key_paths = cfg.services.comin.gpgPublicKeyPaths; }; cominConfigYaml = yaml.generate "comin.yaml" cominConfig; diff --git a/nix/package.nix b/nix/package.nix index 2198a6e..659bd2a 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -36,7 +36,7 @@ buildGoModule rec { ../main.go ]; }; - vendorHash = "sha256-VP8y/iSBIXZFfSmhHsXkp6RxP+2DovX3PbEDtMUMyYE="; + vendorHash = "sha256-8RkxEDnPZJAWOo9uITELewc2UfoJ86DMGUi+Mi801/g="; ldflags = [ "-X github.com/nlewo/comin/cmd.version=${version}" ]; diff --git a/readme.md b/readme.md index 3a4f518..e4e5fbd 100644 --- a/readme.md +++ b/readme.md @@ -13,6 +13,7 @@ NixOS configuration associated to the machine. - :fast_forward: Fast iterations with [local remotes](./docs/howtos.md#iterate-faster-with-local-repository) - :satellite: Observable via [Prometheus metrics](./docs/generated-module-options.md#servicescominexporter) - :pushpin: Create and delete system profiles +- :lock: Optionally check [Git commit signatures](./docs/howtos.md#check-git-commit-signatures) ## Quick start