feat: enhance evobgp with new command-line tools for bundle management, including pull, verify, and apply functionalities. Update go.mod to include necessary dependencies and complete todos in architecture plan for improved observability and deployment practices.
CI / changes (push) Successful in 4s
CI / go (push) Failing after 6s
CI / bird2 (push) Has been skipped
CI / openapi (push) Has been skipped

This commit is contained in:
Denozordec
2026-04-05 14:07:45 +07:00
parent 272542b92a
commit bf52b21150
131 changed files with 9222 additions and 17 deletions
+51
View File
@@ -0,0 +1,51 @@
package bundle
import (
"crypto/ed25519"
"crypto/rand"
"testing"
)
func TestBuildVerifyRoundtrip(t *testing.T) {
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
frag := map[string]string{
"bird.conf": "router id 192.0.2.1;\n\nprotocol device {\n}\n\nprotocol direct {\n ipv4;\n ipv6;\n}\n",
}
raw, err := BuildGzippedTar("rev-1", "sp-1", frag, priv)
if err != nil {
t.Fatal(err)
}
pub := priv.Public().(ed25519.PublicKey)
v, err := VerifyGzippedTar(raw, pub)
if err != nil {
t.Fatal(err)
}
if v.Manifest.RevisionID != "rev-1" {
t.Fatalf("revision %q", v.Manifest.RevisionID)
}
if len(v.Files) != 1 {
t.Fatalf("files %d", len(v.Files))
}
}
func TestVerifyWrongKey(t *testing.T) {
_, privA, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
_, privB, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
raw, err := BuildGzippedTar("rev-x", "sp-x", map[string]string{"a.conf": "x"}, privA)
if err != nil {
t.Fatal(err)
}
pubB := privB.Public().(ed25519.PublicKey)
if _, err := VerifyGzippedTar(raw, pubB); err == nil {
t.Fatal("expected signature error with wrong public key")
}
}
+46
View File
@@ -0,0 +1,46 @@
package bundle
import (
"fmt"
"os"
pathpkg "path"
"path/filepath"
"strings"
)
// WriteExtractedFiles writes payload files from a verified bundle under root (directories created as needed).
func WriteExtractedFiles(root string, v *VerifiedContents) error {
root = filepath.Clean(root)
for _, fe := range v.Manifest.Files {
data, ok := v.Files[fe.Path]
if !ok {
return fmt.Errorf("bundle: missing file %q", fe.Path)
}
rel := pathpkg.Clean(strings.TrimPrefix(fe.Path, "/"))
if rel == "." || strings.HasPrefix(rel, "..") {
return fmt.Errorf("bundle: unsafe path %q", fe.Path)
}
dest := filepath.Join(root, filepath.FromSlash(rel))
relToRoot, err := filepath.Rel(root, dest)
if err != nil || strings.HasPrefix(relToRoot, "..") {
return fmt.Errorf("bundle: path escapes root: %q", fe.Path)
}
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return err
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return err
}
}
return nil
}
// FindMainBirdConf returns the first path ending with bird.conf from extracted manifest order.
func (v *VerifiedContents) FindMainBirdConf() string {
for _, fe := range v.Manifest.Files {
if strings.HasSuffix(fe.Path, "bird.conf") {
return fe.Path
}
}
return ""
}
+131
View File
@@ -0,0 +1,131 @@
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)
}
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
}
+110
View File
@@ -0,0 +1,110 @@
package bundle
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"path"
"strings"
)
// VerifiedContents is the result of verifying a downloaded bundle on evobgp-node.
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))
if err != nil {
return nil, err
}
defer gr.Close()
var manifestRaw []byte
var sig []byte
files := make(map[string][]byte)
tr := tar.NewReader(gr)
for {
hdr, err := tr.Next()
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)
}
data, err := io.ReadAll(tr)
if err != nil {
return nil, err
}
switch name {
case manifestName:
manifestRaw = data
case sigFileName:
sig = data
default:
files[name] = data
}
}
if len(manifestRaw) == 0 || len(sig) == 0 {
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
}
for _, fe := range m.Files {
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)
}
}
return &VerifiedContents{Manifest: m, Files: files}, nil
}
// ParsePublicKeyHex decodes a 64-char hex Ed25519 public key (32 bytes).
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 uses the key embedded in the signed manifest (cross-check with expected pubkey optional).
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
}