2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
openssl_pkey_export($key, $private_key);
$public_key = openssl_pkey_get_details($key)['key'];
$payload = "\n";
$xml .= " \n";
$xml .= " Untrusted Package\n";
$xml .= " Mallory\n";
$xml .= " http://example.invalid\n";
$xml .= " mallory@example.invalid\n";
$xml .= " \n";
$xml .= " \n";
$xml .= " scripts\n";
$xml .= " \n";
$xml .= " \n";
$xml .= " \n";
$xml .= " scripts/planted.php\n";
openssl_sign($payload, $file_sig, $private_key, $algorithm);
$xml .= ' ' . base64_encode($payload) . "\n";
$xml .= ' ' . base64_encode($file_sig) . "\n";
$xml .= " \n";
$xml .= " \n";
$xml .= " Mallory\n";
if ($names_key) {
$xml .= ' ' . base64_encode($public_key) . "\n";
}
openssl_sign($xml . " \n", $base_sig, $private_key, $algorithm);
$xml .= ' ' . base64_encode($base_sig) . "\n";
$path = sys_get_temp_dir() . '/cacti_pkg_trust_' . bin2hex(random_bytes(4)) . '.xml.gz';
$f = fopen("compress.zlib://$path", 'wb');
fwrite($f, $xml, strlen($xml));
fclose($f);
return $path;
}
function importSourceFile(string $name): string {
$src = file_get_contents(dirname(__DIR__, 4) . "/$name");
expect($src)->not->toBeFalse("Failed to read $name");
return $src;
}
beforeEach(function () {
$this->package_key = '';
$this->package = selfSignedPackage($this->package_key);
});
afterEach(function () {
if (file_exists($this->package)) {
unlink($this->package);
}
});
// --- the gate ---
test('import_validate_signature returns a strict bool', function () {
// The array return was the defect: any populated result read as a pass.
expect((string) (new ReflectionFunction('import_validate_signature'))->getReturnType())->toBe('bool');
});
test('a package signed with its own key is not trusted', function () {
expect(import_validate_signature($this->package))->toBeFalse();
});
test('a package that names no key is not trusted', function () {
// import_package_get_public_key() substitutes the built in Cacti key when
// the element is absent, so the gate must not ask it for the verdict.
$key = '';
$package = selfSignedPackage($key, false);
try {
expect(import_get_package_info($package)['pubkey'])->toBe('');
expect(import_validate_signature($package))->toBeFalse();
} finally {
unlink($package);
}
});
test('the shipped device packages pass the trust gate', function () {
// Every install/templates Package is signed with the Cacti key and is
// imported by cli/import_package.php on a fresh install. Rejecting them
// breaks the install, so the whole set is checked rather than a sample.
$packages = glob(dirname(__DIR__, 4) . '/install/templates/*.xml.gz');
expect($packages)->not->toBeEmpty();
foreach ($packages as $package) {
expect(import_validate_signature($package))->toBeTrue(basename($package) . ' is not trusted');
}
});
test('a shipped package key differs from the built in key only in trailing whitespace', function () {
// This is what a strict compare of the raw bytes tripped over.
$package = dirname(__DIR__, 4) . '/install/templates/Local_Linux_Machine.xml.gz';
$pubkey = import_get_package_info($package)['pubkey'];
expect($pubkey)->not->toBe(get_public_key_sha256());
expect(trim($pubkey))->toBe(get_public_key_sha256());
});
test('a real import of a self-signed package is refused', function () {
$public_key = '';
expect(import_read_package_data($this->package, $public_key, false))->toBeFalse();
});
test('a preview of an untrusted package is still readable', function () {
// A preview writes neither files nor rows, so it stays available to let an
// operator inspect a Package before deciding to trust its signer.
$public_key = '';
$data = import_read_package_data($this->package, $public_key, true);
expect($data)->toBeArray();
expect($data['files']['file']['name'])->toBe('scripts/planted.php');
});
test('the gate no longer short circuits before it reads the verdict', function () {
expect(importSourceFile('lib/import.php'))
->not->toContain('!import_validate_signature($xmlfile) && !$preview');
});
// --- accepted keys, and why auto-accepting them was the second hole ---
test('a key in package_public_keys makes the same package importable', function () {
global $database_sessions, $database_hostname, $database_port, $database_default;
$session = "$database_hostname:$database_port:$database_default";
$prior = $database_sessions[$session] ?? null;
$conn = new FakeMySQLPDO();
$conn->exec('CREATE TABLE package_public_keys (id INTEGER PRIMARY KEY AUTOINCREMENT, public_key TEXT)');
// import_validate_public_key() stores the pubkey off the Package verbatim,
// so an already accepted row carries the trailing newline.
expect(str_ends_with($this->package_key, "\n"))->toBeTrue();
$insert = $conn->prepare('INSERT INTO package_public_keys (public_key) VALUES (?)');
$insert->execute([$this->package_key]);
$database_sessions[$session] = $conn;
try {
$public_key = '';
expect(import_validate_signature($this->package))->toBeTrue();
expect(import_read_package_data($this->package, $public_key, false))->toBeArray();
} finally {
if ($prior === null) {
unset($database_sessions[$session]);
} else {
$database_sessions[$session] = $prior;
}
}
});
test('a trusted key cannot make a SHA-1 signed package importable', function () {
global $database_sessions, $database_hostname, $database_port, $database_default;
$publicKey = '';
$package = selfSignedPackage($publicKey, true, OPENSSL_ALGO_SHA1);
$session = "$database_hostname:$database_port:$database_default";
$prior = $database_sessions[$session] ?? null;
$conn = new FakeMySQLPDO();
$conn->exec('CREATE TABLE package_public_keys (id INTEGER PRIMARY KEY AUTOINCREMENT, public_key TEXT)');
$insert = $conn->prepare('INSERT INTO package_public_keys (public_key) VALUES (?)');
$insert->execute([$publicKey]);
$database_sessions[$session] = $conn;
try {
// Trust identifies an accepted signer; it does not override the
// cryptographic SHA-256 verification performed while reading.
expect(import_validate_signature($package))->toBeTrue();
$validatedKey = '';
expect(import_read_package_data($package, $validatedKey, false))->toBeFalse();
} finally {
unlink($package);
if ($prior === null) {
unset($database_sessions[$session]);
} else {
$database_sessions[$session] = $prior;
}
}
});
test('trust_signer no longer reaches the import', function () {
expect(importSourceFile('package_import.php'))->not->toContain('trust_signer');
expect(importSourceFile('include/global_settings.php'))->not->toContain('trust_signer');
});
test('a key is force trusted only by the accept action', function () {
$src = importSourceFile('package_import.php');
$accept = strpos($src, 'function package_accept_key(');
expect($accept)->not->toBeFalse();
$offset = 0;
$found = 0;
while (($pos = strpos($src, 'import_validate_public_key($xmlfile, true)', $offset)) !== false) {
expect($pos)->toBeGreaterThan($accept);
$found++;
$offset = $pos + 1;
}
expect($found)->toBeGreaterThan(0);
});