feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
924f111e48
commit
f8e0520817
197 files changed
+18655
-1416
No files matched your search
+92
-18
@@ -11,44 +11,95 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// VerifiedContents is the result of verifying a downloaded bundle on evobgp-node.
|
||||
// VerifiedContents contains only payloads covered by a verified signed manifest.
|
||||
type VerifiedContents struct {
|
||||
Manifest Manifest
|
||||
Files map[string][]byte
|
||||
}
|
||||
|
||||
// VerifyGzippedTar checks Ed25519 signature over manifest.json and SHA-256 of each file.
|
||||
func VerifyGzippedTar(bundle []byte, pub ed25519.PublicKey) (*VerifiedContents, error) {
|
||||
gr, err := gzip.NewReader(bytes.NewReader(bundle))
|
||||
// VerifyGzippedTar verifies the unchanged signed manifest format with default limits.
|
||||
func VerifyGzippedTar(raw []byte, pub ed25519.PublicKey) (*VerifiedContents, error) {
|
||||
return VerifyGzippedTarWithLimits(raw, pub, Limits{})
|
||||
}
|
||||
|
||||
// VerifyGzippedTarWithLimits verifies the signature and hashes under explicit
|
||||
// compressed, expanded, per-entry and metadata budgets.
|
||||
func VerifyGzippedTarWithLimits(raw []byte, pub ed25519.PublicKey, limits Limits) (*VerifiedContents, error) {
|
||||
l, err := limits.Normalized()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if int64(len(raw)) > l.MaxCompressedBytes {
|
||||
return nil, ErrLimit
|
||||
}
|
||||
if len(pub) != ed25519.PublicKeySize {
|
||||
return nil, fmt.Errorf("bundle: invalid public key size")
|
||||
}
|
||||
gr, err := gzip.NewReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = gr.Close() }()
|
||||
|
||||
var manifestRaw []byte
|
||||
var sig []byte
|
||||
files := make(map[string][]byte)
|
||||
tr := tar.NewReader(gr)
|
||||
decoded := &io.LimitedReader{R: gr, N: l.MaxExpandedBytes + 1}
|
||||
tr := tar.NewReader(decoded)
|
||||
var manifestRaw, sig []byte
|
||||
files := map[string][]byte{}
|
||||
seen := map[string]bool{}
|
||||
count := 0
|
||||
var logicalBytes int64
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if decoded.N == 0 {
|
||||
return nil, ErrLimit
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := path.Clean(hdr.Name)
|
||||
if strings.HasPrefix(name, "..") {
|
||||
return nil, fmt.Errorf("bundle: illegal tar entry %q", hdr.Name)
|
||||
count++
|
||||
if count > l.MaxEntries {
|
||||
return nil, ErrLimit
|
||||
}
|
||||
data, err := io.ReadAll(tr)
|
||||
if err = validatePath(hdr.Name, l.MaxPathBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := hdr.Name
|
||||
folded := strings.ToLower(name)
|
||||
if seen[folded] {
|
||||
return nil, fmt.Errorf("bundle: duplicate tar entry %q", name)
|
||||
}
|
||||
seen[folded] = true
|
||||
if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA {
|
||||
return nil, fmt.Errorf("bundle: nonregular tar entry %q", name)
|
||||
}
|
||||
for key := range hdr.PAXRecords {
|
||||
if strings.HasPrefix(key, "GNU.sparse.") {
|
||||
return nil, fmt.Errorf("bundle: sparse entry %q is unsupported", name)
|
||||
}
|
||||
}
|
||||
max := l.MaxFileBytes
|
||||
if name == manifestName {
|
||||
max = l.MaxManifestBytes
|
||||
}
|
||||
if name == sigFileName {
|
||||
max = ed25519.SignatureSize
|
||||
}
|
||||
if hdr.Size < 0 || hdr.Size > max || hdr.Size >= decoded.N || hdr.Size > l.MaxExpandedBytes-logicalBytes {
|
||||
return nil, ErrLimit
|
||||
}
|
||||
logicalBytes += hdr.Size
|
||||
data, err := io.ReadAll(io.LimitReader(tr, max+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if int64(len(data)) > max || decoded.N == 0 {
|
||||
return nil, ErrLimit
|
||||
}
|
||||
switch name {
|
||||
case manifestName:
|
||||
manifestRaw = data
|
||||
@@ -58,17 +109,36 @@ func VerifyGzippedTar(bundle []byte, pub ed25519.PublicKey) (*VerifiedContents,
|
||||
files[name] = data
|
||||
}
|
||||
}
|
||||
if len(manifestRaw) == 0 || len(sig) == 0 {
|
||||
// tar EOF precedes the gzip trailer. Drain through the same byte budget so
|
||||
// checksums, extra gzip members and trailing decompression are also bounded.
|
||||
if _, err = io.Copy(io.Discard, decoded); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if decoded.N == 0 {
|
||||
return nil, ErrLimit
|
||||
}
|
||||
if len(manifestRaw) == 0 || len(sig) != ed25519.SignatureSize {
|
||||
return nil, fmt.Errorf("bundle: missing manifest or signature")
|
||||
}
|
||||
if !ed25519.Verify(pub, manifestRaw, sig) {
|
||||
return nil, fmt.Errorf("bundle: ed25519 signature mismatch")
|
||||
}
|
||||
var m Manifest
|
||||
if err := json.Unmarshal(manifestRaw, &m); err != nil {
|
||||
if err = json.Unmarshal(manifestRaw, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(m.Files) > l.MaxEntries-2 {
|
||||
return nil, ErrLimit
|
||||
}
|
||||
listed := map[string]bool{}
|
||||
for _, fe := range m.Files {
|
||||
if err = validatePath(fe.Path, l.MaxPathBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fe.Path == manifestName || fe.Path == sigFileName || listed[strings.ToLower(fe.Path)] {
|
||||
return nil, fmt.Errorf("bundle: duplicate or reserved manifest path %q", fe.Path)
|
||||
}
|
||||
listed[strings.ToLower(fe.Path)] = true
|
||||
body, ok := files[fe.Path]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("bundle: missing file %q", fe.Path)
|
||||
@@ -78,10 +148,13 @@ func VerifyGzippedTar(bundle []byte, pub ed25519.PublicKey) (*VerifiedContents,
|
||||
return nil, fmt.Errorf("bundle: checksum mismatch for %q", fe.Path)
|
||||
}
|
||||
}
|
||||
if len(listed) != len(files) {
|
||||
return nil, fmt.Errorf("bundle: payload is not covered by manifest")
|
||||
}
|
||||
return &VerifiedContents{Manifest: m, Files: files}, nil
|
||||
}
|
||||
|
||||
// ParsePublicKeyHex decodes a 64-char hex Ed25519 public key (32 bytes).
|
||||
// ParsePublicKeyHex decodes a 32-byte Ed25519 public key from hexadecimal.
|
||||
func ParsePublicKeyHex(s string) (ed25519.PublicKey, error) {
|
||||
s = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(s, "0x"), "0X"))
|
||||
raw, err := hex.DecodeString(s)
|
||||
@@ -94,7 +167,8 @@ func ParsePublicKeyHex(s string) (ed25519.PublicKey, error) {
|
||||
return ed25519.PublicKey(raw), nil
|
||||
}
|
||||
|
||||
// PublicKeyFromManifestBase64 uses the key embedded in the signed manifest (cross-check with expected pubkey optional).
|
||||
// PublicKeyFromManifestBase64 decodes the manifest key. Callers must still trust
|
||||
// an independently provisioned key when authenticating the bundle.
|
||||
func PublicKeyFromManifestBase64(m *Manifest) (ed25519.PublicKey, error) {
|
||||
if m.PublicKeyB64 == "" {
|
||||
return nil, fmt.Errorf("bundle: manifest missing public_key_base64")
|
||||
|
||||
Reference in new issue
Block a user