feat: 部署文档与单元文件;P6 渗透自查并修复一处真实缺陷
- docs/deployment.md:中继(反代要点含长轮询超时)、agent(msa-keygen 生成配置)、 配对窗口、按拒绝原因分流的排障表 - agent/cmd/msa-keygen:生成节点密钥、nodeID、agent token 与配对秘密,打印中继注册条目; 不生成设备密钥(那是手机的事,能生成就能冒充) - relay/systemd/simadmin-relay.service:保护指令与 agent 单元的差异逐条写明 (无 MemoryDenyWriteExecute,因为 V8 JIT 需要 W+X 页) - test/adversarial.test.js(12 项):回环硬校验(结构性断言中继无出站路径)、 重放与新鲜度的责任划分、邮箱深度与 handover、设备注册越权、信封校验边界 - 修 registry.resolve 未校验 nodeID:pending 表只按 reqID 索引,被控节点知道 另一节点的在途 reqID 即可抢先应答使其变 orphan,合法应答被丢弃 - .gitignore 排除交叉编译产物
This commit is contained in:
@@ -0,0 +1,125 @@
|
|||||||
|
// Command msa-keygen mints the key material and identifiers a node needs, and prints a config file
|
||||||
|
// skeleton to stdout.
|
||||||
|
//
|
||||||
|
// It exists because the agent's config cannot be written by hand: the two private keys are raw
|
||||||
|
// 32-byte values that must be base64, and a node id, an agent token and a device id all have to
|
||||||
|
// match patterns the agent and the relay both enforce. An operator generating those with openssl and
|
||||||
|
// a text editor will eventually produce a file that fails at startup with a validation error whose
|
||||||
|
// cause is a character in the wrong place.
|
||||||
|
//
|
||||||
|
// It generates the *agent's* keys only. The phone's keys are generated on the phone, at pairing
|
||||||
|
// time, and never leave it — a tool that could mint a device key would be a tool that can impersonate
|
||||||
|
// a phone.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"fun.chick/msa-agent/internal/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
nodeID = flag.String("node", "", "node id (nd_...); generated when empty")
|
||||||
|
relayURL = flag.String("relay", "https://relay.example", "relay base URL, https")
|
||||||
|
localPort = flag.Int("port", 3000, "the local SimAdmin port on 127.0.0.1")
|
||||||
|
auditPath = flag.String("audit", "/var/log/msa-agent/audit.log", "audit log path")
|
||||||
|
agentToken = flag.String("token", "", "relay bearer for this agent; generated when empty")
|
||||||
|
secret = flag.String("provision-secret", "", "pairing secret for an unprotected node; generated when empty")
|
||||||
|
write = flag.String("write", "", "write the config here instead of stdout (mode 0600)")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// The signing key is Ed25519 for challenge-response at the relay and for signing responses.
|
||||||
|
_, signingKey, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
check(err)
|
||||||
|
// The key-agreement key is raw X25519: clamping is the curve library's job, and a stored key
|
||||||
|
// that had already been clamped would be a second, non-standard encoding to keep in sync across
|
||||||
|
// three languages.
|
||||||
|
kxKey := make([]byte, wire.KeyBytes)
|
||||||
|
_, err = rand.Read(kxKey)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
id := *nodeID
|
||||||
|
if id == "" {
|
||||||
|
id = "nd_" + randomHex(5)
|
||||||
|
}
|
||||||
|
token := *agentToken
|
||||||
|
if token == "" {
|
||||||
|
token = randomHex(32)
|
||||||
|
}
|
||||||
|
// The pairing secret is generated even though it is only needed for an unprotected node: a
|
||||||
|
// config without one cannot pair at all, and discovering that at pairing time means another trip
|
||||||
|
// to the node. When the node *is* protected, `nodePassword` takes precedence over this value, so
|
||||||
|
// carrying it is harmless.
|
||||||
|
pairingSecret := *secret
|
||||||
|
if pairingSecret == "" {
|
||||||
|
pairingSecret = randomHex(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := map[string]any{
|
||||||
|
"nodeId": id,
|
||||||
|
"relayUrl": *relayURL,
|
||||||
|
"agentToken": token,
|
||||||
|
"localPort": *localPort,
|
||||||
|
"signKey": base64.StdEncoding.EncodeToString(signingKey),
|
||||||
|
"kxKey": base64.StdEncoding.EncodeToString(kxKey),
|
||||||
|
// The node password is intentionally absent: it is the operator's, and it is the one secret
|
||||||
|
// this tool must not invent. An unprotected node needs no value; a protected one needs the
|
||||||
|
// real password or every command fails with node_auth_failed.
|
||||||
|
"auditPath": *auditPath,
|
||||||
|
// Only used to authorise the pairing window on an unprotected node. Delete it after pairing
|
||||||
|
// if you like; nothing else reads it.
|
||||||
|
"provisionSecret": pairingSecret,
|
||||||
|
// Empty on purpose: devices are added by pairing (`msa-agent --pair`), which is the only
|
||||||
|
// path that should ever be able to introduce a trusted phone.
|
||||||
|
"devices": []any{},
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := json.MarshalIndent(config, "", " ")
|
||||||
|
check(err)
|
||||||
|
encoded = append(encoded, '\n')
|
||||||
|
|
||||||
|
if *write == "" {
|
||||||
|
// The relay's own registration is printed alongside, because the two files have to agree on
|
||||||
|
// the node id and token and copying them by hand is where that agreement breaks.
|
||||||
|
fmt.Fprintf(os.Stderr, "# agent config (paste into %s, then add \"nodePassword\" if the node is protected)\n", "/etc/msa-agent/config.json")
|
||||||
|
os.Stdout.Write(encoded)
|
||||||
|
fmt.Fprintf(os.Stderr, "\n# relay registration: add this entry to MSA_AGENTS\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "{\"nodeID\":%q,\"token\":%q}\n", id, token)
|
||||||
|
fmt.Fprintf(os.Stderr, "\n# pairing secret (unprotected nodes only; protected ones use nodePassword): %s\n", pairingSecret)
|
||||||
|
fmt.Fprintf(os.Stderr, "# next: run `msa-agent --pair` on the node once, then pair the phone from the app\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(*write), 0o755); err != nil {
|
||||||
|
check(err)
|
||||||
|
}
|
||||||
|
// 0600: this file holds the agent's private keys and the node's password.
|
||||||
|
if err := os.WriteFile(*write, encoded, 0o600); err != nil {
|
||||||
|
check(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("wrote %s (mode 0600)\n", *write)
|
||||||
|
fmt.Printf("relay registration: {\"nodeID\":%q,\"token\":%q}\n", id, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomHex(bytes int) string {
|
||||||
|
raw := make([]byte, bytes)
|
||||||
|
_, err := rand.Read(raw)
|
||||||
|
check(err)
|
||||||
|
return hex.EncodeToString(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func check(err error) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msa-keygen:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user