mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-13 07:41:27 +00:00
e66f6a26f7
Verify the signed release checksum manifest against Pulse's pinned SSH key before either MCP installer accepts a downloaded binary. Remove the unsigned bypass and require one exact digest entry. Include bare Unix MCP executables in release checksum/signature assembly, cover unavailable, invalid, ambiguous, mismatched, and successful evidence paths with executable regression tests, and enforce MCP installer pins against the configured release key.
222 lines
7.0 KiB
Go
222 lines
7.0 KiB
Go
package installtests
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const productionMCPReleaseKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
|
|
|
|
func TestInstallMCPRequiresSignedChecksumEvidence(t *testing.T) {
|
|
for _, command := range []string{"bash", "ssh-keygen"} {
|
|
if _, err := exec.LookPath(command); err != nil {
|
|
t.Skipf("%s not installed", command)
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
manifest func(binary []byte) string
|
|
invalidSig bool
|
|
missingAsset string
|
|
wantSuccess bool
|
|
wantOutput string
|
|
}{
|
|
{
|
|
name: "valid signed manifest",
|
|
manifest: func(binary []byte) string {
|
|
digest := sha256.Sum256(binary)
|
|
return fmt.Sprintf("%x pulse-mcp-linux-amd64\n", digest)
|
|
},
|
|
wantSuccess: true,
|
|
wantOutput: "release signature verified",
|
|
},
|
|
{
|
|
name: "manifest unavailable",
|
|
manifest: func(binary []byte) string {
|
|
digest := sha256.Sum256(binary)
|
|
return fmt.Sprintf("%x pulse-mcp-linux-amd64\n", digest)
|
|
},
|
|
missingAsset: "checksums.txt",
|
|
wantOutput: "could not fetch checksums.txt; refusing unverified install",
|
|
},
|
|
{
|
|
name: "signature unavailable",
|
|
manifest: func(binary []byte) string {
|
|
digest := sha256.Sum256(binary)
|
|
return fmt.Sprintf("%x pulse-mcp-linux-amd64\n", digest)
|
|
},
|
|
missingAsset: "checksums.txt.sshsig",
|
|
wantOutput: "could not fetch checksums.txt.sshsig; refusing unverified install",
|
|
},
|
|
{
|
|
name: "signature invalid",
|
|
manifest: func(binary []byte) string {
|
|
digest := sha256.Sum256(binary)
|
|
return fmt.Sprintf("%x pulse-mcp-linux-amd64\n", digest)
|
|
},
|
|
invalidSig: true,
|
|
wantOutput: "cryptographic signature verification failed for checksums.txt",
|
|
},
|
|
{
|
|
name: "binary omitted",
|
|
manifest: func(binary []byte) string {
|
|
digest := sha256.Sum256(binary)
|
|
return fmt.Sprintf("%x another-file\n", digest)
|
|
},
|
|
wantOutput: "checksums.txt must contain exactly one valid SHA256 entry",
|
|
},
|
|
{
|
|
name: "binary duplicated",
|
|
manifest: func(binary []byte) string {
|
|
digest := sha256.Sum256(binary)
|
|
return fmt.Sprintf("%x pulse-mcp-linux-amd64\n%x pulse-mcp-linux-amd64\n", digest, digest)
|
|
},
|
|
wantOutput: "checksums.txt must contain exactly one valid SHA256 entry",
|
|
},
|
|
{
|
|
name: "digest mismatch",
|
|
manifest: func(_ []byte) string {
|
|
return strings.Repeat("0", 64) + " pulse-mcp-linux-amd64\n"
|
|
},
|
|
wantOutput: "sha256 mismatch for pulse-mcp-linux-amd64",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
fixtureDir := filepath.Join(tmp, "fixtures")
|
|
binDir := filepath.Join(tmp, "bin")
|
|
installDir := filepath.Join(tmp, "install")
|
|
for _, dir := range []string{fixtureDir, binDir, installDir} {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", dir, err)
|
|
}
|
|
}
|
|
|
|
binary := []byte("test pulse-mcp executable\n")
|
|
writeTestFile(t, filepath.Join(fixtureDir, "pulse-mcp-linux-amd64"), binary, 0o644)
|
|
manifestPath := filepath.Join(fixtureDir, "checksums.txt")
|
|
writeTestFile(t, manifestPath, []byte(tt.manifest(binary)), 0o644)
|
|
|
|
privateKeyPath := filepath.Join(tmp, "release-key")
|
|
runTestCommand(t, exec.Command("ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "pulse-installer", "-f", privateKeyPath))
|
|
publicKeyBytes, err := os.ReadFile(privateKeyPath + ".pub")
|
|
if err != nil {
|
|
t.Fatalf("read test public key: %v", err)
|
|
}
|
|
publicKey := strings.TrimSpace(string(publicKeyBytes))
|
|
|
|
sign := exec.Command("ssh-keygen", "-q", "-Y", "sign", "-f", privateKeyPath, "-n", "pulse-install", manifestPath)
|
|
runTestCommand(t, sign)
|
|
if err := os.Rename(manifestPath+".sig", filepath.Join(fixtureDir, "checksums.txt.sshsig")); err != nil {
|
|
t.Fatalf("rename signature: %v", err)
|
|
}
|
|
if tt.invalidSig {
|
|
writeTestFile(t, filepath.Join(fixtureDir, "checksums.txt.sshsig"), []byte("not a signature\n"), 0o644)
|
|
}
|
|
|
|
scriptBytes, err := os.ReadFile(repoFile("scripts", "install-mcp.sh"))
|
|
if err != nil {
|
|
t.Fatalf("read installer: %v", err)
|
|
}
|
|
script := strings.Replace(string(scriptBytes), productionMCPReleaseKey, publicKey, 1)
|
|
if script == string(scriptBytes) {
|
|
t.Fatal("installer no longer contains the expected pinned production key")
|
|
}
|
|
scriptPath := filepath.Join(tmp, "install-mcp.sh")
|
|
writeTestFile(t, scriptPath, []byte(script), 0o755)
|
|
|
|
curlStub := `#!/bin/sh
|
|
set -eu
|
|
output=''
|
|
url=''
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
-o) output="$2"; shift 2 ;;
|
|
http*) url="$1"; shift ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
name="${url##*/}"
|
|
if [ -n "${MISSING_ASSET:-}" ] && [ "$name" = "$MISSING_ASSET" ]; then
|
|
exit 22
|
|
fi
|
|
cp "$FIXTURE_DIR/$name" "$output"
|
|
`
|
|
writeTestFile(t, filepath.Join(binDir, "curl"), []byte(curlStub), 0o755)
|
|
|
|
cmd := exec.Command("bash", scriptPath)
|
|
cmd.Env = append(os.Environ(),
|
|
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
|
|
"HOME="+tmp,
|
|
"FIXTURE_DIR="+fixtureDir,
|
|
"MISSING_ASSET="+tt.missingAsset,
|
|
"PULSE_MCP_BIN_DIR="+installDir,
|
|
"PULSE_MCP_VERSION=v-test",
|
|
)
|
|
output, runErr := cmd.CombinedOutput()
|
|
if tt.wantSuccess && runErr != nil {
|
|
t.Fatalf("installer failed: %v\n%s", runErr, output)
|
|
}
|
|
if !tt.wantSuccess && runErr == nil {
|
|
t.Fatalf("installer unexpectedly succeeded:\n%s", output)
|
|
}
|
|
if !strings.Contains(string(output), tt.wantOutput) {
|
|
t.Fatalf("installer output missing %q:\n%s", tt.wantOutput, output)
|
|
}
|
|
|
|
installedPath := filepath.Join(installDir, "pulse-mcp")
|
|
_, statErr := os.Stat(installedPath)
|
|
if tt.wantSuccess && statErr != nil {
|
|
t.Fatalf("verified binary was not installed: %v", statErr)
|
|
}
|
|
if !tt.wantSuccess && !os.IsNotExist(statErr) {
|
|
t.Fatalf("unverified binary reached install destination: stat err=%v", statErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInstallMCPPowerShellFailsClosed(t *testing.T) {
|
|
path := repoFile("scripts", "install-mcp.ps1")
|
|
assertFileContainsAll(t, path,
|
|
"$PinnedReleaseSshPublicKey = 'ssh-ed25519 ",
|
|
"Get-SshKeygenPath",
|
|
"Assert-ChecksumManifestSignature $manifestPath $signaturePath",
|
|
"could not fetch checksums.txt; refusing unverified install",
|
|
"could not fetch checksums.txt.sshsig; refusing unverified install",
|
|
"checksums.txt must contain exactly one valid SHA256 entry",
|
|
"sha256 mismatch for ${binaryName}",
|
|
)
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read PowerShell installer: %v", err)
|
|
}
|
|
for _, insecure := range []string{"PULSE_MCP_NO_VERIFY", "NoVerify", "skipping verification"} {
|
|
if strings.Contains(string(content), insecure) {
|
|
t.Fatalf("PowerShell installer retains fail-open control %q", insecure)
|
|
}
|
|
}
|
|
}
|
|
|
|
func writeTestFile(t *testing.T, path string, content []byte, mode os.FileMode) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, content, mode); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func runTestCommand(t *testing.T, cmd *exec.Cmd) {
|
|
t.Helper()
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("%s failed: %v\n%s", strings.Join(cmd.Args, " "), err, output)
|
|
}
|
|
}
|