Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
591 changes: 591 additions & 0 deletions .github/workflows/test-cred-security.yaml

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ MIN_MACOS_VERSION ?= 11.0
FINCH_DAEMON_LOCATION_ROOT ?= $(FINCH_OS_IMAGE_LOCATION_ROOT)/finch-daemon
FINCH_DAEMON_LOCATION ?= $(FINCH_DAEMON_LOCATION_ROOT)/finch-daemon
FINCH_DAEMON_CREDHELPER_LOCATION ?= $(FINCH_DAEMON_LOCATION_ROOT)/docker-credential-finch
FINCH_CREDHELPER_DIR ?= $(OUTDIR)/finch-credhelper
FINCH_CREDHELPER_SOCKET_LOCATION ?= $(FINCH_CREDHELPER_DIR)/native-creds.sock

GOOS ?= $(shell $(GO) env GOOS)
ifeq ($(GOOS),windows)
Expand Down Expand Up @@ -79,7 +81,10 @@ endif

FINCH_CORE_DIR := $(CURDIR)/deps/finch-core

remote-all: arch-test finch install.finch-core-dependencies finch.yaml networks.yaml config.yaml $(OUTDIR)/finch-daemon/[email protected]
# Include credential helper targets
include Makefile.creds

remote-all: arch-test finch make-creds install.finch-core-dependencies finch.yaml networks.yaml config.yaml $(OUTDIR)/finch-daemon/[email protected]

ifeq ($(BUILD_OS), Windows_NT)
include Makefile.windows
Expand Down Expand Up @@ -260,6 +265,8 @@ download-licenses:

mkdir -p "$(LICENSEDIR)/github.com/lima-vm/lima"
curl https://raw.githubusercontent.com/lima-vm/lima/master/LICENSE --output "$(LICENSEDIR)/github.com/lima-vm/lima/LICENSE"
mkdir -p "$(LICENSEDIR)/github.com/docker/docker-credential-helpers"
curl https://raw.githubusercontent.com/docker/docker-credential-helpers/master/LICENSE --output "$(LICENSEDIR)/github.com/docker/docker-credential-helpers/LICENSE"

### system-level dependencies - end ###

Expand Down Expand Up @@ -406,6 +413,14 @@ mdlint:
mdlint-ctr:
$(BINARYNAME) run --rm -v "$(shell pwd):/repo:ro" -w /repo avtodev/markdown-lint:v1 --ignore CHANGELOG.md '**/*.md'

.PHONY: dev-clean
dev-clean:
-@rm -rf $(OUTDIR) 2>/dev/null || true
-@$(MAKE) -C $(FINCH_CORE_DIR) clean
-@rm ./*.tar.gz 2>/dev/null || true
-@rm ./*.qcow2 2>/dev/null || true
-@rm ./test-coverage.* 2>/dev/null || true

.PHONY: clean
ifeq ($(GOOS),windows)
clean:
Expand Down
122 changes: 122 additions & 0 deletions Makefile.creds
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Credential helper configuration
CRED_HELPER_BASE_URL := https://github.com/docker/docker-credential-helpers/releases/download/v0.9.4

# Platform-specific artifacts and checksums
ifeq ($(BUILD_OS), Darwin)
ifeq ($(ARCH), arm64)
CRED_HELPER_ARTIFACT := docker-credential-osxkeychain-v0.9.4.darwin-arm64
CRED_HELPER_DIGEST := 8db5b7cbcbe0870276e56aa416416161785e450708af64cda0f1be4c392dc2e5
else
CRED_HELPER_ARTIFACT := docker-credential-osxkeychain-v0.9.4.darwin-amd64
CRED_HELPER_DIGEST := ad76d1a1e03def49edfa57fdb2874adf2c468cfa0438aae1b2589434796f7c01
endif
CRED_HELPER_NAME := docker-credential-osxkeychain
else ifeq ($(BUILD_OS), Windows_NT)
ifeq ($(ARCH), arm64)
CRED_HELPER_ARTIFACT := docker-credential-wincred-v0.9.4.windows-arm64.exe
CRED_HELPER_DIGEST := 80a6ddbbabc51a8952308acf4d03c044308357cf217300461c44df066c57fe03
else
CRED_HELPER_ARTIFACT := docker-credential-wincred-v0.9.4.windows-amd64.exe
CRED_HELPER_DIGEST := 66fdf4b50c83aeb04a9ea04af960abaf1a7b739ab263115f956b98bb0d16aa7e
endif
CRED_HELPER_NAME := docker-credential-wincred.exe
endif

CRED_HELPER_URL := $(CRED_HELPER_BASE_URL)/$(CRED_HELPER_ARTIFACT)
CRED_HELPER_OUTPUT := $(HOME)/.finch/cred-helpers/$(CRED_HELPER_NAME)

# Build finch credential bridge
.PHONY: finch-cred-bridge
finch-cred-bridge:
mkdir -p $(FINCH_CREDHELPER_DIR)
$(GO) build -ldflags $(LDFLAGS) -tags "$(GO_BUILD_TAGS)" -o $(FINCH_CREDHELPER_DIR)/finch-cred-bridge $(PACKAGE)/cmd/finch-credhelper
chmod 700 $(FINCH_CREDHELPER_DIR)/finch-cred-bridge

# Download and verify credential helper
.PHONY: docker-credential-helper
docker-credential-helper:
ifeq ($(BUILD_OS), Linux)
@echo "No credential helper needed for Linux"
else
mkdir -p $(dir $(CRED_HELPER_OUTPUT))
curl -L $(CRED_HELPER_URL) -o $(CRED_HELPER_OUTPUT)
@echo "Verifying SHA256 checksum..."
@if echo "$(CRED_HELPER_DIGEST) $(CRED_HELPER_OUTPUT)" | $(if $(findstring Darwin,$(BUILD_OS)),shasum -a 256,sha256sum) -c -; then \
echo "Checksum verification passed"; \
else \
echo "Checksum verification failed" && exit 1; \
fi
chmod 700 $(CRED_HELPER_OUTPUT)
endif

# macOS LaunchAgent management
ifeq ($(BUILD_OS), Darwin)
PLIST_NAME := com.runfinch.cred-bridge.plist
PLIST_TEMPLATE := installer-builder/templates/$(PLIST_NAME).template
PLIST_DEST := $(HOME)/Library/LaunchAgents/$(PLIST_NAME)

.PHONY: install-launch-agent
install-launch-agent:
@echo "Installing LaunchAgent..."
mkdir -p $(dir $(PLIST_DEST))
sed -e "s|\$$(FINCH_CREDHELPER_DIR)/finch-cred-bridge|$(FINCH_CREDHELPER_DIR)/finch-cred-bridge|g" \
-e "s|\$$(FINCH_CREDHELPER_SOCKET_LOCATION)|$(FINCH_CREDHELPER_SOCKET_LOCATION)|g" \
$(PLIST_TEMPLATE) > $(PLIST_DEST)
-launchctl bootout gui/$$(id -u)/com.runfinch.cred-bridge 2>/dev/null || true
launchctl bootstrap gui/$$(id -u) $(PLIST_DEST)
@echo "LaunchAgent installed and loaded"

.PHONY: uninstall-launch-agent
uninstall-launch-agent:
@echo "Uninstalling LaunchAgent..."
-launchctl unload $(PLIST_DEST) 2>/dev/null || true
-rm $(PLIST_DEST) 2>/dev/null || true
@echo "LaunchAgent uninstalled"

.PHONY: setup-cred-bridge
ifeq ($(INSTALLED), true)
setup-cred-bridge: finch-cred-bridge
@echo "Built credential bridge binary - service installation deferred to installer"
else
setup-cred-bridge: finch-cred-bridge install-launch-agent
@echo "Credential bridge setup complete"
endif

else ifeq ($(BUILD_OS), Windows_NT)
# Windows Service management
.PHONY: install-service
install-service:
@echo "Installing Windows Service..."
sc create "FinchCredBridge" binPath= "$(FINCH_CREDHELPER_DIR)\finch-cred-bridge.exe" start= demand
sc description "FinchCredBridge" "Finch Credential Bridge Service"
@echo "Windows Service installed"

.PHONY: uninstall-service
uninstall-service:
@echo "Uninstalling Windows Service..."
-sc stop "FinchCredBridge" 2>nul
-sc delete "FinchCredBridge" 2>nul
@echo "Windows Service uninstalled"

.PHONY: setup-cred-bridge
ifeq ($(INSTALLED), true)
setup-cred-bridge: finch-cred-bridge
@echo "Built credential bridge binary - service installation deferred to installer"
else
setup-cred-bridge: finch-cred-bridge install-service
@echo "Credential bridge setup complete"
endif

else
.PHONY: install-launch-agent uninstall-launch-agent setup-cred-bridge install-service uninstall-service
install-launch-agent uninstall-launch-agent setup-cred-bridge install-service uninstall-service:
@echo "Service management is platform-specific"
endif

.PHONY: make-creds
ifeq ($(BUILD_OS), Linux)
make-creds:
@echo "No credential helpers needed for Linux"
else
make-creds: finch-cred-bridge docker-credential-helper setup-cred-bridge
endif
1 change: 1 addition & 0 deletions Makefile.darwin
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ $(OS_OUTDIR)/finch.yaml: $(OS_OUTDIR) finch.yaml.d/common.yaml finch.yaml.d/mac.
sed -i.bak -e "s|<finch_daemon_root>|$(FINCH_DAEMON_LOCATION_ROOT)|g" finch.yaml.temp
sed -i.bak -e "s|<finch_daemon_location>|$(FINCH_DAEMON_LOCATION)|g" finch.yaml.temp
sed -i.bak -e "s|<finch_daemon_credhelper_location>|$(FINCH_DAEMON_CREDHELPER_LOCATION)|g" finch.yaml.temp
sed -i.bak -e "s|<finch_credhelper_socket_location>|$(FINCH_CREDHELPER_SOCKET_LOCATION)|g" finch.yaml.temp
sed -i.bak -e "s|<runc_override_aarch64_location>|$(RUNC_OVERRIDE_AARCH64_LOCATION)|g" finch.yaml.temp
sed -i.bak -e "s/<runc_override_aarch64_digest>/$(RUNC_OVERRIDE_AARCH64_DIGEST)/g" finch.yaml.temp
sed -i.bak -e "s|<runc_override_x86_64_location>|$(RUNC_OVERRIDE_X86_64_LOCATION)|g" finch.yaml.temp
Expand Down
120 changes: 120 additions & 0 deletions cmd/finch-credhelper/helper-utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Package main implements the finch credential helper bridge.

Check failure on line 1 in cmd/finch-credhelper/helper-utils.go

View workflow job for this annotation

GitHub Actions / lint

Actual: Package main implements the finch credential helper bridge.
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)

const (
maxBufferSize = 4096
credHelpersDir = "cred-helpers"
finchConfigDir = ".finch"
)

var credentialHelperNames = map[string]string{
"darwin": "docker-credential-osxkeychain",
"windows": "docker-credential-wincred.exe",
}

// Parses JSON requests.
func parseCredstoreRequest(request string) (command, input string, err error) {
lines := strings.Split(strings.TrimSpace(request), "\n")
if len(lines) == 0 {
return "", "", fmt.Errorf("empty request")
}

command = strings.TrimSpace(lines[0])
if command == "list" {
return command, "", nil
}
if len(lines) < 2 {
return "", "", fmt.Errorf("command %s requires input", command)
}

return command, strings.TrimSpace(lines[1]), nil
}

// Determines and validates the credential helper path.
func getCredentialHelperPath() (string, error) {
helperName, exists := credentialHelperNames[runtime.GOOS]
if !exists {
return "", fmt.Errorf("credential helper not supported on this platform")
}

homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory")
}

path := filepath.Join(homeDir, finchConfigDir, credHelpersDir, helperName)
if _, err := os.Stat(path); err != nil {
return "", fmt.Errorf("credential helper not found")
}

return path, nil
}

// Invokes the platform-specific credential helper binary.
func executeCredentialHelper(command, input string) (string, error) {
credHelperPath, err := getCredentialHelperPath()
if err != nil {
return "", err
}

// #nosec G204 -- credHelperPath is validated and command is from trusted source
cmd := exec.Command(credHelperPath, command)
if input != "" {
cmd.Stdin = strings.NewReader(input)
}
cmd.Env = os.Environ()

output, err := cmd.CombinedOutput()
response := strings.TrimSpace(string(output))

// Handling errors, with special case for "get" command requiring empty cred. JSON
if err != nil {
if command == "get" {
return createEmptyCredentials(input), nil
}
return "", fmt.Errorf("credential helper failed")
}

return response, nil
}

// Creates default credentials when credentials are not found.
func createEmptyCredentials(serverURL string) string {
return fmt.Sprintf(`{"ServerURL":"%s","Username":"","Secret":""}`, serverURL)
}

// Processes inbound credential requests from Lima VM bridge.
func processCredentialRequest(conn interface {
Read([]byte) (int, error)
Write([]byte) (int, error)
}) error {

Check failure on line 99 in cmd/finch-credhelper/helper-utils.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gofumpt)
buffer := make([]byte, 0, maxBufferSize)
buffer = buffer[:maxBufferSize]
n, err := conn.Read(buffer)
if err != nil {
return fmt.Errorf("failed to read request")
}

request := strings.TrimSpace(string(buffer[:n]))
command, input, err := parseCredstoreRequest(request)
if err != nil {
return err
}

response, err := executeCredentialHelper(command, input)
if err != nil {
return err
}

_, err = conn.Write([]byte(response))
return err
}
29 changes: 29 additions & 0 deletions cmd/finch-credhelper/mac-creds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//go:build darwin

package main

import (
"fmt"
"net"
"os"
)

// handleCredstoreRequest processes credential requests via socket activation
func handleCredstoreRequest() error {
conn, err := net.FileConn(os.Stdin)
if err != nil {
return fmt.Errorf("failed to create connection from stdin: %w", err)
}
defer conn.Close()

return processCredentialRequest(conn)
}

func main() {
// macOS credential helper using socket activation via launchd
// launchd passes the socket connection through stdin
if err := handleCredstoreRequest(); err != nil {
fmt.Fprintf(os.Stderr, "credential helper failed\n")
os.Exit(1)
}
}
57 changes: 57 additions & 0 deletions cmd/finch-credhelper/windows-creds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//go:build windows

package main

import (
"fmt"
"log"
"net"
"os"
"path/filepath"
)

// Windows socket server (for WSL2 socket forwarding)

Check failure on line 13 in cmd/finch-credhelper/windows-creds.go

View workflow job for this annotation

GitHub Actions / lint

Comment should end in a period (godot)
func startWindowsCredentialServer() error {
userProfile := os.Getenv("USERPROFILE")
if userProfile == "" {
return fmt.Errorf("USERPROFILE not set")
}
socketPath := filepath.Join(userProfile, ".finch", "native-creds.sock")
_ = os.Remove(socketPath) // Ignore error if file doesn't exist

listener, err := net.Listen("unix", socketPath)
if err != nil {
return fmt.Errorf("failed to create socket: %w", err)
}

// set socket file permissions to owner only
if err := os.Chmod(socketPath, 0600); err != nil {

Check failure on line 28 in cmd/finch-credhelper/windows-creds.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gofumpt)
return fmt.Errorf("failed to set socket permissions: %w", err)
}

defer func() { _ = listener.Close() }()

// Accept connections
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("Failed to accept connection")
continue
}

// Handle each connection
go func(c net.Conn) {
defer func() { _ = c.Close() }()
if err := processCredentialRequest(c); err != nil {
log.Printf("Error processing credential request")
}
}(conn)
}
}

func main() {
if err := startWindowsCredentialServer(); err != nil {
log.Printf("Windows credential server failed")
os.Exit(1)
}
}
Loading
Loading