Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions tinycloud-threshold/vectors/check/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,131 @@ Development dependencies are TypeScript 5.9.3 (Apache-2.0), `@types/bun` 1.3.1,
read from the npm registry metadata for those exact versions.

Details: TC-2219

## Certificate-core checker portion

The default `bun run vectors` now checks both module-relative inputs:
`../v1/primitives.json` and `../v1/cert-core.json`. An explicit path checks
only that document:

```sh
bun run vectors /absolute/path/to/cert-core.json
```

The existing frozen-install / `bun run check` entry runs all test files,
typechecking and both real CLI paths. No dependency versions changed.

`cert-schema.ts` defines the current named certificate document shape.
`certificates.ts` computes canonical certificate and ceremony encodings,
tagged hashes, Ed25519 signatures, public-share commitment evaluations,
lineage, authenticated closed rounds, readiness/activation and fault verdicts.
`cert-vectors.ts` executes the operations and compares every declared output
or computed error. Format metadata, tags, bounds and role constants come from
compiled definitions, not fixture metadata.

All 17 explicit synthetic-DID seed/public-key mappings are independently
checked with noble Ed25519. These labels form a test signing-key directory;
the checker does not resolve production DIDs. Certificates require the
external owner/network anchor, owner signature, canonical member directory,
commitments and pinned lineage. The checker evaluates public shares from the
input commitments rather than using expected public shares.

Reference lookup indexes only independently recomputed hashes of input
bodies. It returns certificates and ceremonies only after verification.
Expected-output digests are comparison values, never reference authority.
Unresolved, ambiguous and unanchored references are rejected. Record order
does not establish trust.

### Fixture-only omitted-anchor conventions

For `stale-ceremony-predecessor`, `inputs.pinned_predecessor` selects the
independently verified certificate and its external anchor. The body's
encoded current digest does not select a predecessor; it must equal that
explicit pin. The declared stale current value is therefore checked against
the pin, not used as a fallback.

For `genuine-old-response-after-rekey`, the inline certificate is
canonical-decoded to locate its previous digest. That digest must resolve to
an independently verified predecessor. The checker inherits only that
predecessor's rooted external anchor, verifies the inline successor's owner,
network, lineage and signature, then uses its verified current context.
The candidate body's owner/network do not establish an anchor.

These are disclosed cross-record conventions for this fixture, not production
trust or freshness defaults. No trusted-log freshness input is invented.
`certificate-anchors.test.ts` exercises both conventions and the absence of
candidate-owner or first-record fallbacks.

### Round, activation and fault boundaries

Closed-round checking authenticates each exact member envelope under its
signed cohort and directory without fallback, preserves raw payloads and
owner order, verifies the closure, and computes its recursive transcript hash.
The owner relay is trusted; this is not consensus or a freshness mechanism.
Opaque inner payloads are retained, not interpreted as DKG phase outcomes.

Activation checks every receipt signature, including surplus receipts, before
counting, receipt hashing or sorting. Receipts encode as
`01 || index:u16be || lp(signature)` and hash under READY. Their 32-byte hashes
sort in binary lexicographic order. Readiness and activation signatures remain
signed claims, not proofs of possession or durable installation.

Fault checking first requires a verified certificate, matching amended
request/share bindings and the member signature. Only then do the two share
equations determine `Fault(member)` versus `NotAFault(ValidShare)`. Context,
helper or signature failures do not attribute an equation fault.

### Chosen single-byte CLI probes

Each probe uses an in-memory clone and the real CLI through `/dev/stdin`;
the producer file is not rewritten. Offsets below are zero-based decoded
field-byte offsets; each substitution is checked to change exactly one byte.
Offsets are decimal; byte values and XOR masks in the table are hexadecimal.

| Record | Field / byte basis | Changed decision |
| --- | --- | --- |
| genesis-2of3 | outputs.digest[31] XOR 01 | Computed digest comparison |
| reshare-3of4 | outputs.digest[31] XOR 01 | Computed digest comparison |
| rekey-2of3 | outputs.digest[31] XOR 01 | Computed digest comparison |
| ceremony-epoch-0 | outputs.digest[31] XOR 01 | Computed authorization comparison |
| ceremony-epoch-1 | outputs.digest[31] XOR 01 | Computed authorization comparison |
| ceremony-epoch-2 | outputs.digest[31] XOR 01 | Computed authorization comparison |
| closed-round-1 | outputs.transcript_hash[31] XOR 01 | Computed recursive hash comparison |
| activate-all-2of3 | inputs.receipts[0].receipt_hash[31] XOR 01 | Computed receipt hash comparison |
| duplicate-member-did | inputs.body[143], 31 to 32 | Decoder succeeds instead of declared error |
| trailing-certificate-byte | error UTF-8[0] XOR 01 | Schema-only error-label probe |
| stale-ceremony-predecessor | inputs.body[0] XOR 01 | Actual error becomes InvalidEncoding |
| signed-valid-share | outputs.signing_preimage[31] XOR 01 | Computed signing hash comparison |
| signed-bad-share | outputs.signing_preimage[31] XOR 01 | Computed signing hash comparison |
| genuine-old-response-after-rekey | inputs.owner_signature[0] XOR 01 | NotAFault reason becomes InvalidSignature |
| activation-under-quorum | inputs.receipts[0].signature[0] XOR 01 | InvalidReadiness before counting |
| activation-zero-budget | budget encoded u16be[1] XOR 01 | Budget becomes 1; activation succeeds |
| activation-budget-overflow | inputs.candidate[31] XOR 01 | UnresolvedReference before budget evaluation |
| activation-duplicate-member | inputs.candidate[31] XOR 01 | UnresolvedReference before duplicate evaluation |

The trailing-byte row also has a separate valid-body substitution proving its
actual expected-error comparison. Overflow and duplicate rows additionally
substitute a valid budget and unique receipts, respectively, proving semantic
success disagrees with their declared errors. Their one-byte reference probes
are not direct repairs of the budget or duplicated signature. No arbitrary
bit-flip assertion is made.

Independent behavior tests also cover external anchors, canonical boundaries,
lineage, cohort selection, signature-before-quorum ordering and fault-check
precedence. Constant-substitution tests cover all eight tags, fixed format
metadata and alternate-domain matching signatures/outputs.

### Certificate portion provenance and scope

This portion uses the immutable certificate JSON, the approved Rust-free
byte-layout inventory and its two clarified cross-reference conventions,
existing independently written checker utilities, and the
[noble-curves 2.4.0 API README](https://github.com/paulmillr/noble-curves/blob/2.4.0/README.md).
No Rust source/tests/diffs, dependency implementation or other threshold
implementation was consulted.

This is certificate-core checking only. DKG/resharing execution and transcript
outcome recomputation, and full S0.5 completion, remain outside this portion.
Producer API execution is distinct from this independent checker execution.

Details: TC-2223
75 changes: 75 additions & 0 deletions tinycloud-threshold/vectors/check/cert-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { array, hexField, intField, invalid, literal, object, type Parsed, type Parser } from './schema.ts';
import { CERT_TAGS } from './certificates.ts';

const text: Parser<string> = (v, p) => typeof v === 'string' && new TextEncoder().encode(v).length >= 1 && new TextEncoder().encode(v).length <= 1024 ? v : invalid(p);
const h32 = hexField(32), h96 = hexField(96), index = intField(1, 16);
function hexBytes(max: number, min = 0): Parser<string> {
return (v, p) => typeof v === 'string' && v.length >= min * 2 && v.length <= max * 2 && /^(?:[0-9a-f]{2})*$/.test(v) ? v : invalid(p);
}
function list<T>(parse: Parser<T>, min: number, max: number): Parser<T[]> {
return (v, p) => {
if (!Array.isArray(v) || v.length < min || v.length > max) return invalid(p);
return v.map((entry: unknown, i) => parse(entry, `${p}[${i}]`));
};
}
function nullable<T>(parse: Parser<T>): Parser<T | null> { return (v, p) => v === null ? null : parse(v, p); }
const body = hexBytes(65536, 1), signature = hexBytes(512), prior = nullable(h32);
const certificateInputs = object({ body, expected_owner: text, network: text, owner_signature: signature, pinned_predecessor: prior });
const ceremonyInputs = object({ body, expected_owner: text, network: text, signature, pinned_predecessor: prior });
const certificateOutput = object({
commitments: list(h96, 2, 16), digest: h32, epoch: intField(0, Number.MAX_SAFE_INTEGER), genesis_t: intField(2, 16), members: intField(2, 16), mpk: h96, owner_statement: h32,
public_shares: list(object({ index, point: h96 }), 2, 16), rekey: intField(0, Number.MAX_SAFE_INTEGER), threshold: intField(2, 16),
});
const receipt = object({ index, signature });
const receiptOutput = object({ encoding: hexBytes(519, 7), index, readiness_preimage: h32, receipt_hash: h32, signature });
const activationErrorInputs = object({ activation_preimage: h32, budget: nullable(intField(0, 65535)), candidate: h32, owner_signature: signature, receipts: list(receipt, 0, 16) });
const faultInputs = object({ certificate: h32, request: body, share: hexField(259), signature });
const cases = {
'genesis-2of3': object({ name: literal('genesis-2of3'), operation: literal('verify_certificate'), inputs: certificateInputs, outputs: certificateOutput }),
'reshare-3of4': object({ name: literal('reshare-3of4'), operation: literal('verify_certificate'), inputs: certificateInputs, outputs: certificateOutput }),
'rekey-2of3': object({ name: literal('rekey-2of3'), operation: literal('verify_certificate'), inputs: certificateInputs, outputs: certificateOutput }),
'ceremony-epoch-0': object({ name: literal('ceremony-epoch-0'), operation: literal('authorize_ceremony'), inputs: ceremonyInputs, outputs: object({ digest: h32, signing_preimage: h32 }) }),
'ceremony-epoch-1': object({ name: literal('ceremony-epoch-1'), operation: literal('authorize_ceremony'), inputs: ceremonyInputs, outputs: object({ digest: h32, signing_preimage: h32 }) }),
'ceremony-epoch-2': object({ name: literal('ceremony-epoch-2'), operation: literal('authorize_ceremony'), inputs: ceremonyInputs, outputs: object({ digest: h32, signing_preimage: h32 }) }),
'closed-round-1': object({ name: literal('closed-round-1'), operation: literal('verify_closed_round'), inputs: object({ ceremony_digest: h32, closure: body, messages: list(object({ body, signature }), 0, 65535), owner_signature: signature, previous: h32, round: intField(0, 255) }), outputs: object({ owner_preimage: h32, transcript_hash: h32 }) }),
'activate-all-2of3': object({ name: literal('activate-all-2of3'), operation: literal('verify_activation'), inputs: object({ candidate: h32, owner_signature: signature, quorum: literal('all'), receipts: list(receiptOutput, 0, 16) }), outputs: object({ accepted: (v: unknown, p: string) => v === true ? true : invalid(p), activation_preimage: h32 }) }),
'duplicate-member-did': object({ name: literal('duplicate-member-did'), operation: literal('decode_certificate'), inputs: object({ body }), error: literal('InvalidEncoding') }),
'trailing-certificate-byte': object({ name: literal('trailing-certificate-byte'), operation: literal('decode_certificate'), inputs: object({ body }), error: literal('InvalidEncoding') }),
'stale-ceremony-predecessor': object({ name: literal('stale-ceremony-predecessor'), operation: literal('authorize_ceremony'), inputs: object({ body, pinned_predecessor: h32, signature }), error: literal('InvalidCeremony') }),
'signed-valid-share': object({ name: literal('signed-valid-share'), operation: literal('verify_fault_proof'), inputs: faultInputs, outputs: object({ signing_preimage: h32, verdict: literal('NotAFault'), reason: literal('ValidShare') }) }),
'signed-bad-share': object({ name: literal('signed-bad-share'), operation: literal('verify_fault_proof'), inputs: faultInputs, outputs: object({ signing_preimage: h32, verdict: literal('Fault'), member: index }) }),
'genuine-old-response-after-rekey': object({ name: literal('genuine-old-response-after-rekey'), operation: literal('verify_fault_proof'), inputs: object({ certificate_body: body, owner_signature: signature, request: body, share: hexField(259), signature }), outputs: object({ verdict: literal('NotAFault'), reason: literal('ContextMismatch') }) }),
'activation-under-quorum': object({ name: literal('activation-under-quorum'), operation: literal('verify_activation'), inputs: activationErrorInputs, error: literal('NotEnoughReady') }),
'activation-zero-budget': object({ name: literal('activation-zero-budget'), operation: literal('verify_activation'), inputs: activationErrorInputs, error: literal('InvalidReadiness') }),
'activation-budget-overflow': object({ name: literal('activation-budget-overflow'), operation: literal('verify_activation'), inputs: activationErrorInputs, error: literal('InvalidReadiness') }),
'activation-duplicate-member': object({ name: literal('activation-duplicate-member'), operation: literal('verify_activation'), inputs: activationErrorInputs, error: literal('InvalidReadiness') }),
};
export type CertificateCase = Parsed<(typeof cases)[keyof typeof cases]>;
const parseCases: Parser<CertificateCase[]> = (v, p) => {
if (!Array.isArray(v) || v.length !== Object.keys(cases).length) return invalid(p);
const seen = new Set<string>();
return v.map((entry: unknown, i) => {
if (!entry || typeof entry !== 'object' || !('name' in entry) || typeof entry.name !== 'string' || !Object.hasOwn(cases, entry.name) || seen.has(entry.name)) return invalid(`${p}[${i}]`);
seen.add(entry.name);
return cases[entry.name as keyof typeof cases](entry, `${p}[${i}].${entry.name}`);
});
};
const sections: Parser<string[]> = (v, p) => {
const expected = ['3.0', '3.8', '3.9', '3.12', 'S0.1 amendment'];
if (!Array.isArray(v) || v.length !== expected.length || v.some((entry, i) => entry !== expected[i])) return invalid(p);
return expected;
};
const parseFile = object({
cases: parseCases,
dst: object({ activation: literal(CERT_TAGS.activation), ceremony: literal(CERT_TAGS.ceremony), certificate: literal(CERT_TAGS.certificate), closure: literal(CERT_TAGS.closure), message: literal(CERT_TAGS.message), readiness: literal(CERT_TAGS.readiness), statement: literal(CERT_TAGS.statement), transcript: literal(CERT_TAGS.transcript) }),
encoding: literal('lowercase hex; unsigned integers big-endian; lp=u32be length followed by bytes'), format_version: literal(1),
seeds: object({ ed25519: array(object({ did: text, public_key: h32, seed: h32 }), 17), test_dealer_genesis: intField(0, 255), test_dealer_rekey: intField(0, 255), test_dealer_reshare: intField(0, 255) }),
spec_sections: sections, suite: literal('TC-THRESHOLD-V1'),
});
export type CertificateFile = Parsed<typeof parseFile>;
export function parseCertificateVectors(value: unknown): CertificateFile { return parseFile(value, 'certificates'); }
export function certificateCase<N extends CertificateCase['name']>(file: CertificateFile, name: N): Extract<CertificateCase, { name: N }> {
const found = file.cases.find((entry): entry is Extract<CertificateCase, { name: N }> => entry.name === name);
if (!found) return invalid(name);
return found;
}
Loading
Loading