Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
This commit is contained in:
186
internal/forge/build.go
Normal file
186
internal/forge/build.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package forge
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"forge-mesh/internal/api/types"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var targets = []struct {
|
||||
OS string
|
||||
Arch string
|
||||
}{
|
||||
{"linux", "amd64"},
|
||||
{"linux", "arm64"},
|
||||
}
|
||||
|
||||
// Pipeline builds and optionally signs agent binaries.
|
||||
type Pipeline struct {
|
||||
db *sql.DB
|
||||
artifactsDir string
|
||||
signingKey *KeyPair
|
||||
agentMainPath string
|
||||
version string
|
||||
}
|
||||
|
||||
func NewPipeline(db *sql.DB, artifactsDir, signingKeyPath, agentMainPath, version string) (*Pipeline, error) {
|
||||
key, err := LoadOrCreateKey(signingKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Pipeline{
|
||||
db: db,
|
||||
artifactsDir: artifactsDir,
|
||||
signingKey: key,
|
||||
agentMainPath: agentMainPath,
|
||||
version: version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildAll cross-compiles agent for linux amd64/arm64, signs, and stores artifacts.
|
||||
func (p *Pipeline) BuildAll(public bool) ([]types.Build, error) {
|
||||
var builds []types.Build
|
||||
for _, tgt := range targets {
|
||||
b, err := p.buildOne(tgt.OS, tgt.Arch, public)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
builds = append(builds, *b)
|
||||
}
|
||||
return builds, nil
|
||||
}
|
||||
|
||||
func (p *Pipeline) buildOne(osName, arch string, public bool) (*types.Build, error) {
|
||||
outName := fmt.Sprintf("forge-mesh-agent-%s-%s", osName, arch)
|
||||
outPath := filepath.Join(p.artifactsDir, outName)
|
||||
|
||||
cmd := exec.Command("go", "build", "-trimpath", "-ldflags=-s -w",
|
||||
"-o", outPath,
|
||||
p.agentMainPath,
|
||||
)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GOOS="+osName,
|
||||
"GOARCH="+arch,
|
||||
"CGO_ENABLED=0",
|
||||
)
|
||||
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return nil, fmt.Errorf("build %s/%s: %w\n%s", osName, arch, err, out)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(outPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(data)
|
||||
checksum := hex.EncodeToString(sum[:])
|
||||
sig := p.signingKey.Sign(data)
|
||||
|
||||
build := types.Build{
|
||||
ID: uuid.NewString(),
|
||||
OS: osName,
|
||||
Arch: arch,
|
||||
Version: p.version,
|
||||
Checksum: checksum,
|
||||
Signature: sig,
|
||||
Public: public,
|
||||
Path: outPath,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
if err := p.saveBuild(&build); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &build, nil
|
||||
}
|
||||
|
||||
func (p *Pipeline) saveBuild(b *types.Build) error {
|
||||
pub := 0
|
||||
if b.Public {
|
||||
pub = 1
|
||||
}
|
||||
_, err := p.db.Exec(`
|
||||
INSERT INTO builds (id, os, arch, version, checksum, signature, public, path, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, b.ID, b.OS, b.Arch, b.Version, b.Checksum, b.Signature, pub, b.Path, b.CreatedAt.Format(time.RFC3339))
|
||||
return err
|
||||
}
|
||||
|
||||
// LatestPublic returns the newest public build for os/arch.
|
||||
func LatestPublic(db *sql.DB, osName, arch string) (*types.Build, error) {
|
||||
row := db.QueryRow(`
|
||||
SELECT id, os, arch, version, checksum, signature, public, path, created_at
|
||||
FROM builds
|
||||
WHERE public = 1 AND os = ? AND arch = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`, osName, arch)
|
||||
|
||||
var b types.Build
|
||||
var pub int
|
||||
var path sql.NullString
|
||||
var createdAt string
|
||||
|
||||
err := row.Scan(&b.ID, &b.OS, &b.Arch, &b.Version, &b.Checksum, &b.Signature, &pub, &path, &createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Public = pub == 1
|
||||
if path.Valid {
|
||||
b.Path = path.String
|
||||
}
|
||||
b.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
// GetBuild loads a build by ID.
|
||||
func GetBuild(db *sql.DB, id string) (*types.Build, error) {
|
||||
row := db.QueryRow(`
|
||||
SELECT id, os, arch, version, checksum, signature, public, path, created_at
|
||||
FROM builds WHERE id = ?
|
||||
`, id)
|
||||
|
||||
var b types.Build
|
||||
var pub int
|
||||
var path sql.NullString
|
||||
var createdAt string
|
||||
|
||||
err := row.Scan(&b.ID, &b.OS, &b.Arch, &b.Version, &b.Checksum, &b.Signature, &pub, &path, &createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.Public = pub == 1
|
||||
if path.Valid {
|
||||
b.Path = path.String
|
||||
}
|
||||
b.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
// PublicKey returns the pipeline signing public key hex.
|
||||
func (p *Pipeline) PublicKey() string {
|
||||
return p.signingKey.PublicKeyHex()
|
||||
}
|
||||
|
||||
// CopyArtifact streams a build artifact to w.
|
||||
func CopyArtifact(path string, w io.Writer) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(w, f)
|
||||
return err
|
||||
}
|
||||
19
internal/forge/keys.go
Normal file
19
internal/forge/keys.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package forge
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ParsePublicKeyHex decodes a hex-encoded ed25519 public key.
|
||||
func ParsePublicKeyHex(hexKey string) (ed25519.PublicKey, error) {
|
||||
raw, err := hex.DecodeString(hexKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode pubkey: %w", err)
|
||||
}
|
||||
if len(raw) != ed25519.PublicKeySize {
|
||||
return nil, fmt.Errorf("invalid pubkey size %d", len(raw))
|
||||
}
|
||||
return ed25519.PublicKey(raw), nil
|
||||
}
|
||||
66
internal/forge/sign.go
Normal file
66
internal/forge/sign.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package forge
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// KeyPair holds an ed25519 signing key.
|
||||
type KeyPair struct {
|
||||
Private ed25519.PrivateKey
|
||||
Public ed25519.PublicKey
|
||||
}
|
||||
|
||||
// LoadOrCreateKey loads an ed25519 key from disk or generates a new one.
|
||||
func LoadOrCreateKey(path string) (*KeyPair, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
if len(data) == ed25519.PrivateKeySize {
|
||||
priv := ed25519.PrivateKey(data)
|
||||
return &KeyPair{Private: priv, Public: priv.Public().(ed25519.PublicKey)}, nil
|
||||
}
|
||||
if len(data) == ed25519.SeedSize {
|
||||
priv := ed25519.NewKeyFromSeed(data)
|
||||
return &KeyPair{Private: priv, Public: priv.Public().(ed25519.PublicKey)}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid key size %d", len(data))
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("read key: %w", err)
|
||||
}
|
||||
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate key: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, priv.Seed(), 0o600); err != nil {
|
||||
return nil, fmt.Errorf("write key: %w", err)
|
||||
}
|
||||
|
||||
return &KeyPair{Private: priv, Public: pub}, nil
|
||||
}
|
||||
|
||||
// Sign returns a base64-encoded ed25519 signature of data.
|
||||
func (k *KeyPair) Sign(data []byte) string {
|
||||
sig := ed25519.Sign(k.Private, data)
|
||||
return base64.StdEncoding.EncodeToString(sig)
|
||||
}
|
||||
|
||||
// Verify checks a base64 signature against data using the public key.
|
||||
func Verify(pub ed25519.PublicKey, data []byte, signatureB64 string) bool {
|
||||
sig, err := base64.StdEncoding.DecodeString(signatureB64)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ed25519.Verify(pub, data, sig)
|
||||
}
|
||||
|
||||
// PublicKeyHex returns the hex-encoded public key for embedding in agents.
|
||||
func (k *KeyPair) PublicKeyHex() string {
|
||||
return hex.EncodeToString(k.Public)
|
||||
}
|
||||
22
internal/forge/sign_test.go
Normal file
22
internal/forge/sign_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package forge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSignVerify(t *testing.T) {
|
||||
path := t.TempDir() + "/signing.key"
|
||||
kp, err := LoadOrCreateKey(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data := []byte("forge-mesh artifact")
|
||||
sig := kp.Sign(data)
|
||||
if !Verify(kp.Public, data, sig) {
|
||||
t.Fatal("signature verification failed")
|
||||
}
|
||||
if Verify(kp.Public, []byte("tampered"), sig) {
|
||||
t.Fatal("tampered data should not verify")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user