Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 68 additions & 17 deletions .github/workflows/release-swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,41 +234,92 @@ jobs:
# Stage the bundle: bin/darkbloom, bin/darkbloom-enclave, bin/mlx.metallib
# ----------------------------------------------------------------------

- name: Embed provisioning profile
env:
PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
run: |
set -euo pipefail
if [ -z "${PROVISIONING_PROFILE_BASE64:-}" ]; then
echo "::warning::PROVISIONING_PROFILE_BASE64 not set — persistent SE key will fall back to ephemeral"
echo "profile_available=false" >> "$GITHUB_OUTPUT"
else
echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > /tmp/embedded.provisionprofile
security cms -D -i /tmp/embedded.provisionprofile | head -20
echo "profile_available=true" >> "$GITHUB_OUTPUT"
fi
id: profile

- name: Stage and sign bundle
id: bundle
run: |
set -euo pipefail
STAGE=/tmp/darkbloom-bundle
rm -rf "$STAGE"
mkdir -p "$STAGE/bin"

cp "$BIN_DIR/$CLI_NAME" "$STAGE/bin/"
cp "$BIN_DIR/$ENCLAVE_NAME" "$STAGE/bin/"

# The MLX C++ runtime checks for a colocated mlx.metallib first;
# placing it in bin/ next to darkbloom satisfies that lookup.
cp "${{ steps.metallib.outputs.metallib }}" "$STAGE/bin/mlx.metallib"
# Wrap the CLI in a minimal .app bundle so the provisioning profile
# authorizes keychain-access-groups for the persistent SE key.
APP="$STAGE/Darkbloom.app"
mkdir -p "$APP/Contents/MacOS"

cp "$BIN_DIR/$CLI_NAME" "$APP/Contents/MacOS/"
cp "$BIN_DIR/$ENCLAVE_NAME" "$APP/Contents/MacOS/"
cp "${{ steps.metallib.outputs.metallib }}" "$APP/Contents/MacOS/mlx.metallib"

cat > "$APP/Contents/Info.plist" << PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>io.darkbloom.provider</string>
<key>CFBundleExecutable</key>
<string>${CLI_NAME}</string>
<key>CFBundleName</key>
<string>Darkbloom</string>
<key>CFBundleVersion</key>
<string>${VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
</dict>
</plist>
PLIST

if [ "${{ steps.profile.outputs.profile_available }}" = "true" ]; then
cp /tmp/embedded.provisionprofile "$APP/Contents/embedded.provisionprofile"
echo "Provisioning profile embedded in app bundle"
fi

# Hardened-runtime sign each Mach-O binary.
# Hardened-runtime sign each binary, then the bundle.
codesign --force --options runtime --timestamp \
--entitlements provider-swift/entitlements.plist \
--keychain "$KEYCHAIN_PATH" \
--sign "$DEVELOPER_ID" "$APP/Contents/MacOS/$ENCLAVE_NAME"
codesign --force --options runtime --timestamp \
--entitlements scripts/entitlements.plist \
--entitlements provider-swift/entitlements.plist \
--keychain "$KEYCHAIN_PATH" \
--sign "$DEVELOPER_ID" "$STAGE/bin/$CLI_NAME"
--sign "$DEVELOPER_ID" "$APP/Contents/MacOS/$CLI_NAME"
codesign --force --options runtime --timestamp \
--entitlements scripts/entitlements.plist \
--entitlements provider-swift/entitlements.plist \
--keychain "$KEYCHAIN_PATH" \
--sign "$DEVELOPER_ID" "$STAGE/bin/$ENCLAVE_NAME"
--sign "$DEVELOPER_ID" "$APP"

codesign --verify --verbose=2 "$APP"
codesign --verify --verbose=2 "$APP/Contents/MacOS/$CLI_NAME"

codesign --verify --verbose=2 "$STAGE/bin/$CLI_NAME"
codesign --verify --verbose=2 "$STAGE/bin/$ENCLAVE_NAME"
# Backward-compatible flat layout alongside the .app bundle.
mkdir -p "$STAGE/bin"
ln -s "../Darkbloom.app/Contents/MacOS/$CLI_NAME" "$STAGE/bin/$CLI_NAME"
ln -s "../Darkbloom.app/Contents/MacOS/$ENCLAVE_NAME" "$STAGE/bin/$ENCLAVE_NAME"
ln -s "../Darkbloom.app/Contents/MacOS/mlx.metallib" "$STAGE/bin/mlx.metallib"

# Two artifact shapes: zip for notarization, tar.gz for distribution.
ditto -c -k --keepParent "$STAGE" /tmp/darkbloom-notarize.zip
tar czf /tmp/darkbloom-bundle-macos-arm64.tar.gz -C "$STAGE" .
tar tzf /tmp/darkbloom-bundle-macos-arm64.tar.gz | sort | tee /tmp/darkbloom-bundle-files.txt
grep -qx './bin/darkbloom' /tmp/darkbloom-bundle-files.txt
grep -qx './bin/darkbloom-enclave' /tmp/darkbloom-bundle-files.txt
grep -qx './bin/mlx.metallib' /tmp/darkbloom-bundle-files.txt
echo "stage=$STAGE" >> "$GITHUB_OUTPUT"
ls -lh /tmp/darkbloom-bundle-macos-arm64.tar.gz

Expand Down
36 changes: 24 additions & 12 deletions coordinator/internal/api/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ set -euo pipefail
# Direct-fetch copy: no serve-time templating applied. Override with
# curl ... | COORD_URL=https://api.dev.darkbloom.xyz bash
# Or fetch the coordinator-served copy at $COORD_URL/install.sh for templating.
COORD_URL="${COORD_URL:-__DARKBLOOM_COORD_URL__}"
COORD_URL="${COORD_URL:-https://api.darkbloom.dev}"
INSTALL_DIR="$HOME/.darkbloom"
BIN_DIR="$INSTALL_DIR/bin"

Expand Down Expand Up @@ -104,20 +104,32 @@ fi
echo " Bundle hash verified ✓"

echo " Installing into $INSTALL_DIR ..."
# The bundle ships as bin/{darkbloom,darkbloom-enclave,mlx.metallib}.
# Older bundles named the helper `eigeninference-enclave`; accept either,
# install as the canonical `darkbloom-enclave`, and leave a backward-compat
# symlink for the old name so already-deployed scripts keep working.
# The bundle ships as Darkbloom.app/ (contains provisioning profile for
# keychain-access-groups) with bin/ symlinks for backward compatibility.
# Older flat bundles (bin/darkbloom directly) are also handled.
tar xzf "$TARBALL" -C "$INSTALL_DIR"
[ -f "$INSTALL_DIR/darkbloom" ] && mv -f "$INSTALL_DIR/darkbloom" "$BIN_DIR/darkbloom"
[ -f "$INSTALL_DIR/darkbloom-enclave" ] && mv -f "$INSTALL_DIR/darkbloom-enclave" "$BIN_DIR/darkbloom-enclave"
if [ -f "$INSTALL_DIR/eigeninference-enclave" ] && [ ! -f "$BIN_DIR/darkbloom-enclave" ]; then
mv -f "$INSTALL_DIR/eigeninference-enclave" "$BIN_DIR/darkbloom-enclave"

# New .app bundle layout: Darkbloom.app/Contents/MacOS/{darkbloom,darkbloom-enclave,mlx.metallib}
if [ -d "$INSTALL_DIR/Darkbloom.app" ]; then
APP_BIN="$INSTALL_DIR/Darkbloom.app/Contents/MacOS"
chmod +x "$APP_BIN/darkbloom" "$APP_BIN/darkbloom-enclave" 2>/dev/null || true
# bin/ gets symlinks pointing into the .app bundle
mkdir -p "$BIN_DIR"
ln -sfn "$APP_BIN/darkbloom" "$BIN_DIR/darkbloom"
ln -sfn "$APP_BIN/darkbloom-enclave" "$BIN_DIR/darkbloom-enclave"
ln -sfn "$APP_BIN/mlx.metallib" "$BIN_DIR/mlx.metallib" 2>/dev/null || true
echo " Installed .app bundle with provisioning profile"
else
# Legacy flat layout fallback
[ -f "$INSTALL_DIR/darkbloom" ] && mv -f "$INSTALL_DIR/darkbloom" "$BIN_DIR/darkbloom"
[ -f "$INSTALL_DIR/darkbloom-enclave" ] && mv -f "$INSTALL_DIR/darkbloom-enclave" "$BIN_DIR/darkbloom-enclave"
if [ -f "$INSTALL_DIR/eigeninference-enclave" ] && [ ! -f "$BIN_DIR/darkbloom-enclave" ]; then
mv -f "$INSTALL_DIR/eigeninference-enclave" "$BIN_DIR/darkbloom-enclave"
fi
[ -f "$INSTALL_DIR/mlx.metallib" ] && mv -f "$INSTALL_DIR/mlx.metallib" "$BIN_DIR/mlx.metallib"
chmod +x "$BIN_DIR/darkbloom" "$BIN_DIR/darkbloom-enclave" 2>/dev/null || true
fi
[ -f "$INSTALL_DIR/mlx.metallib" ] && mv -f "$INSTALL_DIR/mlx.metallib" "$BIN_DIR/mlx.metallib"

chmod +x "$BIN_DIR/darkbloom" "$BIN_DIR/darkbloom-enclave" 2>/dev/null || true
# Backward-compat: keep the legacy helper name resolvable.
ln -sfn "$BIN_DIR/darkbloom-enclave" "$BIN_DIR/eigeninference-enclave" 2>/dev/null || true
rm -f "$TARBALL"

Expand Down
30 changes: 26 additions & 4 deletions provider-swift/Sources/ProviderCore/ProviderLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public struct ProviderLoopConfig: Sendable {
public actor ProviderLoop {
private let loopConfig: ProviderLoopConfig
private let keyPair: NodeKeyPair
private let seIdentity: SecureEnclaveIdentity?
private let signer: (any AttestationSigner)?
private let attestationBuilder: AttestationBuilder?
private let scheduler: BatchScheduler
private let stats: AtomicProviderStats
Expand Down Expand Up @@ -113,8 +113,8 @@ public actor ProviderLoop {
self.loopConfig = config
NodeKeyPair.purgeLegacyFiles()
self.keyPair = NodeKeyPair.generate()
self.seIdentity = try SecureEnclaveIdentity.createEphemeral()
self.attestationBuilder = seIdentity.map { AttestationBuilder(identity: $0) }
self.signer = Self.createAttestationSigner()
self.attestationBuilder = signer.map { AttestationBuilder(identity: $0) }
self.stats = AtomicProviderStats()
self.state = ProviderState()
self.cancellationRegistry = InferenceCancellationRegistry()
Expand All @@ -125,6 +125,28 @@ public actor ProviderLoop {
)
}

/// Try persistent keychain-backed SE key first; fall back to ephemeral CryptoKit key.
private static func createAttestationSigner() -> (any AttestationSigner)? {
let log = ProviderLogger(subsystem: "dev.darkbloom.provider", category: "loop")

if PersistentEnclaveKey.isAvailable {
do {
let key = try PersistentEnclaveKey.loadOrCreate()
log.info("Using persistent keychain-backed Secure Enclave key for attestation")
return key
} catch {
log.warning("Persistent SE key unavailable (\(error)), falling back to ephemeral")
}
}

do {
return try SecureEnclaveIdentity.createEphemeral()
} catch {
log.warning("Ephemeral SE identity also unavailable: \(error)")
return nil
}
}

// MARK: - Main Run Loop

public func run() async throws {
Expand Down Expand Up @@ -416,7 +438,7 @@ public actor ProviderLoop {
let providerStats = self.stats
let providerState = self.state
let registry = self.cancellationRegistry
let signingIdentity = self.seIdentity
let signingIdentity = self.signer
let log = self.logger

// 7. Spawn inference task
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,21 @@ public enum StatusCanonical {

// MARK: - Builder

/// Builds and signs attestation blobs using a Secure Enclave identity.
/// Builds and signs attestation blobs using a Secure Enclave signing key.
///
/// Accepts any `AttestationSigner` -- either the ephemeral
/// `SecureEnclaveIdentity` (CryptoKit) or the persistent
/// `PersistentEnclaveKey` (Security framework, keychain-backed).
///
/// Usage:
/// 1. Create or load a SecureEnclaveIdentity
/// 2. Create an AttestationBuilder with that identity
/// 1. Create or load a signing key (ephemeral or persistent)
/// 2. Create an AttestationBuilder with that signer
/// 3. Call `buildAttestation()` to get a SignedAttestation
/// 4. Serialize to JSON and include in the Register message
public final class AttestationBuilder: @unchecked Sendable {
private let identity: SecureEnclaveIdentity
private let identity: any AttestationSigner

public init(identity: SecureEnclaveIdentity) {
public init(identity: any AttestationSigner) {
self.identity = identity
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// AttestationSigner -- protocol abstracting over ephemeral and persistent
/// Secure Enclave signing keys for attestation.
///
/// Both `SecureEnclaveIdentity` (CryptoKit, ephemeral) and
/// `PersistentEnclaveKey` (Security framework, keychain-backed) conform.
/// `AttestationBuilder` and `ProviderLoop` use this protocol to accept
/// either implementation.

import Foundation

public protocol AttestationSigner: Sendable {
/// Sign arbitrary data, returning a DER-encoded ECDSA signature.
func sign(_ data: Data) throws -> Data

/// Base64-encoded P-256 public key (raw 64 bytes: X || Y).
var publicKeyBase64: String { get }
}

// MARK: - Conformances

extension SecureEnclaveIdentity: AttestationSigner {}

extension PersistentEnclaveKey: AttestationSigner {}
Loading