Sign manifest with Ed25519 at build time (#5226)

Add optional Ed25519 signing to the manifest_manager. When a signing key
is provided, the manifest is signed and the signature is prepended as
a header comment that clients can verify.

Changes:
- manifest_manager: Accept optional private key file as 3rd argument
- manifest_manager: Sign manifest content and prepend signature line
- installer_build.yml: Pass MANIFEST_SIGNING_KEY to manifest_manager
- unity_build.yml: Pass MANIFEST_SIGNING_KEY to manifest_manager
- Add generate_manifest_keys.go script to create key pairs

The signature line format is: # signature=<base64-encoded-ed25519-signature>

To enable signing:
1. Run: go run scripts/generate_manifest_keys.go
2. Add the private key as GitHub secret MANIFEST_SIGNING_KEY
3. Embed the public key in the installer for verification (PR 4)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-12 12:08:54 -08:00
committed by GitHub
co-authored by Claude Opus 4.5
parent ded06ba815
commit 353eb3907c
4 changed files with 137 additions and 17 deletions
+20 -5
View File
@@ -68,15 +68,30 @@ jobs:
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
MANIFEST_SIGNING_KEY: ${{ secrets.MANIFEST_SIGNING_KEY }}
run: |
# Create installer manifest content
# Create installer manifest content
INSTALLER_SHA=$(sha256sum ./installer-output/EagleInstaller.exe | cut -d' ' -f1)
echo "installer_version=$INSTALLER_SHA" > /tmp/installer_manifest.txt
echo "installer_url=installer/EagleInstaller.exe" >> /tmp/installer_manifest.txt
echo "=== Installer manifest content ==="
cat /tmp/installer_manifest.txt
echo "=================================="
# Update the unified manifest
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer /tmp/installer_manifest.txt
# Write signing key to temp file (if available)
SIGNING_ARGS=""
if [ -n "$MANIFEST_SIGNING_KEY" ]; then
echo "$MANIFEST_SIGNING_KEY" > /tmp/manifest_signing_key
chmod 600 /tmp/manifest_signing_key
SIGNING_ARGS="/tmp/manifest_signing_key"
echo "Manifest signing key available"
else
echo "Warning: MANIFEST_SIGNING_KEY not set, manifest will be unsigned"
fi
# Update the unified manifest (with optional signing)
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer /tmp/installer_manifest.txt $SIGNING_ARGS
# Cleanup
rm -f /tmp/manifest_signing_key
+18 -1
View File
@@ -63,7 +63,24 @@ jobs:
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d /tmp/unity_manifest.txt
MANIFEST_SIGNING_KEY: ${{ secrets.MANIFEST_SIGNING_KEY }}
run: |
# Write signing key to temp file (if available)
SIGNING_ARGS=""
if [ -n "$MANIFEST_SIGNING_KEY" ]; then
echo "$MANIFEST_SIGNING_KEY" > /tmp/manifest_signing_key
chmod 600 /tmp/manifest_signing_key
SIGNING_ARGS="/tmp/manifest_signing_key"
echo "Manifest signing key available"
else
echo "Warning: MANIFEST_SIGNING_KEY not set, manifest will be unsigned"
fi
# Update the unified manifest (with optional signing)
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d /tmp/unity_manifest.txt $SIGNING_ARGS
# Cleanup
rm -f /tmp/manifest_signing_key
- name: Archive build log
if: success() || failure()
uses: actions/upload-artifact@v4
+41
View File
@@ -0,0 +1,41 @@
// +build ignore
// Script to generate Ed25519 key pair for manifest signing.
// Run with: go run scripts/generate_manifest_keys.go
//
// This will output:
// - Private key (base64): Store as MANIFEST_SIGNING_KEY GitHub secret
// - Public key (base64): Embed in EagleInstaller for verification
package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
)
func main() {
// Generate Ed25519 key pair
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
log.Fatalf("Failed to generate key pair: %v", err)
}
// Encode keys as base64
privateKeyB64 := base64.StdEncoding.EncodeToString(privateKey)
publicKeyB64 := base64.StdEncoding.EncodeToString(publicKey)
fmt.Println("=== Ed25519 Key Pair for Manifest Signing ===")
fmt.Println()
fmt.Println("PRIVATE KEY (store as GitHub secret MANIFEST_SIGNING_KEY):")
fmt.Println(privateKeyB64)
fmt.Println()
fmt.Println("PUBLIC KEY (embed in EagleInstaller.cs for verification):")
fmt.Println(publicKeyB64)
fmt.Println()
fmt.Printf("Private key size: %d bytes\n", len(privateKey))
fmt.Printf("Public key size: %d bytes\n", len(publicKey))
}
@@ -1,12 +1,15 @@
package main
import (
"crypto/ed25519"
"encoding/base64"
"fmt"
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
"log"
"os"
"strings"
"time"
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
)
const bucketName = "eagle0-windows"
@@ -64,7 +67,7 @@ func (mm *ManifestManager) parseManifest(content string) map[string]string {
return sections
}
func (mm *ManifestManager) buildManifest(sections map[string]string) string {
func (mm *ManifestManager) buildManifest(sections map[string]string, privateKey ed25519.PrivateKey) string {
var manifest strings.Builder
manifest.WriteString(fmt.Sprintf("# Generated: %s\n", time.Now().Format(time.RFC3339)))
@@ -90,10 +93,21 @@ func (mm *ManifestManager) buildManifest(sections map[string]string) string {
}
}
return manifest.String()
// The content to sign is everything we've built so far
contentToSign := manifest.String()
// If we have a private key, sign the manifest
if privateKey != nil {
signature := ed25519.Sign(privateKey, []byte(contentToSign))
signatureB64 := base64.StdEncoding.EncodeToString(signature)
// Prepend signature line at the very beginning
return fmt.Sprintf("# signature=%s\n%s", signatureB64, contentToSign)
}
return contentToSign
}
func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) error {
func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string, privateKey ed25519.PrivateKey) error {
log.Printf("Updating manifest section: %s", sectionName)
// Fetch existing manifest
@@ -103,14 +117,14 @@ func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) err
existing = ""
}
// Parse sections
// Parse sections (strips out old signature line if present)
sections := mm.parseManifest(existing)
// Update the specified section
sections[sectionName] = sectionContent
// Build new manifest
newManifest := mm.buildManifest(sections)
// Build new manifest (with signature if key provided)
newManifest := mm.buildManifest(sections, privateKey)
// Upload updated manifest with public ACL
err = mm.bucketBasics.UploadBytesPublic(bucketName, manifestPath, []byte(newManifest))
@@ -118,13 +132,36 @@ func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) err
return fmt.Errorf("failed to upload manifest: %v", err)
}
log.Printf("Successfully updated manifest section: %s", sectionName)
if privateKey != nil {
log.Printf("Successfully updated and signed manifest section: %s", sectionName)
} else {
log.Printf("Successfully updated manifest section: %s (unsigned)", sectionName)
}
return nil
}
func loadPrivateKey(keyPath string) (ed25519.PrivateKey, error) {
keyData, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to read private key file: %v", err)
}
// Key should be base64-encoded 64-byte Ed25519 private key
keyBytes, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(keyData)))
if err != nil {
return nil, fmt.Errorf("failed to decode private key: %v", err)
}
if len(keyBytes) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid private key size: expected %d bytes, got %d", ed25519.PrivateKeySize, len(keyBytes))
}
return ed25519.PrivateKey(keyBytes), nil
}
func main() {
if len(os.Args) != 3 {
log.Fatal("Usage: manifest_manager <section_name> <section_content_file>")
if len(os.Args) < 3 || len(os.Args) > 4 {
log.Fatal("Usage: manifest_manager <section_name> <section_content_file> [private_key_file]")
}
sectionName := os.Args[1]
@@ -136,6 +173,16 @@ func main() {
log.Fatalf("Failed to read content file: %v", err)
}
// Load private key if provided
var privateKey ed25519.PrivateKey
if len(os.Args) == 4 {
privateKey, err = loadPrivateKey(os.Args[3])
if err != nil {
log.Fatalf("Failed to load private key: %v", err)
}
log.Println("Ed25519 signing key loaded")
}
// Create manifest manager
mm, err := NewManifestManager()
if err != nil {
@@ -143,7 +190,7 @@ func main() {
}
// Update the section
err = mm.UpdateSection(sectionName, string(content))
err = mm.UpdateSection(sectionName, string(content), privateKey)
if err != nil {
log.Fatalf("Failed to update manifest: %v", err)
}