Add GPG signing support for manifest generation
- Add --sign-key flag and MFER_SIGN_KEY env var to gen and freshen commands - Sign inner message multihash with GPG detached signature - Include signer fingerprint and public key in outer wrapper - Add comprehensive tests with temporary GPG keyring - Increase test timeout to 10s for GPG key generation
This commit is contained in:
236
mfer/gpg_test.go
Normal file
236
mfer/gpg_test.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package mfer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testGPGEnv sets up a temporary GPG home directory with a test key.
|
||||
// Returns the key ID and a cleanup function.
|
||||
func testGPGEnv(t *testing.T) (GPGKeyID, func()) {
|
||||
t.Helper()
|
||||
|
||||
// Check if gpg is installed
|
||||
if _, err := exec.LookPath("gpg"); err != nil {
|
||||
t.Skip("gpg not installed, skipping signing test")
|
||||
return "", func() {}
|
||||
}
|
||||
|
||||
// Create temporary GPG home directory
|
||||
gpgHome, err := os.MkdirTemp("", "mfer-gpg-test-*")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set restrictive permissions on GPG home
|
||||
require.NoError(t, os.Chmod(gpgHome, 0o700))
|
||||
|
||||
// Save original GNUPGHOME and set new one
|
||||
origGPGHome := os.Getenv("GNUPGHOME")
|
||||
os.Setenv("GNUPGHOME", gpgHome)
|
||||
|
||||
cleanup := func() {
|
||||
if origGPGHome == "" {
|
||||
os.Unsetenv("GNUPGHOME")
|
||||
} else {
|
||||
os.Setenv("GNUPGHOME", origGPGHome)
|
||||
}
|
||||
os.RemoveAll(gpgHome)
|
||||
}
|
||||
|
||||
// Generate a test key with no passphrase
|
||||
keyParams := `%no-protection
|
||||
Key-Type: RSA
|
||||
Key-Length: 2048
|
||||
Name-Real: MFER Test Key
|
||||
Name-Email: test@mfer.test
|
||||
Expire-Date: 0
|
||||
%commit
|
||||
`
|
||||
paramsFile := filepath.Join(gpgHome, "key-params")
|
||||
require.NoError(t, os.WriteFile(paramsFile, []byte(keyParams), 0o600))
|
||||
|
||||
cmd := exec.Command("gpg", "--batch", "--gen-key", paramsFile)
|
||||
cmd.Env = append(os.Environ(), "GNUPGHOME="+gpgHome)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
t.Skipf("failed to generate test GPG key: %v: %s", err, output)
|
||||
return "", func() {}
|
||||
}
|
||||
|
||||
// Get the key fingerprint
|
||||
cmd = exec.Command("gpg", "--list-keys", "--with-colons", "test@mfer.test")
|
||||
cmd.Env = append(os.Environ(), "GNUPGHOME="+gpgHome)
|
||||
output, err = cmd.Output()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
t.Fatalf("failed to list test key: %v", err)
|
||||
}
|
||||
|
||||
// Parse fingerprint from output
|
||||
var keyID string
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
fields := strings.Split(line, ":")
|
||||
if len(fields) >= 10 && fields[0] == "fpr" {
|
||||
keyID = fields[9]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if keyID == "" {
|
||||
cleanup()
|
||||
t.Fatal("failed to find test key fingerprint")
|
||||
}
|
||||
|
||||
return GPGKeyID(keyID), cleanup
|
||||
}
|
||||
|
||||
func TestGPGSign(t *testing.T) {
|
||||
keyID, cleanup := testGPGEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
data := []byte("test data to sign")
|
||||
sig, err := gpgSign(data, keyID)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, sig)
|
||||
assert.Contains(t, string(sig), "-----BEGIN PGP SIGNATURE-----")
|
||||
assert.Contains(t, string(sig), "-----END PGP SIGNATURE-----")
|
||||
}
|
||||
|
||||
func TestGPGExportPublicKey(t *testing.T) {
|
||||
keyID, cleanup := testGPGEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
pubKey, err := gpgExportPublicKey(keyID)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, pubKey)
|
||||
assert.Contains(t, string(pubKey), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
|
||||
assert.Contains(t, string(pubKey), "-----END PGP PUBLIC KEY BLOCK-----")
|
||||
}
|
||||
|
||||
func TestGPGGetKeyFingerprint(t *testing.T) {
|
||||
keyID, cleanup := testGPGEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
fingerprint, err := gpgGetKeyFingerprint(keyID)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, fingerprint)
|
||||
// The fingerprint should be 40 hex chars
|
||||
assert.Len(t, fingerprint, 40, "fingerprint should be 40 hex chars")
|
||||
}
|
||||
|
||||
func TestGPGSignInvalidKey(t *testing.T) {
|
||||
// Set up test environment (we need GNUPGHOME set)
|
||||
_, cleanup := testGPGEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
data := []byte("test data")
|
||||
_, err := gpgSign(data, GPGKeyID("NONEXISTENT_KEY_ID_12345"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBuilderWithSigning(t *testing.T) {
|
||||
keyID, cleanup := testGPGEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create a builder with signing options
|
||||
b := NewBuilder()
|
||||
b.SetSigningOptions(&SigningOptions{
|
||||
KeyID: keyID,
|
||||
})
|
||||
|
||||
// Add a test file
|
||||
content := []byte("test file content")
|
||||
reader := bytes.NewReader(content)
|
||||
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, reader, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Build the manifest
|
||||
var buf bytes.Buffer
|
||||
err = b.Build(&buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the manifest and verify signature fields are populated
|
||||
manifest, err := NewManifestFromReader(&buf)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, manifest.pbOuter)
|
||||
|
||||
assert.NotEmpty(t, manifest.pbOuter.Signature, "signature should be populated")
|
||||
assert.NotEmpty(t, manifest.pbOuter.Signer, "signer should be populated")
|
||||
assert.NotEmpty(t, manifest.pbOuter.SigningPubKey, "signing public key should be populated")
|
||||
|
||||
// Verify signature is a valid PGP signature
|
||||
assert.Contains(t, string(manifest.pbOuter.Signature), "-----BEGIN PGP SIGNATURE-----")
|
||||
|
||||
// Verify public key is a valid PGP public key block
|
||||
assert.Contains(t, string(manifest.pbOuter.SigningPubKey), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
|
||||
}
|
||||
|
||||
func TestScannerWithSigning(t *testing.T) {
|
||||
keyID, cleanup := testGPGEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
// Create in-memory filesystem with test files
|
||||
fs := afero.NewMemMapFs()
|
||||
require.NoError(t, fs.MkdirAll("/testdir", 0o755))
|
||||
require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("content1"), 0o644))
|
||||
require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("content2"), 0o644))
|
||||
|
||||
// Create scanner with signing options
|
||||
opts := &ScannerOptions{
|
||||
Fs: fs,
|
||||
SigningOptions: &SigningOptions{
|
||||
KeyID: keyID,
|
||||
},
|
||||
}
|
||||
s := NewScannerWithOptions(opts)
|
||||
|
||||
// Enumerate files
|
||||
require.NoError(t, s.EnumeratePath("/testdir", nil))
|
||||
assert.Equal(t, FileCount(2), s.FileCount())
|
||||
|
||||
// Generate signed manifest
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, s.ToManifest(context.Background(), &buf, nil))
|
||||
|
||||
// Parse and verify
|
||||
manifest, err := NewManifestFromReader(&buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, manifest.pbOuter.Signature)
|
||||
assert.NotEmpty(t, manifest.pbOuter.Signer)
|
||||
assert.NotEmpty(t, manifest.pbOuter.SigningPubKey)
|
||||
}
|
||||
|
||||
func TestBuilderWithoutSigning(t *testing.T) {
|
||||
// Create a builder without signing options
|
||||
b := NewBuilder()
|
||||
|
||||
// Add a test file
|
||||
content := []byte("test file content")
|
||||
reader := bytes.NewReader(content)
|
||||
_, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, reader, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Build the manifest
|
||||
var buf bytes.Buffer
|
||||
err = b.Build(&buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the manifest and verify signature fields are empty
|
||||
manifest, err := NewManifestFromReader(&buf)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, manifest.pbOuter)
|
||||
|
||||
assert.Empty(t, manifest.pbOuter.Signature, "signature should be empty when not signing")
|
||||
assert.Empty(t, manifest.pbOuter.Signer, "signer should be empty when not signing")
|
||||
assert.Empty(t, manifest.pbOuter.SigningPubKey, "signing public key should be empty when not signing")
|
||||
}
|
||||
Reference in New Issue
Block a user