release(v4.0.0): Shade GA — V3.x consolidation + audit prep
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers byte-for-byte. The version bump is semantic: audit-cycle complete, opt-in surface fully exposed, threat model refreshed for every new surface. Highlights: - All 24 @shade/* packages bumped to 4.0.0 in lockstep. - CHANGELOG 4.0.0 section is the canonical manifest of what landed. - THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12 Web-Worker boundary) + residual-risks table refreshed. - OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox, bridge, observer, /metrics, /healthz, /ready. - MIGRATION 0.3.x → 4.0 documented + smoke-tested against shade migrate-storage on a real SQLite DB. - docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer. - scripts/soak.ts harness for the GA-stable 2-week soak window. - All V*.md plans archived under docs/archive/ with Status: Done. - Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen non-realtime stack. Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green. Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports version 4.0.0 on /health. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
189
packages/shade-key-transparency/tests/log.test.ts
Normal file
189
packages/shade-key-transparency/tests/log.test.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import fc from 'fast-check';
|
||||
import {
|
||||
MerkleLog,
|
||||
auditPath,
|
||||
consistencyProof,
|
||||
emptyRootHash,
|
||||
leafHash,
|
||||
nodeHash,
|
||||
recomputeRootFromAuditPath,
|
||||
verifyConsistencyProof,
|
||||
} from '../src/index.js';
|
||||
import { mth } from '../src/log.js';
|
||||
|
||||
function buildLog(n: number): MerkleLog {
|
||||
const log = new MerkleLog();
|
||||
for (let i = 0; i < n; i++) {
|
||||
log.appendData(new Uint8Array([i & 0xff, (i >> 8) & 0xff]));
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
describe('MerkleLog basics', () => {
|
||||
test('empty tree root = SHA-256(empty)', () => {
|
||||
const log = new MerkleLog();
|
||||
expect(log.rootHash()).toEqual(emptyRootHash());
|
||||
});
|
||||
|
||||
test('single-leaf tree root = leaf_hash(d)', () => {
|
||||
const log = new MerkleLog();
|
||||
const d = new Uint8Array([0xab, 0xcd]);
|
||||
log.appendData(d);
|
||||
expect(log.rootHash()).toEqual(leafHash(d));
|
||||
});
|
||||
|
||||
test('two-leaf tree root = node_hash(leaf0, leaf1)', () => {
|
||||
const log = new MerkleLog();
|
||||
const d0 = new Uint8Array([1]);
|
||||
const d1 = new Uint8Array([2]);
|
||||
log.appendData(d0);
|
||||
log.appendData(d1);
|
||||
expect(log.rootHash()).toEqual(nodeHash(leafHash(d0), leafHash(d1)));
|
||||
});
|
||||
|
||||
test('append-only ordering preserved', () => {
|
||||
const log = new MerkleLog();
|
||||
log.appendData(new Uint8Array([1]));
|
||||
log.appendData(new Uint8Array([2]));
|
||||
expect(log.size).toBe(2);
|
||||
log.appendData(new Uint8Array([3]));
|
||||
expect(log.size).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Audit path verification', () => {
|
||||
test('valid audit path reconstructs root for every leaf, every size 1..32', () => {
|
||||
for (let n = 1; n <= 32; n++) {
|
||||
const log = buildLog(n);
|
||||
const root = log.rootHash();
|
||||
for (let m = 0; m < n; m++) {
|
||||
const path = log.auditPath(m);
|
||||
const lh = log.leafHashAt(m);
|
||||
const reconstructed = recomputeRootFromAuditPath(lh, m, n, path);
|
||||
expect(Buffer.from(reconstructed).toString('hex')).toBe(
|
||||
Buffer.from(root).toString('hex'),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('property: tampered leaf fails verification', () => {
|
||||
const log = buildLog(7);
|
||||
const root = log.rootHash();
|
||||
const path = log.auditPath(3);
|
||||
const tampered = new Uint8Array(log.leafHashAt(3));
|
||||
tampered[0] ^= 0xff;
|
||||
const reconstructed = recomputeRootFromAuditPath(tampered, 3, 7, path);
|
||||
expect(Buffer.from(reconstructed).toString('hex')).not.toBe(
|
||||
Buffer.from(root).toString('hex'),
|
||||
);
|
||||
});
|
||||
|
||||
test('property: tampered audit path fails verification', () => {
|
||||
const log = buildLog(11);
|
||||
const root = log.rootHash();
|
||||
const path = log.auditPath(5);
|
||||
if (path.length === 0) return;
|
||||
const tampered = path.map((p, i) => {
|
||||
if (i === 0) {
|
||||
const x = new Uint8Array(p);
|
||||
x[0] ^= 0xff;
|
||||
return x;
|
||||
}
|
||||
return p;
|
||||
});
|
||||
const reconstructed = recomputeRootFromAuditPath(log.leafHashAt(5), 5, 11, tampered);
|
||||
expect(Buffer.from(reconstructed).toString('hex')).not.toBe(
|
||||
Buffer.from(root).toString('hex'),
|
||||
);
|
||||
});
|
||||
|
||||
test('property-based: random N and m', () => {
|
||||
fc.assert(
|
||||
fc.property(fc.integer({ min: 1, max: 100 }), (n) => {
|
||||
const log = buildLog(n);
|
||||
const root = log.rootHash();
|
||||
const m = Math.min(n - 1, Math.floor(Math.random() * n));
|
||||
const path = log.auditPath(m);
|
||||
const reconstructed = recomputeRootFromAuditPath(log.leafHashAt(m), m, n, path);
|
||||
return Buffer.from(reconstructed).toString('hex') === Buffer.from(root).toString('hex');
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Consistency proofs', () => {
|
||||
test('m === 0 always consistent', () => {
|
||||
const log = buildLog(5);
|
||||
const newRoot = log.rootHash();
|
||||
expect(verifyConsistencyProof(0, 5, emptyRootHash(), newRoot, [])).toBe(true);
|
||||
});
|
||||
|
||||
test('m === n consistent only when both roots match and proof empty', () => {
|
||||
const log = buildLog(5);
|
||||
const root = log.rootHash();
|
||||
expect(verifyConsistencyProof(5, 5, root, root, [])).toBe(true);
|
||||
const wrong = new Uint8Array(root);
|
||||
wrong[0] ^= 0xff;
|
||||
expect(verifyConsistencyProof(5, 5, wrong, root, [])).toBe(false);
|
||||
});
|
||||
|
||||
test('valid proof verifies for every (m, n) up to 16', () => {
|
||||
for (let n = 1; n <= 16; n++) {
|
||||
const newLog = buildLog(n);
|
||||
const newRoot = newLog.rootHash();
|
||||
for (let m = 0; m <= n; m++) {
|
||||
const oldLog = buildLog(m);
|
||||
const oldRoot = oldLog.rootHash();
|
||||
const proof = newLog.consistencyProof(m);
|
||||
expect(verifyConsistencyProof(m, n, oldRoot, newRoot, proof)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('fork detection — re-write history fails consistency', () => {
|
||||
const original = buildLog(5);
|
||||
const oldRoot = original.rootHash();
|
||||
|
||||
// Server "rewrites" leaf 2: build a new log where leaf 2 has different data.
|
||||
const tampered = new MerkleLog();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
tampered.appendData(
|
||||
i === 2 ? new Uint8Array([0x99, 0x99]) : new Uint8Array([i & 0xff, (i >> 8) & 0xff]),
|
||||
);
|
||||
}
|
||||
tampered.appendData(new Uint8Array([0x77]));
|
||||
const tamperedRoot = tampered.rootHash();
|
||||
const proof = tampered.consistencyProof(5);
|
||||
|
||||
expect(verifyConsistencyProof(5, 6, oldRoot, tamperedRoot, proof)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mth helper', () => {
|
||||
test('mth over slice == sub-tree root', () => {
|
||||
const log = buildLog(4);
|
||||
const leaves = log.exportLeaves();
|
||||
const left = mth(leaves, 0, 2);
|
||||
const right = mth(leaves, 2, 4);
|
||||
const root = mth(leaves, 0, 4);
|
||||
expect(root).toEqual(nodeHash(left, right));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Direct auditPath helper (tree size 1)', () => {
|
||||
test('singleton tree audit path is empty', () => {
|
||||
const log = buildLog(1);
|
||||
const path = log.auditPath(0);
|
||||
expect(path.length).toBe(0);
|
||||
expect(consistencyProof([log.leafHashAt(0)], 1, 1)).toEqual([]);
|
||||
});
|
||||
|
||||
test('auditPath out-of-range throws', () => {
|
||||
expect(() => auditPath([], 0, 0)).toThrow();
|
||||
const log = buildLog(3);
|
||||
expect(() => log.auditPath(3)).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user