initial commit: @arbiter/core authorization engine with js-rigor hardening
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/ binary modes, condensed snapshots, value relations) with 39 rigor test campaigns. Includes fixes for snapshot binary writer/reader format mismatch (snapshot-of-snapshot corruption), possibility write-boundary validation, empty-graph snapshot serialization, relation lookup cache direction collision, config-redefinition cache invalidation, binary threshold semantics, defeasible compiled routing, and comparator reason whitelisting.
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* rigor/challenge-proof.test.js — js-rigor property tests for
|
||||
* PartialGraphContext.getChallengeProof.
|
||||
*
|
||||
* rigor.fn receives args positionally, not as a single destructured object.
|
||||
* getChallengeProof is a deterministic pure function on a Map of challenge
|
||||
* records; perfect target for property-based testing.
|
||||
* Properties verified:
|
||||
* - skipped: proof with expiresAt <= now is never returned
|
||||
* - skipped: proof with (now - issuedAt) > withinMs is never returned
|
||||
* - pick: returned proof has the largest issuedAt among passes
|
||||
* - null: when no proof passes filters, returns null
|
||||
* - passthrough: when withinMs is null/undefined, time filter is off
|
||||
*/
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { rigor } from '@rigor/core';
|
||||
import { PartialGraphContext } from '../../src/core/PartialGraphContext.js';
|
||||
|
||||
const CHALLENGE_NAMES = ['mfa', 'captcha', 'webauthn', 'password'];
|
||||
const SUBJECTS = ['user:abc', 'user:def', 'user:ghi', 'user:jkl'];
|
||||
|
||||
/**
|
||||
* Brute-force oracle: re-implements getChallengeProof in 6 lines.
|
||||
* Used as the oracle in the campaign — if it ever disagrees with the
|
||||
* production code, that's a bug.
|
||||
*/
|
||||
function bruteForceOracle(proofs, name, subjectId, withinMs, now) {
|
||||
const matching = proofs.filter(p =>
|
||||
p.name === name && p.subject === subjectId
|
||||
);
|
||||
let best = null;
|
||||
for (const proof of matching) {
|
||||
if (proof.expiresAt != null && proof.expiresAt <= now) continue;
|
||||
if (withinMs != null && now - proof.issuedAt > withinMs) continue;
|
||||
if (!best || proof.issuedAt > best.issuedAt) best = proof;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fresh PartialGraphContext for the given subject + proofs.
|
||||
* PartialGraphContext._resolveNodeId reads `arbiter.nodeIdByKey` to
|
||||
* share IDs with the parent graph. Pass a stub arbiter so the lookup
|
||||
* doesn't crash when the campaign shrinks inputs down to edge cases.
|
||||
*/
|
||||
function buildContext({ subjectIds, proofs }) {
|
||||
const stubArbiter = { nodeIdByKey: new Map() };
|
||||
const context = new PartialGraphContext(stubArbiter, { skipContext: false });
|
||||
for (const id of subjectIds) {
|
||||
context._resolveNodeId(id);
|
||||
}
|
||||
for (const proof of proofs) {
|
||||
context._addChallengeProof(proof);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
describe('PartialGraphContext.getChallengeProof (rigor)', () => {
|
||||
it('reference oracle matches production across fuzzed inputs', async () => {
|
||||
// Args are positional: (proofs, name, subject, withinMs, now).
|
||||
async function referenceCheck(proofs, name, subject, withinMs, now) {
|
||||
const ctx = buildContext({ subjectIds: [subject], proofs });
|
||||
const subjectId = ctx._resolveNodeId(subject);
|
||||
const actual = ctx.getChallengeProof(name, subjectId, withinMs, now);
|
||||
const expected = bruteForceOracle(proofs, name, subject, withinMs, now);
|
||||
if (expected === null) {
|
||||
if (actual !== null) {
|
||||
throw new Error(
|
||||
`expected null but got proof with issuedAt=${actual.issuedAt}`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (actual === null) {
|
||||
throw new Error(
|
||||
`expected proof with issuedAt=${expected.issuedAt} but got null`
|
||||
);
|
||||
}
|
||||
if (actual.issuedAt !== expected.issuedAt) {
|
||||
throw new Error(
|
||||
`wrong proof returned: expected issuedAt=${expected.issuedAt}, got ${actual.issuedAt}`
|
||||
);
|
||||
}
|
||||
return actual;
|
||||
}
|
||||
|
||||
const report = await rigor.campaign(
|
||||
[
|
||||
rigor.fn('check', referenceCheck,
|
||||
rigor.args(
|
||||
rigor.gen.array(
|
||||
rigor.gen.object({
|
||||
name: rigor.gen.enum(CHALLENGE_NAMES),
|
||||
subject: rigor.gen.enum(SUBJECTS),
|
||||
issuedAt: rigor.gen.int(0, 100000),
|
||||
expiresAt: rigor.gen.option(rigor.gen.int(0, 100000))
|
||||
}),
|
||||
0, 5
|
||||
),
|
||||
rigor.gen.enum(CHALLENGE_NAMES),
|
||||
rigor.gen.enum(SUBJECTS),
|
||||
rigor.gen.option(rigor.gen.int(0, 100000)),
|
||||
rigor.gen.int(0, 200000)
|
||||
)
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant(
|
||||
'oracle-matches-brute-force',
|
||||
({ error, errorMessage }) => !error && !errorMessage
|
||||
)
|
||||
])
|
||||
).run({ effort: 1500 });
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') {
|
||||
console.log('TAP:', report.toTAP());
|
||||
}
|
||||
const oracle = report.crucibleVerdict?.invariants?.find(
|
||||
i => i.name === 'oracle-matches-brute-force'
|
||||
);
|
||||
assert.ok(oracle, 'oracle invariant reported');
|
||||
assert.equal(oracle.passed, true,
|
||||
`getChallengeProof disagrees with brute-force oracle: ${oracle.failureCount} failures`);
|
||||
});
|
||||
|
||||
it('never returns an expired proof (expiresAt <= now)', async () => {
|
||||
async function expiredCheck(proof, now) {
|
||||
const ctx = buildContext({ subjectIds: [proof.subject], proofs: [proof] });
|
||||
const subjectId = ctx._resolveNodeId(proof.subject);
|
||||
const result = ctx.getChallengeProof(proof.name, subjectId, null, now);
|
||||
if (result && result.expiresAt != null && result.expiresAt <= now) {
|
||||
throw new Error(`returned expired proof: expiresAt=${result.expiresAt}, now=${now}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const report = await rigor.campaign(
|
||||
[
|
||||
rigor.fn('expired', expiredCheck,
|
||||
rigor.args(
|
||||
rigor.gen.object({
|
||||
name: rigor.gen.enum(CHALLENGE_NAMES),
|
||||
subject: rigor.gen.enum(SUBJECTS),
|
||||
issuedAt: rigor.gen.int(0, 50000),
|
||||
expiresAt: rigor.gen.int(0, 100000)
|
||||
}),
|
||||
rigor.gen.int(50001, 200000) // now is always after the issuedAt range
|
||||
)
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('no-expired', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
])
|
||||
).run({ effort: 800 });
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') {
|
||||
console.log('TAP:', report.toTAP());
|
||||
}
|
||||
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'no-expired');
|
||||
assert.ok(inv);
|
||||
assert.equal(inv.passed, true,
|
||||
`getChallengeProof returned an expired proof in ${inv.failureCount} cases`);
|
||||
});
|
||||
|
||||
it('returns the most-recently-issued proof when withinMs=null', async () => {
|
||||
async function recentCheck(proofs, name, subject, now) {
|
||||
const ctx = buildContext({ subjectIds: [subject], proofs });
|
||||
const subjectId = ctx._resolveNodeId(subject);
|
||||
const result = ctx.getChallengeProof(name, subjectId, null, now);
|
||||
const nonExpired = proofs.filter(p =>
|
||||
p.name === name && p.subject === subject &&
|
||||
(p.expiresAt == null || p.expiresAt > now)
|
||||
);
|
||||
if (nonExpired.length === 0) {
|
||||
if (result !== null) throw new Error('expected null, got result');
|
||||
return null;
|
||||
}
|
||||
let maxIssued = nonExpired[0].issuedAt;
|
||||
for (const p of nonExpired) {
|
||||
if (p.issuedAt > maxIssued) maxIssued = p.issuedAt;
|
||||
}
|
||||
if (!result || result.issuedAt !== maxIssued) {
|
||||
throw new Error(
|
||||
`expected issuedAt=${maxIssued}, got ${result ? result.issuedAt : 'null'}`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const report = await rigor.campaign(
|
||||
[
|
||||
rigor.fn('recent', recentCheck,
|
||||
rigor.args(
|
||||
rigor.gen.array(
|
||||
rigor.gen.object({
|
||||
name: rigor.gen.constant('mfa'),
|
||||
subject: rigor.gen.constant('user:abc'),
|
||||
issuedAt: rigor.gen.int(0, 100000),
|
||||
expiresAt: rigor.gen.option(rigor.gen.int(0, 100000))
|
||||
}),
|
||||
1, 5
|
||||
),
|
||||
rigor.gen.constant('mfa'),
|
||||
rigor.gen.constant('user:abc'),
|
||||
rigor.gen.int(0, 200000)
|
||||
)
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('most-recent', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
])
|
||||
).run({ effort: 800 });
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') {
|
||||
console.log('TAP:', report.toTAP());
|
||||
}
|
||||
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'most-recent');
|
||||
assert.ok(inv);
|
||||
assert.equal(inv.passed, true,
|
||||
`getChallengeProof did not return the most-recent proof in ${inv.failureCount} cases`);
|
||||
});
|
||||
|
||||
it('enforces withinMs freshness window', async () => {
|
||||
async function withinCheck(proof, withinMs, now) {
|
||||
const ctx = buildContext({
|
||||
subjectIds: [proof.subject],
|
||||
proofs: [proof]
|
||||
});
|
||||
const subjectId = ctx._resolveNodeId(proof.subject);
|
||||
const result = ctx.getChallengeProof(proof.name, subjectId, withinMs, now);
|
||||
const age = now - proof.issuedAt;
|
||||
const expired = proof.expiresAt != null && proof.expiresAt <= now;
|
||||
if (age > withinMs || expired) {
|
||||
if (result !== null) {
|
||||
throw new Error(
|
||||
`returned proof past withinMs window: age=${age}, withinMs=${withinMs}, expired=${expired}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!result) {
|
||||
throw new Error(
|
||||
`expected non-null result, got null. age=${age}, withinMs=${withinMs}, expired=${expired}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const report = await rigor.campaign(
|
||||
[
|
||||
rigor.fn('within', withinCheck,
|
||||
rigor.args(
|
||||
rigor.gen.object({
|
||||
name: rigor.gen.constant('mfa'),
|
||||
subject: rigor.gen.constant('user:abc'),
|
||||
issuedAt: rigor.gen.int(0, 50000),
|
||||
expiresAt: rigor.gen.option(rigor.gen.int(0, 100000))
|
||||
}),
|
||||
rigor.gen.int(1, 100000),
|
||||
rigor.gen.int(0, 100000)
|
||||
)
|
||||
)
|
||||
],
|
||||
rigor.crucible([
|
||||
rigor.invariant('within-window', ({ error, errorMessage }) => !error && !errorMessage)
|
||||
])
|
||||
).run({ effort: 1500 });
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') {
|
||||
console.log('TAP:', report.toTAP());
|
||||
}
|
||||
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'within-window');
|
||||
assert.ok(inv);
|
||||
assert.equal(inv.passed, true,
|
||||
`withinMs window not enforced: ${inv.failureCount} failures`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user