gosuda/keyless_tls
GitHub: gosuda/keyless_tls
Go 语言实现的无密钥 TLS 方案,将私钥隔离在远程签名服务中,隧道应用仅持有公钥证书并委托签名操作,从而降低私钥泄露风险。
Stars: 2 | Forks: 0
# 无密钥 TLS

`keyless_tls` 的设计宗旨是让隧道应用处理 TLS 握手和流量加密/解密,而仅将 `CertificateVerify` 签名委托给远程签名者。
- TLS 引擎、会话密钥、流量加密:`tunneling app`
- TLS 签名(`CertificateVerify`):远程 `relay signer`
- 签名者传输:带有强制 `mTLS` 的 `HTTPS + JSON`
本仓库支持两种使用模式:
1. 作为 SDK 库使用(`keyless` 包)
2. 运行 `cmd/*` 下提供的二进制文件
## 首先选择您的集成路径
- **我想直接附加到我的应用(`http.Server`)**:SDK 模式
- **我想立即运行并验证行为**:二进制模式
## 1) 使用 SDK 库
### 核心概念
隧道应用仅保留公共证书链(`cert PEM`),并且**不**持有私钥。
`keyless` SDK 将远程签名者作为 `crypto.Signer` 附加,因此握手签名是远程执行的。
### 公共 API
- `keyless.AttachToHTTPServer`:最简单的入口点(直接附加到 `http.Server`)
- `keyless.NewRemoteSigner`:显式创建远程签名者客户端
- `keyless.NewServerTLSConfig`:手动构建 `tls.Config`
### 最简单的设置(`AttachToHTTPServer`)
```
package main
import (
"log"
"net/http"
"os"
"github.com/gosuda/keyless_tls/keyless"
)
func main() {
certPEM := mustRead("certs/public-chain.crt")
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok\n"))
})
srv := &http.Server{
Addr: ":8443",
Handler: mux,
}
remoteSigner, err := keyless.AttachToHTTPServer(srv, keyless.HTTPServerAttachConfig{
CertPEM: certPEM,
RemoteSigner: keyless.RemoteSignerConfig{
Endpoint: "127.0.0.1:9443",
ServerName: "relay.internal",
KeyID: "relay-cert",
RootCAPEM: mustRead("certs/relay-ca.crt"),
ClientCertPEM: mustRead("certs/tunnel-client.crt"),
ClientKeyPEM: mustRead("certs/tunnel-client.key"),
},
})
if err != nil {
log.Fatal(err)
}
defer remoteSigner.Close()
log.Fatal(srv.ListenAndServeTLS("", ""))
}
func mustRead(path string) []byte {
b, err := os.ReadFile(path)
if err != nil {
panic(err)
}
return b
}
```
### 高级设置(`NewRemoteSigner` + `NewServerTLSConfig`)
当您已有自己的 `tls.Config` 构建流程,或与 `http.Server` 以外的组件集成时使用此选项。
```
rSigner, err := keyless.NewRemoteSigner(remoteSignerCfg, certPEM)
if err != nil {
// handle error
}
defer rSigner.Close()
tlsConf, err := keyless.NewServerTLSConfig(keyless.ServerTLSConfig{
CertPEM: certPEM,
Signer: rSigner,
NextProtos: []string{"h2", "http/1.1"},
// MinVersion: tls.VersionTLS13,
})
if err != nil {
// handle error
}
```
### SDK:用于 Relay 路由的 SNI 元数据(调用者控制)
如果您正在使用此库实现自己的 relay/proxy,请使用 `relay/l4`
API 来检查 ClientHello 并根据 SNI/ALPN 进行路由,同时将所有策略保留在调用者代码中。
- `l4.InspectClientHello(conn, timeout)`:解析 `ServerName`/`ALPNProtocols` 并返回一个包装后的 `net.Conn`
- `l4.Proxy.DialByClientHello(ctx, info, parseErr)`:由调用者决定路由/回退/拒绝策略
这在实践中的工作原理:
1. 传入的 TCP 连接到达
2. 库仅读取 ClientHello 元数据(无 TLS 终止)
3. 您的回调接收 `info.ServerName`、`info.ALPNProtocols` 和 `parseErr`
4. 您的代码选择上游目标(或拒绝)
5. Relay 继续原始 TCP 转发,无负载丢失
典型的 SDK 路由策略:
- 多租户主机路由:`app1.example.com -> tenant A`,`app2.example.com -> tenant B`
- 协议感知路由:优先选择 `h2` 上游还是 `http/1.1` 上游
- 严格安全模式:当 ClientHello 解析失败时拒绝
- 兼容模式:当解析失败时回退到默认上游
具体策略示例(易于调整):
```
routes := map[string]string{
"app1.demo.local": "127.0.0.1:9001",
"app2.demo.local": "127.0.0.1:9002",
}
proxy := &l4.Proxy{
ListenAddr: ":443",
ClientHelloTimeout: 2 * time.Second,
DialByClientHello: func(ctx context.Context, info l4.ClientHelloInfo, parseErr error) (net.Conn, error) {
d := net.Dialer{Timeout: 3 * time.Second}
// 1) Decide what to do with non-TLS / invalid ClientHello
if parseErr != nil {
// strict mode: return nil, parseErr
// compatibility mode: send to default route
return d.DialContext(ctx, "tcp", "127.0.0.1:9011")
}
// 2) SNI host-based route
if target, ok := routes[strings.ToLower(strings.TrimSuffix(info.ServerName, "."))]; ok {
return d.DialContext(ctx, "tcp", target)
}
// 3) Optional ALPN-aware split
for _, proto := range info.ALPNProtocols {
if proto == "h2" {
return d.DialContext(ctx, "tcp", "127.0.0.1:9443")
}
}
// 4) Default route
return d.DialContext(ctx, "tcp", "127.0.0.1:9011")
},
}
```
有关包含 10 个主机的完整可运行 SDK 样式路由示例,请参阅 `examples/relay-10-targets`。
### SDK 集成检查清单
- 在隧道应用中仅部署公共证书链(`cert PEM`)
- 配置签名者端点/服务器名称/`KeyID`/根 CA
- 提供 mTLS 客户端材料(`client cert/key`)
- 在关闭时调用 `remoteSigner.Close()`
## 2) 使用二进制文件
`cmd/` 包含面向生产的 `main` 包(可运行的二进制文件)。
示例应用程序分离在 `examples/` 下。
### 命令布局
- `cmd/relay-signer`:远程签名者 HTTPS 服务器
- `cmd/relay-l4`:带有可选基于 SNI 路由映射的 L4 TCP Relay
- `examples/tunnel-http`:与 SDK 集成的示例隧道 HTTP 服务器
- `examples/relay-10-targets`:通过 SNI 路由到 10 个目标主机的一个 Relay 服务器
### 用于自定义 Relay 的 SNI/ALPN 路由钩子
如果您正在构建自己的 relay/proxy,请使用 `relay/l4.InspectClientHello` 在不终止 TLS 的情况下读取 ClientHello 元数据(`ServerName`、`ALPNProtocols`)。
该辅助函数返回一个包装后的 `net.Conn`,它会重放已读取的字节,因此您的 Relay 可以在做出路由决策后继续正常的 TCP 转发。
`relay/l4.Proxy` 还通过 `DialByClientHello(ctx, info, parseErr)` 支持基于回调的拨号,因此所有策略决策(回退、拒绝、默认路由)保留在调用者代码中。
### 三进程快速入门
1. 运行签名者服务器
```
go run ./cmd/relay-signer \
-listen :9443 \
-key-id relay-cert \
-tls-cert certs/relay-server.crt \
-tls-key certs/relay-server.key \
-sign-key certs/relay-signing.key
```
2. 运行隧道应用
```
go run ./examples/tunnel-http \
-listen :8443 \
-cert certs/public-chain.crt \
-signer-addr 127.0.0.1:9443 \
-signer-name relay.internal \
-key-id relay-cert \
-client-cert certs/tunnel-client.crt \
-client-key certs/tunnel-client.key \
-root-ca certs/relay-ca.crt
```
3. 运行 L4 Relay
```
go run ./cmd/relay-l4 \
-listen :443 \
-route app1.example.com=127.0.0.1:8443 \
-default-upstream 127.0.0.1:8443
```
SNI 路由模式(`-route` 可重复):
```
go run ./cmd/relay-l4 \
-listen :443 \
-route app1.example.com=127.0.0.1:8441 \
-route app2.example.com=127.0.0.1:8442 \
-default-upstream 127.0.0.1:8440
```
`cmd/relay-l4` 不强制执行路由策略。调用者侧策略由标志控制,包括 ClientHello 解析失败是否可以使用默认上游。
有用的 `cmd/relay-l4` 路由模式标志:
- `-route host=upstream`(可重复):显式 SNI 映射
- `-default-upstream`:未知 SNI 的回退目标
- `-allow-parse-error`:允许非 TLS/无效 ClientHello 使用回退
- `-clienthello-timeout`:最大 ClientHello 检查时间
### 示例应用:一个 Relay 路由 10 个目标主机
`examples/relay-10-targets` 演示了一个实用的入口布局:
- 一个公共 Relay 监听器
- 十个目标隧道应用
- 由调用者代码实现的基于 SNI 的目标选择
运行示例 Relay:
```
go run ./examples/relay-10-targets \
-listen :443 \
-upstream-host 127.0.0.1 \
-base-port 9001 \
-domain demo.local \
-default-upstream 127.0.0.1:9011
```
生成的静态路由:
- `app1.demo.local -> 127.0.0.1:9001`
- `app2.demo.local -> 127.0.0.1:9002`
- `app3.demo.local -> 127.0.0.1:9003`
- `app4.demo.local -> 127.0.0.1:9004`
- `app5.demo.local -> 127.0.0.1:9005`
- `app6.demo.local -> 127.0.0.1:9006`
- `app7.demo.local -> 127.0.0.1:9007`
- `app8.demo.local -> 127.0.0.1:9008`
- `app9.demo.local -> 127.0.0.1:9009`
- `app10.demo.local -> 127.0.0.1:9010`
策略仍归调用者所有:
- 已知 SNI:路由到映射的上游
- 未知 SNI:配置时路由到 `-default-upstream`
- 非 TLS 或无效 ClientHello:配置时路由到 `-default-upstream`,否则拒绝
`examples/relay-10-targets` 的重要标志:
- `-listen`:公共 Relay 地址
- `-upstream-host`:用于生成目标的主机
- `-base-port`:第一个目标端口(`app1`)
- `-domain`:用于 SNI 匹配的主机后缀
- `-default-upstream`:可选的回退上游
- `-dial-timeout`:上游拨号超时
- `-clienthello-timeout`:ClientHello 检查超时
### 签名者传输需要 mTLS
签名者和隧道客户端必须始终配置为双向 TLS。
```
go run ./cmd/relay-signer \
-listen :9443 \
-key-id relay-cert \
-tls-cert certs/relay-server.crt \
-tls-key certs/relay-server.key \
-client-ca certs/client-ca.crt \
-sign-key certs/relay-signing.key
go run ./examples/tunnel-http \
-listen :8443 \
-cert certs/public-chain.crt \
-signer-addr 127.0.0.1:9443 \
-signer-name relay.internal \
-key-id relay-cert \
-client-cert certs/tunnel-client.crt \
-client-key certs/tunnel-client.key \
-root-ca certs/relay-ca.crt
```
## 安全和操作说明
- 仅在 `relay-signer` 中存储私钥;永远不要将它们分发到隧道应用
- 在隧道应用中仅保留公共证书链
- 强制执行签名者 mTLS 并将其与 `KeyID` 范围的 ACL 配对
### 签名者 API 协议(`/v1/sign`)
请求:
```
{
"key_id": "relay-cert",
"algorithm": "RSA_PSS_SHA256",
"digest": "",
"timestamp_unix": 1735628400,
"nonce": "c4d76ad40f5d8f95a1fe4b2f1c922f4a"
}
```
响应:
```
{
"key_id": "relay-cert",
"algorithm": "RSA_PSS_SHA256",
"signature": ""
}
```
## 包结构
- `keyless`:面向应用开发者的 SDK(隧道应用集成点)
- `keyless/signerclient`:远程签名者客户端实现
- `relay/signrpc`:签名者 JSON 请求/响应类型
- `relay/signer`:签名服务/密钥存储
- `relay/server`:签名者 HTTPS(强制 mTLS)服务器启动器
- `keyless/lifecycle`:每次租约的 mTLS 身份管理(颁发、续订、验证、磁盘备份的加密存储)
- `relay/l4`:TCP 直通 Relay + 可选的 ClientHello(SNI/ALPN)检查钩子
## 当前状态
此实现处于早期阶段。在生产使用之前,请考虑添加:
- 重放缓存
- 速率限制
- 密钥轮换策略
- 可观测性(OTel/指标/日志关联)
标签:Delegated Cryptography, EVTX分析, EVTX分析, Go语言, Homebrew安装, HSM, HTTPs, JSONLines, mTLS, SamuraiWTF, TLS, 加密技术, 安全中间件, 安全通信, 密码学, 底层编程, 手动系统调用, 无密钥TLS, 日志审计, 私钥保护, 程序破解, 网络安全, 证书验证, 远程签名, 防御工具, 隐私保护, 零信任