Golang中解决子进程获取"https://google.com"时证书验证失败问题:tls: failed to verify certificate: SecPolicyCreateSSL error: 0

Golang中解决子进程获取"https://google.com"时证书验证失败问题:tls: failed to verify certificate: SecPolicyCreateSSL error: 0 以下是可以使用 go run main.go 复现的 main.go 文件:

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"syscall"
	"time"
)

// Mark of daemon process - system environment variable _GO_DAEMON=1
const (
	MARK_NAME  = "_GO_DAEMON"
	MARK_VALUE = "1"
)

// Default file permissions for log and pid files.
const FILE_PERM = os.FileMode(0640)

// WasReborn returns true in child process (daemon) and false in parent process.
func WasReborn() bool {
	return os.Getenv(MARK_NAME) == MARK_VALUE
}

// A Context describes daemon context.
type Context struct {
	// If PidFileName is non-empty, parent process will try to create and lock
	// pid file with given name. Child process writes process id to file.
	PidFileName string
	// Permissions for new pid file.
	PidFilePerm os.FileMode

	// If LogFileName is non-empty, parent process will create file with given name
	// and will link to fd 2 (stderr) for child process.
	LogFileName string
	// Permissions for new log file.
	LogFilePerm os.FileMode

	// If WorkDir is non-empty, the child changes into the directory before
	// creating the process.
	WorkDir string
	// If Chroot is non-empty, the child changes root directory
	Chroot string

	// If Env is non-nil, it gives the environment variables for the
	// daemon-process in the form returned by os.Environ.
	// If it is nil, the result of os.Environ will be used.
	Env []string
	// If Args is non-nil, it gives the command-line args for the
	// daemon-process. If it is nil, the result of os.Args will be used.
	Args []string

	// Credential holds user and group identities to be assumed by a daemon-process.
	Credential *syscall.Credential
	// If Umask is non-zero, the daemon-process call Umask() func with given value.
	Umask int

	// Struct contains only serializable public fields (!!!)
	abspath  string
	logFile  *os.File
	nullFile *os.File

	rpipe, wpipe *os.File
}

func (d *Context) SetLogFile(fd *os.File) {
	d.logFile = fd
}

func (d *Context) openFiles() (err error) {
	if d.PidFilePerm == 0 {
		d.PidFilePerm = FILE_PERM
	}
	if d.LogFilePerm == 0 {
		d.LogFilePerm = FILE_PERM
	}

	if d.nullFile, err = os.Open(os.DevNull); err != nil {
		return
	}

	if len(d.LogFileName) > 0 {
		if d.LogFileName == "/dev/stdout" {
			d.logFile = os.Stdout
		} else if d.LogFileName == "/dev/stderr" {
			d.logFile = os.Stderr
		} else if d.logFile, err = os.OpenFile(d.LogFileName,
			os.O_WRONLY|os.O_CREATE|os.O_APPEND, d.LogFilePerm); err != nil {
			return
		}
	}

	d.rpipe, d.wpipe, err = os.Pipe()
	return
}

func (d *Context) closeFiles() (err error) {
	fmt.Println("closeFiles")
	cl := func(file **os.File) {
		if *file != nil {
			(*file).Close()
			*file = nil
		}
	}
	cl(&d.rpipe)
	cl(&d.wpipe)
	cl(&d.logFile)
	cl(&d.nullFile)
	return
}

func (d *Context) prepareEnv() (err error) {
	if d.abspath, err = os.Executable(); err != nil {
		return
	}
	fmt.Println("abspath", d.abspath)

	if len(d.Args) == 0 {
		d.Args = os.Args
	}

	mark := fmt.Sprintf("%s=%s", MARK_NAME, MARK_VALUE)
	if len(d.Env) == 0 {
		d.Env = os.Environ()
	}
	d.Env = append(d.Env, mark)

	return
}

func (d *Context) files() (f []*os.File) {
	log := d.nullFile
	if d.logFile != nil {
		log = d.logFile
	}

	f = []*os.File{
		d.rpipe,    // (0) stdin
		log,        // (1) stdout
		log,        // (2) stderr
		d.nullFile, // (3) dup on fd 0 after initialization
	}

	return
}

func (d *Context) reborn() (child *os.Process, err error) {
	if !WasReborn() {
		child, err = d.parent()
	} else {
		err = d.child()
	}
	return
}

func (d *Context) parent() (child *os.Process, err error) {
	if err = d.prepareEnv(); err != nil {
		return
	}

	defer d.closeFiles()
	if err = d.openFiles(); err != nil {
		return
	}

	attr := &os.ProcAttr{
		Dir:   d.WorkDir,
		Env:   d.Env,
		Files: d.files(),
		Sys:   &syscall.SysProcAttr{},
	}

	fmt.Println("parent startProcess")
	if child, err = os.StartProcess(d.abspath, d.Args, attr); err != nil {
		return
	}

	d.rpipe.Close()
	encoder := json.NewEncoder(d.wpipe)
	err = encoder.Encode(d)

	fmt.Println("parent done")

	// if this sleep keeps the parent around, it will work.
	// time.Sleep(3 * time.Second)

	return
}

func (d *Context) child() (err error) {

	decoder := json.NewDecoder(os.Stdin)
	if err = decoder.Decode(d); err != nil {
		return
	}

	return
}

func main() {

	daemonContext := &Context{
		PidFileName: "test.pid",
		PidFilePerm: 0644,
		LogFileName: "test.log",
		LogFilePerm: 0640,
		WorkDir:     "/tmp",
		Umask:       027,
		Args:        []string{"SecPolicyCreateSSL"},
	}

	d, err := daemonContext.reborn()
	if err != nil {
		log.Fatalln(err)
	}
	if d != nil { // return the parent process since it's now forked into a child
		return
	}

	time.Sleep(2 * time.Second)

	fmt.Println("doing get...")
	resp, err := http.Get("https://google.com")
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Println(err)
		return
	}
	fmt.Println(string(body)[:100])

	return
}

运行后,你会在 test.log 中看到:

doing get...
2024/07/23 13:07:21 Get "https://google.com": tls: failed to verify certificate: SecPolicyCreateSSL error: 0

现在,在 parent 函数中,取消注释 // time.Sleep(3 * time.Second) 然后再次运行。

你应该会看到它正常工作:

doing get...
<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="en"><head><meta content

看起来父进程过早关闭会导致子进程(我猜测)在 Linux 和 macOS 上无法获得适当的环境。

不过,我并不完全确定原因。我已经尝试深入到我已知的底层去探究,但还没有找到原因。有人知道为什么这不可能实现吗?

我希望父进程能够在子进程之前正常退出,这样我就可以使用 Go 二进制文件作为守护进程/服务的控制器。但我无法保证 HTTPS 调用会在父进程退出之前发生。


更多关于Golang中解决子进程获取"https://google.com"时证书验证失败问题:tls: failed to verify certificate: SecPolicyCreateSSL error: 0的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中解决子进程获取"https://google.com"时证书验证失败问题:tls: failed to verify certificate: SecPolicyCreateSSL error: 0的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这个问题是由于父进程退出时关闭了文件描述符,导致子进程继承的某些环境或资源丢失。在 macOS 上,当父进程退出时,子进程可能无法正确访问系统证书存储。

以下是解决方案:

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"syscall"
	"time"
)

// Mark of daemon process - system environment variable _GO_DAEMON=1
const (
	MARK_NAME  = "_GO_DAEMON"
	MARK_VALUE = "1"
)

// Default file permissions for log and pid files.
const FILE_PERM = os.FileMode(0640)

// WasReborn returns true in child process (daemon) and false in parent process.
func WasReborn() bool {
	return os.Getenv(MARK_NAME) == MARK_VALUE
}

// A Context describes daemon context.
type Context struct {
	// If PidFileName is non-empty, parent process will try to create and lock
	// pid file with given name. Child process writes process id to file.
	PidFileName string
	// Permissions for new pid file.
	PidFilePerm os.FileMode

	// If LogFileName is non-empty, parent process will create file with given name
	// and will link to fd 2 (stderr) for child process.
	LogFileName string
	// Permissions for new log file.
	LogFilePerm os.FileMode

	// If WorkDir is non-empty, the child changes into the directory before
	// creating the process.
	WorkDir string
	// If Chroot is non-empty, the child changes root directory
	Chroot string

	// If Env is non-nil, it gives the environment variables for the
	// daemon-process in the form returned by os.Environ.
	// If it is nil, the result of os.Environ will be used.
	Env []string
	// If Args is non-nil, it gives the command-line args for the
	// daemon-process. If it is nil, the result of os.Args will be used.
	Args []string

	// Credential holds user and group identities to be assumed by a daemon-process.
	Credential *syscall.Credential
	// If Umask is non-zero, the daemon-process call Umask() func with given value.
	Umask int

	// Struct contains only serializable public fields (!!!)
	abspath  string
	logFile  *os.File
	nullFile *os.File

	rpipe, wpipe *os.File
}

func (d *Context) SetLogFile(fd *os.File) {
	d.logFile = fd
}

func (d *Context) openFiles() (err error) {
	if d.PidFilePerm == 0 {
		d.PidFilePerm = FILE_PERM
	}
	if d.LogFilePerm == 0 {
		d.LogFilePerm = FILE_PERM
	}

	if d.nullFile, err = os.Open(os.DevNull); err != nil {
		return
	}

	if len(d.LogFileName) > 0 {
		if d.LogFileName == "/dev/stdout" {
			d.logFile = os.Stdout
		} else if d.LogFileName == "/dev/stderr" {
			d.logFile = os.Stderr
		} else if d.logFile, err = os.OpenFile(d.LogFileName,
			os.O_WRONLY|os.O_CREATE|os.O_APPEND, d.LogFilePerm); err != nil {
			return
		}
	}

	d.rpipe, d.wpipe, err = os.Pipe()
	return
}

func (d *Context) closeFiles() (err error) {
	fmt.Println("closeFiles")
	cl := func(file **os.File) {
		if *file != nil {
			(*file).Close()
			*file = nil
		}
	}
	cl(&d.rpipe)
	cl(&d.wpipe)
	cl(&d.logFile)
	cl(&d.nullFile)
	return
}

func (d *Context) prepareEnv() (err error) {
	if d.abspath, err = os.Executable(); err != nil {
		return
	}
	fmt.Println("abspath", d.abspath)

	if len(d.Args) == 0 {
		d.Args = os.Args
	}

	mark := fmt.Sprintf("%s=%s", MARK_NAME, MARK_VALUE)
	if len(d.Env) == 0 {
		d.Env = os.Environ()
	}
	d.Env = append(d.Env, mark)

	return
}

func (d *Context) files() (f []*os.File) {
	log := d.nullFile
	if d.logFile != nil {
		log = d.logFile
	}

	f = []*os.File{
		d.rpipe,    // (0) stdin
		log,        // (1) stdout
		log,        // (2) stderr
		d.nullFile, // (3) dup on fd 0 after initialization
	}

	return
}

func (d *Context) reborn() (child *os.Process, err error) {
	if !WasReborn() {
		child, err = d.parent()
	} else {
		err = d.child()
	}
	return
}

func (d *Context) parent() (child *os.Process, err error) {
	if err = d.prepareEnv(); err != nil {
		return
	}

	defer d.closeFiles()
	if err = d.openFiles(); err != nil {
		return
	}

	attr := &os.ProcAttr{
		Dir:   d.WorkDir,
		Env:   d.Env,
		Files: d.files(),
		Sys:   &syscall.SysProcAttr{},
	}

	fmt.Println("parent startProcess")
	if child, err = os.StartProcess(d.abspath, d.Args, attr); err != nil {
		return
	}

	d.rpipe.Close()
	encoder := json.NewEncoder(d.wpipe)
	err = encoder.Encode(d)

	fmt.Println("parent done")

	// 关键修改:等待子进程完成初始化
	time.Sleep(100 * time.Millisecond)
	return
}

func (d *Context) child() (err error) {
	// 关键修改:在解码前设置 Setpgid 以避免成为孤儿进程
	syscall.Setpgid(0, 0)

	decoder := json.NewDecoder(os.Stdin)
	if err = decoder.Decode(d); err != nil {
		return
	}

	return
}

func main() {
	daemonContext := &Context{
		PidFileName: "test.pid",
		PidFilePerm: 0644,
		LogFileName: "test.log",
		LogFilePerm: 0640,
		WorkDir:     "/tmp",
		Umask:       027,
		Args:        []string{"SecPolicyCreateSSL"},
	}

	d, err := daemonContext.reborn()
	if err != nil {
		log.Fatalln(err)
	}
	if d != nil {
		// 关键修改:等待子进程完成初始化后再退出
		time.Sleep(100 * time.Millisecond)
		return
	}

	time.Sleep(2 * time.Second)

	fmt.Println("doing get...")
	
	// 可选:自定义 Transport 以更好地控制 TLS 验证
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: false,
		},
	}
	client := &http.Client{Transport: tr}
	
	resp, err := client.Get("https://google.com")
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Println(err)
		return
	}
	fmt.Println(string(body)[:100])

	return
}

主要修改:

  1. 在子进程中设置进程组IDsyscall.Setpgid(0, 0) 使子进程成为新的进程组领导,避免成为孤儿进程。

  2. 增加适当的等待时间:父进程等待100毫秒确保子进程完成初始化。

  3. 使用自定义HTTP客户端:虽然问题主要在于进程管理,但使用自定义Transport可以提供更好的控制。

问题的根本原因是macOS的安全框架(Security Framework)在验证证书时需要访问系统钥匙串(Keychain),当父进程过早退出时,子进程可能无法正确继承必要的安全上下文。通过上述修改,可以确保子进程在父进程退出前完成必要的初始化。

回到顶部