Files
XC_VM/tests/Support/ClusterReference.php
T
Claude 69d706b395 feat(cluster): cluster crypto contract on the panel side (Phase 1)
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
2026-09-25 10:28:53 +00:00

53 lines
2.1 KiB
PHP

<?php
namespace XcVm\Tests\Support;
use XcVm\Core\Cluster\Crypto\Enc;
use XcVm\Core\Cluster\Crypto\PanelSig;
/**
* Reference crypto for the tests: the parts of ADR-002 only `xcvm_core` (MAIN)
* or the Go agent compute in production: licence binding, CK_B, the token
* MAC, the session keys and panel signing with a known seed. It lives under
* tests/ so it never ships; CI rejects any archive that carries it.
*/
final class ClusterReference {
/** @return array<string, mixed> */
public static function vectors(): array {
return json_decode((string) file_get_contents(__DIR__ . '/cluster_vectors.json'), true);
}
public static function binding(string $rJti, string $rKeySha256): string {
return substr(hash('sha256', 'xcvm-lic-v1' . 'wl:' . Enc::lp($rJti) . $rKeySha256, true), 0, 16);
}
public static function ck(string $rPrk, string $rB): string {
return hash_hkdf('sha256', $rPrk, 32, 'xcvm/cluster/v1', $rB);
}
public static function tokenMsg(string $rUuid, int $rSid, int $rGen, string $rNodeSignPub, int $rEpoch, int $rNbf, int $rExp, string $rZ): string {
return 'xcvm-node-token' . "\x01" . Enc::lp($rUuid) . Enc::u64($rSid) . Enc::u64($rGen)
. hash('sha256', $rNodeSignPub, true) . Enc::u64($rEpoch) . Enc::u64($rNbf) . Enc::u64($rExp)
. hash('sha256', $rZ, true);
}
/** @return array{mac_up: string, mac_down: string, enc_up: string, enc_down: string} */
public static function sessionKeys(string $rT): array {
return [
'mac_up' => hash_hmac('sha256', 'xcvm/mac/up', $rT, true),
'mac_down' => hash_hmac('sha256', 'xcvm/mac/down', $rT, true),
'enc_up' => hash_hmac('sha256', 'xcvm/enc/up', $rT, true),
'enc_down' => hash_hmac('sha256', 'xcvm/enc/down', $rT, true),
];
}
public static function panelSign(string $rSeed, string $rTag, string $rPayload): string {
$rPair = sodium_crypto_sign_seed_keypair($rSeed);
return sodium_crypto_sign_detached(PanelSig::input($rTag, $rPayload), sodium_crypto_sign_secretkey($rPair));
}
public static function panelPub(string $rSeed): string {
return sodium_crypto_sign_publickey(sodium_crypto_sign_seed_keypair($rSeed));
}
}