ProofSpine input validator
Sprint reference: SPRINT_20260429_003_Scanner_proofspine_input_validation.md.
Why this exists
ProofSpine is a verification surface. By the time JSON reaches a consumer (an endpoint, a downstream replay tool, a CLI offline verifier), the contents may be:
- Tampered - a single field rewritten to test the verifier’s resistance to forgery.
- Truncated - payload cut mid-array because of a transport failure.
- Oversized - 10 MB of segments meant to amplify CPU usage.
- Adversarial - structurally pathological JSON (deeply nested arrays, repeated keys, non-UTF-8 bytes) crafted to exploit a parsing bug.
For ProofSpine, all four are business cases, not exception paths. Crashing on malformed input gives an attacker a denial-of-service primitive against the very verification API that downstream tooling depends on.
StellaOps.Scanner.ProofSpine.Validation.ProofSegmentValidator is the single entry point that converts raw JsonElement (or UTF-8 byte buffer) into either a ValidProofResult carrying an immutable list of ProofSegmentSnapshot, or an InvalidProofResult carrying a stable, structured failure code.
Hard contract
- Never throws. Any internal exception is converted into
InvalidProofResult("parse_error", ...). Verified by 30k random inputs in the property + fuzz harness. - Bounded resource use. Caps segment count at
MaxSegmentCount = 4096and any single string field atMaxStringLength = 64 * 1024bytes. TryGetPropertyeverywhere. No directGetProperty, no[i]without a priorGetArrayLength()check.- Immutable output.
ValidProofResult.SegmentsisImmutableArray<ProofSegmentSnapshot>. - Stable reason codes. Every
InvalidProofResultcarries one of the codes below.
ReasonCode reference
| ReasonCode | When emitted |
|---|---|
payload_null | Root JSON token is null. |
payload_kind_undefined | Caller passed default(JsonElement) (ValueKind == Undefined). |
payload_not_object | Root token is not a JSON object (string, number, array, bool, etc.). |
segments_property_missing | Root object has no segments property. |
segments_not_array | segments exists but is not a JSON array. |
segments_too_many | Segment count exceeds MaxSegmentCount. |
segment_not_object | An entry inside segments[] is not a JSON object. |
segment_field_missing | A required field (segmentId, segmentType, status, index, inputHash, resultHash) is missing. |
segment_field_wrong_kind | A required string field is not a JSON string, or prevSegmentHash is neither string nor null. |
segment_index_invalid | index is missing, non-numeric, fractional, negative, or out of int32 range. |
segment_status_invalid | status is not one of the known ProofSegmentStatus names (case-insensitive). |
segment_string_too_long | A required string exceeds MaxStringLength. |
parse_error | Catch-all defensive code for malformed JSON or unexpected internal failures. |
These codes are stable; dashboards and downstream consumers may key on them.
Usage
var validator = new ProofSegmentValidator();
var result = validator.Validate(jsonElement); // or .Validate(utf8Bytes)
switch (result)
{
case ValidProofResult valid:
foreach (var snapshot in valid.Segments) { /* ... */ }
break;
case InvalidProofResult invalid:
logger.LogWarning("ProofSpine input rejected: {Reason} ({Code})",
invalid.Reason, invalid.ReasonCode);
metrics.Record(invalid, tenantId);
break;
}
The Scanner WebService registers a singleton ProofSegmentValidator plus ProofSpineValidationMetrics; consumers should pull both from the DI container.
Threat model
The validator defends against:
- Algorithmic-complexity attacks: capped segment count and string length.
- Stack-blowing depth attacks: only descends into
segments[]; the JSON parser’s built-in 64-deep recursion limit catches deeper structures and the validator surfaces them asparse_error. - Type-confusion attacks: every property access checks
ValueKindbefore extracting. - Indexed-access attacks: every
[i]is preceded byGetArrayLength().
It does not defend against:
- DSSE signature forgery (handled by
ProofSpineVerifier). - Replay or rollback (handled at the policy layer).
- Network-level attacks (handled by the Router/gateway).
Test coverage
- 21 unit tests (
ProofSegmentValidatorTests) covering happy path and every reason code. - 2 FsCheck properties (
ProofSegmentValidatorPropertyTests) at MaxTest=5000 each. - 2 deterministic fuzz harnesses at 10k iterations each.
- 3 metrics counter contract tests (
ProofSpineValidationMetricsTests).
Total ≥30k random inputs exercised; zero throws confirmed.
A long-running SharpFuzz corpus run is scheduled as a nightly CI job; the in-process harness above is the per-PR signal.
See also
- Scanner architecture - cross-link to the ProofSpine surface.
- Scanner observability - metric and Grafana panel definitions.
