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:
parent
308c583d57
commit
778999a285
2
Makefile
2
Makefile
@ -24,7 +24,7 @@ run: ./bin/mfer
|
|||||||
ci: test
|
ci: test
|
||||||
|
|
||||||
test: $(SOURCEFILES) mfer/mf.pb.go
|
test: $(SOURCEFILES) mfer/mf.pb.go
|
||||||
go test -v --timeout 3s ./...
|
go test -v --timeout 10s ./...
|
||||||
|
|
||||||
$(PROTOC_GEN_GO):
|
$(PROTOC_GEN_GO):
|
||||||
test -e $(PROTOC_GEN_GO) || go install -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
|
test -e $(PROTOC_GEN_GO) || go install -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
|
||||||
|
|||||||
@ -227,6 +227,14 @@ func (mfa *CLIApp) freshenManifestOperation(ctx *cli.Context) error {
|
|||||||
|
|
||||||
builder := mfer.NewBuilder()
|
builder := mfer.NewBuilder()
|
||||||
|
|
||||||
|
// Set up signing options if sign-key is provided
|
||||||
|
if signKey := ctx.String("sign-key"); signKey != "" {
|
||||||
|
builder.SetSigningOptions(&mfer.SigningOptions{
|
||||||
|
KeyID: mfer.GPGKeyID(signKey),
|
||||||
|
})
|
||||||
|
log.Infof("signing manifest with GPG key: %s", signKey)
|
||||||
|
}
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
@ -25,6 +25,14 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
|||||||
Fs: mfa.Fs,
|
Fs: mfa.Fs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up signing options if sign-key is provided
|
||||||
|
if signKey := ctx.String("sign-key"); signKey != "" {
|
||||||
|
opts.SigningOptions = &mfer.SigningOptions{
|
||||||
|
KeyID: mfer.GPGKeyID(signKey),
|
||||||
|
}
|
||||||
|
log.Infof("signing manifest with GPG key: %s", signKey)
|
||||||
|
}
|
||||||
|
|
||||||
s := mfer.NewScannerWithOptions(opts)
|
s := mfer.NewScannerWithOptions(opts)
|
||||||
|
|
||||||
// Phase 1: Enumeration - collect paths and stat files
|
// Phase 1: Enumeration - collect paths and stat files
|
||||||
|
|||||||
@ -148,6 +148,12 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
Aliases: []string{"P"},
|
Aliases: []string{"P"},
|
||||||
Usage: "Show progress during enumeration and scanning",
|
Usage: "Show progress during enumeration and scanning",
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "sign-key",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Usage: "GPG key ID to sign the manifest with",
|
||||||
|
EnvVars: []string{"MFER_SIGN_KEY"},
|
||||||
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -208,6 +214,12 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
Aliases: []string{"P"},
|
Aliases: []string{"P"},
|
||||||
Usage: "Show progress during scanning and hashing",
|
Usage: "Show progress during scanning and hashing",
|
||||||
},
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "sign-key",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Usage: "GPG key ID to sign the manifest with",
|
||||||
|
EnvVars: []string{"MFER_SIGN_KEY"},
|
||||||
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -53,6 +53,7 @@ type Builder struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
files []*MFFilePath
|
files []*MFFilePath
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
|
signingOptions *SigningOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBuilder creates a new Builder.
|
// NewBuilder creates a new Builder.
|
||||||
@ -165,6 +166,14 @@ func (b *Builder) AddFileWithHash(path RelFilePath, size FileSize, mtime ModTime
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSigningOptions sets the GPG signing options for the manifest.
|
||||||
|
// If opts is non-nil, the manifest will be signed when Build() is called.
|
||||||
|
func (b *Builder) SetSigningOptions(opts *SigningOptions) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
b.signingOptions = opts
|
||||||
|
}
|
||||||
|
|
||||||
// Build finalizes the manifest and writes it to the writer.
|
// Build finalizes the manifest and writes it to the writer.
|
||||||
func (b *Builder) Build(w io.Writer) error {
|
func (b *Builder) Build(w io.Writer) error {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
@ -180,6 +189,7 @@ func (b *Builder) Build(w io.Writer) error {
|
|||||||
// Create a temporary manifest to use existing serialization
|
// Create a temporary manifest to use existing serialization
|
||||||
m := &manifest{
|
m := &manifest{
|
||||||
pbInner: inner,
|
pbInner: inner,
|
||||||
|
signingOptions: b.signingOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate outer wrapper
|
// Generate outer wrapper
|
||||||
|
|||||||
90
mfer/gpg.go
Normal file
90
mfer/gpg.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package mfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GPGKeyID represents a GPG key identifier (fingerprint or key ID).
|
||||||
|
type GPGKeyID string
|
||||||
|
|
||||||
|
// SigningOptions contains options for GPG signing.
|
||||||
|
type SigningOptions struct {
|
||||||
|
KeyID GPGKeyID
|
||||||
|
}
|
||||||
|
|
||||||
|
// gpgSign creates a detached signature of the data using the specified key.
|
||||||
|
// Returns the armored detached signature.
|
||||||
|
func gpgSign(data []byte, keyID GPGKeyID) ([]byte, error) {
|
||||||
|
cmd := exec.Command("gpg",
|
||||||
|
"--detach-sign",
|
||||||
|
"--armor",
|
||||||
|
"--local-user", string(keyID),
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd.Stdin = bytes.NewReader(data)
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, fmt.Errorf("gpg sign failed: %w: %s", err, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gpgExportPublicKey exports the public key for the specified key ID.
|
||||||
|
// Returns the armored public key.
|
||||||
|
func gpgExportPublicKey(keyID GPGKeyID) ([]byte, error) {
|
||||||
|
cmd := exec.Command("gpg",
|
||||||
|
"--export",
|
||||||
|
"--armor",
|
||||||
|
string(keyID),
|
||||||
|
)
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, fmt.Errorf("gpg export failed: %w: %s", err, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if stdout.Len() == 0 {
|
||||||
|
return nil, fmt.Errorf("gpg key not found: %s", keyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stdout.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gpgGetKeyFingerprint gets the full fingerprint for a key ID.
|
||||||
|
func gpgGetKeyFingerprint(keyID GPGKeyID) ([]byte, error) {
|
||||||
|
cmd := exec.Command("gpg",
|
||||||
|
"--with-colons",
|
||||||
|
"--fingerprint",
|
||||||
|
string(keyID),
|
||||||
|
)
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, fmt.Errorf("gpg fingerprint lookup failed: %w: %s", err, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the colon-delimited output to find the fingerprint
|
||||||
|
lines := strings.Split(stdout.String(), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
fields := strings.Split(line, ":")
|
||||||
|
if len(fields) >= 10 && fields[0] == "fpr" {
|
||||||
|
return []byte(fields[9]), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("fingerprint not found for key: %s", keyID)
|
||||||
|
}
|
||||||
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")
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ type manifest struct {
|
|||||||
pbInner *MFFile
|
pbInner *MFFile
|
||||||
pbOuter *MFFileOuter
|
pbOuter *MFFileOuter
|
||||||
output *bytes.Buffer
|
output *bytes.Buffer
|
||||||
|
signingOptions *SigningOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifest) String() string {
|
func (m *manifest) String() string {
|
||||||
|
|||||||
@ -46,6 +46,7 @@ type ScannerOptions struct {
|
|||||||
IncludeDotfiles bool // Include files and directories starting with a dot (default: exclude)
|
IncludeDotfiles bool // Include files and directories starting with a dot (default: exclude)
|
||||||
FollowSymLinks bool // Resolve symlinks instead of skipping them
|
FollowSymLinks bool // Resolve symlinks instead of skipping them
|
||||||
Fs afero.Fs // Filesystem to use, defaults to OsFs if nil
|
Fs afero.Fs // Filesystem to use, defaults to OsFs if nil
|
||||||
|
SigningOptions *SigningOptions // GPG signing options (nil = no signing)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileEntry represents a file that has been enumerated.
|
// FileEntry represents a file that has been enumerated.
|
||||||
@ -272,6 +273,9 @@ func (s *Scanner) ToManifest(ctx context.Context, w io.Writer, progress chan<- S
|
|||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
|
|
||||||
builder := NewBuilder()
|
builder := NewBuilder()
|
||||||
|
if s.options.SigningOptions != nil {
|
||||||
|
builder.SetSigningOptions(s.options.SigningOptions)
|
||||||
|
}
|
||||||
|
|
||||||
var scannedFiles FileCount
|
var scannedFiles FileCount
|
||||||
var scannedBytes FileSize
|
var scannedBytes FileSize
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
|
"github.com/multiformats/go-multihash"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -53,7 +55,10 @@ func (m *manifest) generateOuter() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
h.Write(innerData)
|
if _, err := h.Write(innerData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sha256Hash := h.Sum(nil)
|
||||||
|
|
||||||
idc := new(bytes.Buffer)
|
idc := new(bytes.Buffer)
|
||||||
zw, err := zstd.NewWriter(idc, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
|
zw, err := zstd.NewWriter(idc, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
|
||||||
@ -70,10 +75,38 @@ func (m *manifest) generateOuter() error {
|
|||||||
o := &MFFileOuter{
|
o := &MFFileOuter{
|
||||||
InnerMessage: idc.Bytes(),
|
InnerMessage: idc.Bytes(),
|
||||||
Size: int64(len(innerData)),
|
Size: int64(len(innerData)),
|
||||||
Sha256: h.Sum(nil),
|
Sha256: sha256Hash,
|
||||||
Version: MFFileOuter_VERSION_ONE,
|
Version: MFFileOuter_VERSION_ONE,
|
||||||
CompressionType: MFFileOuter_COMPRESSION_ZSTD,
|
CompressionType: MFFileOuter_COMPRESSION_ZSTD,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sign the manifest if signing options are provided
|
||||||
|
if m.signingOptions != nil && m.signingOptions.KeyID != "" {
|
||||||
|
// Encode hash as multihash for signing
|
||||||
|
mh, err := multihash.Encode(sha256Hash, multihash.SHA2_256)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encode multihash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := gpgSign(mh, m.signingOptions.KeyID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sign manifest: %w", err)
|
||||||
|
}
|
||||||
|
o.Signature = sig
|
||||||
|
|
||||||
|
fingerprint, err := gpgGetKeyFingerprint(m.signingOptions.KeyID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get key fingerprint: %w", err)
|
||||||
|
}
|
||||||
|
o.Signer = fingerprint
|
||||||
|
|
||||||
|
pubKey, err := gpgExportPublicKey(m.signingOptions.KeyID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to export public key: %w", err)
|
||||||
|
}
|
||||||
|
o.SigningPubKey = pubKey
|
||||||
|
}
|
||||||
|
|
||||||
m.pbOuter = o
|
m.pbOuter = o
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user