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:

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

  1. Never throws. Any internal exception is converted into InvalidProofResult("parse_error", ...). Verified by 30k random inputs in the property + fuzz harness.
  2. Bounded resource use. Caps segment count at MaxSegmentCount = 4096 and any single string field at MaxStringLength = 64 * 1024 bytes.
  3. TryGetProperty everywhere. No direct GetProperty, no [i] without a prior GetArrayLength() check.
  4. Immutable output. ValidProofResult.Segments is ImmutableArray<ProofSegmentSnapshot>.
  5. Stable reason codes. Every InvalidProofResult carries one of the codes below.

ReasonCode reference

ReasonCodeWhen emitted
payload_nullRoot JSON token is null.
payload_kind_undefinedCaller passed default(JsonElement) (ValueKind == Undefined).
payload_not_objectRoot token is not a JSON object (string, number, array, bool, etc.).
segments_property_missingRoot object has no segments property.
segments_not_arraysegments exists but is not a JSON array.
segments_too_manySegment count exceeds MaxSegmentCount.
segment_not_objectAn entry inside segments[] is not a JSON object.
segment_field_missingA required field (segmentId, segmentType, status, index, inputHash, resultHash) is missing.
segment_field_wrong_kindA required string field is not a JSON string, or prevSegmentHash is neither string nor null.
segment_index_invalidindex is missing, non-numeric, fractional, negative, or out of int32 range.
segment_status_invalidstatus is not one of the known ProofSegmentStatus names (case-insensitive).
segment_string_too_longA required string exceeds MaxStringLength.
parse_errorCatch-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:

It does not defend against:

Test coverage

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