deadbyte-mcp 0.15.48 → 0.15.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.SHA256 +48 -22
- package/PROVENANCE.json +11 -22
- package/README.txt +18 -18
- package/RELEASE.json +368 -378
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +2 -2
- package/bench/r121-epoch-readhead.mjs +54 -0
- package/bench/r124-epoch-predecessor.mjs +53 -0
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/HARNESS2-ADAPTATION-20261020.md +24 -0
- package/docs/R128-RELEASE-CANDIDATE.md +26 -0
- package/package.json +1 -1
- package/scripts/npm-release-r48.mjs +0 -4
- package/scripts/verify-authority-epoch-history.mjs +38 -0
- package/src/activation-continuity-engine-r65.mjs +9 -15
- package/src/agent-eval-r42.mjs +30 -0
- package/src/authority-continuity-r65.mjs +182 -13
- package/src/autonomous-local-supervisor.mjs +77 -47
- package/src/deadbyte-cli.mjs +18 -28
- package/src/remote-live-log.mjs +15 -8
- package/src/remote-log-decode-r60.mjs +65 -0
- package/src/remote-session-guardian.mjs +4 -5
- package/src/version.mjs +1 -1
- package/test/autonomous-local-supervisor.test.mjs +1 -1
- package/test/harness2-eval-context-isolation.test.mjs +90 -0
- package/test/r108-remote-powershell-path.test.mjs +59 -0
- package/test/r110-supervisor-powershell-path.test.mjs +28 -0
- package/test/r111-supervisor-marker-cas.test.mjs +116 -0
- package/test/r112-supervisor-claim-cas.test.mjs +63 -0
- package/test/r113-supervisor-cleanup-terminality.test.mjs +100 -0
- package/test/r114-supervisor-absence-proof.test.mjs +45 -0
- package/test/r115-posix-continuity-entrypoint.test.mjs +17 -0
- package/test/r116-authority-crash-matrix.test.mjs +83 -0
- package/test/r117-concurrent-orphan-recovery.test.mjs +54 -0
- package/test/r118-cross-process-orphan-recovery.test.mjs +99 -0
- package/test/r119-recovery-crash-after-commit.test.mjs +91 -0
- package/test/r120-authority-history-audit.test.mjs +103 -0
- package/test/r121-signed-transition-invariant.test.mjs +132 -0
- package/test/r122-epoch-bounded-read.test.mjs +58 -0
- package/test/r123-recovery-bounded-read.test.mjs +64 -0
- package/test/r124-live-log-integrity.test.mjs +66 -0
- package/test/r124-live-log-tail-boundaries.test.mjs +57 -0
- package/test/r124-signed-predecessor-shape.test.mjs +66 -0
- package/test/r125-restore-binding-types.test.mjs +63 -0
- package/test/r126-restore-policy-envelope.test.mjs +89 -0
- package/test/r127-restore-append-policy-boundary.test.mjs +25 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r65-authority-epoch-store.test.mjs +9 -6
- package/test/r74-prepublish-contract.test.mjs +3 -1
- package/scripts/npm-release-readiness-r108.mjs +0 -134
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {randomBytes} from 'node:crypto';
|
|
4
|
+
import childProcess from 'node:child_process';
|
|
5
|
+
import {syncBuiltinESMExports} from 'node:module';
|
|
6
|
+
import {promisify} from 'node:util';
|
|
7
|
+
import {mkdtemp, readFile, rm} from 'node:fs/promises';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
const moduleUrl=new URL('../src/autonomous-local-supervisor.mjs',import.meta.url);
|
|
12
|
+
|
|
13
|
+
test('CIM false absence cannot authorize takeover of a live marker owner', {skip:process.platform!=='win32'}, async()=>{
|
|
14
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r114-cim-absence-'));
|
|
15
|
+
const markerPath=path.join(root,'owner.json');
|
|
16
|
+
const originalExec=childProcess.execFile;
|
|
17
|
+
try {
|
|
18
|
+
const baseline=await import(`${moduleUrl.href}?baseline=${randomBytes(8).toString('hex')}`);
|
|
19
|
+
await baseline.claimSupervisor({markerPath});
|
|
20
|
+
const before=await readFile(markerPath);
|
|
21
|
+
let deniedLookups=0, falseAbsences=0;
|
|
22
|
+
const mock=function(...args){return originalExec.apply(this,args);};
|
|
23
|
+
mock[promisify.custom]=function(...args){
|
|
24
|
+
const shellArgs=Array.isArray(args[1])?args[1]:[];
|
|
25
|
+
const script=String(shellArgs[shellArgs.indexOf('-Command')+1]??'');
|
|
26
|
+
if(script.includes('Get-Process -Id')) {deniedLookups++;return Promise.reject(new Error('process identity access denied'));}
|
|
27
|
+
if(script.includes('Get-CimInstance Win32_Process')) {falseAbsences++;return Promise.resolve({stdout:'ABSENT\n',stderr:''});}
|
|
28
|
+
return originalExec[promisify.custom].apply(originalExec,args);
|
|
29
|
+
};
|
|
30
|
+
childProcess.execFile=mock;
|
|
31
|
+
syncBuiltinESMExports();
|
|
32
|
+
const module=await import(`${moduleUrl.href}?attack=${randomBytes(8).toString('hex')}`);
|
|
33
|
+
const observed=await module.observeSupervisor({markerPath});
|
|
34
|
+
assert.equal(observed.status,'unverified','a native live handle must outweigh a false CIM null');
|
|
35
|
+
assert.equal(observed.reason,'process_unobservable');
|
|
36
|
+
await assert.rejects(module.takeoverSupervisor({markerPath}),/unverified|unobservable|ownership/i);
|
|
37
|
+
assert.deepEqual(await readFile(markerPath),before,'live owner marker must remain byte-identical');
|
|
38
|
+
assert.ok(deniedLookups>=2);
|
|
39
|
+
assert.ok(falseAbsences>=2);
|
|
40
|
+
} finally {
|
|
41
|
+
childProcess.execFile=originalExec;
|
|
42
|
+
syncBuiltinESMExports();
|
|
43
|
+
await rm(root,{recursive:true,force:true});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp, rm, access} from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {createEpochNativeContinuityEngine} from '../src/activation-continuity-engine-r65.mjs';
|
|
7
|
+
|
|
8
|
+
test('POSIX continuity engine refuses activation authority before any filesystem mutation',
|
|
9
|
+
{skip:process.platform==='win32'},async()=>{
|
|
10
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r115-posix-continuity-'));
|
|
11
|
+
try{
|
|
12
|
+
assert.throws(()=>createEpochNativeContinuityEngine({
|
|
13
|
+
stateRoot:root,predecessorPolicyPaths:{},successorPolicyPaths:async()=>({}),updateId:'r115-posix'
|
|
14
|
+
}),/POSIX.*unsupported|Windows-only/i);
|
|
15
|
+
await assert.rejects(access(path.join(root,'runtime')),{code:'ENOENT'});
|
|
16
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
17
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {generateKeyPairSync, randomBytes, createHash} from 'node:crypto';
|
|
4
|
+
import {mkdtemp, mkdir, writeFile, rm} from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {createAuthorityEpochStore} from '../src/authority-continuity-r65.mjs';
|
|
8
|
+
|
|
9
|
+
const phases = ['event_created', 'event_written', 'event_file_synced',
|
|
10
|
+
'event_directory_barrier', 'head_temp_created', 'head_temp_written',
|
|
11
|
+
'head_temp_file_synced', 'head_renamed', 'head_directory_barrier'];
|
|
12
|
+
const sha256 = value => createHash('sha256').update(value).digest('hex');
|
|
13
|
+
const grant = nonce => Buffer.from(JSON.stringify({instance_nonce:nonce, fixture:'R116 isolated'}));
|
|
14
|
+
|
|
15
|
+
async function fixture(run) {
|
|
16
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r116-epoch-'));
|
|
17
|
+
try {
|
|
18
|
+
const runtimeDir = path.join(root, 'runtime');
|
|
19
|
+
const trust = path.join(root, 'trust');
|
|
20
|
+
await mkdir(runtimeDir); await mkdir(trust);
|
|
21
|
+
const keys = generateKeyPairSync('ed25519');
|
|
22
|
+
await writeFile(path.join(trust, 'ed25519-private.pem'), keys.privateKey.export({type:'pkcs8', format:'pem'}), {flag:'wx'});
|
|
23
|
+
await writeFile(path.join(trust, 'ed25519-public.pem'), keys.publicKey.export({type:'spki', format:'pem'}), {flag:'wx'});
|
|
24
|
+
await run(runtimeDir);
|
|
25
|
+
} finally {
|
|
26
|
+
await rm(root, {recursive:true, force:true});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const phase of phases) {
|
|
31
|
+
test(`R116 interrupted user revoke at ${phase} never verifies the old grant`, () => fixture(async runtimeDir => {
|
|
32
|
+
const oldNonce = randomBytes(32).toString('hex');
|
|
33
|
+
const oldGrant = grant(oldNonce);
|
|
34
|
+
const store = createAuthorityEpochStore({runtimeDir});
|
|
35
|
+
await store.append({plane:'process', action:'grant', grantBytes:oldGrant, grantInstanceNonce:oldNonce});
|
|
36
|
+
assert.equal((await store.verifyGrant({plane:'process', grantBytes:oldGrant, grantInstanceNonce:oldNonce})).trusted, true);
|
|
37
|
+
const interrupted = createAuthorityEpochStore({runtimeDir, onDurabilityStage:at => {
|
|
38
|
+
if (at === phase) throw new Error(`R116_INTERRUPTED:${at}`);
|
|
39
|
+
}});
|
|
40
|
+
await assert.rejects(interrupted.append({plane:'process', action:'user_revoke', grantBytes:oldGrant,
|
|
41
|
+
grantInstanceNonce:oldNonce}), /R116_INTERRUPTED/);
|
|
42
|
+
const fresh = createAuthorityEpochStore({runtimeDir});
|
|
43
|
+
assert.equal((await fresh.verifyGrant({plane:'process', grantBytes:oldGrant,
|
|
44
|
+
grantInstanceNonce:oldNonce})).trusted, false);
|
|
45
|
+
if (phase === 'head_renamed' || phase === 'head_directory_barrier') {
|
|
46
|
+
assert.equal((await fresh.readHead({plane:'process'})).action, 'user_revoke');
|
|
47
|
+
} else {
|
|
48
|
+
await assert.rejects(fresh.readHead({plane:'process'}), /replay or incomplete write/);
|
|
49
|
+
}
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
test(`R116 interrupted transaction restore at ${phase} is observed, never inferred`, () => fixture(async runtimeDir => {
|
|
53
|
+
const oldNonce = randomBytes(32).toString('hex');
|
|
54
|
+
const newNonce = randomBytes(32).toString('hex');
|
|
55
|
+
const oldGrant = grant(oldNonce);
|
|
56
|
+
const newGrant = grant(newNonce);
|
|
57
|
+
const store = createAuthorityEpochStore({runtimeDir});
|
|
58
|
+
const original = await store.append({plane:'process', action:'grant', grantBytes:oldGrant,
|
|
59
|
+
grantInstanceNonce:oldNonce});
|
|
60
|
+
const revoked = await store.append({plane:'process', action:'transaction_revoke', grantBytes:oldGrant,
|
|
61
|
+
grantInstanceNonce:oldNonce, updateId:'r116-isolated', expectedHeadSha256:original.event_sha256});
|
|
62
|
+
assert.equal((await store.verifyGrant({plane:'process', grantBytes:oldGrant,
|
|
63
|
+
grantInstanceNonce:oldNonce})).trusted, false);
|
|
64
|
+
const interrupted = createAuthorityEpochStore({runtimeDir, onDurabilityStage:at => {
|
|
65
|
+
if (at === phase) throw new Error(`R116_INTERRUPTED:${at}`);
|
|
66
|
+
}});
|
|
67
|
+
const restoreBinding = {
|
|
68
|
+
prior_grant_sha256:sha256(oldGrant), prior_grant_instance_nonce:oldNonce,
|
|
69
|
+
predecessor_policy_envelope_sha256:'1'.repeat(64),
|
|
70
|
+
successor_policy_envelope_sha256:'2'.repeat(64),
|
|
71
|
+
candidate_manifest_sha256:'3'.repeat(64), continuity_subject_sha256:'4'.repeat(64)
|
|
72
|
+
};
|
|
73
|
+
await assert.rejects(interrupted.append({plane:'process', action:'transaction_restore', grantBytes:newGrant,
|
|
74
|
+
grantInstanceNonce:newNonce, updateId:'r116-isolated', expectedHeadSha256:revoked.event_sha256,
|
|
75
|
+
restoreBinding}), /R116_INTERRUPTED/);
|
|
76
|
+
const fresh = createAuthorityEpochStore({runtimeDir});
|
|
77
|
+
const observed = await fresh.verifyGrant({plane:'process', grantBytes:newGrant, grantInstanceNonce:newNonce});
|
|
78
|
+
const committed = phase === 'head_renamed' || phase === 'head_directory_barrier';
|
|
79
|
+
assert.equal(observed.trusted, committed);
|
|
80
|
+
if (committed) assert.equal((await fresh.readHead({plane:'process'})).action, 'transaction_restore');
|
|
81
|
+
else await assert.rejects(fresh.readHead({plane:'process'}), /replay or incomplete write/);
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {generateKeyPairSync, randomBytes} from 'node:crypto';
|
|
4
|
+
import {mkdtemp, mkdir, readFile, writeFile, rm} from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {pathToFileURL} from 'node:url';
|
|
8
|
+
|
|
9
|
+
// A writable development checkout cannot load the privileged Windows addon.
|
|
10
|
+
// The optional test-only override must point to a protected, byte-identical
|
|
11
|
+
// source installation; callers verify that hash before using this mode.
|
|
12
|
+
const epochModule = process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE
|
|
13
|
+
? pathToFileURL(process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE).href
|
|
14
|
+
: new URL('../src/authority-continuity-r65.mjs', import.meta.url).href;
|
|
15
|
+
const {createAuthorityEpochStore} = await import(epochModule);
|
|
16
|
+
|
|
17
|
+
test('R117 two independent recoverers commit one orphaned revoke at most once', async () => {
|
|
18
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r117-recovery-'));
|
|
19
|
+
try {
|
|
20
|
+
const runtimeDir = path.join(root, 'runtime');
|
|
21
|
+
const trust = path.join(root, 'trust');
|
|
22
|
+
await mkdir(runtimeDir); await mkdir(trust);
|
|
23
|
+
const keys = generateKeyPairSync('ed25519');
|
|
24
|
+
await writeFile(path.join(trust, 'ed25519-private.pem'), keys.privateKey.export({type:'pkcs8', format:'pem'}), {flag:'wx'});
|
|
25
|
+
await writeFile(path.join(trust, 'ed25519-public.pem'), keys.publicKey.export({type:'spki', format:'pem'}), {flag:'wx'});
|
|
26
|
+
const nonce = randomBytes(32).toString('hex');
|
|
27
|
+
const bytes = Buffer.from(JSON.stringify({instance_nonce:nonce, fixture:'R117 isolated'}));
|
|
28
|
+
const writer = createAuthorityEpochStore({runtimeDir});
|
|
29
|
+
const granted = await writer.append({plane:'process', action:'grant', grantBytes:bytes, grantInstanceNonce:nonce});
|
|
30
|
+
const headPath = path.join(runtimeDir, 'authority-epochs', 'process', 'head.json');
|
|
31
|
+
const predecessorBytes = await readFile(headPath);
|
|
32
|
+
const orphan = await writer.append({plane:'process', action:'user_revoke', grantBytes:bytes, grantInstanceNonce:nonce});
|
|
33
|
+
// Simulate interruption before the new head becomes visible. The signed
|
|
34
|
+
// revoke event remains, while the exact prior signed head is restored.
|
|
35
|
+
await writeFile(headPath, predecessorBytes);
|
|
36
|
+
const observed = createAuthorityEpochStore({runtimeDir});
|
|
37
|
+
assert.equal((await observed.verifyGrant({plane:'process', grantBytes:bytes, grantInstanceNonce:nonce})).trusted, false);
|
|
38
|
+
const recover = () => createAuthorityEpochStore({runtimeDir}).reconcileOrphanedRevocation({
|
|
39
|
+
plane:'process', expectedHeadSha256:granted.event_sha256,
|
|
40
|
+
expectedOrphanSha256:orphan.event_sha256, operatorConfirmed:true
|
|
41
|
+
});
|
|
42
|
+
const outcomes = await Promise.allSettled([recover(), recover()]);
|
|
43
|
+
assert.equal(outcomes.filter(item => item.status === 'fulfilled').length, 1,
|
|
44
|
+
'two independent attempts cannot both claim the same recovery');
|
|
45
|
+
assert.equal(outcomes.filter(item => item.status === 'rejected').length, 1);
|
|
46
|
+
assert.equal(outcomes.find(item => item.status === 'fulfilled').value.status, 'REVOKED_ONLY_RECONCILED');
|
|
47
|
+
const finalStore = createAuthorityEpochStore({runtimeDir});
|
|
48
|
+
assert.equal((await finalStore.readHead({plane:'process'})).event_sha256, orphan.event_sha256);
|
|
49
|
+
assert.equal((await finalStore.verifyGrant({plane:'process', grantBytes:bytes,
|
|
50
|
+
grantInstanceNonce:nonce})).trusted, false);
|
|
51
|
+
} finally {
|
|
52
|
+
await rm(root, {recursive:true, force:true});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {generateKeyPairSync, randomBytes} from 'node:crypto';
|
|
4
|
+
import {spawn} from 'node:child_process';
|
|
5
|
+
import {mkdtemp, mkdir, readFile, writeFile, rm} from 'node:fs/promises';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import {pathToFileURL} from 'node:url';
|
|
9
|
+
|
|
10
|
+
const moduleUrl = process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE
|
|
11
|
+
? pathToFileURL(process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE).href
|
|
12
|
+
: new URL('../src/authority-continuity-r65.mjs', import.meta.url).href;
|
|
13
|
+
const {createAuthorityEpochStore} = await import(moduleUrl);
|
|
14
|
+
|
|
15
|
+
function recoverer(runtimeDir, expectedHeadSha256, expectedOrphanSha256) {
|
|
16
|
+
// Each invocation is a separate OS process using the real native lock.
|
|
17
|
+
// IPC is only a starting barrier and result channel; it cannot bypass the
|
|
18
|
+
// authority store's signature, file identity, or ownership checks.
|
|
19
|
+
const code = `
|
|
20
|
+
import {createAuthorityEpochStore} from ${JSON.stringify(moduleUrl)};
|
|
21
|
+
process.once('message', async () => {
|
|
22
|
+
let result;
|
|
23
|
+
try {
|
|
24
|
+
const value = await createAuthorityEpochStore({runtimeDir:${JSON.stringify(runtimeDir)}})
|
|
25
|
+
.reconcileOrphanedRevocation({plane:'process',
|
|
26
|
+
expectedHeadSha256:${JSON.stringify(expectedHeadSha256)},
|
|
27
|
+
expectedOrphanSha256:${JSON.stringify(expectedOrphanSha256)},operatorConfirmed:true});
|
|
28
|
+
result = {kind:'RESULT',status:'FULFILLED',value:value.status};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
result = {kind:'RESULT',status:'REJECTED',reason:String(error?.message??error)};
|
|
31
|
+
}
|
|
32
|
+
process.send(result, () => process.disconnect());
|
|
33
|
+
});
|
|
34
|
+
process.send({kind:'READY'});
|
|
35
|
+
`;
|
|
36
|
+
const child = spawn(process.execPath, ['--input-type=module', '-e', code], {
|
|
37
|
+
env:process.env, stdio:['ignore', 'ignore', 'pipe', 'ipc'], windowsHide:true
|
|
38
|
+
});
|
|
39
|
+
let stderr = '';
|
|
40
|
+
child.stderr.on('data', chunk => { stderr = (stderr + chunk.toString()).slice(-2048); });
|
|
41
|
+
let readyResolve, readyReject, resultResolve, resultReject;
|
|
42
|
+
const ready = new Promise((resolve, reject) => {readyResolve=resolve; readyReject=reject;});
|
|
43
|
+
const result = new Promise((resolve, reject) => {resultResolve=resolve; resultReject=reject;});
|
|
44
|
+
let readySeen = false, resultSeen = false;
|
|
45
|
+
child.on('message', message => {
|
|
46
|
+
if (message?.kind === 'READY') {readySeen=true; readyResolve();}
|
|
47
|
+
if (message?.kind === 'RESULT') {resultSeen=true; resultResolve(message);}
|
|
48
|
+
});
|
|
49
|
+
child.once('error', error => {readyReject(error); resultReject(error);});
|
|
50
|
+
child.once('exit', (code, signal) => {
|
|
51
|
+
const error = new Error(`R118 child exited before result code=${code} signal=${signal} stderr=${stderr}`);
|
|
52
|
+
if (!readySeen) readyReject(error);
|
|
53
|
+
if (!resultSeen) resultReject(error);
|
|
54
|
+
});
|
|
55
|
+
return {child, ready, result};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test('R118 two OS processes cannot both commit one signed orphaned user revoke',
|
|
59
|
+
{timeout:30000}, async () => {
|
|
60
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r118-cross-process-'));
|
|
61
|
+
const children = [];
|
|
62
|
+
try {
|
|
63
|
+
const runtimeDir = path.join(root, 'runtime');
|
|
64
|
+
const trust = path.join(root, 'trust');
|
|
65
|
+
await mkdir(runtimeDir); await mkdir(trust);
|
|
66
|
+
const keys = generateKeyPairSync('ed25519');
|
|
67
|
+
await writeFile(path.join(trust, 'ed25519-private.pem'),
|
|
68
|
+
keys.privateKey.export({type:'pkcs8', format:'pem'}), {flag:'wx'});
|
|
69
|
+
await writeFile(path.join(trust, 'ed25519-public.pem'),
|
|
70
|
+
keys.publicKey.export({type:'spki', format:'pem'}), {flag:'wx'});
|
|
71
|
+
const nonce = randomBytes(32).toString('hex');
|
|
72
|
+
const grantBytes = Buffer.from(JSON.stringify({instance_nonce:nonce, fixture:'R118 isolated'}));
|
|
73
|
+
const store = createAuthorityEpochStore({runtimeDir});
|
|
74
|
+
const granted = await store.append({plane:'process', action:'grant',
|
|
75
|
+
grantBytes, grantInstanceNonce:nonce});
|
|
76
|
+
const headPath = path.join(runtimeDir, 'authority-epochs', 'process', 'head.json');
|
|
77
|
+
const predecessorBytes = await readFile(headPath);
|
|
78
|
+
const orphan = await store.append({plane:'process', action:'user_revoke',
|
|
79
|
+
grantBytes, grantInstanceNonce:nonce});
|
|
80
|
+
await writeFile(headPath, predecessorBytes);
|
|
81
|
+
assert.equal((await store.verifyGrant({plane:'process', grantBytes,
|
|
82
|
+
grantInstanceNonce:nonce})).trusted, false);
|
|
83
|
+
children.push(recoverer(runtimeDir, granted.event_sha256, orphan.event_sha256));
|
|
84
|
+
children.push(recoverer(runtimeDir, granted.event_sha256, orphan.event_sha256));
|
|
85
|
+
await Promise.all(children.map(item => item.ready));
|
|
86
|
+
children.forEach(item => item.child.send({kind:'GO'}));
|
|
87
|
+
const results = await Promise.all(children.map(item => item.result));
|
|
88
|
+
assert.equal(results.filter(item => item.status === 'FULFILLED').length, 1, JSON.stringify(results));
|
|
89
|
+
assert.equal(results.filter(item => item.status === 'REJECTED').length, 1, JSON.stringify(results));
|
|
90
|
+
assert.equal(results.find(item => item.status === 'FULFILLED').value, 'REVOKED_ONLY_RECONCILED');
|
|
91
|
+
const fresh = createAuthorityEpochStore({runtimeDir});
|
|
92
|
+
assert.equal((await fresh.readHead({plane:'process'})).event_sha256, orphan.event_sha256);
|
|
93
|
+
assert.equal((await fresh.verifyGrant({plane:'process', grantBytes,
|
|
94
|
+
grantInstanceNonce:nonce})).trusted, false);
|
|
95
|
+
} finally {
|
|
96
|
+
for (const item of children) if (item.child.exitCode === null) item.child.kill();
|
|
97
|
+
await rm(root, {recursive:true, force:true, maxRetries:8, retryDelay:100});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {generateKeyPairSync, randomBytes} from 'node:crypto';
|
|
4
|
+
import {spawn} from 'node:child_process';
|
|
5
|
+
import {mkdtemp, mkdir, readFile, writeFile, access, rm} from 'node:fs/promises';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import {pathToFileURL} from 'node:url';
|
|
9
|
+
|
|
10
|
+
const moduleUrl = process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE
|
|
11
|
+
? pathToFileURL(process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE).href
|
|
12
|
+
: new URL('../src/authority-continuity-r65.mjs', import.meta.url).href;
|
|
13
|
+
const {createAuthorityEpochStore} = await import(moduleUrl);
|
|
14
|
+
const exists = file => access(file).then(() => true, () => false);
|
|
15
|
+
|
|
16
|
+
function crashRecovery(runtimeDir, expectedHeadSha256, expectedOrphanSha256) {
|
|
17
|
+
const code = `
|
|
18
|
+
import {createAuthorityEpochStore} from ${JSON.stringify(moduleUrl)};
|
|
19
|
+
const store = createAuthorityEpochStore({runtimeDir:${JSON.stringify(runtimeDir)},
|
|
20
|
+
onDurabilityStage:stage => {
|
|
21
|
+
if (stage === 'head_renamed') process.kill(process.pid, 'SIGKILL');
|
|
22
|
+
}});
|
|
23
|
+
await store.reconcileOrphanedRevocation({plane:'process',
|
|
24
|
+
expectedHeadSha256:${JSON.stringify(expectedHeadSha256)},
|
|
25
|
+
expectedOrphanSha256:${JSON.stringify(expectedOrphanSha256)},
|
|
26
|
+
operatorConfirmed:true});
|
|
27
|
+
`;
|
|
28
|
+
const child = spawn(process.execPath, ['--input-type=module', '-e', code], {
|
|
29
|
+
env:{...process.env, NODE_TEST_CONTEXT:'child-v8'},
|
|
30
|
+
stdio:['ignore', 'ignore', 'pipe'], windowsHide:true
|
|
31
|
+
});
|
|
32
|
+
let stderr = '';
|
|
33
|
+
child.stderr.on('data', bytes => {stderr = (stderr + bytes.toString()).slice(-1000);});
|
|
34
|
+
const done = new Promise((resolve, reject) => {
|
|
35
|
+
const timer = setTimeout(() => {child.kill(); reject(new Error('R119 recovery crash child timeout'));}, 12000);
|
|
36
|
+
child.once('error', error => {clearTimeout(timer); reject(error);});
|
|
37
|
+
child.once('exit', (code, signal) => {clearTimeout(timer); resolve({code, signal, stderr});});
|
|
38
|
+
});
|
|
39
|
+
return {child, done};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('R119 crash after recovered head rename leaves revoke committed and dead lock recoverable',
|
|
43
|
+
{skip:process.platform !== 'win32', timeout:30000}, async () => {
|
|
44
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r119-recovery-crash-'));
|
|
45
|
+
let worker;
|
|
46
|
+
try {
|
|
47
|
+
const runtimeDir = path.join(root, 'runtime');
|
|
48
|
+
const trust = path.join(root, 'trust');
|
|
49
|
+
await mkdir(runtimeDir); await mkdir(trust);
|
|
50
|
+
const keys = generateKeyPairSync('ed25519');
|
|
51
|
+
await writeFile(path.join(trust, 'ed25519-private.pem'),
|
|
52
|
+
keys.privateKey.export({type:'pkcs8', format:'pem'}), {flag:'wx'});
|
|
53
|
+
await writeFile(path.join(trust, 'ed25519-public.pem'),
|
|
54
|
+
keys.publicKey.export({type:'spki', format:'pem'}), {flag:'wx'});
|
|
55
|
+
const nonce = randomBytes(32).toString('hex');
|
|
56
|
+
const grantBytes = Buffer.from(JSON.stringify({instance_nonce:nonce, fixture:'R119 isolated'}));
|
|
57
|
+
const store = createAuthorityEpochStore({runtimeDir});
|
|
58
|
+
const prior = await store.append({plane:'process', action:'grant',
|
|
59
|
+
grantBytes, grantInstanceNonce:nonce});
|
|
60
|
+
const epochDir = path.join(runtimeDir, 'authority-epochs', 'process');
|
|
61
|
+
const headPath = path.join(epochDir, 'head.json');
|
|
62
|
+
const priorHeadBytes = await readFile(headPath);
|
|
63
|
+
const orphan = await store.append({plane:'process', action:'user_revoke',
|
|
64
|
+
grantBytes, grantInstanceNonce:nonce});
|
|
65
|
+
await writeFile(headPath, priorHeadBytes);
|
|
66
|
+
assert.equal((await store.verifyGrant({plane:'process', grantBytes,
|
|
67
|
+
grantInstanceNonce:nonce})).trusted, false);
|
|
68
|
+
|
|
69
|
+
worker = crashRecovery(runtimeDir, prior.event_sha256, orphan.event_sha256);
|
|
70
|
+
const outcome = await worker.done;
|
|
71
|
+
assert.notEqual(outcome.code, 0, `recovery worker must die at head rename: ${outcome.stderr}`);
|
|
72
|
+
assert.equal((await store.readHead({plane:'process'})).event_sha256, orphan.event_sha256);
|
|
73
|
+
assert.equal((await store.verifyGrant({plane:'process', grantBytes,
|
|
74
|
+
grantInstanceNonce:nonce})).trusted, false);
|
|
75
|
+
const lockPath = path.join(epochDir, '.lock');
|
|
76
|
+
assert.equal(await exists(lockPath), true, 'dead recovery owner leaves a lock identity');
|
|
77
|
+
await assert.rejects(store.reconcileOrphanedRevocation({plane:'process',
|
|
78
|
+
expectedHeadSha256:prior.event_sha256,
|
|
79
|
+
expectedOrphanSha256:orphan.event_sha256, operatorConfirmed:true}),
|
|
80
|
+
/exactly one orphan event/);
|
|
81
|
+
const next = await store.append({plane:'process', action:'user_revoke',
|
|
82
|
+
grantBytes, grantInstanceNonce:nonce});
|
|
83
|
+
assert.equal(next.seq, orphan.seq + 1);
|
|
84
|
+
assert.equal((await store.verifyGrant({plane:'process', grantBytes,
|
|
85
|
+
grantInstanceNonce:nonce})).trusted, false);
|
|
86
|
+
assert.equal(await exists(lockPath), false, 'verified dead lock is reclaimed and released');
|
|
87
|
+
} finally {
|
|
88
|
+
if (worker?.child.exitCode === null) worker.child.kill();
|
|
89
|
+
await rm(root, {recursive:true, force:true, maxRetries:8, retryDelay:100});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {generateKeyPairSync, randomBytes} from 'node:crypto';
|
|
4
|
+
import {spawnSync} from 'node:child_process';
|
|
5
|
+
import {mkdtemp, mkdir, readdir, readFile, writeFile, unlink, rm} from 'node:fs/promises';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
9
|
+
import {createAuthorityEpochStore as createCandidateStore} from '../src/authority-continuity-r65.mjs';
|
|
10
|
+
|
|
11
|
+
const protectedModule = process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE;
|
|
12
|
+
const createWriterStore = protectedModule
|
|
13
|
+
? (await import(pathToFileURL(protectedModule).href)).createAuthorityEpochStore
|
|
14
|
+
: createCandidateStore;
|
|
15
|
+
const repo = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
16
|
+
const auditScript = path.join(repo, 'scripts', 'verify-authority-epoch-history.mjs');
|
|
17
|
+
|
|
18
|
+
async function threeEvents(run) {
|
|
19
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r120-history-'));
|
|
20
|
+
try {
|
|
21
|
+
const runtimeDir = path.join(root, 'runtime');
|
|
22
|
+
const trust = path.join(root, 'trust');
|
|
23
|
+
await mkdir(runtimeDir); await mkdir(trust);
|
|
24
|
+
const keys = generateKeyPairSync('ed25519');
|
|
25
|
+
await writeFile(path.join(trust, 'ed25519-private.pem'),
|
|
26
|
+
keys.privateKey.export({type:'pkcs8', format:'pem'}), {flag:'wx'});
|
|
27
|
+
await writeFile(path.join(trust, 'ed25519-public.pem'),
|
|
28
|
+
keys.publicKey.export({type:'spki', format:'pem'}), {flag:'wx'});
|
|
29
|
+
const writer = createWriterStore({runtimeDir});
|
|
30
|
+
const firstNonce = randomBytes(32).toString('hex');
|
|
31
|
+
const latestNonce = randomBytes(32).toString('hex');
|
|
32
|
+
const firstBytes = Buffer.from(JSON.stringify({instance_nonce:firstNonce, fixture:'R120 first'}));
|
|
33
|
+
const latestBytes = Buffer.from(JSON.stringify({instance_nonce:latestNonce, fixture:'R120 latest'}));
|
|
34
|
+
const first = await writer.append({plane:'process', action:'grant',
|
|
35
|
+
grantBytes:firstBytes, grantInstanceNonce:firstNonce});
|
|
36
|
+
await writer.append({plane:'process', action:'user_revoke',
|
|
37
|
+
grantBytes:firstBytes, grantInstanceNonce:firstNonce});
|
|
38
|
+
const latest = await writer.append({plane:'process', action:'grant',
|
|
39
|
+
grantBytes:latestBytes, grantInstanceNonce:latestNonce});
|
|
40
|
+
const auditor = createCandidateStore({runtimeDir});
|
|
41
|
+
await run({runtimeDir, auditor, first, latest, latestBytes, latestNonce});
|
|
42
|
+
} finally {
|
|
43
|
+
await rm(root, {recursive:true, force:true, maxRetries:8, retryDelay:100});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('R120 independent audit verifies the complete signed history, not just newest two events',
|
|
48
|
+
() => threeEvents(async ({auditor, latest}) => {
|
|
49
|
+
const result = await auditor.auditHistory({plane:'process'});
|
|
50
|
+
assert.equal(result.status, 'PASS');
|
|
51
|
+
assert.equal(result.seq, 3);
|
|
52
|
+
assert.equal(result.event_sha256, latest.event_sha256);
|
|
53
|
+
assert.equal(result.verified_events, 3);
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
test('R120 oldest event corruption is rejected even when current grant head still verifies',
|
|
57
|
+
() => threeEvents(async ({runtimeDir, auditor, first, latestBytes, latestNonce}) => {
|
|
58
|
+
const events = path.join(runtimeDir, 'authority-epochs', 'process', 'events');
|
|
59
|
+
const names = await readdir(events);
|
|
60
|
+
const firstName = names.find(name => name.startsWith('0000000000000001-'));
|
|
61
|
+
assert.ok(firstName);
|
|
62
|
+
const firstPath = path.join(events, firstName);
|
|
63
|
+
const original = await readFile(firstPath);
|
|
64
|
+
await writeFile(firstPath, Buffer.from('{}'));
|
|
65
|
+
assert.equal((await auditor.verifyGrant({plane:'process', grantBytes:latestBytes,
|
|
66
|
+
grantInstanceNonce:latestNonce})).trusted, true,
|
|
67
|
+
'hot-path head and immediate predecessor do not inspect oldest event');
|
|
68
|
+
await assert.rejects(auditor.auditHistory({plane:'process'}), /history.*hash mismatch/);
|
|
69
|
+
assert.notEqual(first.event_sha256, latestNonce);
|
|
70
|
+
assert.notDeepEqual(await readFile(firstPath), original);
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
test('R120 CLI audit binds the exact expected head and rejects a wrong identity',
|
|
74
|
+
() => threeEvents(async ({runtimeDir, latest}) => {
|
|
75
|
+
const base = [auditScript, '--runtime-dir', runtimeDir, '--plane', 'process',
|
|
76
|
+
'--expected-head', latest.event_sha256];
|
|
77
|
+
const correct = spawnSync(process.execPath, base, {cwd:repo, encoding:'utf8', timeout:10000});
|
|
78
|
+
assert.equal(correct.status, 0, correct.stderr);
|
|
79
|
+
const result = JSON.parse(correct.stdout);
|
|
80
|
+
assert.equal(result.status, 'PASS');
|
|
81
|
+
assert.equal(result.event_sha256, latest.event_sha256);
|
|
82
|
+
const wrong = spawnSync(process.execPath,
|
|
83
|
+
[auditScript, '--runtime-dir', runtimeDir, '--plane', 'process',
|
|
84
|
+
'--expected-head', 'f'.repeat(64)], {cwd:repo, encoding:'utf8', timeout:10000});
|
|
85
|
+
assert.notEqual(wrong.status, 0);
|
|
86
|
+
assert.match(wrong.stderr, /expected head mismatch/);
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
test('R120 missing oldest event and duplicate signed-looking history fail the audit',
|
|
90
|
+
() => threeEvents(async ({runtimeDir, auditor, latestBytes, latestNonce}) => {
|
|
91
|
+
const events = path.join(runtimeDir, 'authority-epochs', 'process', 'events');
|
|
92
|
+
const names = await readdir(events);
|
|
93
|
+
const oldest = names.find(name => name.startsWith('0000000000000001-'));
|
|
94
|
+
assert.ok(oldest);
|
|
95
|
+
const original = await readFile(path.join(events, oldest));
|
|
96
|
+
await unlink(path.join(events, oldest));
|
|
97
|
+
assert.equal((await auditor.verifyGrant({plane:'process', grantBytes:latestBytes,
|
|
98
|
+
grantInstanceNonce:latestNonce})).trusted, true);
|
|
99
|
+
await assert.rejects(auditor.auditHistory({plane:'process'}), /history event count mismatch/);
|
|
100
|
+
await writeFile(path.join(events, oldest), original, {flag:'wx'});
|
|
101
|
+
await writeFile(path.join(events, `0000000000000001-${'f'.repeat(64)}.json`), original, {flag:'wx'});
|
|
102
|
+
await assert.rejects(auditor.auditHistory({plane:'process'}), /history event count mismatch/);
|
|
103
|
+
}));
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createHash, generateKeyPairSync, randomBytes, sign as cryptoSign} from 'node:crypto';
|
|
4
|
+
import {mkdtemp, mkdir, readFile, writeFile, rm} from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {pathToFileURL} from 'node:url';
|
|
8
|
+
import {canonicalJson} from '../src/canonical-json.mjs';
|
|
9
|
+
import {createAuthorityEpochStore as createCandidateStore} from '../src/authority-continuity-r65.mjs';
|
|
10
|
+
|
|
11
|
+
const protectedModule = process.env.DEADBYTE_TEST_PROTECTED_EPOCH_MODULE;
|
|
12
|
+
const createWriterStore = protectedModule
|
|
13
|
+
? (await import(pathToFileURL(protectedModule).href)).createAuthorityEpochStore
|
|
14
|
+
: createCandidateStore;
|
|
15
|
+
const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
16
|
+
const domain = Buffer.from('DEADBYTE-AUTHORITY-DURABLE-EPOCH-R65-V1\0', 'utf8');
|
|
17
|
+
|
|
18
|
+
test('R121 correctly signed restore after user revoke is still an invalid authority transition',
|
|
19
|
+
async () => {
|
|
20
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r121-transition-'));
|
|
21
|
+
try {
|
|
22
|
+
const runtimeDir = path.join(root, 'runtime');
|
|
23
|
+
const trust = path.join(root, 'trust');
|
|
24
|
+
await mkdir(runtimeDir); await mkdir(trust);
|
|
25
|
+
const keys = generateKeyPairSync('ed25519');
|
|
26
|
+
await writeFile(path.join(trust, 'ed25519-private.pem'),
|
|
27
|
+
keys.privateKey.export({type:'pkcs8', format:'pem'}), {flag:'wx'});
|
|
28
|
+
await writeFile(path.join(trust, 'ed25519-public.pem'),
|
|
29
|
+
keys.publicKey.export({type:'spki', format:'pem'}), {flag:'wx'});
|
|
30
|
+
const firstNonce = randomBytes(32).toString('hex');
|
|
31
|
+
const nextNonce = randomBytes(32).toString('hex');
|
|
32
|
+
const oldGrant = Buffer.from(JSON.stringify({instance_nonce:firstNonce, fixture:'R121 old'}));
|
|
33
|
+
const nextGrant = Buffer.from(JSON.stringify({instance_nonce:nextNonce, fixture:'R121 next'}));
|
|
34
|
+
const writer = createWriterStore({runtimeDir});
|
|
35
|
+
await writer.append({plane:'process', action:'grant', grantBytes:oldGrant,
|
|
36
|
+
grantInstanceNonce:firstNonce});
|
|
37
|
+
const revoked = await writer.append({plane:'process', action:'user_revoke',
|
|
38
|
+
grantBytes:oldGrant, grantInstanceNonce:firstNonce});
|
|
39
|
+
const dir = path.join(runtimeDir, 'authority-epochs', 'process');
|
|
40
|
+
const restoreBinding = {
|
|
41
|
+
prior_grant_sha256:sha256(oldGrant), prior_grant_instance_nonce:firstNonce,
|
|
42
|
+
predecessor_policy_envelope_sha256:'1'.repeat(64),
|
|
43
|
+
successor_policy_envelope_sha256:'2'.repeat(64),
|
|
44
|
+
candidate_manifest_sha256:'3'.repeat(64),
|
|
45
|
+
continuity_subject_sha256:'4'.repeat(64)
|
|
46
|
+
};
|
|
47
|
+
const body = {
|
|
48
|
+
schema:'deadbyte.authority-epoch-event.r65.v1', plane:'process', seq:3,
|
|
49
|
+
action:'transaction_restore', update_id:'forged-restore',
|
|
50
|
+
grant_sha256:sha256(nextGrant), grant_instance_nonce:nextNonce,
|
|
51
|
+
restore_binding:restoreBinding, previous_event_sha256:revoked.event_sha256,
|
|
52
|
+
recorded_at_utc:new Date().toISOString()
|
|
53
|
+
};
|
|
54
|
+
const keyId = sha256(keys.publicKey.export({format:'der', type:'spki'}));
|
|
55
|
+
const signature = cryptoSign(null,
|
|
56
|
+
Buffer.concat([domain, Buffer.from(canonicalJson(body), 'utf8')]), keys.privateKey);
|
|
57
|
+
const event = {...body, attestation:{algorithm:'ed25519',
|
|
58
|
+
domain:'DEADBYTE-AUTHORITY-DURABLE-EPOCH-R65-V1', key_id:keyId,
|
|
59
|
+
signature_base64:signature.toString('base64')}};
|
|
60
|
+
const eventBytes = Buffer.from(canonicalJson(event), 'utf8');
|
|
61
|
+
const eventSha = sha256(eventBytes);
|
|
62
|
+
const eventFile = `${String(3).padStart(16, '0')}-${eventSha}.json`;
|
|
63
|
+
await writeFile(path.join(dir, 'events', eventFile), eventBytes, {flag:'wx'});
|
|
64
|
+
const head = {
|
|
65
|
+
schema:'deadbyte.authority-epoch-head.r65.v1', plane:'process', seq:3,
|
|
66
|
+
action:body.action, update_id:body.update_id,
|
|
67
|
+
grant_sha256:body.grant_sha256, grant_instance_nonce:body.grant_instance_nonce,
|
|
68
|
+
restore_binding:restoreBinding, previous_event_sha256:revoked.event_sha256,
|
|
69
|
+
event_sha256:eventSha, event_file:eventFile
|
|
70
|
+
};
|
|
71
|
+
await writeFile(path.join(dir, 'head.json'), Buffer.from(canonicalJson(head), 'utf8'));
|
|
72
|
+
const candidate = createCandidateStore({runtimeDir});
|
|
73
|
+
assert.equal((await candidate.verifyGrant({plane:'process', grantBytes:nextGrant,
|
|
74
|
+
grantInstanceNonce:nextNonce})).trusted, false);
|
|
75
|
+
await assert.rejects(candidate.readHead({plane:'process'}), /authority epoch transition invalid/);
|
|
76
|
+
await assert.rejects(candidate.auditHistory({plane:'process'}), /authority epoch transition invalid/);
|
|
77
|
+
assert.equal((await readFile(path.join(dir, 'events', eventFile))).equals(eventBytes), true);
|
|
78
|
+
|
|
79
|
+
// An older source can append a fresh grant over this forged head because
|
|
80
|
+
// its immediate-predecessor check lacks transition semantics. The new
|
|
81
|
+
// hot path checks the newest pair, while explicit audit must detect the
|
|
82
|
+
// invalid transition that is now deeper in history.
|
|
83
|
+
const finalNonce = randomBytes(32).toString('hex');
|
|
84
|
+
const finalGrant = Buffer.from(JSON.stringify({instance_nonce:finalNonce, fixture:'R121 final'}));
|
|
85
|
+
const finalHead = await writer.append({plane:'process', action:'grant',
|
|
86
|
+
grantBytes:finalGrant, grantInstanceNonce:finalNonce});
|
|
87
|
+
assert.equal((await candidate.readHead({plane:'process'})).event_sha256, finalHead.event_sha256);
|
|
88
|
+
assert.equal((await candidate.verifyGrant({plane:'process', grantBytes:finalGrant,
|
|
89
|
+
grantInstanceNonce:finalNonce})).trusted, true);
|
|
90
|
+
await assert.rejects(candidate.auditHistory({plane:'process'}), /authority epoch transition invalid/);
|
|
91
|
+
} finally {
|
|
92
|
+
await rm(root, {recursive:true, force:true, maxRetries:8, retryDelay:100});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('R121 valid grant → transaction revoke → transaction restore remains trusted', async () => {
|
|
97
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r121-valid-restore-'));
|
|
98
|
+
try {
|
|
99
|
+
const runtimeDir = path.join(root, 'runtime');
|
|
100
|
+
const trust = path.join(root, 'trust');
|
|
101
|
+
await mkdir(runtimeDir); await mkdir(trust);
|
|
102
|
+
const keys = generateKeyPairSync('ed25519');
|
|
103
|
+
await writeFile(path.join(trust, 'ed25519-private.pem'),
|
|
104
|
+
keys.privateKey.export({type:'pkcs8', format:'pem'}), {flag:'wx'});
|
|
105
|
+
await writeFile(path.join(trust, 'ed25519-public.pem'),
|
|
106
|
+
keys.publicKey.export({type:'spki', format:'pem'}), {flag:'wx'});
|
|
107
|
+
const firstNonce = randomBytes(32).toString('hex');
|
|
108
|
+
const nextNonce = randomBytes(32).toString('hex');
|
|
109
|
+
const oldGrant = Buffer.from(JSON.stringify({instance_nonce:firstNonce, fixture:'R121 valid old'}));
|
|
110
|
+
const nextGrant = Buffer.from(JSON.stringify({instance_nonce:nextNonce, fixture:'R121 valid next'}));
|
|
111
|
+
const writer = createWriterStore({runtimeDir});
|
|
112
|
+
const granted = await writer.append({plane:'process', action:'grant',
|
|
113
|
+
grantBytes:oldGrant, grantInstanceNonce:firstNonce});
|
|
114
|
+
const revoked = await writer.append({plane:'process', action:'transaction_revoke',
|
|
115
|
+
grantBytes:oldGrant, grantInstanceNonce:firstNonce, updateId:'r121-valid',
|
|
116
|
+
expectedHeadSha256:granted.event_sha256});
|
|
117
|
+
const restored = await writer.append({plane:'process', action:'transaction_restore',
|
|
118
|
+
grantBytes:nextGrant, grantInstanceNonce:nextNonce, updateId:'r121-valid',
|
|
119
|
+
expectedHeadSha256:revoked.event_sha256,
|
|
120
|
+
restoreBinding:{prior_grant_sha256:sha256(oldGrant), prior_grant_instance_nonce:firstNonce,
|
|
121
|
+
predecessor_policy_envelope_sha256:'1'.repeat(64),
|
|
122
|
+
successor_policy_envelope_sha256:'2'.repeat(64),
|
|
123
|
+
candidate_manifest_sha256:'3'.repeat(64), continuity_subject_sha256:'4'.repeat(64)}});
|
|
124
|
+
const candidate = createCandidateStore({runtimeDir});
|
|
125
|
+
assert.equal((await candidate.readHead({plane:'process'})).event_sha256, restored.event_sha256);
|
|
126
|
+
assert.equal((await candidate.verifyGrant({plane:'process', grantBytes:nextGrant,
|
|
127
|
+
grantInstanceNonce:nextNonce})).trusted, true);
|
|
128
|
+
assert.equal((await candidate.auditHistory({plane:'process'})).status, 'PASS');
|
|
129
|
+
} finally {
|
|
130
|
+
await rm(root, {recursive:true, force:true, maxRetries:8, retryDelay:100});
|
|
131
|
+
}
|
|
132
|
+
});
|