Scert/backend/internal/bootstrap/certificate_root.go

102 lines
3.4 KiB
Go

package bootstrap
import (
"context"
"crypto/rand"
"errors"
"os"
"path/filepath"
"strings"
"git.pcux.cn/xuans/Scert/backend/internal/certstorage"
"git.pcux.cn/xuans/Scert/backend/internal/config"
)
// prepareCertificateRoot 在申请证书前验证路径,并以实际运行身份探测创建、写入和清理权限。
func prepareCertificateRoot(paths config.PathsConfig, requested string) (string, error) {
return prepareCertificateRootContext(context.Background(), paths, requested)
}
func prepareCertificateRootContext(ctx context.Context, paths config.PathsConfig, requested string) (string, error) {
root := strings.TrimSpace(requested)
if root == "" {
root = paths.Certificate
}
root = filepath.Clean(root)
invalid := func(err error) (string, error) {
return "", completeError(CompleteErrorCertificatePath, "证书部署根目录无效", err)
}
unwritable := func(err error) (string, error) {
return "", completeError(CompleteErrorCertificateAccess, "证书部署根目录不可写", err)
}
if !filepath.IsAbs(root) || filepath.Dir(root) == root || len(root) > 4096 || strings.ContainsAny(root, "\x00\r\n") {
return invalid(nil)
}
// 允许安装目录下独立的证书子目录,但不能覆盖安装目录本身或其他运行数据。
if pathWithinCertificateRoot(root, paths.Install) {
return invalid(nil)
}
for _, protected := range []string{paths.Config, paths.Data, paths.Log, paths.Backup} {
if pathWithinCertificateRoot(root, protected) || pathWithinCertificateRoot(protected, root) {
return invalid(nil)
}
}
for current := root; ; current = filepath.Dir(current) {
info, err := os.Lstat(current)
if err == nil && (!info.IsDir() || info.Mode()&os.ModeSymlink != 0) {
return invalid(nil)
}
if err != nil && !errors.Is(err, os.ErrNotExist) {
return unwritable(err)
}
if filepath.Dir(current) == current {
break
}
}
if err := os.MkdirAll(root, 0o700); err != nil {
if prepareErr := certstorage.Prepare(ctx, root); prepareErr != nil {
return unwritable(errors.Join(err, prepareErr))
}
}
directory, err := os.OpenRoot(root)
if err != nil {
return unwritable(err)
}
defer directory.Close()
// 探测文件使用随机名称和排他创建,只清理本次创建的文件,不修改已有文件。
name := ".scert-write-check-" + rand.Text()
probe, err := directory.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
if prepareErr := certstorage.Prepare(ctx, root); prepareErr != nil {
return unwritable(errors.Join(err, prepareErr))
}
// systemd 已替换该挂载点;必须重新打开目录,不能复用旧的只读描述符。
_ = directory.Close()
directory, err = os.OpenRoot(root)
if err != nil {
return unwritable(err)
}
defer directory.Close()
probe, err = directory.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
return unwritable(err)
}
}
_, writeErr := probe.Write([]byte("scert"))
syncErr := probe.Sync()
closeErr := probe.Close()
removeErr := directory.Remove(name)
if err := errors.Join(writeErr, syncErr, closeErr, removeErr); err != nil {
return unwritable(err)
}
return root, nil
}
func pathWithinCertificateRoot(parent, child string) bool {
if strings.TrimSpace(parent) == "" || strings.TrimSpace(child) == "" {
return false
}
relative, err := filepath.Rel(parent, child)
return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) && !filepath.IsAbs(relative)
}