Class: Ibex::VerificationReport::Validator

Inherits:
Object
  • Object
show all
Defined in:
lib/ibex/verification_report/validator.rb,
sig/ibex/verification_report/validator.rbs

Overview

Validates a report and its cross-artifact manifest/table bindings. rubocop:disable Metrics/ClassLength -- closed report and cross-artifact invariants form one validator contract.

Constant Summary collapse

ROOT_KEYS =

RBS:

  • type json_value = String | Integer | Float | bool | nil | Array[json_value] | Hash[String, json_value]

Returns:

  • (Array[String])
%w[
  ibex_report schema_version checker profile bounds input ir table outcome excluded_trust evidence_digest
].freeze
CHECKER_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[name version].freeze
BOUNDS_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[max_states max_items].freeze
INPUT_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[digest files].freeze
INPUT_FILE_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[logical_path sha256 bytesize].freeze
IR_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[identity_scope grammar automaton].freeze
GRAMMAR_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[schema_version digest].freeze
AUTOMATON_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[schema_version algorithm digest].freeze
TABLE_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[
  logical_path artifact_type schema_version representation artifact_digest payload_digest
].freeze
OUTCOME_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[status requested_checks executed_checks violations exhaustion].freeze
VIOLATION_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[id location message].freeze
EXHAUSTION_KEYS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[kind message].freeze
DIGEST =

Signature:

  • Array[String]

Returns:

  • (Regexp)
/\Asha256:[0-9a-f]{64}\z/
ALGORITHMS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[slr lalr1 ielr1 lr1].freeze

Instance Method Summary collapse

Instance Method Details

#array(value, path) ⇒ Array[json_value]

RBS:

  • (json_value value, String path) -> Array[json_value]

Parameters:

  • value (json_value)
  • path (String)

Returns:

  • (Array[json_value])


279
280
281
282
283
# File 'lib/ibex/verification_report/validator.rb', line 279

def array(value, path)
  raise TypeError, "#{path} must be an array" unless value.is_a?(Array)

  value
end

#checks(value, path) ⇒ Array[String]

RBS:

  • (json_value value, String path) -> Array[String]

Parameters:

  • value (json_value)
  • path (String)

Returns:

  • (Array[String])


254
255
256
257
258
259
260
261
262
263
# File 'lib/ibex/verification_report/validator.rb', line 254

def checks(value, path)
  entries = array(value, path).map do |entry|
    check = string(entry, path)
    enum(check, Verify::Verifier::DEFAULT_CHECKS + Verify::Verifier::STRICT_CHECKS, path)
    check
  end
  raise TypeError, "#{path} must contain unique checks" unless entries.uniq == entries

  entries
end

#digest(value, path) ⇒ String

RBS:

  • (json_value value, String path) -> String

Parameters:

  • value (json_value)
  • path (String)

Returns:

  • (String)


293
294
295
296
297
# File 'lib/ibex/verification_report/validator.rb', line 293

def digest(value, path)
  return value if value.is_a?(String) && value.match?(DIGEST)

  raise TypeError, "#{path} must be a SHA-256 identity"
end

#enum(value, values, path) ⇒ void

This method returns an undefined value.

RBS:

  • [T] (T value, Array[T] values, String path) -> T



314
315
316
317
318
# File 'lib/ibex/verification_report/validator.rb', line 314

def enum(value, values, path)
  return value if values.include?(value)

  raise TypeError, "#{path} has an unsupported value"
end

#equal(actual, expected, path) ⇒ void

This method returns an undefined value.

RBS:

  • (json_value actual, json_value expected, String path) -> void

Parameters:

  • actual (json_value)
  • expected (json_value)
  • path (String)


321
322
323
324
325
# File 'lib/ibex/verification_report/validator.rb', line 321

def equal(actual, expected, path)
  return if actual == expected

  raise TypeError, "#{path} mismatch"
end

#nonnegative_integer(value, path) ⇒ Integer

RBS:

  • (json_value value, String path) -> Integer

Parameters:

  • value (json_value)
  • path (String)

Returns:

  • (Integer)


307
308
309
310
311
# File 'lib/ibex/verification_report/validator.rb', line 307

def nonnegative_integer(value, path)
  return value if value.is_a?(Integer) && value >= 0

  raise TypeError, "#{path} must be a non-negative integer"
end

#object(value, keys, path) ⇒ Hash[String, json_value]

RBS:

  • (json_value value, Array[String] keys, String path) -> Hash[String, json_value]

Parameters:

  • value (json_value)
  • keys (Array[String])
  • path (String)

Returns:

  • (Hash[String, json_value])


266
267
268
269
270
271
272
273
274
275
276
# File 'lib/ibex/verification_report/validator.rb', line 266

def object(value, keys, path)
  raise TypeError, "#{path} must be an object" unless value.is_a?(Hash)
  raise TypeError, "#{path} keys must be strings" unless value.keys.all?(String)

  extras = value.keys - keys
  missing = keys - value.keys
  raise TypeError, "#{path} has unknown property #{extras.fetch(0)}" unless extras.empty?
  raise KeyError, "#{path} is missing #{missing.fetch(0)}" unless missing.empty?

  value
end

#positive_integer(value, path) ⇒ Integer

RBS:

  • (json_value value, String path) -> Integer

Parameters:

  • value (json_value)
  • path (String)

Returns:

  • (Integer)


300
301
302
303
304
# File 'lib/ibex/verification_report/validator.rb', line 300

def positive_integer(value, path)
  return value if value.is_a?(Integer) && value.positive?

  raise TypeError, "#{path} must be a positive integer"
end

#string(value, path, allow_empty: false) ⇒ String

RBS:

  • (json_value value, String path, ?allow_empty: bool) -> String

Parameters:

  • value (json_value)
  • path (String)
  • allow_empty: (Boolean) (defaults to: false)

Returns:

  • (String)


286
287
288
289
290
# File 'lib/ibex/verification_report/validator.rb', line 286

def string(value, path, allow_empty: false)
  return value if value.is_a?(String) && (allow_empty || !value.empty?)

  raise TypeError, "#{path} must be #{allow_empty ? 'a string' : 'a non-empty string'}"
end

#unique_artifact(manifest, kind) ⇒ Hash[String, json_value]

RBS:

  • (Hash[String, json_value] manifest, String kind) -> Hash[String, json_value]

Parameters:

  • manifest (Hash[String, json_value])
  • kind (String)

Returns:

  • (Hash[String, json_value])


206
207
208
209
210
211
212
# File 'lib/ibex/verification_report/validator.rb', line 206

def unique_artifact(manifest, kind)
  artifacts = manifest.fetch("artifacts") #: Array[Hash[String, json_value]]
  entries = artifacts.select { |entry| entry.fetch("kind") == kind }
  raise TypeError, "manifest must list exactly one #{kind} artifact" unless entries.one?

  entries.fetch(0)
end

#validate(source) ⇒ Hash[String, json_value]

RBS:

  • (String source) -> Hash[String, json_value]

Parameters:

  • source (String)

Returns:

  • (Hash[String, json_value])


34
35
36
37
38
39
40
# File 'lib/ibex/verification_report/validator.rb', line 34

def validate(source)
  document = JSON.parse(source) #: json_value
  validate_document(document)
  document #: Hash[String, json_value]
rescue JSON::ParserError, KeyError, TypeError, ArgumentError => e
  raise ValidationError, "(verification-report):1:1: invalid report: #{e.message}"
end

#validate_bounds(value) ⇒ void

This method returns an undefined value.

RBS:

  • (json_value value) -> void

Parameters:

  • value (json_value)


111
112
113
114
# File 'lib/ibex/verification_report/validator.rb', line 111

def validate_bounds(value)
  bounds = object(value, BOUNDS_KEYS, "bounds")
  BOUNDS_KEYS.each { |key| positive_integer(bounds.fetch(key), "bounds.#{key}") }
end

#validate_bundle(manifest_source:, report_source:, table_source:) ⇒ Hash[String, json_value]

RBS:

  • (manifest_source: String, report_source: String, table_source: String) -> Hash[String, json_value]

Parameters:

  • manifest_source: (String)
  • report_source: (String)
  • table_source: (String)

Returns:

  • (Hash[String, json_value])


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/ibex/verification_report/validator.rb', line 43

def validate_bundle(manifest_source:, report_source:, table_source:)
  manifest = GenerationManifest.validate(manifest_source, verify_artifacts: false) #: Hash[String, json_value]
  report = validate(report_source)
  table = TableArtifact.load(table_source)
  report_entry = unique_artifact(manifest, "verification_report")
  table_entry = unique_artifact(manifest, "parser_table")
  unique_artifact(manifest, "parser")

  validate_manifest_bytes!(report_entry, report_source, "verification report")
  validate_manifest_bytes!(table_entry, table_source, "parser table")
  validate_inputs!(manifest, report)
  validate_table_binding!(report, table, table_entry)
  report
rescue Ibex::Error => e
  raise e if e.is_a?(ValidationError)

  raise ValidationError, "(verification-bundle):1:1: #{e.message}"
rescue KeyError, TypeError, ArgumentError => e
  raise ValidationError, "(verification-bundle):1:1: invalid bundle: #{e.message}"
end

#validate_bundle_file(manifest_path) ⇒ Hash[String, json_value]

RBS:

  • (String manifest_path) -> Hash[String, json_value]

Parameters:

  • manifest_path (String)

Returns:

  • (Hash[String, json_value])


65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/ibex/verification_report/validator.rb', line 65

def validate_bundle_file(manifest_path)
  manifest_source = File.binread(manifest_path)
  manifest = GenerationManifest.validate(manifest_source, verify_artifacts: false) #: Hash[String, json_value]
  report_entry = unique_artifact(manifest, "verification_report")
  table_entry = unique_artifact(manifest, "parser_table")
  report_path = report_entry.fetch("path") #: String
  table_path = table_entry.fetch("path") #: String
  report_source = File.binread(report_path)
  table_source = File.binread(table_path)
  result = validate_bundle(
    manifest_source: manifest_source, report_source: report_source, table_source: table_source
  )
  GenerationManifest.validate(manifest_source)
  result
rescue SystemCallError => e
  raise ValidationError, "#{manifest_path}:1:1: cannot read verification bundle: #{e.message}"
end

#validate_checker(value) ⇒ void

This method returns an undefined value.

RBS:

  • (json_value value) -> void

Parameters:

  • value (json_value)


104
105
106
107
108
# File 'lib/ibex/verification_report/validator.rb', line 104

def validate_checker(value)
  checker = object(value, CHECKER_KEYS, "checker")
  equal(checker.fetch("name"), "ibex.verify", "checker.name")
  string(checker.fetch("version"), "checker.version")
end

#validate_document(document) ⇒ void

This method returns an undefined value.

RBS:

  • (json_value document) -> void

Parameters:

  • document (json_value)


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/ibex/verification_report/validator.rb', line 86

def validate_document(document)
  document = object(document, ROOT_KEYS, "document")
  equal(document.fetch("ibex_report"), IDENTIFIER, "ibex_report")
  equal(document.fetch("schema_version"), SCHEMA_VERSION, "schema_version")
  validate_checker(document.fetch("checker"))
  profile = enum(string(document.fetch("profile"), "profile"), %w[default strict], "profile")
  validate_bounds(document.fetch("bounds"))
  validate_input(document.fetch("input"))
  validate_ir(document.fetch("ir"))
  validate_table(document.fetch("table"))
  validate_outcome(document.fetch("outcome"), profile)
  equal(document.fetch("excluded_trust"), EXCLUDED_TRUST, "excluded_trust")
  digest(document.fetch("evidence_digest"), "evidence_digest")
  unsigned = document.except("evidence_digest")
  equal(document.fetch("evidence_digest"), TableArtifact::Serializer.digest(unsigned), "evidence_digest")
end

#validate_input(value) ⇒ void

This method returns an undefined value.

RBS:

  • (json_value value) -> void

Parameters:

  • value (json_value)


117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/ibex/verification_report/validator.rb', line 117

def validate_input(value)
  input = object(value, INPUT_KEYS, "input")
  files = array(input.fetch("files"), "input.files")
  raise TypeError, "input.files must not be empty" if files.empty?

  files.each_with_index do |entry, index|
    file = object(entry, INPUT_FILE_KEYS, "input.files[#{index}]")
    path = file.fetch("logical_path")
    unless LogicalPath.canonical_input?(path, index)
      raise TypeError, "input.files[#{index}].logical_path must be input/NNNN/BASENAME"
    end

    digest(file.fetch("sha256"), "input.files[#{index}].sha256")
    nonnegative_integer(file.fetch("bytesize"), "input.files[#{index}].bytesize")
  end
  digest(input.fetch("digest"), "input.digest")
  equal(input.fetch("digest"), TableArtifact::Serializer.digest(files), "input.digest")
end

#validate_inputs!(manifest, report) ⇒ void

This method returns an undefined value.

RBS:

  • (Hash[String, json_value] manifest, Hash[String, json_value] report) -> void

Parameters:

  • manifest (Hash[String, json_value])
  • report (Hash[String, json_value])


222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/ibex/verification_report/validator.rb', line 222

def validate_inputs!(manifest, report)
  manifest_files = manifest.dig("input", "files").map.with_index do |entry, index|
    [
      LogicalPath.input(entry.fetch("path"), index),
      "sha256:#{entry.fetch('sha256')}", entry.fetch("bytesize")
    ]
  end
  report_files = report.dig("input", "files").map do |entry|
    [entry.fetch("logical_path"), entry.fetch("sha256"), entry.fetch("bytesize")]
  end
  raise TypeError, "report input identity does not match manifest input" unless report_files == manifest_files
end

#validate_ir(value) ⇒ void

This method returns an undefined value.

RBS:

  • (json_value value) -> void

Parameters:

  • value (json_value)


137
138
139
140
141
142
143
144
145
146
147
# File 'lib/ibex/verification_report/validator.rb', line 137

def validate_ir(value)
  ir = object(value, IR_KEYS, "ir")
  equal(ir.fetch("identity_scope"), IR_IDENTITY_SCOPE, "ir.identity_scope")
  grammar = object(ir.fetch("grammar"), GRAMMAR_KEYS, "ir.grammar")
  automaton = object(ir.fetch("automaton"), AUTOMATON_KEYS, "ir.automaton")
  positive_integer(grammar.fetch("schema_version"), "ir.grammar.schema_version")
  positive_integer(automaton.fetch("schema_version"), "ir.automaton.schema_version")
  digest(grammar.fetch("digest"), "ir.grammar.digest")
  digest(automaton.fetch("digest"), "ir.automaton.digest")
  enum(automaton.fetch("algorithm"), ALGORITHMS, "ir.automaton.algorithm")
end

#validate_manifest_bytes!(entry, source, label) ⇒ void

This method returns an undefined value.

RBS:

  • (Hash[String, json_value] entry, String source, String label) -> void

Parameters:

  • entry (Hash[String, json_value])
  • source (String)
  • label (String)


215
216
217
218
219
# File 'lib/ibex/verification_report/validator.rb', line 215

def validate_manifest_bytes!(entry, source, label)
  expected_digest = Digest::SHA256.hexdigest(source)
  raise TypeError, "#{label} manifest bytesize mismatch" unless entry.fetch("bytesize") == source.bytesize
  raise TypeError, "#{label} manifest digest mismatch" unless entry.fetch("sha256") == expected_digest
end

#validate_outcome(value, profile) ⇒ void

This method returns an undefined value.

RBS:

  • (json_value value, String profile) -> void

Parameters:

  • value (json_value)
  • profile (String)


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/ibex/verification_report/validator.rb', line 164

def validate_outcome(value, profile)
  outcome = object(value, OUTCOME_KEYS, "outcome")
  status = enum(outcome.fetch("status"), %w[pass violations exhausted], "outcome.status")
  expected_checks = Verify::Verifier::DEFAULT_CHECKS +
                    (profile == "strict" ? Verify::Verifier::STRICT_CHECKS : [])
  requested = checks(outcome.fetch("requested_checks"), "outcome.requested_checks")
  executed = checks(outcome.fetch("executed_checks"), "outcome.executed_checks")
  equal(requested, expected_checks, "outcome.requested_checks")
  violations = validate_violations(outcome.fetch("violations"), requested)

  if status == "exhausted"
    equal(executed, [], "outcome.executed_checks")
    equal(violations, [], "outcome.violations")
    exhaustion = object(outcome.fetch("exhaustion"), EXHAUSTION_KEYS, "outcome.exhaustion")
    equal(exhaustion.fetch("kind"), "reference_collection_budget", "outcome.exhaustion.kind")
    string(exhaustion.fetch("message"), "outcome.exhaustion.message")
    return
  end

  equal(executed, requested, "outcome.executed_checks")
  equal(outcome.fetch("exhaustion"), nil, "outcome.exhaustion")
  if status == "pass"
    equal(violations, [], "outcome.violations")
  elsif violations.empty?
    raise TypeError, "outcome.violations must not be empty for violations status"
  end
end

#validate_table(value) ⇒ void

This method returns an undefined value.

RBS:

  • (json_value value) -> void

Parameters:

  • value (json_value)


150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/ibex/verification_report/validator.rb', line 150

def validate_table(value)
  table = object(value, TABLE_KEYS, "table")
  unless LogicalPath.canonical_table?(table.fetch("logical_path"))
    raise TypeError, "table.logical_path must be table/BASENAME"
  end

  equal(table.fetch("artifact_type"), TableArtifact::ARTIFACT_TYPE, "table.artifact_type")
  equal(table.fetch("schema_version"), TableArtifact::SCHEMA_VERSION, "table.schema_version")
  enum(table.fetch("representation"), %w[plain compact], "table.representation")
  digest(table.fetch("artifact_digest"), "table.artifact_digest")
  digest(table.fetch("payload_digest"), "table.payload_digest")
end

#validate_table_binding!(report, table, table_entry) ⇒ void

This method returns an undefined value.

RBS:

  • (Hash[String, json_value] report, TableArtifact::Document table, Hash[String, json_value] table_entry) -> void

Parameters:



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/ibex/verification_report/validator.rb', line 237

def validate_table_binding!(report, table, table_entry)
  claim = report.fetch("table") #: Hash[String, json_value]
  table_path = table_entry.fetch("path") #: String
  table_sha256 = table_entry.fetch("sha256") #: String
  expected_logical_path = LogicalPath.table(table_path)
  equal(claim.fetch("logical_path"), expected_logical_path, "table.logical_path")
  equal(claim.fetch("artifact_digest"), "sha256:#{table_sha256}", "table.artifact_digest")
  equal(claim.fetch("payload_digest"), table.identity.fetch("payload_digest"), "table.payload_digest")
  equal(claim.fetch("representation"), table.payload.dig("table_format", "representation"),
        "table.representation")
  equal(report.dig("ir", "grammar", "digest"), table.identity.fetch("grammar_digest"),
        "ir.grammar.digest")
  equal(report.dig("ir", "automaton", "digest"), table.identity.fetch("automaton_digest"),
        "ir.automaton.digest")
end

#validate_violations(value, requested) ⇒ Array[json_value]

RBS:

  • (json_value value, Array[String] requested) -> Array[json_value]

Parameters:

  • value (json_value)
  • requested (Array[String])

Returns:

  • (Array[json_value])


193
194
195
196
197
198
199
200
201
202
203
# File 'lib/ibex/verification_report/validator.rb', line 193

def validate_violations(value, requested)
  violations = array(value, "outcome.violations")
  violations.each_with_index do |entry, index|
    violation = object(entry, VIOLATION_KEYS, "outcome.violations[#{index}]")
    id = string(violation.fetch("id"), "outcome.violations[#{index}].id")
    enum(id, requested, "outcome.violations[#{index}].id")
    string(violation.fetch("location"), "outcome.violations[#{index}].location", allow_empty: true)
    string(violation.fetch("message"), "outcome.violations[#{index}].message")
  end
  violations
end