Skip to content

Commit

Permalink
feat: provide more client PGP functions
Browse files Browse the repository at this point in the history
This was a bit refactored for easier access.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
  • Loading branch information
smira committed Oct 21, 2022
1 parent 2b682ec commit 5d3647e
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -5,6 +5,7 @@ go 1.19
require (
github.com/ProtonMail/go-crypto v0.0.0-20220930113650-c6815a8c17ad
github.com/ProtonMail/gopenpgp/v2 v2.4.10
github.com/adrg/xdg v0.4.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3
github.com/hashicorp/go-multierror v1.1.1
github.com/stretchr/testify v1.8.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Expand Up @@ -6,6 +6,8 @@ github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f h1:CGq7OieOz3wy
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/gopenpgp/v2 v2.4.10 h1:EYgkxzwmQvsa6kxxkgP1AwzkFqKHscF2UINxaSn6rdI=
github.com/ProtonMail/gopenpgp/v2 v2.4.10/go.mod h1:CTRA7/toc/4DxDy5Du4hPDnIZnJvXSeQ8LsRTOUJoyc=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
Expand Down Expand Up @@ -41,6 +43,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
Expand Down Expand Up @@ -70,6 +73,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
Expand Down
6 changes: 6 additions & 0 deletions pkg/pgp/client/client.go
@@ -0,0 +1,6 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Package client provides utilities for handling client-side PGP keys.
package client
17 changes: 17 additions & 0 deletions pkg/pgp/client/key.go
@@ -0,0 +1,17 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package client

import (
"github.com/siderolabs/go-api-signature/pkg/pgp"
)

// Key represents an OpenPGP client key pair associated with a context and an identity.
// It is stored on the filesystem.
type Key struct {
*pgp.Key
context string
identity string
}
136 changes: 136 additions & 0 deletions pkg/pgp/client/provider.go
@@ -0,0 +1,136 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package client

import (
"fmt"
"os"
"path/filepath"
"runtime"
"time"

pgpcrypto "github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/adrg/xdg"

"github.com/siderolabs/go-api-signature/pkg/pgp"
)

const (
keyLifetime = 4 * time.Hour
)

// KeyProvider handles loading/saving client keys.
type KeyProvider struct {
dataFileDirectory string
keyLifetime time.Duration
}

// NewKeyProvider creates a new KeyProvider.
func NewKeyProvider(dataFileDirectory string) *KeyProvider {
return &KeyProvider{
dataFileDirectory: dataFileDirectory,
keyLifetime: keyLifetime,
}
}

// ReadValidKey reads a PGP key from the filesystem.
//
// If the key is missing or invalid (e.g. expired, revoked), an error will be returned.
func (provider *KeyProvider) ReadValidKey(context, email string) (*Key, error) {
keyPath, err := provider.getKeyFilePath(context, email)
if err != nil {
return nil, err
}

keyF, err := os.Open(keyPath)
if err != nil {
return nil, err
}

defer keyF.Close() //nolint:errcheck

key, err := pgpcrypto.NewKeyFromArmoredReader(keyF)
if err != nil {
return nil, err
}

pgpKey, err := pgp.NewKey(key)
if err != nil {
return nil, err
}

err = pgpKey.Validate()
if err != nil {
return nil, err
}

unlocked, err := pgpKey.IsUnlocked()
if err != nil {
return nil, err
}

if !unlocked {
return nil, fmt.Errorf("private key is locked")
}

return &Key{
Key: pgpKey,
context: context,
identity: email,
}, nil
}

// GenerateKey generates a new PGP key pair.
func (provider *KeyProvider) GenerateKey(context, email, clientNameWithVersion string) (*Key, error) {
name := clientNameWithVersion
comment := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)

key, err := pgp.GenerateKey(name, comment, email, provider.keyLifetime)
if err != nil {
return nil, err
}

return &Key{
Key: key,
context: context,
identity: email,
}, nil
}

// DeleteKey deletes the key pair from disk.
func (provider *KeyProvider) DeleteKey(context, email string) error {
keyPath, err := provider.getKeyFilePath(context, email)
if err != nil {
return err
}

return os.Remove(keyPath)
}

// WriteKey saves the key pair to disk and returns the save path.
func (provider *KeyProvider) WriteKey(c *Key) (string, error) {
armored, err := c.Armor()
if err != nil {
return "", err
}

keyPath, err := provider.getKeyFilePath(c.context, c.identity)
if err != nil {
return "", err
}

err = os.WriteFile(keyPath, []byte(armored), 0o600)
if err != nil {
return "", err
}

return keyPath, err
}

func (provider *KeyProvider) getKeyFilePath(context, identity string) (string, error) {
keyName := fmt.Sprintf("%s-%s.pgp", context, identity)

return xdg.DataFile(filepath.Join(provider.dataFileDirectory, keyName))
}
46 changes: 46 additions & 0 deletions pkg/pgp/client/provider_test.go
@@ -0,0 +1,46 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package client_test

import (
"testing"

"github.com/adrg/xdg"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/siderolabs/go-api-signature/pkg/pgp/client"
)

func TestKeyProvider(t *testing.T) {
t.Cleanup(xdg.Reload)

// fake XDG paths
t.Setenv("HOME", t.TempDir())
xdg.Reload()

provider := client.NewKeyProvider("test/keys")

key, err := provider.GenerateKey("testapp", "john@example.com", "Linux")
require.NoError(t, err)

assert.True(t, key.IsPrivate())

path, err := provider.WriteKey(key)
require.NoError(t, err)

t.Logf("saved key to %s", path)

k, err := provider.ReadValidKey("testapp", "john@example.com")
require.NoError(t, err)

assert.True(t, k.IsPrivate())

err = provider.DeleteKey("testapp", "john@example.com")
require.NoError(t, err)

_, err = provider.ReadValidKey("testapp", "john@example.com")
require.Error(t, err)
}
5 changes: 5 additions & 0 deletions pkg/pgp/key.go
Expand Up @@ -85,6 +85,11 @@ func (p *Key) IsPrivate() bool {
return p.key.IsPrivate()
}

// IsUnlocked returns true if the private key is unlocked.
func (p *Key) IsUnlocked() (bool, error) {
return p.key.IsUnlocked()
}

// Armor returns the key in the armored format.
func (p *Key) Armor() (string, error) {
return p.key.Armor()
Expand Down

0 comments on commit 5d3647e

Please sign in to comment.