234 lines
7.7 KiB
Go
234 lines
7.7 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.pcux.cn/xuans/Smail/utils/atomicfile"
|
|
"git.pcux.cn/xuans/Smail/utils/runtimepath"
|
|
)
|
|
|
|
const flatSSLPrefix = "smail-tls-"
|
|
|
|
func legacyManagedSSLReference(value string) bool {
|
|
value = runtimepath.LegacyReference(value)
|
|
return strings.HasPrefix(value, "data/ssl/managed/") || strings.HasPrefix(value, "ssl/managed/")
|
|
}
|
|
|
|
func flatSSLPair(root, certificatePath, privateKeyPath string) bool {
|
|
directory, err := filepath.Abs(filepath.Join(root, "ssl"))
|
|
if err != nil || filepath.Dir(certificatePath) != directory || filepath.Dir(privateKeyPath) != directory {
|
|
return false
|
|
}
|
|
name := strings.TrimSuffix(filepath.Base(certificatePath), ".crt")
|
|
return strings.HasSuffix(certificatePath, ".crt") && strings.HasPrefix(name, flatSSLPrefix) &&
|
|
validSSLVersion("version-"+strings.TrimPrefix(name, flatSSLPrefix)) && filepath.Base(privateKeyPath) == name+".key"
|
|
}
|
|
|
|
// 新材料使用不可变的成对文件;配置事务是唯一的生效指针,不覆盖当前文件。
|
|
func stageFlatSSLAtRoot(root, mode string, privateKey, certificate, issuer []byte, domains []string) (ManagedSSLReference, error) {
|
|
managedSSLStorage.Lock()
|
|
defer managedSSLStorage.Unlock()
|
|
if !validManagedSSLMode(mode) {
|
|
return ManagedSSLReference{}, ErrInvalidSSLMode
|
|
}
|
|
if err := validateSSLMaterial(privateKey, certificate, issuer, domains); err != nil {
|
|
return ManagedSSLReference{}, err
|
|
}
|
|
if runtimepath.DeploymentLayout(root) {
|
|
unlock, err := NewTransactionStore(root, RuntimeValidationOptions{}).lockState()
|
|
if err != nil {
|
|
return ManagedSSLReference{}, err
|
|
}
|
|
defer unlock()
|
|
}
|
|
directory, err := filepath.Abs(filepath.Join(root, "ssl"))
|
|
if err != nil {
|
|
return ManagedSSLReference{}, err
|
|
}
|
|
if err := ensureManagedDirectoryAtRoot(root, directory); err != nil {
|
|
return ManagedSSLReference{}, err
|
|
}
|
|
token, err := randomStorageToken()
|
|
if err != nil {
|
|
return ManagedSSLReference{}, err
|
|
}
|
|
base := flatSSLPrefix + token
|
|
storage, err := os.OpenRoot(directory)
|
|
if err != nil {
|
|
return ManagedSSLReference{}, err
|
|
}
|
|
defer storage.Close()
|
|
// 证书链合并到一个证书文件,不额外生成 issuer 文件。
|
|
certificates, _ := parseCertificatePEM(certificate)
|
|
issuers, _ := parseCertificatePEM(issuer)
|
|
certificate = encodeSSLCertificates(appendUniqueCertificates(certificates, issuers...))
|
|
if len(certificate) > maxManagedSSLFileSize {
|
|
return ManagedSSLReference{}, ErrInvalidSSLMaterial
|
|
}
|
|
var written []string
|
|
complete := false
|
|
defer func() {
|
|
if !complete {
|
|
for _, name := range written {
|
|
_ = storage.Remove(name)
|
|
}
|
|
}
|
|
}()
|
|
for _, file := range []struct {
|
|
name string
|
|
data []byte
|
|
}{{base + ".key", privateKey}, {base + ".crt", certificate}} {
|
|
if _, err := storage.Lstat(file.name); !os.IsNotExist(err) {
|
|
return ManagedSSLReference{}, ErrUnsafeSSLFile
|
|
}
|
|
written = append(written, file.name)
|
|
if err := atomicfile.WriteRoot(storage, file.name, file.data, 0600); err != nil {
|
|
return ManagedSSLReference{}, err
|
|
}
|
|
}
|
|
publicPath, privatePath := filepath.Join(directory, base+".crt"), filepath.Join(directory, base+".key")
|
|
if err := validateManagedSSLFiles(publicPath, privatePath); err != nil {
|
|
return ManagedSSLReference{}, err
|
|
}
|
|
lease, err := openSSLLease(privatePath)
|
|
if err != nil {
|
|
return ManagedSSLReference{}, err
|
|
}
|
|
unlock, err := lockStateFile(lease)
|
|
if err != nil {
|
|
return ManagedSSLReference{}, errors.Join(err, lease.Close())
|
|
}
|
|
managedSSLStorage.staged[publicPath] = func() { releaseConfigLease(unlock, lease) }
|
|
complete = true
|
|
return ManagedSSLReference{Mode: mode, CertificateRef: "ssl/" + base + ".crt", PrivateKeyRef: "ssl/" + base + ".key", CertificatePath: publicPath, PrivateKeyPath: privatePath}, nil
|
|
}
|
|
|
|
// 只有上一套由本程序生成的证书才纳入到期自动删除;手动文件不会被接管。
|
|
func retainPreviousSSL(root string, next *SSLConfig, previous SSLConfig) {
|
|
next.PreviousCertificateRef = previous.PreviousCertificateRef
|
|
next.PreviousPrivateKeyRef = previous.PreviousPrivateKeyRef
|
|
if next.CertificateRef == previous.CertificateRef && next.PrivateKeyRef == previous.PrivateKeyRef {
|
|
return
|
|
}
|
|
ref, err := resolveManagedSSLPathsAtRoot(root, previous)
|
|
if err == nil && flatSSLPair(root, ref.CertificatePath, ref.PrivateKeyPath) {
|
|
if validateManagedSSLFiles(ref.CertificatePath, ref.PrivateKeyPath) == nil {
|
|
next.PreviousCertificateRef = previous.CertificateRef
|
|
next.PreviousPrivateKeyRef = previous.PrivateKeyRef
|
|
}
|
|
}
|
|
if next.PreviousCertificateRef == next.CertificateRef {
|
|
next.PreviousCertificateRef, next.PreviousPrivateKeyRef = "", ""
|
|
}
|
|
}
|
|
|
|
func removeFlatSSLFile(directory, path string) error {
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() || hasMultipleFileLinks(path) {
|
|
return ErrUnsafeSSLFile
|
|
}
|
|
storage, err := os.OpenRoot(directory)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer storage.Close()
|
|
return storage.Remove(filepath.Base(path))
|
|
}
|
|
|
|
func pruneFlatSSLLocked(root string, protected []SSLConfig) error {
|
|
// 当前材料不可用时暂停回收,避免丢失仍可恢复的历史证书。
|
|
if len(protected) == 0 {
|
|
return nil
|
|
}
|
|
if _, err := resolveManagedSSLReferencesAtRoot(root, protected[0]); err != nil {
|
|
return err
|
|
}
|
|
directory, err := filepath.Abs(filepath.Join(root, "ssl"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entries, err := os.ReadDir(directory)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resolved, err := filepath.EvalSymlinks(directory)
|
|
if err != nil || filepath.Clean(resolved) != directory {
|
|
return ErrUnsafeSSLFile
|
|
}
|
|
keep := make(map[string]bool)
|
|
for _, ssl := range protected {
|
|
if path, err := resolveCustomSSLInput(root, ssl.CertificateRef); ssl.CertificateRef != "" && err == nil {
|
|
keep[path] = true
|
|
}
|
|
// 已经降为上一套的配置不能继续保护更早一代,避免保留链无限延长。
|
|
if ssl.CertificateRef == protected[0].PreviousCertificateRef {
|
|
continue
|
|
}
|
|
publicPath, err := resolveCustomSSLInput(root, ssl.PreviousCertificateRef)
|
|
privatePath, keyErr := resolveCustomSSLInput(root, ssl.PreviousPrivateKeyRef)
|
|
if err == nil && keyErr == nil && flatSSLPair(root, publicPath, privatePath) {
|
|
// 即使已经到期,也必须等配置事务解除引用后才能删除。
|
|
keep[publicPath] = true
|
|
}
|
|
}
|
|
for _, entry := range entries {
|
|
publicPath := filepath.Join(directory, entry.Name())
|
|
privatePath := strings.TrimSuffix(publicPath, ".crt") + ".key"
|
|
if !flatSSLPair(root, publicPath, privatePath) || keep[publicPath] || managedSSLStorage.staged[publicPath] != nil {
|
|
continue
|
|
}
|
|
// 租约覆盖整个删除过程;未知、链接、不完整材料保留给管理员处理。
|
|
certificate, err := readExternalSSLFile(publicPath, false)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
certificates, err := parseCertificatePEM(certificate)
|
|
if err != nil || len(certificates) == 0 {
|
|
continue
|
|
}
|
|
privateKey, err := readExternalSSLFile(privatePath, true)
|
|
if err != nil {
|
|
// 先删私钥、后删证书;中断留下的孤立证书可在下轮安全重试。
|
|
if _, statErr := os.Lstat(privatePath); os.IsNotExist(statErr) {
|
|
if err := removeFlatSSLFile(directory, publicPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if _, err := tls.X509KeyPair(certificate, privateKey); err != nil {
|
|
continue
|
|
}
|
|
lease, err := openSSLLease(privatePath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
unlock, err := lockStateFile(lease)
|
|
if err != nil {
|
|
if closeErr := lease.Close(); closeErr != nil {
|
|
return errors.Join(err, closeErr)
|
|
}
|
|
continue
|
|
}
|
|
err = removeFlatSSLFile(directory, privatePath)
|
|
if err == nil {
|
|
err = removeFlatSSLFile(directory, publicPath)
|
|
}
|
|
unlock()
|
|
if err := errors.Join(err, lease.Close()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|