mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-25 11:03:14 +01:00
src/Core/Cluster/Crypto implements the wire formats of the extension's cluster API v1 (xcvm_core ADR-002), in plain PHP with sodium and OpenSSL: - Enc: the lp()/u64 canonical encoding. - Box: XCVM-BOX-v1 session bodies. - Seal: XCVM-SEAL-v1 to an X25519 key; refuses the all-zero shared secret and binds purpose and context. - PanelSig: verifies panel signatures against the closed tag registry. - NodeSig: the node key's own signatures, under a separate domain string so they can never pass as panel records. - Canonical: the panel-owned request/response serialisation. It is the BOX context and the MAC input, with a ±90 s window and header parsing. - Ticket, RelayAuth, FileDigest: relay and file tickets, the child's per-connect relay proof, and owner-signed file digests. - ClusterCrypto: a facade over XC_VM::cluster_*. Refusals throw ClusterRefusedException with the extension's reason code. - ClusterCryptoFactory: fails closed (ClusterUnavailableException) without the extension, outside API 1–1, or without sodium/AES-GCM. No PHP fallback ships. Tests: - The panel matches the extension's shared vectors byte for byte (tests/Support/cluster_vectors.json, copied from xcvm_core): SEAL, BOX, panel signatures, the licence binding and CK_B, and the whole token chain an agent runs. - tests/Support/cluster_canonical_vectors.json freezes the canonical request/response bytes for the Go agent. - Also added: SignTagSeparationTest, ClusterTicketsTest, ClusterCryptoFailClosedTest (including a check that nothing under Core/Cluster signs as the panel or computes tokens) and ClusterCryptoBenchTest. - Timing: 64 KB is ~0.25 ms p99 against the 1 ms budget; 8 MB is ~41 ms here against a 40 ms target, guarded at 2x. The reference crypto that only xcvm_core or the Go agent computes (binding, token MAC, session keys, panel signing with a seed) lives in tests/Support and never ships. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Es8UhyPS8MNjuYTagqfGWi
56 lines
2.5 KiB
PHP
56 lines
2.5 KiB
PHP
<?php
|
||
|
||
use PHPUnit\Framework\TestCase;
|
||
use XcVm\Core\Cluster\Crypto\ClusterCrypto;
|
||
use XcVm\Core\Cluster\Crypto\ClusterCryptoFactory;
|
||
use XcVm\Core\Cluster\Crypto\ClusterUnavailableException;
|
||
|
||
/**
|
||
* No extension, or one outside the API range, means no cluster crypto at all:
|
||
* the factory throws and the node stays legacy. There is no PHP fallback.
|
||
*/
|
||
final class ClusterCryptoFailClosedTest extends TestCase {
|
||
protected function tearDown(): void {
|
||
ClusterCryptoFactory::useProbe(null);
|
||
}
|
||
|
||
/** @return iterable<string, array{0: ?array, 1: string}> */
|
||
public static function refusals(): iterable {
|
||
yield 'no extension' => [null, 'NO_EXTENSION'];
|
||
yield 'no api field' => [['ext_version' => '3.0.0'], 'API_OUT_OF_RANGE'];
|
||
yield 'too old' => [['api' => ClusterCryptoFactory::API_MIN - 1], 'API_OUT_OF_RANGE'];
|
||
yield 'too new' => [['api' => ClusterCryptoFactory::API_MAX + 1], 'API_OUT_OF_RANGE'];
|
||
}
|
||
|
||
/** @dataProvider refusals */
|
||
public function testRefuses(?array $rInfo, string $rReason): void {
|
||
ClusterCryptoFactory::useProbe(fn() => $rInfo);
|
||
$this->assertFalse(ClusterCryptoFactory::available());
|
||
$this->assertSame($rReason, ClusterCryptoFactory::status()['reason']);
|
||
$this->expectException(ClusterUnavailableException::class);
|
||
ClusterCryptoFactory::create();
|
||
}
|
||
|
||
public function testInRange(): void {
|
||
ClusterCryptoFactory::useProbe(fn() => ['api' => ClusterCryptoFactory::API_MAX, 'ext_version' => '3.1.0']);
|
||
$this->assertSame(['available' => true, 'reason' => 'OK', 'api' => 1, 'ext_version' => '3.1.0', 'range' => '1–1'], ClusterCryptoFactory::status());
|
||
$this->assertInstanceOf(ClusterCrypto::class, ClusterCryptoFactory::create());
|
||
}
|
||
|
||
public function testRealProbeWithoutTheExtension(): void {
|
||
if (class_exists('XC_VM', false) && method_exists('XC_VM', 'cluster_session')) {
|
||
$this->markTestSkipped('xcvm_core with the cluster API is loaded here');
|
||
}
|
||
$this->assertSame('NO_EXTENSION', ClusterCryptoFactory::status()['reason']);
|
||
}
|
||
|
||
public function testNoReferenceCryptoShips(): void {
|
||
$rSrc = dirname(__DIR__, 2) . '/src/';
|
||
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($rSrc . 'Core/Cluster', FilesystemIterator::SKIP_DOTS)) as $rFile) {
|
||
$rCode = (string) file_get_contents($rFile->getPathname());
|
||
$this->assertStringNotContainsString('sodium_crypto_sign_seed_keypair', $rCode, $rFile->getPathname() . ' must not sign as the panel');
|
||
$this->assertStringNotContainsString('xcvm-node-token', $rCode, $rFile->getPathname() . ' must not compute tokens');
|
||
}
|
||
}
|
||
}
|