You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Golang中通过exec包实现进程标准输入替换验证GPG签名

解决Golang中exec调用gpg --verify的签名验证失败问题

问题背景

尝试在Golang中通过exec包调用gpg --verify,实现类似bash进程替换gpg --verify <(cat ./test.txt.sig) <(cat ./test.txt)的功能——用io.Reader传递签名和原文件内容,而非直接使用文件名。编写的代码如下:

VerifyStreamsWithContext(ctx context.Context, sr io.Reader, fr io.Reader) (<-chan bool, <-chan error) {
    okChan := make(chan bool, 1)
    errChan := make(chan error)
    cmd := exec.CommandContext(ctx, "gpg", "--verify", "-", "-")

    r := io.MultiReader(sr, fr)
    cmd.Stdin = r

    go func() {
        defer close(okChan)
        defer close(errChan)

        out, err := cmd.CombinedOutput()
        if err != nil {
            fmt.Println(string(out), err)
            errChan <- err
            return
        }

        if strings.Contains(string(out), ValidSignatureCheck) {
            okChan <- true
        } else {
            okChan <- false
        }
    }()

    return okChan, errChan
}

错误现象

执行后出现签名验证失败:

gpg: Signature made jeu. 16 mai 2024 09:27:28 CEST
gpg:                using RSA key xxx
gpg: BAD signature from "x x <xxx@x.com>" [ultimate]
 exit status 1
panic: exit status 1

问题原因

根据gpg --verify的文档说明:

--verify:
Assume that the first argument is a signed file and verify it without generating any output. With no arguments, the signature packet is read from STDIN. If only one argument is given, the specified file is expected to include a complete signature. With more than one argument, the first argument should specify a file with a detached signature and the remaining files should contain the signed data. To read the signed data from STDIN, use - as the second filename. For security reasons, a detached signature will not read the signed material from STDIN if not explicitly specified.

原代码的核心问题:使用MultiReader把签名流和原文件流合并到同一个stdin中,而gpg --verify - -的逻辑是第一个-从stdin读取签名文件,第二个-从stdin读取原文件——但stdin是单一流,gpg会将整个合并后的流当作签名文件解析,后续读取原文件时要么无内容可读,要么读到错误内容,最终导致签名验证失败。

解决方案

需要给gpg提供两个独立的输入流,通过额外文件描述符传递签名和原文件内容,具体实现如下:

import (
    "context"
    "io"
    "os"
    "os/exec"
    "strings"
)

const ValidSignatureCheck = "Good signature from"

func VerifyStreamsWithContext(ctx context.Context, sr io.Reader, fr io.Reader) (<-chan bool, <-chan error) {
    okChan := make(chan bool, 1)
    errChan := make(chan error, 1)

    // 创建两个管道,分别传递签名流和原文件流
    sigR, sigW, err := os.Pipe()
    if err != nil {
        errChan <- err
        close(okChan)
        close(errChan)
        return okChan, errChan
    }
    dataR, dataW, err := os.Pipe()
    if err != nil {
        sigR.Close()
        sigW.Close()
        errChan <- err
        close(okChan)
        close(errChan)
        return okChan, errChan
    }

    // 启动gpg命令,通过/dev/fd读取额外的文件描述符
    // ExtraFiles中的文件从fd=3开始索引(0=stdin,1=stdout,2=stderr)
    cmd := exec.CommandContext(ctx, "gpg", "--verify", "/dev/fd/3", "/dev/fd/4")
    cmd.ExtraFiles = []*os.File{sigR, dataR}

    go func() {
        defer sigW.Close()
        defer dataW.Close()
        // 分别将签名和原文件内容写入管道
        _, sigErr := io.Copy(sigW, sr)
        _, dataErr := io.Copy(dataW, fr)
        // 若写入出错,提前取消上下文
        if sigErr != nil || dataErr != nil {
            if cancel, ok := ctx.Value("cancelFunc").(context.CancelFunc); ok {
                cancel()
            }
        }
    }()

    go func() {
        defer close(okChan)
        defer close(errChan)
        defer sigR.Close()
        defer dataR.Close()

        out, err := cmd.CombinedOutput()
        if err != nil {
            errChan <- err
            return
        }

        okChan <- strings.Contains(string(out), ValidSignatureCheck)
    }()

    return okChan, errChan
}

关键说明:

  • 通过os.Pipe()创建两个独立管道,分别承载签名流和原文件流;
  • 利用cmd.ExtraFiles将管道的读端文件描述符传递给gpg进程;
  • gpg通过/dev/fd/3和/dev/fd/4读取这两个管道的内容,对应签名文件和原文件;
  • 启动goroutine分别将输入流写入管道的写端,确保两个流独立传输。

内容的提问来源于stack exchange,提问作者bragdon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 01:04:58