Skip to content

Commit

Permalink
Add GPG key verification for RPM packages
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Shishkin <me@teran.dev>
  • Loading branch information
teran committed Aug 18, 2024
1 parent 7dd0970 commit ca2b51e
Show file tree
Hide file tree
Showing 20 changed files with 654 additions and 108 deletions.
55 changes: 55 additions & 0 deletions cli/service/gpg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package service

import (
"bytes"
"context"
"io"
"net/http"
"os"
"strings"

"github.com/ProtonMail/go-crypto/openpgp"
"github.com/pkg/errors"
)

func getGPGKey(ctx context.Context, filepath string) (openpgp.EntityList, error) {
p := strings.SplitN(filepath, "://", 2)
if len(p) != 2 {
return nil, errors.New("unexpected public key file path format. Please use file:///path/to/file.gpg or http://example.com/file.gpg")
}

var data []byte
switch p[0] {
case "file":
fp, err := os.Open(p[1])
if err != nil {
return nil, errors.Wrap(err, "error opening public key file")
}
defer fp.Close()

data, err = io.ReadAll(fp)
if err != nil {
return nil, errors.Wrap(err, "error reading public key file")
}
case "http", "https":
req, err := http.NewRequestWithContext(ctx, http.MethodGet, filepath, nil)
if err != nil {
return nil, errors.Wrap(err, "error constructing HTTP request object")
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error performing HTTP request")
}
defer resp.Body.Close()

data, err = io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "error reading public key data")
}
default:
return nil, errors.Errorf("unsupported key file access scheme: `%s`", p[0])
}

return openpgp.ReadArmoredKeyRing(bytes.NewReader(data))
}
100 changes: 100 additions & 0 deletions cli/service/gpg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package service

import (
"context"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/ProtonMail/go-crypto/openpgp"
echo "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/stretchr/testify/require"
ptr "github.com/teran/go-ptr"

"github.com/teran/archived/repositories/blob/mock"
)

func TestGetGPGKey(t *testing.T) {
ctx := context.TODO()

m := &testHandlerMock{}
defer m.AssertExpectations(t)

data, err := os.ReadFile("./testdata/gpg/somekey.gpg")
if err != nil {
t.Fatal(err)
}

m.On("StaticFile", "/").Return(http.StatusOK, "text/plain", data).Once()

e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/*", m.StaticFile)

srv := httptest.NewServer(e)
defer srv.Close()

type testCase struct {
name string
url string
expKeyIDs []uint64
expErrorText *string
}

tcs := []testCase{
{
name: "read from file",
url: "file://./testdata/gpg/somekey.gpg",
expKeyIDs: []uint64{11127004574349501168},
},
{
name: "read form HTTP URL",
url: srv.URL,
expKeyIDs: []uint64{11127004574349501168},
},
{
name: "incorrect scheme",
expErrorText: ptr.String(
"unexpected public key file path format. Please use file:///path/to/file.gpg or http://example.com/file.gpg"),
},
{
name: "unknown scheme",
url: "ftp://example.com/file.gpg",
expErrorText: ptr.String(
"unsupported key file access scheme: `ftp`"),
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
r := require.New(t)

keys, err := getGPGKey(ctx, tc.url)
if tc.expErrorText != nil {
r.Error(err)
r.Equal(*tc.expErrorText, err.Error())
} else {
r.NoError(err)
r.Equal(tc.expKeyIDs, func(el openpgp.EntityList) []uint64 {
keyIDs := []uint64{}
for _, key := range el {
keyIDs = append(keyIDs, key.PrimaryKey.KeyId)
}
return keyIDs
}(keys))
}
})
}
}

type testHandlerMock struct {
mock.Mock
}

func (m *testHandlerMock) StaticFile(c echo.Context) error {
args := m.Called(c.Request().RequestURI)
return c.Blob(args.Int(0), args.String(1), args.Get(2).([]byte))
}
Loading

0 comments on commit ca2b51e

Please # to comment.