Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
185 lines
5.2 KiB
Go
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
|
|
}
|