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 }