mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-11-14 14:47:19 +00:00
Debian co-signs each suite's InRelease with more than one archive key, so gpgv reports a good signature from the key you trust and NO_PUBKEY for the others, exiting 2. Both verifiers treat that as failure, which means mirroring Debian requires importing every key Debian happens to sign with. apt does not require this: it is fatal only when there is no good signature or a BADSIG. Adopt that rule for unknown signers. When gpgv exits non-zero the status stream is parsed per NEWSIG block and the result is a success only when the process exited, at least one block is a good signature from a held key, at least one block is an unknown issuer (ERRSIG reason 9 with NO_PUBKEY), and nothing else appears. BADSIG, EXPSIG, EXPKEYSIG and REVKEYSIG from a held key still fail, as does any unexplained failure. The exit-zero path is unchanged. KEYEXPIRED and KEYREVOKED are deliberately not disqualifiers: doc/DETAILS notes KEYEXPIRED is emitted for an expired subkey even when that subkey was not used, so treating it as a verdict rejects valid signatures. checkDetachedSignature gets the equivalent change for the internal verifier.
517 lines
13 KiB
Go
517 lines
13 KiB
Go
package pgp
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Test interface
|
|
var (
|
|
_ Signer = &GpgSigner{}
|
|
_ Verifier = &GpgVerifier{}
|
|
)
|
|
|
|
// GpgSigner is implementation of Signer interface using gpg as external program
|
|
type GpgSigner struct {
|
|
gpg string
|
|
version GPGVersion
|
|
keyRefs []string
|
|
keyring, secretKeyring string
|
|
passphrase, passphraseFile string
|
|
batch bool
|
|
}
|
|
|
|
// SetBatch control --no-tty flag to gpg
|
|
func (g *GpgSigner) SetBatch(batch bool) {
|
|
g.batch = batch
|
|
}
|
|
|
|
// SetKey sets key ID to use when signing files
|
|
func (g *GpgSigner) SetKey(keyRef string) {
|
|
keyRef = strings.TrimSpace(keyRef)
|
|
if keyRef != "" {
|
|
if g.keyRefs == nil {
|
|
g.keyRefs = []string{keyRef}
|
|
} else {
|
|
g.keyRefs = append(g.keyRefs, keyRef)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetKeyRing allows to set custom keyring and secretkeyring
|
|
func (g *GpgSigner) SetKeyRing(keyring, secretKeyring string) {
|
|
g.keyring, g.secretKeyring = keyring, secretKeyring
|
|
}
|
|
|
|
// SetPassphrase sets passphrase params
|
|
func (g *GpgSigner) SetPassphrase(passphrase, passphraseFile string) {
|
|
g.passphrase, g.passphraseFile = passphrase, passphraseFile
|
|
}
|
|
|
|
func (g *GpgSigner) gpgArgs() []string {
|
|
args := []string{}
|
|
if g.keyring != "" {
|
|
args = append(args, "--no-auto-check-trustdb", "--no-default-keyring", "--keyring", g.keyring)
|
|
}
|
|
if g.secretKeyring != "" && g.version == GPG1x {
|
|
args = append(args, "--secret-keyring", g.secretKeyring)
|
|
}
|
|
|
|
for _, k := range g.keyRefs {
|
|
args = append(args, "-u", k)
|
|
}
|
|
|
|
if g.passphrase != "" || g.passphraseFile != "" {
|
|
if g.version == GPG1x {
|
|
args = append(args, "--no-use-agent")
|
|
}
|
|
}
|
|
|
|
if g.passphrase != "" {
|
|
args = append(args, "--passphrase", g.passphrase)
|
|
}
|
|
|
|
if g.passphraseFile != "" {
|
|
args = append(args, "--passphrase-file", g.passphraseFile)
|
|
}
|
|
|
|
if g.batch {
|
|
args = append(args, "--no-tty", "--batch")
|
|
if g.version >= GPG21x {
|
|
args = append(args, "--pinentry-mode", "loopback")
|
|
}
|
|
}
|
|
|
|
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
|
|
if _, err := strconv.ParseInt(epoch, 10, 64); err == nil {
|
|
args = append(args, "--faked-system-time", epoch)
|
|
}
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
// NewGpgSigner creates a new gpg signer
|
|
func NewGpgSigner(finder GPGFinder) *GpgSigner {
|
|
gpg, version, err := finder.FindGPG()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return &GpgSigner{gpg: gpg, version: version}
|
|
}
|
|
|
|
// Init verifies availability of gpg & presence of keys
|
|
func (g *GpgSigner) Init() error {
|
|
output, err := exec.Command(g.gpg, "--list-keys", "--dry-run", "--no-auto-check-trustdb").CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to execute gpg: %s (is gpg installed?): %s", err, string(output))
|
|
}
|
|
|
|
if g.keyring == "" && g.secretKeyring == "" && len(output) == 0 {
|
|
return fmt.Errorf("looks like there are no keys in gpg, please create one (official manual: http://www.gnupg.org/gph/en/manual.html)")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// DetachedSign signs file with detached signature in ASCII format
|
|
func (g *GpgSigner) DetachedSign(source string, destination string) error {
|
|
fmt.Printf("Signing file '%s' with gpg, please enter your passphrase when prompted:\n", filepath.Base(source))
|
|
|
|
args := []string{"-o", destination, "--digest-algo", "SHA256", "--armor", "--yes"}
|
|
args = append(args, g.gpgArgs()...)
|
|
args = append(args, "--detach-sign", source)
|
|
cmd := exec.Command(g.gpg, args...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
// ClearSign clear-signs the file
|
|
func (g *GpgSigner) ClearSign(source string, destination string) error {
|
|
fmt.Printf("Clearsigning file '%s' with gpg, please enter your passphrase when prompted:\n", filepath.Base(source))
|
|
args := []string{"-o", destination, "--digest-algo", "SHA256", "--yes"}
|
|
args = append(args, g.gpgArgs()...)
|
|
args = append(args, "--clearsign", source)
|
|
cmd := exec.Command(g.gpg, args...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
// GpgVerifier is implementation of Verifier interface using gpgv as external program
|
|
type GpgVerifier struct {
|
|
gpg string
|
|
gpgv string
|
|
version GPGVersion
|
|
keyRings []string
|
|
}
|
|
|
|
// NewGpgVerifier creates a new gpg verifier
|
|
func NewGpgVerifier(finder GPGFinder) *GpgVerifier {
|
|
gpg, versionGPG, err := finder.FindGPG()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
gpgv, versionGPGV, err := finder.FindGPGV()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if versionGPG != versionGPGV {
|
|
panic(errors.New("gpg and gpgv versions don't match"))
|
|
}
|
|
|
|
return &GpgVerifier{gpg: gpg, gpgv: gpgv, version: versionGPG}
|
|
}
|
|
|
|
// InitKeyring verifies that gpg is installed and some keys are trusted
|
|
func (g *GpgVerifier) InitKeyring(verbose bool) error {
|
|
if len(g.keyRings) == 0 && verbose {
|
|
// using default keyring
|
|
output, err := exec.Command(g.gpg, "--no-default-keyring", "--no-auto-check-trustdb", "--keyring", "trustedkeys.gpg", "--list-keys").Output()
|
|
if err == nil && len(output) == 0 {
|
|
fmt.Printf("\nLooks like your keyring with trusted keys is empty. You might consider importing some keys.\n")
|
|
fmt.Printf("If you're running Debian or Ubuntu, it's a good idea to import current archive keys by running:\n\n")
|
|
fmt.Printf(" gpg --no-default-keyring --keyring /usr/share/keyrings/debian-archive-keyring.gpg --export | gpg --no-default-keyring --keyring trustedkeys.gpg --import\n")
|
|
fmt.Printf("\n(for Ubuntu, use /usr/share/keyrings/ubuntu-archive-keyring.gpg)\n\n")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddKeyring adds custom keyring to GPG parameters
|
|
func (g *GpgVerifier) AddKeyring(keyring string) {
|
|
g.keyRings = append(g.keyRings, keyring)
|
|
}
|
|
|
|
func (g *GpgVerifier) argsKeyrings() (args []string) {
|
|
if len(g.keyRings) > 0 {
|
|
args = make([]string, 0, 2*len(g.keyRings))
|
|
for _, keyring := range g.keyRings {
|
|
args = append(args, "--keyring", keyring)
|
|
}
|
|
} else {
|
|
args = []string{"--keyring", "trustedkeys.gpg"}
|
|
}
|
|
return
|
|
}
|
|
|
|
type gpgvSignatureDisposition int
|
|
|
|
const (
|
|
gpgvSignatureOther gpgvSignatureDisposition = iota
|
|
gpgvSignatureGood
|
|
gpgvSignatureUnknown
|
|
gpgvSignatureDisqualified
|
|
)
|
|
|
|
type gpgvSignatureStatus struct {
|
|
goodSig bool
|
|
validSig bool
|
|
missingKeyErrSig bool
|
|
noPubKey bool
|
|
disqualified bool
|
|
otherFailure bool
|
|
}
|
|
|
|
func (status gpgvSignatureStatus) disposition() gpgvSignatureDisposition {
|
|
if status.disqualified {
|
|
return gpgvSignatureDisqualified
|
|
}
|
|
if status.otherFailure {
|
|
return gpgvSignatureOther
|
|
}
|
|
if status.goodSig && status.validSig {
|
|
return gpgvSignatureGood
|
|
}
|
|
if status.missingKeyErrSig && status.noPubKey && !status.goodSig && !status.validSig {
|
|
return gpgvSignatureUnknown
|
|
}
|
|
return gpgvSignatureOther
|
|
}
|
|
|
|
func (g *GpgVerifier) runGpgv(args []string, context string, showKeyTip bool) (*KeyInfo, error) {
|
|
args = append([]string{"--status-fd", "3"}, args...)
|
|
cmd := exec.Command(g.gpgv, args...)
|
|
|
|
tempf, err := os.CreateTemp("", "aptly-gpg-status")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = tempf.Close()
|
|
}()
|
|
|
|
err = os.Remove(tempf.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd.ExtraFiles = []*os.File{tempf}
|
|
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = stderr.Close()
|
|
}()
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buffer := &bytes.Buffer{}
|
|
|
|
_, err = io.Copy(io.MultiWriter(os.Stderr, buffer), stderr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmderr := cmd.Wait()
|
|
|
|
_, _ = tempf.Seek(0, 0)
|
|
|
|
statusr := bufio.NewScanner(tempf)
|
|
|
|
result := &KeyInfo{}
|
|
statuses := []gpgvSignatureStatus{}
|
|
var status *gpgvSignatureStatus
|
|
flushStatus := func() {
|
|
if status != nil {
|
|
statuses = append(statuses, *status)
|
|
}
|
|
}
|
|
|
|
for statusr.Scan() {
|
|
line := strings.TrimSpace(statusr.Text())
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 2 && fields[0] == "[GNUPG:]" && fields[1] == "NEWSIG" {
|
|
flushStatus()
|
|
status = &gpgvSignatureStatus{}
|
|
continue
|
|
}
|
|
if status == nil {
|
|
status = &gpgvSignatureStatus{otherFailure: true}
|
|
}
|
|
if len(fields) < 2 || fields[0] != "[GNUPG:]" {
|
|
status.otherFailure = true
|
|
continue
|
|
}
|
|
|
|
switch fields[1] {
|
|
case "GOODSIG":
|
|
if len(fields) < 3 {
|
|
status.otherFailure = true
|
|
continue
|
|
}
|
|
status.goodSig = true
|
|
result.GoodKeys = append(result.GoodKeys, Key(fields[2]))
|
|
case "VALIDSIG":
|
|
status.validSig = true
|
|
case "NO_PUBKEY":
|
|
if len(fields) < 3 {
|
|
status.otherFailure = true
|
|
continue
|
|
}
|
|
status.noPubKey = true
|
|
result.MissingKeys = append(result.MissingKeys, Key(fields[2]))
|
|
case "ERRSIG":
|
|
// Field 7 is the gpg-error reason; 9 is GPG_ERR_NO_PUBKEY.
|
|
if len(fields) > 7 && fields[7] == "9" {
|
|
status.missingKeyErrSig = true
|
|
} else {
|
|
status.otherFailure = true
|
|
}
|
|
case "BADSIG", "EXPSIG", "EXPKEYSIG", "REVKEYSIG":
|
|
status.disqualified = true
|
|
}
|
|
}
|
|
flushStatus()
|
|
|
|
if err = statusr.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if cmderr != nil {
|
|
goodSignatures := 0
|
|
unknownSignatures := 0
|
|
failureExplained := cmd.ProcessState != nil && cmd.ProcessState.Exited()
|
|
for _, signatureStatus := range statuses {
|
|
switch signatureStatus.disposition() {
|
|
case gpgvSignatureGood:
|
|
goodSignatures++
|
|
case gpgvSignatureUnknown:
|
|
unknownSignatures++
|
|
default:
|
|
failureExplained = false
|
|
}
|
|
}
|
|
if failureExplained && goodSignatures > 0 && unknownSignatures > 0 {
|
|
return result, nil
|
|
}
|
|
|
|
if showKeyTip && len(g.keyRings) == 0 && len(result.MissingKeys) > 0 {
|
|
fmt.Printf("\nLooks like some keys are missing in your trusted keyring, you may consider importing them from keyserver:\n\n")
|
|
|
|
keys := make([]string, len(result.MissingKeys))
|
|
|
|
for i := range result.MissingKeys {
|
|
keys[i] = string(result.MissingKeys[i])
|
|
}
|
|
|
|
fmt.Printf("gpg --no-default-keyring --keyring trustedkeys.gpg --keyserver keyserver.ubuntu.com --recv-keys %s\n\n",
|
|
strings.Join(keys, " "))
|
|
|
|
fmt.Printf("Sometimes keys are stored in repository root in file named Release.key, to import such key:\n\n")
|
|
fmt.Printf("wget -O - https://some.repo/repository/Release.key | gpg --no-default-keyring --keyring trustedkeys.gpg --import\n\n")
|
|
}
|
|
return result, fmt.Errorf("verification of %s failed: %s", context, cmderr)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// VerifyDetachedSignature verifies combination of signature and cleartext using gpgv
|
|
func (g *GpgVerifier) VerifyDetachedSignature(signature, cleartext io.Reader, showKeyTip bool) error {
|
|
args := g.argsKeyrings()
|
|
|
|
sigf, err := os.CreateTemp("", "aptly-gpg")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
_ = os.Remove(sigf.Name())
|
|
_ = sigf.Close()
|
|
}()
|
|
|
|
_, err = io.Copy(sigf, signature)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clearf, err := os.CreateTemp("", "aptly-gpg")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
_ = os.Remove(clearf.Name())
|
|
_ = clearf.Close()
|
|
}()
|
|
|
|
_, err = io.Copy(clearf, cleartext)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
args = append(args, sigf.Name(), clearf.Name())
|
|
_, err = g.runGpgv(args, "detached signature", showKeyTip)
|
|
return err
|
|
}
|
|
|
|
// IsClearSigned returns true if file contains signature
|
|
func (g *GpgVerifier) IsClearSigned(clearsigned io.Reader) (bool, error) {
|
|
scanner := bufio.NewScanner(clearsigned)
|
|
for scanner.Scan() {
|
|
if strings.Contains(scanner.Text(), "BEGIN PGP SIGN") {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, scanner.Err()
|
|
}
|
|
|
|
// VerifyClearsigned verifies clearsigned file using gpgv
|
|
func (g *GpgVerifier) VerifyClearsigned(clearsigned io.Reader, showKeyTip bool) (*KeyInfo, error) {
|
|
args := g.argsKeyrings()
|
|
|
|
clearf, err := os.CreateTemp("", "aptly-gpg")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = os.Remove(clearf.Name())
|
|
_ = clearf.Close()
|
|
}()
|
|
|
|
_, err = io.Copy(clearf, clearsigned)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
args = append(args, clearf.Name())
|
|
return g.runGpgv(args, "clearsigned file", showKeyTip)
|
|
}
|
|
|
|
// ExtractClearsigned extracts cleartext from clearsigned file WITHOUT signature verification
|
|
func (g *GpgVerifier) ExtractClearsigned(clearsigned io.Reader) (text *os.File, err error) {
|
|
clearf, err := os.CreateTemp("", "aptly-gpg")
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() {
|
|
_ = os.Remove(clearf.Name())
|
|
_ = clearf.Close()
|
|
}()
|
|
|
|
_, err = io.Copy(clearf, clearsigned)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
text, err = os.CreateTemp("", "aptly-gpg")
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() {
|
|
_ = os.Remove(text.Name())
|
|
}()
|
|
|
|
args := []string{"--no-auto-check-trustdb", "--decrypt", "--batch", "--skip-verify", "--output", "-", clearf.Name()}
|
|
|
|
cmd := exec.Command(g.gpg, args...)
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
_ = stdout.Close()
|
|
}()
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = io.Copy(text, stdout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = cmd.Wait()
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extraction of clearsigned file failed: %s", err)
|
|
}
|
|
|
|
_, err = text.Seek(0, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return
|
|
}
|