Invariants Checklist & PR Gates¶
Purpose: Release-blocking requirements that can be statically and dynamically verified. Credit: Invariant definitions from GPT5.2 review.
Invariants Checklist¶
These are release-blockers. Every PR that touches vault-core must pass.
Classification and Downgrade Safety¶
| ID | Invariant | Verification Method |
|---|---|---|
| INV-01 | Every encrypted field/document is tagged with encryption_class ∈ {A,B,C} at creation; no "unknown/nullable" paths |
Schema constraint + NOT NULL + CHECK constraint |
| INV-02 | No class downgrade paths exist by default (C→A prohibited; B→A and C→B require explicit typed confirmation + waiting period + audit) | Code review + integration test |
| INV-03 | Metadata may be Class A even if content is B/C, but the boundary must be explicit (separate fields/classes) | Schema review + field naming convention |
Class C: Zero-Knowledge Means Zero-Knowledge¶
| ID | Invariant | Verification Method |
|---|---|---|
| INV-04 | Server code contains no Class C plaintext operations: no decrypt, no PDF gen, no content search, no content in notifications, no "verifyContent" | Static analysis scan |
| INV-05 | Server releases ciphertext only for Class C; executor access delivers ciphertext blob + metadata only | Integration test assertion |
| INV-06 | Any attempt to perform a forbidden Class C operation is blocked and produces an audit event (encryption_class_violation_attempt) |
Runtime middleware + integration test |
Class B: Escrowable, But Only Via Explicit Release¶
| ID | Invariant | Verification Method |
|---|---|---|
| INV-07 | Client encrypts before upload; server does not casually decrypt B content | Code path analysis |
| INV-08 | Every server-side Class B decryption is only inside executeTrigger() (or designated release handler), never elsewhere |
Static analysis + code review |
| INV-09 | Each Class B decryption emits an audit event with: reason, triggerId, actor/system attribution, jurisdiction/policy context | Integration test assertion |
Key Management Correctness¶
| ID | Invariant | Verification Method |
|---|---|---|
| INV-10 | Class B uses "random master key + password-derived wrapping key"; escrow splits the master key, not the wrapping key | Unit test + code review |
| INV-11 | Password change re-wraps the same master key; escrow shares remain valid | Integration test |
| INV-12 | Class C uses envelope encryption (random DEK + password-derived KEK); password change re-wraps, no bulk re-encrypt | Unit test + integration test |
Trigger State Machine & Timing¶
| ID | Invariant | Verification Method |
|---|---|---|
| INV-13 | armed → triggered requires 2+ signals (single missed check-in only sends reminder) |
State machine unit tests |
| INV-14 | Challenge and abort windows are enforced: no premature transitions | Timing integration tests |
| INV-15 | Abort works in armed, triggered, pending_execution; does not work once executing begins |
State machine tests |
Audit Trail Integrity¶
| ID | Invariant | Verification Method |
|---|---|---|
| INV-16 | Every state transition and side effect has an audit entry; sequence numbers have no gaps; hash chain validates | Audit verification tests |
| INV-17 | Executor cannot access vault content before release; access links are time-limited | Authorization integration tests |
PR Gate Implementation¶
Gate 1: Static Scan (CI)¶
Fail CI if server code contains forbidden Class C operations:
// Forbidden patterns for Class C documents
const FORBIDDEN_CLASS_C_PATTERNS = [
/decrypt\s*\(/, // No decryption
/generatePDF\s*\(/, // No server-side PDF
/searchContent\s*\(/, // No content search
/verifyContent\s*\(/, // No content verification
/includeContent\s*\(/, // No content in notifications
];
// Static scan should fail if these appear in paths that could touch Class C
Implementation:
# Example grep-based scan (replace with AST-based for production)
grep -r "decrypt(" src/server/ --include="*.ts" | grep -v "// Class A only"
Gate 2: Runtime Enforcement¶
The enforceEncryptionClass() middleware must be exercised:
// lib/security/middleware.ts
export async function enforceEncryptionClass(
document: Document,
operation: string,
context: OperationContext
) {
const rules = ENCRYPTION_CLASS_RULES[document.encryption_class];
if (!rules.allowedOperations.includes(operation)) {
// Log violation attempt
await auditLog.record({
type: 'encryption_class_violation_attempt',
documentId: document.id,
encryptionClass: document.encryption_class,
attemptedOperation: operation,
actor: context.actor,
blocked: true,
});
throw new EncryptionClassViolationError(
`Operation '${operation}' not allowed for Class ${document.encryption_class}`
);
}
}
Gate 3: Integration Tests¶
Run North Star acceptance test twice (B + C variants):
describe('North Star Acceptance Test', () => {
describe.each(['B', 'C'])('Class %s Document', (encryptionClass) => {
// ... test steps 1-6 ...
test('Step 5: Execution delivers correct content type', async () => {
if (encryptionClass === 'C') {
// Class C: ciphertext only
const download = await executor.downloadDocument(documentId);
expect(download.contentType).toBe('application/octet-stream');
expect(download.content).toEqual(originalCiphertext);
// Verify no decryption events
const audit = await getAuditLog(documentId);
expect(audit).not.toContainEventType('class_b_decryption');
expect(audit).not.toContainEventType('generate_pdf');
expect(audit).toContainEventType('class_c_document_released');
} else {
// Class B: plaintext or recovered key material
const download = await executor.downloadDocument(documentId);
// Assert based on Phase 0.5 promise (plaintext package or ciphertext + key)
// Verify decryption event exists
const audit = await getAuditLog(documentId);
expect(audit).toContainEventType('class_b_decryption');
expect(audit.find(e => e.type === 'class_b_decryption')).toMatchObject({
reason: 'trigger_execution',
triggerId: expect.any(String),
});
}
});
});
});
Negative Assertions¶
Add to test harness:
NEG-01: Class C Zero-Knowledge Verification¶
test('NEG-01: Class C has no server-side content operations', async () => {
// After full C-variant test run
const audit = await getFullAuditLog(vaultId);
// No decryption events for C documents
const cDocEvents = audit.filter(e => e.documentClass === 'C');
expect(cDocEvents).not.toContainEventType('class_b_decryption');
expect(cDocEvents).not.toContainEventType('generate_pdf');
expect(cDocEvents).not.toContainEventType('content_search');
expect(cDocEvents).not.toContainEventType('content_preview');
});
NEG-02: Pre-Release Access Denied¶
test('NEG-02: Executor access denied before release', async () => {
// Setup: trigger armed but not executed
const { triggerId, documentId } = await setupArmedTrigger();
// Executor attempts access
const response = await executor.attemptAccess(documentId);
expect(response.status).toBe(403);
// Verify denial is audited
const audit = await getAuditLog(documentId);
expect(audit).toContainEventType('access_denied');
});
Audit Event Types¶
CANONICAL REGISTRY: 13-event-schema.md is the single source of truth for event definitions.
This section lists event types for quick reference. For payload schemas and sequence mapping, see 13-event-schema.md.
Trigger Lifecycle Events¶
trigger_createdtrigger_armedtrigger_updatedtrigger_disarmedcheck_in_recordedcheck_in_missedreminder_sentreminder_acknowledgedreminder_unacknowledgedsecondary_signal_confirmedtrigger_firedchallenge_window_startedchallenge_window_elapsedabort_requestedtrigger_abortedexecution_startedaccess_grantedexecutor_notifiedexecution_completedtrigger_releasedtrigger_finalized
Document Events¶
document_uploadeddocument_downloadeddocument_deleteddocument_version_created
Vault Events¶
vault_createdvault_updatedvault_deletedexecutor_assignedexecutor_removed
Encryption Events¶
master_key_unwrappedclass_b_decryptionclass_c_document_releasedencryption_class_violation_attempt
Access Events¶
access_link_generatedaccess_link_usedaccess_link_expiredaccess_denied
Recovery Events¶
recovery_requestedrecovery_share_submittedrecovery_completedrecovery_cancelled
This document defines the invariants that must hold for the system to be trustworthy. All PRs must pass these gates.