213 lines
5.7 KiB
Go
213 lines
5.7 KiB
Go
package mfer
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"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)
|
|
}
|
|
|
|
// gpgExtractPubKeyFingerprint imports a public key into a temporary keyring
|
|
// and extracts its fingerprint. This verifies the key is valid and returns
|
|
// the actual fingerprint from the key material.
|
|
func gpgExtractPubKeyFingerprint(pubKey []byte) (string, error) {
|
|
// Create temporary directory for GPG operations
|
|
tmpDir, err := os.MkdirTemp("", "mfer-gpg-fingerprint-*")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create temp dir: %w", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Set restrictive permissions
|
|
if err := os.Chmod(tmpDir, 0o700); err != nil {
|
|
return "", fmt.Errorf("failed to set temp dir permissions: %w", err)
|
|
}
|
|
|
|
// Write public key to temp file
|
|
pubKeyFile := filepath.Join(tmpDir, "pubkey.asc")
|
|
if err := os.WriteFile(pubKeyFile, pubKey, 0o600); err != nil {
|
|
return "", fmt.Errorf("failed to write public key: %w", err)
|
|
}
|
|
|
|
// Import the public key into the temporary keyring
|
|
importCmd := exec.Command("gpg",
|
|
"--homedir", tmpDir,
|
|
"--import",
|
|
pubKeyFile,
|
|
)
|
|
var importStderr bytes.Buffer
|
|
importCmd.Stderr = &importStderr
|
|
if err := importCmd.Run(); err != nil {
|
|
return "", fmt.Errorf("failed to import public key: %w: %s", err, importStderr.String())
|
|
}
|
|
|
|
// List keys to get fingerprint
|
|
listCmd := exec.Command("gpg",
|
|
"--homedir", tmpDir,
|
|
"--with-colons",
|
|
"--fingerprint",
|
|
)
|
|
var listStdout, listStderr bytes.Buffer
|
|
listCmd.Stdout = &listStdout
|
|
listCmd.Stderr = &listStderr
|
|
if err := listCmd.Run(); err != nil {
|
|
return "", fmt.Errorf("failed to list keys: %w: %s", err, listStderr.String())
|
|
}
|
|
|
|
// Parse the colon-delimited output to find the fingerprint
|
|
lines := strings.Split(listStdout.String(), "\n")
|
|
for _, line := range lines {
|
|
fields := strings.Split(line, ":")
|
|
if len(fields) >= 10 && fields[0] == "fpr" {
|
|
return fields[9], nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("fingerprint not found in imported key")
|
|
}
|
|
|
|
// gpgVerify verifies a detached signature against data using the provided public key.
|
|
// It creates a temporary keyring to import the public key for verification.
|
|
func gpgVerify(data, signature, pubKey []byte) error {
|
|
// Create temporary directory for GPG operations
|
|
tmpDir, err := os.MkdirTemp("", "mfer-gpg-verify-*")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create temp dir: %w", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Set restrictive permissions
|
|
if err := os.Chmod(tmpDir, 0o700); err != nil {
|
|
return fmt.Errorf("failed to set temp dir permissions: %w", err)
|
|
}
|
|
|
|
// Write public key to temp file
|
|
pubKeyFile := filepath.Join(tmpDir, "pubkey.asc")
|
|
if err := os.WriteFile(pubKeyFile, pubKey, 0o600); err != nil {
|
|
return fmt.Errorf("failed to write public key: %w", err)
|
|
}
|
|
|
|
// Write signature to temp file
|
|
sigFile := filepath.Join(tmpDir, "signature.asc")
|
|
if err := os.WriteFile(sigFile, signature, 0o600); err != nil {
|
|
return fmt.Errorf("failed to write signature: %w", err)
|
|
}
|
|
|
|
// Write data to temp file
|
|
dataFile := filepath.Join(tmpDir, "data")
|
|
if err := os.WriteFile(dataFile, data, 0o600); err != nil {
|
|
return fmt.Errorf("failed to write data: %w", err)
|
|
}
|
|
|
|
// Import the public key into the temporary keyring
|
|
importCmd := exec.Command("gpg",
|
|
"--homedir", tmpDir,
|
|
"--import",
|
|
pubKeyFile,
|
|
)
|
|
var importStderr bytes.Buffer
|
|
importCmd.Stderr = &importStderr
|
|
if err := importCmd.Run(); err != nil {
|
|
return fmt.Errorf("failed to import public key: %w: %s", err, importStderr.String())
|
|
}
|
|
|
|
// Verify the signature
|
|
verifyCmd := exec.Command("gpg",
|
|
"--homedir", tmpDir,
|
|
"--verify",
|
|
sigFile,
|
|
dataFile,
|
|
)
|
|
var verifyStderr bytes.Buffer
|
|
verifyCmd.Stderr = &verifyStderr
|
|
if err := verifyCmd.Run(); err != nil {
|
|
return fmt.Errorf("signature verification failed: %w: %s", err, verifyStderr.String())
|
|
}
|
|
|
|
return nil
|
|
}
|