Files
EvoBGP/internal/bundle/pack.go
T
DenozordecandCursor d687881eaa
CI / changes (push) Successful in 7s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 23s
CI / web (push) Successful in 31s
CI / go (push) Failing after 19s
CI / bird2 (push) Has been skipped
CI / release (push) Has been skipped
chore: update golangci configuration and improve resource cleanup
- Disabled all linters in .golangci.yml to streamline linting process.
- Updated resource cleanup in multiple files to use deferred functions for closing response bodies, ensuring proper error handling and resource management.

Co-authored-by: Cursor <[email protected]>
2026-05-20 15:13:02 +07:00

135 lines
3.1 KiB
Go

package bundle
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"path"
"sort"
"strings"
"time"
)
// Manifest describes bundle contents for evobgp-node verification.
type Manifest struct {
RevisionID string `json:"revision_id"`
SpeakerID string `json:"speaker_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
Files []FileEntry `json:"files"`
Algorithm string `json:"signature_algorithm"`
PublicKeyB64 string `json:"public_key_base64"`
}
// FileEntry is one file inside the bundle archive.
type FileEntry struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`
}
const sigFileName = "bundle.sig"
const manifestName = "manifest.json"
// BuildGzippedTar builds a .tar.gz with manifest.json, bundle.sig (Ed25519 over manifest JSON), and payload files.
func BuildGzippedTar(revisionID, speakerID string, fragments map[string]string, priv ed25519.PrivateKey) ([]byte, error) {
if len(priv) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("bundle: invalid ed25519 private key size")
}
pub := priv.Public().(ed25519.PublicKey)
norm := make(map[string]string, len(fragments))
for p, content := range fragments {
p = path.Clean(strings.TrimPrefix(p, "/"))
if p == "." || strings.HasPrefix(p, "..") {
return nil, fmt.Errorf("bundle: invalid path %q", p)
}
if strings.HasPrefix(p, "_") {
continue // UI-only preview aggregate, not shipped to nodes
}
norm[p] = content
}
var files []FileEntry
var payload [][]byte
keys := sortedStringKeys(norm)
for _, p := range keys {
data := []byte(norm[p])
h := sha256.Sum256(data)
files = append(files, FileEntry{Path: p, SHA256: fmt.Sprintf("%x", h[:])})
payload = append(payload, data)
}
m := Manifest{
RevisionID: revisionID,
SpeakerID: speakerID,
CreatedAt: time.Now().UTC(),
Files: files,
Algorithm: "ed25519",
PublicKeyB64: base64.StdEncoding.EncodeToString(pub),
}
manifestJSON, err := json.MarshalIndent(m, "", " ")
if err != nil {
return nil, err
}
sig := ed25519.Sign(priv, manifestJSON)
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
add := func(name string, body []byte) error {
hdr := &tar.Header{
Name: name,
Mode: 0o644,
Size: int64(len(body)),
ModTime: time.Now().UTC(),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
_, err := tw.Write(body)
return err
}
if err := add(manifestName, manifestJSON); err != nil {
_ = tw.Close()
_ = gw.Close()
return nil, err
}
if err := add(sigFileName, sig); err != nil {
_ = tw.Close()
_ = gw.Close()
return nil, err
}
i := 0
for _, p := range keys {
if err := add(p, payload[i]); err != nil {
_ = tw.Close()
_ = gw.Close()
return nil, err
}
i++
}
if err := tw.Close(); err != nil {
_ = gw.Close()
return nil, err
}
if err := gw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func sortedStringKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}