kairos-chain 3.43.0 → 3.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -0
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/synoptis/bin/khab_verify.rb +172 -0
- data/templates/skillsets/synoptis/bin/map_verify.rb +250 -0
- data/templates/skillsets/synoptis/bin/rpr_verify.rb +171 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/anchoring_rule.rb +184 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/attestation_types.rb +107 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/chain_credential.rb +187 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/containment.rb +17 -1
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/conventions/khab-1.md +112 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/conventions/map-1.md +198 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/conventions/rpr-1.md +141 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/cumulative_commitment.rb +229 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/deposit_board.rb +4 -2
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/entry.rb +28 -1
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/head_binding.rb +294 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/log.rb +15 -3
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/public_verifier.rb +5 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/reproduction.rb +316 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/succession.rb +222 -0
- data/templates/skillsets/synoptis/lib/synoptis/anchoring/write_path.rb +5 -2
- data/templates/skillsets/synoptis/test/test_head_anchor.rb +431 -0
- data/templates/skillsets/synoptis/test/test_map_anchoring.rb +359 -0
- data/templates/skillsets/synoptis/test/test_rpr_reproduction.rb +238 -0
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 372dd921a298d45c97b957ae724a253bb22da73b5bb211025fe894eab5de4ff7
|
|
4
|
+
data.tar.gz: f15e37a2caeef2d4d08b92f49608aa52307b1a93124a1742d3f40c10ae145bf8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9fc60d9acee8dcb8b7a2bfb43021d3c8b129cb94471fe8488092683ade4755665faa72860fb8c1bed77d254e977226c3d8cd85bec4dfe95eea2d0dc12352190
|
|
7
|
+
data.tar.gz: e5769c4b23ce1daef9704423f41e097d8969f5b86442ab1592beab4e3dc7623a3a79e30aba51d7ca2d5621e55fd635db740b5489761995bc2b9b33de8eb0b80e
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,72 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [3.46.0] - 2026-07-22
|
|
8
|
+
|
|
9
|
+
### Synoptis — reproduction endorsement rpr-1 (AUD-L3 slice 1)
|
|
10
|
+
|
|
11
|
+
Implements AUD-L3 design v0.4 (RPR-1..5, review-converged) slice 1 in the
|
|
12
|
+
synoptis SkillSet. All additive: rpr-1 builds on map-1/khab-1 exactly as
|
|
13
|
+
map-1 built on khab-1; no pre-existing file changed.
|
|
14
|
+
|
|
15
|
+
- `conventions/rpr-1.md`: content-addressed reproduction-endorsement
|
|
16
|
+
convention — re-execution target (inputs/environment/pipeline/output
|
|
17
|
+
digests; the first three form the committed computation identification,
|
|
18
|
+
output excluded), target-bound result-free tolerance declaration
|
|
19
|
+
(bit-identity at rpr-1), reproduction endorsement carrying the adjudicated
|
|
20
|
+
verdict (reproduced / not-reproduced) and its adjudication mode
|
|
21
|
+
(hand / procedure), declaration-set assessment (order-independent, sibling
|
|
22
|
+
targets pooled by computation identification, residue disclosed), map-1 §3
|
|
23
|
+
retraction reused unchanged.
|
|
24
|
+
- `reproduction.rb`: build/parse/digest for target, tolerance, endorsement
|
|
25
|
+
(closed schemas, canonical-serialization equality, refuse-not-coerce);
|
|
26
|
+
endorsement signing/verification via the unchanged map-1 §1.1 attestation
|
|
27
|
+
signature (ChainCredential); foreignness conformance check (RPR-4);
|
|
28
|
+
declaration-set assessment reporting invoked commitment (earliest
|
|
29
|
+
anterior), posterior commitments, digest-tiebroken rank, distinct-digest
|
|
30
|
+
multiplicity, and unresolved-equivalence residue.
|
|
31
|
+
- `bin/rpr_verify.rb`: offline auditor verifier (target / tolerance /
|
|
32
|
+
endorsement / assess / convention). Strict intake (at most one trailing
|
|
33
|
+
LF via delete_suffix — CRLF refused), canonical base-10 numerals only,
|
|
34
|
+
exit-code discipline (0 verified/report, 1 rejected, 2 unresolvable),
|
|
35
|
+
RPR-1/RPR-4 condition disclosure in output.
|
|
36
|
+
- `test/test_rpr_reproduction.rb`: 37 design-constraint assertions
|
|
37
|
+
(RPR-1..5), including presentation-order independence of the assessment
|
|
38
|
+
and rank, CRLF/padding refusal, and map-1 non-interference (75/75
|
|
39
|
+
regression green).
|
|
40
|
+
|
|
41
|
+
Implementation review: 3 rounds, R3 5/5 APPROVE, zero remaining P0/P1
|
|
42
|
+
(executable-probe personas; dispositions in docs/drafts/).
|
|
43
|
+
|
|
44
|
+
## [3.45.0] - 2026-07-21
|
|
45
|
+
|
|
46
|
+
### Synoptis — mutual anchoring protocol MAP-1..4 (map-1, AUD-L2)
|
|
47
|
+
|
|
48
|
+
Implements AUD-L2 design v0.5 (FROZEN) slice 1 in the synoptis SkillSet:
|
|
49
|
+
|
|
50
|
+
- `conventions/map-1.md`: content-addressed mutual-anchoring convention
|
|
51
|
+
(chain identity credential, typed attestation vocabulary, succession
|
|
52
|
+
records, declared anchoring rule, changeover event). Builds on khab-1
|
|
53
|
+
without modifying it.
|
|
54
|
+
- `chain_credential.rb`: Ed25519 self-authenticating chain identity
|
|
55
|
+
credential — verification needs no registry and no network.
|
|
56
|
+
- `succession.rb`: succession governance (earliest non-retracted designation
|
|
57
|
+
governs; at-or-before changeover boundary; issuer-only retraction;
|
|
58
|
+
retract-and-redesignate trails surfaced in the contested register).
|
|
59
|
+
- `anchoring_rule.rb`: result-free declared anchoring rule (closed schema)
|
|
60
|
+
with a coverage checker that reports and never enforces.
|
|
61
|
+
- `attestation_types.rb`: MAP-4 vocabulary intake validation and anchor-log
|
|
62
|
+
retraction coherence (issuer = depositor at map-1, disclosed deferral).
|
|
63
|
+
- `entry.rb` / `log.rb` / `write_path.rb`: additive `attestation_type` on
|
|
64
|
+
newly appended entries only (AHM-4 byte-invariance preserved).
|
|
65
|
+
- `bin/map_verify.rb`: standalone offline verifier (credential, attestation,
|
|
66
|
+
succession, coverage, pair, retraction subcommands).
|
|
67
|
+
- `test/test_map_anchoring.rb`: 75 design-constraint tests.
|
|
68
|
+
|
|
69
|
+
Design review converged R5 5/6 APPROVE; implementation review converged
|
|
70
|
+
R4 5/5 APPROVE with zero residual P0/P1. The inaugural mutual anchor with a
|
|
71
|
+
second, independently operated HestiaChain instance is deliberately pending.
|
|
72
|
+
|
|
7
73
|
## [3.42.2] - 2026-07-14
|
|
8
74
|
|
|
9
75
|
### Knowledge — loop_engineering_patterns §B: Model/Effort budget allocation
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# khab_verify — offline auditor verifier for khab-1 head bindings (MPR-4).
|
|
5
|
+
#
|
|
6
|
+
# Verifies inclusion and consistency proofs with EXACTLY the disclosed trust
|
|
7
|
+
# base: the proof artifact, the published head binding(s), and the shipped
|
|
8
|
+
# khab-1 convention definition. No internal-chain access, no operator
|
|
9
|
+
# cooperation, no network. (The authentic anchor-log view — that the binding
|
|
10
|
+
# really is committed at its claimed position — is checked against the log's
|
|
11
|
+
# public verification surface, outside this script.)
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# khab_verify.rb inclusion <proof.json> <binding.json>
|
|
15
|
+
# khab_verify.rb consistency <proof.json> <earlier_binding.json> <later_binding.json>
|
|
16
|
+
#
|
|
17
|
+
# A binding file may be the bare head_binding object, or any JSON object
|
|
18
|
+
# carrying it under "head_binding" (e.g. a public-verifier record or an anchor
|
|
19
|
+
# entry body).
|
|
20
|
+
#
|
|
21
|
+
# Exit status: 0 = VERIFIED, 1 = REJECTED, 2 = usage / unresolvable input.
|
|
22
|
+
|
|
23
|
+
require 'json'
|
|
24
|
+
require 'digest'
|
|
25
|
+
require_relative '../lib/synoptis/anchoring/cumulative_commitment'
|
|
26
|
+
|
|
27
|
+
CONVENTION_PATH = File.expand_path('../lib/synoptis/anchoring/conventions/khab-1.md', __dir__)
|
|
28
|
+
CC = Synoptis::Anchoring::CumulativeCommitment
|
|
29
|
+
|
|
30
|
+
def die(msg)
|
|
31
|
+
warn "khab_verify: #{msg}"
|
|
32
|
+
exit 2
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reject(msg)
|
|
36
|
+
puts "REJECTED: #{msg}"
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def load_json(path)
|
|
41
|
+
parsed = JSON.parse(File.read(path))
|
|
42
|
+
die "#{path}: expected a JSON object, got #{parsed.class}" unless parsed.is_a?(Hash)
|
|
43
|
+
parsed
|
|
44
|
+
rescue JSON::ParserError, SystemCallError => e
|
|
45
|
+
die "cannot read #{path}: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extract_binding(obj, path)
|
|
49
|
+
b = obj.is_a?(Hash) && obj.key?('head_binding') ? obj['head_binding'] : obj
|
|
50
|
+
die "#{path} carries no head binding" unless b.is_a?(Hash) && b['convention']
|
|
51
|
+
b
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Structural well-formedness of a binding's verifiable fields, checked BEFORE
|
|
55
|
+
# any verdict logic. The strongest claims this verifier can emit (VERIFIED,
|
|
56
|
+
# DIVERGENCE) must be unreachable from malformed input: a missing or
|
|
57
|
+
# non-canonical root/extent is unresolvable input (exit 2), never evidence.
|
|
58
|
+
def require_wellformed!(binding, path)
|
|
59
|
+
unless binding['cumulative_root'].is_a?(String) && binding['cumulative_root'].match?(/\A[a-f0-9]{64}\z/)
|
|
60
|
+
die "#{path}: cumulative_root missing or not 64-char lowercase hex (khab-1 §4)"
|
|
61
|
+
end
|
|
62
|
+
unless binding['tree_size'].is_a?(Integer) && binding['tree_size'].positive? && binding['tree_size'] < 2**53
|
|
63
|
+
die "#{path}: tree_size missing or not a positive JSON-safe integer (khab-1 §4)"
|
|
64
|
+
end
|
|
65
|
+
return if binding['chain_identity'].is_a?(String) &&
|
|
66
|
+
binding['chain_identity'].match?(/\Ablock1-sha256:[a-f0-9]{64}\z/)
|
|
67
|
+
|
|
68
|
+
die "#{path}: chain_identity missing or not in khab-1 §5 canonical form (block1-sha256:<64-hex>)"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# MPR-3: the convention identifier must resolve to a definition whose own
|
|
72
|
+
# integrity the auditor can verify. A digest mismatch means the binding was
|
|
73
|
+
# computed under a definition this verifier does not hold — unresolvable, so
|
|
74
|
+
# verification refuses rather than guessing.
|
|
75
|
+
def check_convention!(binding)
|
|
76
|
+
reject "unknown convention #{binding['convention'].inspect} (this verifier implements khab-1)" unless
|
|
77
|
+
binding['convention'] == 'khab-1'
|
|
78
|
+
local = begin
|
|
79
|
+
Digest::SHA256.hexdigest(File.binread(CONVENTION_PATH))
|
|
80
|
+
rescue SystemCallError => e
|
|
81
|
+
die "khab-1 convention definition unreadable at #{CONVENTION_PATH}: #{e.message}"
|
|
82
|
+
end
|
|
83
|
+
return if binding['convention_sha256'] == local
|
|
84
|
+
|
|
85
|
+
reject "convention_sha256 #{binding['convention_sha256']} does not match the held khab-1 " \
|
|
86
|
+
"definition (#{local}); convention unresolvable"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def require_match(proof, binding, proof_key, binding_key, label)
|
|
90
|
+
return if proof[proof_key] == binding[binding_key]
|
|
91
|
+
|
|
92
|
+
reject "proof #{label} #{proof[proof_key].inspect} does not match binding #{binding[binding_key].inspect}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
mode = ARGV.shift
|
|
96
|
+
|
|
97
|
+
case mode
|
|
98
|
+
when 'inclusion'
|
|
99
|
+
die 'usage: khab_verify.rb inclusion <proof.json> <binding.json>' unless ARGV.size == 2
|
|
100
|
+
proof = load_json(ARGV[0])
|
|
101
|
+
binding = extract_binding(load_json(ARGV[1]), ARGV[1])
|
|
102
|
+
check_convention!(binding)
|
|
103
|
+
require_wellformed!(binding, ARGV[1])
|
|
104
|
+
reject "proof format #{proof['format'].inspect} is not khab-1/inclusion" unless proof['format'] == 'khab-1/inclusion'
|
|
105
|
+
require_match(proof, binding, 'chain_identity', 'chain_identity', 'chain_identity')
|
|
106
|
+
require_match(proof, binding, 'tree_size', 'tree_size', 'tree_size')
|
|
107
|
+
require_match(proof, binding, 'cumulative_root', 'cumulative_root', 'cumulative_root')
|
|
108
|
+
|
|
109
|
+
ok = CC.verify_inclusion(
|
|
110
|
+
record_commitment: proof['record_commitment'],
|
|
111
|
+
index: proof['index'],
|
|
112
|
+
tree_size: binding['tree_size'],
|
|
113
|
+
path: proof['path'],
|
|
114
|
+
root: binding['cumulative_root']
|
|
115
|
+
)
|
|
116
|
+
reject 'inclusion proof does not recompute the committed cumulative root' unless ok
|
|
117
|
+
|
|
118
|
+
puts 'VERIFIED: record commitment ' \
|
|
119
|
+
"#{proof['record_commitment']} is member ##{proof['index']} of the anchored state " \
|
|
120
|
+
"(tree_size #{binding['tree_size']}, chain #{binding['chain_identity']})."
|
|
121
|
+
puts 'Scope (MPR-5/6): membership, integrity at anchor time, position, order — nothing about ' \
|
|
122
|
+
'content quality or completeness of anchoring; commitment, not content.'
|
|
123
|
+
|
|
124
|
+
when 'consistency'
|
|
125
|
+
die 'usage: khab_verify.rb consistency <proof.json> <earlier.json> <later.json>' unless ARGV.size == 3
|
|
126
|
+
proof = load_json(ARGV[0])
|
|
127
|
+
earlier = extract_binding(load_json(ARGV[1]), ARGV[1])
|
|
128
|
+
later = extract_binding(load_json(ARGV[2]), ARGV[2])
|
|
129
|
+
check_convention!(earlier)
|
|
130
|
+
check_convention!(later)
|
|
131
|
+
require_wellformed!(earlier, ARGV[1])
|
|
132
|
+
require_wellformed!(later, ARGV[2])
|
|
133
|
+
reject "proof format #{proof['format'].inspect} is not khab-1/consistency" unless proof['format'] == 'khab-1/consistency'
|
|
134
|
+
# MPR-9: extension-relatedness is defined only over the same committed chain
|
|
135
|
+
# identity; different identities terminate the claim rather than failing it.
|
|
136
|
+
reject "bindings commit different chain identities (#{earlier['chain_identity']} vs " \
|
|
137
|
+
"#{later['chain_identity']}); extension claim terminated across an identity change" unless
|
|
138
|
+
earlier['chain_identity'] == later['chain_identity']
|
|
139
|
+
# Equal committed extent with different committed roots needs no proof at
|
|
140
|
+
# all: the two states differ at the same size, so no extension relation can
|
|
141
|
+
# exist. This is arithmetic on the committed bindings — a POSITIVE
|
|
142
|
+
# divergence witness (MPR-9), unlike a mere proof-verification failure.
|
|
143
|
+
if earlier['tree_size'] == later['tree_size'] && earlier['cumulative_root'] != later['cumulative_root']
|
|
144
|
+
puts "DIVERGENCE: both bindings commit chain #{later['chain_identity']} at extent " \
|
|
145
|
+
"#{later['tree_size']} with different cumulative roots; conclusive evidence of " \
|
|
146
|
+
'rewriting or forking (MPR-9)'
|
|
147
|
+
exit 1
|
|
148
|
+
end
|
|
149
|
+
require_match(proof, earlier, 'first_root', 'cumulative_root', 'first_root')
|
|
150
|
+
require_match(proof, later, 'second_root', 'cumulative_root', 'second_root')
|
|
151
|
+
require_match(proof, earlier, 'first_size', 'tree_size', 'first_size')
|
|
152
|
+
require_match(proof, later, 'second_size', 'tree_size', 'second_size')
|
|
153
|
+
|
|
154
|
+
ok = CC.verify_consistency(
|
|
155
|
+
first_root: earlier['cumulative_root'],
|
|
156
|
+
first_size: earlier['tree_size'],
|
|
157
|
+
second_root: later['cumulative_root'],
|
|
158
|
+
second_size: later['tree_size'],
|
|
159
|
+
path: proof['path']
|
|
160
|
+
)
|
|
161
|
+
# Design §11: failure of an operator-supplied proof is NOT by itself a
|
|
162
|
+
# positive inconsistency witness — report it as unestablished, not as forgery.
|
|
163
|
+
reject 'consistency proof does not verify; extension claim remains UNESTABLISHED ' \
|
|
164
|
+
'(not by itself evidence of rewriting — MPR-9)' unless ok
|
|
165
|
+
|
|
166
|
+
puts "VERIFIED: the later anchored state (tree_size #{later['tree_size']}) commits to an " \
|
|
167
|
+
"extension of the earlier state (tree_size #{earlier['tree_size']}) unchanged as a prefix " \
|
|
168
|
+
"(chain #{later['chain_identity']})."
|
|
169
|
+
|
|
170
|
+
else
|
|
171
|
+
die "usage: khab_verify.rb {inclusion|consistency} <proof.json> <binding(s).json...>"
|
|
172
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# map_verify — offline auditor verifier for map-1 mutual-anchoring artifacts
|
|
5
|
+
# (MAP-1..4, aud_l2_mutual_anchoring_design v0.5).
|
|
6
|
+
#
|
|
7
|
+
# Verifies with EXACTLY the disclosed trust base: the artifacts named on the
|
|
8
|
+
# command line plus the shipped map-1 convention definition. No registry, no
|
|
9
|
+
# network, no operator cooperation. (Authentic log/chain views are the
|
|
10
|
+
# caller's inputs; their own integrity is checked by the khab-1 surface.)
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# map_verify.rb credential <credential.json>
|
|
14
|
+
# map_verify.rb attestation <credential.json> <payload-file> <signature-hex>
|
|
15
|
+
# map_verify.rb succession <old_credential.json> <records.json> [changeover_position]
|
|
16
|
+
# records.json = JSON array of internal-chain record STRINGS, committed order
|
|
17
|
+
# map_verify.rb coverage <rule-file> <anchors.json> [chain_extent] [rule_position] [rule_moment] [now]
|
|
18
|
+
# anchors.json = JSON array of {"tree_size":..,"moment":".."} observations;
|
|
19
|
+
# every_n_records needs chain_extent (+ optional rule_position);
|
|
20
|
+
# every_n_days needs rule_moment and now (ISO8601) as args 5 and 6 —
|
|
21
|
+
# pass 0 0 for the unused args 3 and 4.
|
|
22
|
+
# map_verify.rb pair <log_a.json> <identity_a> <log_b.json> <identity_b>
|
|
23
|
+
# log_*.json = JSON array of anchor-entry objects (entry.to_h shape);
|
|
24
|
+
# reports what a verified pair establishes (MAP-1) — and ONLY that.
|
|
25
|
+
# map_verify.rb retraction <log.json> <retraction_entry_hash>
|
|
26
|
+
# checks map-1 §3 coherence of an anchor-log retraction against its target.
|
|
27
|
+
#
|
|
28
|
+
# Exit status: 0 = VERIFIED/REPORT, 1 = REJECTED, 2 = usage / unresolvable.
|
|
29
|
+
|
|
30
|
+
require 'json'
|
|
31
|
+
require 'digest'
|
|
32
|
+
require_relative '../lib/synoptis/anchoring/entry'
|
|
33
|
+
require_relative '../lib/synoptis/anchoring/chain_credential'
|
|
34
|
+
require_relative '../lib/synoptis/anchoring/succession'
|
|
35
|
+
require_relative '../lib/synoptis/anchoring/anchoring_rule'
|
|
36
|
+
require_relative '../lib/synoptis/anchoring/attestation_types'
|
|
37
|
+
|
|
38
|
+
ENTRY = Synoptis::Anchoring::Entry
|
|
39
|
+
CRED = Synoptis::Anchoring::ChainCredential
|
|
40
|
+
SUCC = Synoptis::Anchoring::Succession
|
|
41
|
+
RULE = Synoptis::Anchoring::AnchoringRule
|
|
42
|
+
ATYPE = Synoptis::Anchoring::AttestationTypes
|
|
43
|
+
|
|
44
|
+
def die(msg)
|
|
45
|
+
warn "map_verify: #{msg}"
|
|
46
|
+
exit 2
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reject(msg)
|
|
50
|
+
puts "REJECTED: #{msg}"
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def load_json(path)
|
|
55
|
+
JSON.parse(File.read(path))
|
|
56
|
+
rescue JSON::ParserError, SystemCallError => e
|
|
57
|
+
die "cannot read #{path}: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Malformed caller input is unresolvable (exit 2), never a verdict.
|
|
61
|
+
# Explicit base 10: Integer('010') would read octal and silently shift a
|
|
62
|
+
# governance boundary.
|
|
63
|
+
def strict_int(value, label)
|
|
64
|
+
# Digits only: Integer() would also accept '1_0' and padded whitespace.
|
|
65
|
+
die "#{label} must be a base-10 integer, got #{value.inspect}" unless value.is_a?(String) && value.match?(/\A\d+\z/)
|
|
66
|
+
Integer(value, 10)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def load_json_hash(path, label)
|
|
70
|
+
parsed = load_json(path)
|
|
71
|
+
die "#{path}: #{label} must be a JSON object, got #{parsed.class}" unless parsed.is_a?(Hash)
|
|
72
|
+
parsed
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
HEX64 = /\A[a-f0-9]{64}\z/
|
|
76
|
+
|
|
77
|
+
# Structural well-formedness of a matched head binding (khab-1 §4 shapes).
|
|
78
|
+
# The strongest claim this tool prints must be unreachable from malformed
|
|
79
|
+
# input: a garbage binding is unresolvable, never PAIR ESTABLISHED.
|
|
80
|
+
def require_binding_wellformed!(binding, where)
|
|
81
|
+
die "#{where}: head_binding.convention is #{binding['convention'].inspect}, not khab-1" unless binding['convention'] == 'khab-1'
|
|
82
|
+
%w[convention_sha256 cumulative_root].each do |k|
|
|
83
|
+
die "#{where}: head_binding.#{k} missing or not 64-char lowercase hex" unless binding[k].is_a?(String) && binding[k].match?(HEX64)
|
|
84
|
+
end
|
|
85
|
+
die "#{where}: head_binding.tree_size must be a positive Integer" unless binding['tree_size'].is_a?(Integer) && binding['tree_size'].positive?
|
|
86
|
+
unless binding['chain_identity'].is_a?(String) && binding['chain_identity'].match?(/\Ablock1-sha256:[a-f0-9]{64}\z/)
|
|
87
|
+
die "#{where}: head_binding.chain_identity is not block1-sha256:<64-hex> (khab-1 §5)"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Recompute an entry's committed hash from its canonical content. Detects an
|
|
92
|
+
# in-place edited entry without any chain access; the full log hash chain is
|
|
93
|
+
# the khab-1 surface's business (khab_verify / public verifier).
|
|
94
|
+
def require_entry_hash!(entry_h, where)
|
|
95
|
+
content = { 'position' => entry_h['position'], 'prev' => entry_h['prev'],
|
|
96
|
+
'kind' => entry_h['kind'], 'body' => entry_h['body'] }
|
|
97
|
+
recomputed = ENTRY.compute_hash(content)
|
|
98
|
+
return if recomputed == entry_h['entry_hash']
|
|
99
|
+
|
|
100
|
+
reject "#{where}: entry_hash does not recompute from committed content (edited in place?)"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
mode = ARGV.shift
|
|
104
|
+
|
|
105
|
+
case mode
|
|
106
|
+
when 'credential'
|
|
107
|
+
die 'usage: map_verify.rb credential <credential.json>' unless ARGV.size == 1
|
|
108
|
+
cred = load_json_hash(ARGV[0], 'credential')
|
|
109
|
+
begin
|
|
110
|
+
CRED.validate!(cred)
|
|
111
|
+
rescue CRED::CredentialError => e
|
|
112
|
+
reject e.message
|
|
113
|
+
end
|
|
114
|
+
puts "VERIFIED: credential digest #{CRED.credential_digest(cred)} binds #{cred['chain_identity']}"
|
|
115
|
+
puts 'NOTE: binding is a SELF-attestation (map-1 §1); that the credential speaks'
|
|
116
|
+
puts 'for the chain requires the chain to commit the credential digest (map-1 §2).'
|
|
117
|
+
|
|
118
|
+
when 'attestation'
|
|
119
|
+
die 'usage: map_verify.rb attestation <credential.json> <payload-file> <signature-hex>' unless ARGV.size == 3
|
|
120
|
+
cred = load_json_hash(ARGV[0], 'credential')
|
|
121
|
+
payload = begin
|
|
122
|
+
File.binread(ARGV[1])
|
|
123
|
+
rescue SystemCallError => e
|
|
124
|
+
die "cannot read #{ARGV[1]}: #{e.message}"
|
|
125
|
+
end
|
|
126
|
+
begin
|
|
127
|
+
ok = CRED.verify_attestation(cred, payload, ARGV[2])
|
|
128
|
+
rescue CRED::CredentialError => e
|
|
129
|
+
reject e.message
|
|
130
|
+
end
|
|
131
|
+
reject 'attestation signature does not verify under the credential' unless ok
|
|
132
|
+
puts "VERIFIED: attestation signature valid under credential #{CRED.credential_digest(cred)[0, 12]}…"
|
|
133
|
+
|
|
134
|
+
when 'succession'
|
|
135
|
+
die 'usage: map_verify.rb succession <old_credential.json> <records.json> [changeover_position]' unless (2..3).cover?(ARGV.size)
|
|
136
|
+
cred = load_json_hash(ARGV[0], 'credential')
|
|
137
|
+
records = load_json(ARGV[1])
|
|
138
|
+
unless records.is_a?(Array) && records.all? { |r| r.is_a?(String) }
|
|
139
|
+
die "#{ARGV[1]}: expected a JSON array of record STRINGS (each element the exact committed record string)"
|
|
140
|
+
end
|
|
141
|
+
changeover = ARGV[2].nil? ? nil : strict_int(ARGV[2], 'changeover_position')
|
|
142
|
+
die 'changeover_position must be non-negative' if !changeover.nil? && changeover.negative?
|
|
143
|
+
begin
|
|
144
|
+
verdict = SUCC.governance(cred, records, changeover_position: changeover)
|
|
145
|
+
rescue CRED::CredentialError => e
|
|
146
|
+
reject e.message
|
|
147
|
+
end
|
|
148
|
+
puts JSON.pretty_generate(verdict)
|
|
149
|
+
puts 'NOTE: the verdict reports what the records show, never intent (key'
|
|
150
|
+
puts 'compromise is indistinguishable from the issuer — MAP-2 disclosed limit).'
|
|
151
|
+
|
|
152
|
+
when 'coverage'
|
|
153
|
+
unless (2..6).cover?(ARGV.size)
|
|
154
|
+
die 'usage: map_verify.rb coverage <rule-file> <anchors.json> [chain_extent] [rule_position] [rule_moment] [now]'
|
|
155
|
+
end
|
|
156
|
+
rule_string = begin
|
|
157
|
+
File.read(ARGV[0]).strip
|
|
158
|
+
rescue SystemCallError => e
|
|
159
|
+
die "cannot read #{ARGV[0]}: #{e.message}"
|
|
160
|
+
end
|
|
161
|
+
rule = begin
|
|
162
|
+
RULE.parse!(rule_string)
|
|
163
|
+
rescue RULE::RuleError => e
|
|
164
|
+
reject e.message
|
|
165
|
+
end
|
|
166
|
+
anchors = load_json(ARGV[1])
|
|
167
|
+
die "#{ARGV[1]}: expected a JSON array of observations" unless anchors.is_a?(Array)
|
|
168
|
+
extent = ARGV[2].nil? ? nil : strict_int(ARGV[2], 'chain_extent')
|
|
169
|
+
rule_pos = ARGV[3].nil? ? nil : strict_int(ARGV[3], 'rule_position')
|
|
170
|
+
rule_moment = ARGV[4]
|
|
171
|
+
now = ARGV[5]
|
|
172
|
+
# A tool capability gap is a usage error (exit 2), never a REJECTED verdict.
|
|
173
|
+
if rule['trigger'] == 'every_n_records' && extent.nil?
|
|
174
|
+
die 'every_n_records coverage needs chain_extent'
|
|
175
|
+
end
|
|
176
|
+
if rule['trigger'] == 'every_n_days' && (rule_moment.nil? || now.nil?)
|
|
177
|
+
die 'every_n_days coverage needs rule_moment and now (ISO8601), as arguments 5 and 6'
|
|
178
|
+
end
|
|
179
|
+
begin
|
|
180
|
+
report = RULE.coverage(rule_string, anchors, chain_extent: extent, rule_position: rule_pos,
|
|
181
|
+
rule_moment: rule_moment, now: now)
|
|
182
|
+
rescue RULE::RuleError => e
|
|
183
|
+
reject e.message
|
|
184
|
+
end
|
|
185
|
+
puts JSON.pretty_generate(report)
|
|
186
|
+
|
|
187
|
+
when 'pair'
|
|
188
|
+
die 'usage: map_verify.rb pair <log_a.json> <identity_a> <log_b.json> <identity_b>' unless ARGV.size == 4
|
|
189
|
+
log_a, id_a, log_b, id_b = load_json(ARGV[0]), ARGV[1], load_json(ARGV[2]), ARGV[3]
|
|
190
|
+
[[log_a, ARGV[0]], [log_b, ARGV[2]]].each do |(log, path)|
|
|
191
|
+
die "#{path}: expected a JSON array of anchor-entry objects" unless log.is_a?(Array)
|
|
192
|
+
end
|
|
193
|
+
find = lambda do |log, identity, where|
|
|
194
|
+
log.each_with_index.filter_map do |entry, i|
|
|
195
|
+
e = entry.is_a?(Hash) ? entry.transform_keys(&:to_s) : {}
|
|
196
|
+
# Only anchor entries carry head bindings; anything else shaped to look
|
|
197
|
+
# like one never counts toward a pair.
|
|
198
|
+
next unless e['kind'] == 'anchor'
|
|
199
|
+
|
|
200
|
+
body = e['body'].is_a?(Hash) ? e['body'] : {}
|
|
201
|
+
hb = body['head_binding']
|
|
202
|
+
next unless hb.is_a?(Hash) && hb['chain_identity'] == identity
|
|
203
|
+
|
|
204
|
+
require_binding_wellformed!(hb, "#{where} entry #{e['position'] || i}")
|
|
205
|
+
require_entry_hash!(e, "#{where} entry #{e['position'] || i}")
|
|
206
|
+
{ position: e['position'] || i, cumulative_root: hb['cumulative_root'],
|
|
207
|
+
tree_size: hb['tree_size'], attestation_type: body['attestation_type'] }
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
a_holds_b = find.call(log_a, id_b, ARGV[0])
|
|
211
|
+
b_holds_a = find.call(log_b, id_a, ARGV[2])
|
|
212
|
+
reject "no entry in #{ARGV[0]} commits a head binding of #{id_b}" if a_holds_b.empty?
|
|
213
|
+
reject "no entry in #{ARGV[2]} commits a head binding of #{id_a}" if b_holds_a.empty?
|
|
214
|
+
puts 'PAIR ESTABLISHED (MAP-1 — exactly this, nothing stronger):'
|
|
215
|
+
puts " #{id_b} head(s) committed in log A at #{a_holds_b.map { |e| e[:position] }.join(', ')}"
|
|
216
|
+
puts " #{id_a} head(s) committed in log B at #{b_holds_a.map { |e| e[:position] }.join(', ')}"
|
|
217
|
+
untyped = (a_holds_b + b_holds_a).count { |e| e[:attestation_type].nil? }
|
|
218
|
+
puts " untyped (pre-map-1, grandfathered) inscriptions: #{untyped}" if untyped.positive?
|
|
219
|
+
puts 'CONDITIONS (travel with every stronger reading, map-1 §6):'
|
|
220
|
+
puts ' - partner independence is auditor-supplied; a common operator can fabricate the pair'
|
|
221
|
+
puts ' - temporal weight reaches only as far as each log\'s own khab-1 anchoring'
|
|
222
|
+
puts ' - equivocation detection requires authentic views of BOTH logs and cross-comparison'
|
|
223
|
+
puts ' - matched entry hashes recomputed and binding shapes checked here; full log hash-chain'
|
|
224
|
+
puts ' and cumulative-root verification are the khab-1 surface\'s business (khab_verify)'
|
|
225
|
+
puts ' - this pair supplies split-view detection material; it closes nothing'
|
|
226
|
+
|
|
227
|
+
when 'retraction'
|
|
228
|
+
die 'usage: map_verify.rb retraction <log.json> <retraction_entry_hash>' unless ARGV.size == 2
|
|
229
|
+
log = load_json(ARGV[0])
|
|
230
|
+
die "#{ARGV[0]}: expected a JSON array of anchor-entry objects" unless log.is_a?(Array)
|
|
231
|
+
entries = log.map { |e| e.is_a?(Hash) ? e.transform_keys(&:to_s) : {} }
|
|
232
|
+
retraction = entries.find { |e| e['entry_hash'] == ARGV[1] }
|
|
233
|
+
die "no entry with entry_hash #{ARGV[1]} in #{ARGV[0]}" if retraction.nil?
|
|
234
|
+
body = retraction['body'].is_a?(Hash) ? retraction['body'] : {}
|
|
235
|
+
meta = body['metadata'].is_a?(Hash) ? body['metadata'] : {}
|
|
236
|
+
target_ref = meta['target_entry_hash']
|
|
237
|
+
unless target_ref.is_a?(String) && target_ref.match?(HEX64)
|
|
238
|
+
reject "retraction carries no resolvable metadata.target_entry_hash (got #{target_ref.inspect})"
|
|
239
|
+
end
|
|
240
|
+
target = entries.find { |e| e['entry_hash'] == target_ref }
|
|
241
|
+
reject "target entry #{target_ref} not present in the supplied log view" if target.nil?
|
|
242
|
+
require_entry_hash!(retraction, 'retraction')
|
|
243
|
+
require_entry_hash!(target, 'target')
|
|
244
|
+
verdict = ATYPE.retraction_coherence(retraction, target)
|
|
245
|
+
puts JSON.pretty_generate(verdict)
|
|
246
|
+
exit(verdict[:coherent] ? 0 : 1)
|
|
247
|
+
|
|
248
|
+
else
|
|
249
|
+
die 'usage: map_verify.rb credential|attestation|succession|coverage|pair|retraction ... (see header)'
|
|
250
|
+
end
|