Files
EvoBGP/internal/bundle/verify.go
T
DenozordecandCursor f8e0520817 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>
2026-11-02 17:23:59 +07:00

185 lines
5.2 KiB
Go

package bundle
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"strings"
)
// VerifiedContents contains only payloads covered by a verified signed manifest.
type VerifiedContents struct {
Manifest Manifest
Files map[string][]byte
}
// 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() }()
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
}
count++
if count > l.MaxEntries {
return nil, ErrLimit
}
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
case sigFileName:
sig = data
default:
files[name] = data
}
}
// 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 {
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)
}
h := sha256.Sum256(body)
if fmt.Sprintf("%x", h[:]) != fe.SHA256 {
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 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)
if err != nil {
return nil, fmt.Errorf("bundle: hex decode: %w", err)
}
if len(raw) != ed25519.PublicKeySize {
return nil, fmt.Errorf("bundle: want %d-byte ed25519 public key", ed25519.PublicKeySize)
}
return ed25519.PublicKey(raw), nil
}
// 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")
}
raw, err := base64.StdEncoding.DecodeString(m.PublicKeyB64)
if err != nil {
return nil, err
}
if len(raw) != ed25519.PublicKeySize {
return nil, fmt.Errorf("bundle: bad public key length")
}
return ed25519.PublicKey(raw), nil
}