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:
+90
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user