kairos-chain 3.49.0 → 3.50.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 +60 -1
- data/bin/kairos-chain +30 -7
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/chain_distillation/config/chain_distillation.yml +13 -0
- data/templates/skillsets/chain_distillation/lib/chain_distillation/canon.rb +44 -0
- data/templates/skillsets/chain_distillation/lib/chain_distillation/certificate.rb +308 -0
- data/templates/skillsets/chain_distillation/lib/chain_distillation/distiller.rb +305 -0
- data/templates/skillsets/chain_distillation/lib/chain_distillation/recorder.rb +144 -0
- data/templates/skillsets/chain_distillation/skillset.json +18 -0
- data/templates/skillsets/chain_distillation/test/test_chain_distillation.rb +464 -0
- data/templates/skillsets/chain_distillation/tools/cd_distill.rb +67 -0
- data/templates/skillsets/chain_distillation/tools/cd_revoke.rb +57 -0
- data/templates/skillsets/chain_distillation/tools/cd_verify.rb +56 -0
- data/templates/skillsets/confidentiality_guard/config/profile.example.yml +9 -0
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/policy.rb +8 -0
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/recorder.rb +4 -1
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/regime.rb +4 -1
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/surfaces.rb +21 -0
- data/templates/skillsets/confidentiality_guard/lib/confidentiality_guard/verdict.rb +14 -0
- data/templates/skillsets/confidentiality_guard/test/test_cg_guard.rb +47 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 826aabda2632b42bb5e4c688f443616faf29b9b97daba55aa02fa6834bf1629d
|
|
4
|
+
data.tar.gz: 2a743cfa76eb0d56db2220ed40c11ea0cc52f88c9b16ed8dfb84855d54b15981
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0e0ab6e95d0f527e738053fe45e9899e761820809c0f26ab37dd55eacadf13b387b22737475a8685b894d095aa9ba4529df142ab3809adf917c2ff275b46f403
|
|
7
|
+
data.tar.gz: 1272d3d2ffd8cc80615dc1990346758982cb64cb78d947965d754d3ae5d0e148524044c8197efbb836e0ecf067cfcae69f51aa90a934a71da691525c0b190da2
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,65 @@ 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.50.0] - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Chain Distillation SkillSet — slice 1 (CD-1..CD-6)
|
|
10
|
+
|
|
11
|
+
New `chain_distillation` SkillSet implementing design v0.5 (review-converged
|
|
12
|
+
5/6 APPROVE + persona unanimity): provenance-certified distillation of the
|
|
13
|
+
instance chain into distributable SkillSets.
|
|
14
|
+
|
|
15
|
+
- `cd_distill`: closed-world integer designation (genesis excluded,
|
|
16
|
+
decline-not-coerce), guard-judged distillate and certificate crossings
|
|
17
|
+
(separate, in order, single-encoded presentation), CD-6 constitutive
|
|
18
|
+
record before release (salted sdp-1-style commitments over the distillate
|
|
19
|
+
and the claim core excluding the record citation), content-independent
|
|
20
|
+
pre-assigned certificate identity, attestation-carrier seam
|
|
21
|
+
(`proof_id` = certificate identity), designation-overlap re-issuance
|
|
22
|
+
citation against then-revoked predecessors.
|
|
23
|
+
- `cd_verify`: full-citation positive grounding (claim-core commitment
|
|
24
|
+
opening, identity, designation digest, guard policy sha, distillate
|
|
25
|
+
commitment, predecessors, citation block hash) plus chain-identity /
|
|
26
|
+
head-binding checks; pinned checkability-status table with
|
|
27
|
+
both-direction mislabeling fatal; closed claim-core vocabulary at every
|
|
28
|
+
depth; revocation folds into validity.
|
|
29
|
+
- `cd_revoke`: identity-keyed chain-wide revocation, closed reason
|
|
30
|
+
vocabulary (`superseded|defective|withdrawn|other`).
|
|
31
|
+
- Requires an active confidentiality-guard regime (CD-1: decline, never
|
|
32
|
+
degrade). 60 design-constraint tests incl. production-wiring and
|
|
33
|
+
real-Chain regressions (KAIROS_DATA_DIR-isolated).
|
|
34
|
+
|
|
35
|
+
### Confidentiality Guard — slice-2 first increment (distillation crossing)
|
|
36
|
+
|
|
37
|
+
- New `:distillation_outward` crossing class (`cd_release_distillate`,
|
|
38
|
+
`cd_release_certificate`): conjunctive per-destination verdict
|
|
39
|
+
(closed-world `distillation_crossings` profile designation AND content
|
|
40
|
+
classes), outward verdicts recorded pass or deny (CG-4); the general
|
|
41
|
+
outward class stays denied wholesale.
|
|
42
|
+
- Certificate-crossing verdict records cite the certificate identity
|
|
43
|
+
(identifier-only, CG-4-compatible).
|
|
44
|
+
- Template-layout require-path parity fix in `regime.rb` / `recorder.rb`.
|
|
45
|
+
- Guard test suite: 75 design-constraint + regression tests.
|
|
46
|
+
|
|
47
|
+
## [3.49.1] - 2026-07-22
|
|
48
|
+
|
|
49
|
+
### CLI fix — `skillset upgrade` messaging for non-core new SkillSets
|
|
50
|
+
|
|
51
|
+
`kairos-chain skillset upgrade --apply` always runs with `core_only: true`
|
|
52
|
+
(the plain CLI form has no way to pass explicit `names`), so a new,
|
|
53
|
+
non-core SkillSet (e.g. `confidentiality_guard`, `daemon_runtime`,
|
|
54
|
+
`kairos_hook_projector`) was never installed by `--apply`, regardless of
|
|
55
|
+
how many times it was run — but the check-mode display mislabeled these as
|
|
56
|
+
"files changed" and told the user to run `--apply`, which silently did
|
|
57
|
+
nothing for them.
|
|
58
|
+
|
|
59
|
+
- Detected upgrades are now split into "applicable" (existing-installed
|
|
60
|
+
diffs + core new SkillSets, which `--apply` does handle) and "manual"
|
|
61
|
+
(non-core new SkillSets, which require explicit
|
|
62
|
+
`kairos-chain skillset install <name>`). Both the check-mode listing and
|
|
63
|
+
the post-`--apply` summary report each group accurately.
|
|
64
|
+
- Added the missing `upgrade` line to `kairos-chain skillset -h`.
|
|
65
|
+
|
|
7
66
|
## [3.49.0] - 2026-07-22
|
|
8
67
|
|
|
9
68
|
### ZK aggregate reproducibility spike (AUD-L4, Phases 1+2) — KairosChain's first genuine zero-knowledge proof
|
|
@@ -65,7 +124,7 @@ resource-scheme reads are denied wholesale under the CG-1 coverage clause.
|
|
|
65
124
|
records carry the designation id, not the raw path. `canon.rb`: shared
|
|
66
125
|
canonical serialization faithful to false/nil and mixed string/symbol keys.
|
|
67
126
|
- `tools/cg_status.rb`: read-only regime-state tool (CG-1).
|
|
68
|
-
- 67 design-constraint + regression tests (`test/test_cg_guard.rb`).
|
|
127
|
+
- 67 design-constraint + regression tests (`test/test_cg_guard.rb`); 75 as of the chain_distillation guard slice-2 first increment (distillation crossing).
|
|
69
128
|
|
|
70
129
|
Core (minimal, guard-enabling): `tool_registry.rb` adds `FailClosedError`
|
|
71
130
|
(re-raised by the loader, never swallowed), per-skillset isolated loading,
|
data/bin/kairos-chain
CHANGED
|
@@ -134,6 +134,12 @@ when 'skillset'
|
|
|
134
134
|
when 'upgrade'
|
|
135
135
|
apply = ARGV.delete('--apply')
|
|
136
136
|
upgrades = manager.upgrade_check
|
|
137
|
+
# --apply always runs with core_only: true (no way to pass names from this
|
|
138
|
+
# CLI form), so a new, non-core SkillSet is NEVER installed by --apply,
|
|
139
|
+
# no matter how many times it is run. Split the two groups so the
|
|
140
|
+
# displayed guidance matches what --apply will actually do.
|
|
141
|
+
manual = upgrades.select { |u| u[:new_skillset] && !KairosMcp::SkillSetManager::CORE_SKILLSETS.include?(u[:name]) }
|
|
142
|
+
applicable = upgrades - manual
|
|
137
143
|
|
|
138
144
|
if upgrades.empty?
|
|
139
145
|
puts "All SkillSets are up to date."
|
|
@@ -144,6 +150,12 @@ when 'skillset'
|
|
|
144
150
|
end
|
|
145
151
|
puts "Done. #{results.size} SkillSet(s) upgraded."
|
|
146
152
|
|
|
153
|
+
if manual.any?
|
|
154
|
+
puts ""
|
|
155
|
+
puts "Not installed (optional, requires explicit approval):"
|
|
156
|
+
manual.each { |u| puts " kairos-chain skillset install #{u[:name]}" }
|
|
157
|
+
end
|
|
158
|
+
|
|
147
159
|
# Re-project plugin artifacts if any SkillSets changed
|
|
148
160
|
if results.any?
|
|
149
161
|
begin
|
|
@@ -162,15 +174,25 @@ when 'skillset'
|
|
|
162
174
|
end
|
|
163
175
|
end
|
|
164
176
|
else
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
if applicable.any?
|
|
178
|
+
puts "SkillSet upgrades available:"
|
|
179
|
+
puts ""
|
|
180
|
+
applicable.each do |u|
|
|
181
|
+
label = u[:version_bump] ? "v#{u[:installed_version]} -> v#{u[:available_version]}" : "files changed"
|
|
182
|
+
puts " #{u[:name]}: #{label}"
|
|
183
|
+
u[:changed_files].each { |f| puts " Modified: #{f}" }
|
|
184
|
+
puts ""
|
|
185
|
+
end
|
|
186
|
+
puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if manual.any?
|
|
190
|
+
puts "" if applicable.any?
|
|
191
|
+
puts "Optional SkillSets available (not auto-installed; require explicit approval):"
|
|
192
|
+
manual.each { |u| puts " #{u[:name]} v#{u[:available_version]} — #{u[:description]}" }
|
|
171
193
|
puts ""
|
|
194
|
+
puts "Install with: kairos-chain skillset install <name>"
|
|
172
195
|
end
|
|
173
|
-
puts "Run 'kairos-chain skillset upgrade --apply' to apply updates."
|
|
174
196
|
end
|
|
175
197
|
|
|
176
198
|
when 'remove'
|
|
@@ -258,6 +280,7 @@ when 'skillset'
|
|
|
258
280
|
puts " install <path> [--layer L0|L1|L2] Install a SkillSet from path"
|
|
259
281
|
puts " enable <name> Enable a SkillSet"
|
|
260
282
|
puts " disable <name> Disable a SkillSet"
|
|
283
|
+
puts " upgrade [--apply] Check for/apply SkillSet updates from gem templates"
|
|
261
284
|
puts " remove <name> Remove a SkillSet"
|
|
262
285
|
puts " info <name> Show SkillSet details"
|
|
263
286
|
puts " package <name> Package a knowledge-only SkillSet as JSON archive"
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Chain Distillation SkillSet configuration (design v0.5 slice 1).
|
|
2
|
+
#
|
|
3
|
+
# The distiller carries no activation switch of its own: CD-1 couples
|
|
4
|
+
# certified distillation to the confidentiality guard regime, whose
|
|
5
|
+
# activation is environment-level (KAIROS_CONFIDENTIALITY_GUARD / the
|
|
6
|
+
# guard's own config). With the regime inactive the distiller declines.
|
|
7
|
+
#
|
|
8
|
+
# The guard-side designation for the distillation crossings lives in the
|
|
9
|
+
# GUARD's profile (distillation_crossings key) — the guard track owns
|
|
10
|
+
# that surface; see confidentiality_guard/config/profile.example.yml.
|
|
11
|
+
distillation:
|
|
12
|
+
# Reserved for slice-2 (distribution) settings.
|
|
13
|
+
{}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module KairosMcp
|
|
6
|
+
module SkillSets
|
|
7
|
+
module ChainDistillation
|
|
8
|
+
# Canonical serialization for commitments and claim-core hashing
|
|
9
|
+
# (design v0.5 CD-2/CD-6). Same faithfulness rules as the guard's
|
|
10
|
+
# Canon (impl review R1 lineage): false/nil preserved distinctly;
|
|
11
|
+
# mixed string/symbol keys both emitted, sorted by stringified key
|
|
12
|
+
# with original order as tiebreak. One encoder for designation
|
|
13
|
+
# digests, artifact commitments, and claim-core commitments so no
|
|
14
|
+
# two bindings can drift apart.
|
|
15
|
+
module Canon
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def canonical(obj)
|
|
19
|
+
case obj
|
|
20
|
+
when Hash
|
|
21
|
+
entries = obj.map.with_index { |(k, v), i| [k.to_s, i, v] }
|
|
22
|
+
entries.sort_by! { |key, i, _| [key, i] }
|
|
23
|
+
"{#{entries.map { |key, _, v| "#{key.to_json}:#{canonical(v)}" }.join(',')}}"
|
|
24
|
+
when Array
|
|
25
|
+
"[#{obj.map { |v| canonical(v) }.join(',')}]"
|
|
26
|
+
else
|
|
27
|
+
obj.to_json
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stringify(obj)
|
|
32
|
+
case obj
|
|
33
|
+
when Hash
|
|
34
|
+
obj.each_with_object({}) { |(k, v), acc| acc[k.to_s] = stringify(v) }
|
|
35
|
+
when Array
|
|
36
|
+
obj.map { |v| stringify(v) }
|
|
37
|
+
else
|
|
38
|
+
obj
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require_relative 'canon'
|
|
6
|
+
require_relative 'recorder'
|
|
7
|
+
|
|
8
|
+
module KairosMcp
|
|
9
|
+
module SkillSets
|
|
10
|
+
module ChainDistillation
|
|
11
|
+
# Certificate shape and verification (design v0.5 §4, CD-2/CD-3/CD-6).
|
|
12
|
+
#
|
|
13
|
+
# The certificate's vocabulary is exhausted by three claim families —
|
|
14
|
+
# identity, derivation, recording — plus the checkability-status
|
|
15
|
+
# table and the designation-overlap predecessor citations. It is
|
|
16
|
+
# origin-only: there is no field a quality claim could occupy, and
|
|
17
|
+
# verification rejects any claim-core key outside the pinned
|
|
18
|
+
# vocabulary (CD-3's "incapable of expressing quality" made
|
|
19
|
+
# mechanical).
|
|
20
|
+
#
|
|
21
|
+
# Grounding is fixed at issuance (CD-6): the authoritative
|
|
22
|
+
# distillation record is the one whose claim-core commitment the
|
|
23
|
+
# certificate's claim core matches and which its finalized form
|
|
24
|
+
# cites — a positive check, never by exhaustion. Identity uniqueness,
|
|
25
|
+
# designation-overlap citation observance, the drawn-from link, and
|
|
26
|
+
# revocation status are trusted claims; mislabeling any status in
|
|
27
|
+
# either direction fails verification as a whole (CD-2).
|
|
28
|
+
module Certificate
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
CONVENTION = 'cd-1'
|
|
32
|
+
|
|
33
|
+
# Pinned claim-core vocabulary (CD-3): verification fails on any
|
|
34
|
+
# key outside this set, so the claim language cannot be extended
|
|
35
|
+
# into quality territory by an issuer.
|
|
36
|
+
CLAIM_CORE_KEYS = %w[
|
|
37
|
+
convention
|
|
38
|
+
certificate_identity
|
|
39
|
+
identity
|
|
40
|
+
derivation
|
|
41
|
+
recording
|
|
42
|
+
predecessors
|
|
43
|
+
statuses
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
# Pinned per-claim-family checkability-status table (design v0.5
|
|
47
|
+
# §6 slice 1; CD-2). The certificate must carry exactly this
|
|
48
|
+
# table — labeling a checkable claim as trusted is the same defect
|
|
49
|
+
# as the converse, and either fails verification as a whole.
|
|
50
|
+
STATUS_TABLE = {
|
|
51
|
+
'identity.binding' => 'checkable',
|
|
52
|
+
'identity.continuity' => 'trusted',
|
|
53
|
+
'identity.uniqueness' => 'trusted',
|
|
54
|
+
'derivation' => 'anchor-pending',
|
|
55
|
+
'recording' => 'anchor-pending',
|
|
56
|
+
'reissuance_citation_observance' => 'trusted',
|
|
57
|
+
'drawn_from' => 'trusted',
|
|
58
|
+
'revocation_status' => 'trusted'
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# Claim core: the certificate's claim content EXCLUDING its
|
|
62
|
+
# citation of the CD-6 record, carrier lifecycle metadata, and
|
|
63
|
+
# commitment openings (all outside the core, CD-6) — which is what
|
|
64
|
+
# breaks the self-citation fixed point.
|
|
65
|
+
def build_claim_core(certificate_identity:, chain_identity:, head_index:, head_hash:,
|
|
66
|
+
designation:, guard_policy_sha256:, distillate_commitment:,
|
|
67
|
+
verdict_block_indices:, predecessors:)
|
|
68
|
+
{
|
|
69
|
+
'convention' => CONVENTION,
|
|
70
|
+
'certificate_identity' => certificate_identity,
|
|
71
|
+
'identity' => {
|
|
72
|
+
'chain_identity' => chain_identity,
|
|
73
|
+
'chain_head_index' => head_index,
|
|
74
|
+
'chain_head_hash' => head_hash
|
|
75
|
+
},
|
|
76
|
+
'derivation' => {
|
|
77
|
+
'designation' => designation.sort,
|
|
78
|
+
'designation_digest' => Recorder.designation_digest(designation),
|
|
79
|
+
'span' => { 'first_index' => designation.min, 'last_index' => designation.max },
|
|
80
|
+
'guard_policy_sha256' => guard_policy_sha256,
|
|
81
|
+
'distillate_commitment' => distillate_commitment,
|
|
82
|
+
'verdict_block_indices' => verdict_block_indices
|
|
83
|
+
},
|
|
84
|
+
'recording' => {
|
|
85
|
+
'revocation_channel' => 'source-chain:cd_revocation'
|
|
86
|
+
},
|
|
87
|
+
'predecessors' => predecessors,
|
|
88
|
+
'statuses' => STATUS_TABLE.dup
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Finalization adds only what sits outside the claim core: the
|
|
93
|
+
# CD-6 record citation and the commitment openings for the
|
|
94
|
+
# released artifacts the holder possesses (CD-2). Later cites
|
|
95
|
+
# earlier; nothing here feeds back into the committed core.
|
|
96
|
+
def finalize(claim_core:, record_citation:, openings:)
|
|
97
|
+
{
|
|
98
|
+
'claim_core' => claim_core,
|
|
99
|
+
'record_citation' => record_citation,
|
|
100
|
+
'openings' => openings
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Verification (CD-2/CD-6): positive checks only. `chain_entries`
|
|
105
|
+
# is the parsed record list of the source chain (index => entry),
|
|
106
|
+
# available to an in-instance or chain-access verifier; claims
|
|
107
|
+
# whose witness is the private chain alone are trusted-status and
|
|
108
|
+
# verified here only when chain access is provided.
|
|
109
|
+
# Returns { valid:, errors:, revoked: }.
|
|
110
|
+
# Pinned nested vocabulary per claim family (CD-3): nested extras
|
|
111
|
+
# and nested missing fields are both defects, so no quality-shaped
|
|
112
|
+
# content can hide one level down.
|
|
113
|
+
NESTED_KEYS = {
|
|
114
|
+
'identity' => %w[chain_identity chain_head_index chain_head_hash],
|
|
115
|
+
'derivation' => %w[designation designation_digest span guard_policy_sha256
|
|
116
|
+
distillate_commitment verdict_block_indices],
|
|
117
|
+
'recording' => %w[revocation_channel]
|
|
118
|
+
}.freeze
|
|
119
|
+
|
|
120
|
+
# The span sub-mapping's closed vocabulary (CD-3 at every depth).
|
|
121
|
+
SPAN_KEYS = %w[first_index last_index].freeze
|
|
122
|
+
|
|
123
|
+
def verify(certificate, chain_entries: nil, distillate_json: nil, chain_hashes: nil)
|
|
124
|
+
unless certificate.is_a?(Hash)
|
|
125
|
+
return { valid: false, errors: ['certificate/not-a-mapping'], revoked: nil }
|
|
126
|
+
end
|
|
127
|
+
errors = []
|
|
128
|
+
cert = Canon.stringify(certificate)
|
|
129
|
+
core = cert['claim_core']
|
|
130
|
+
return { valid: false, errors: ['certificate/missing-claim-core'], revoked: nil } unless core.is_a?(Hash)
|
|
131
|
+
|
|
132
|
+
# CD-3 vocabulary bound: no key outside the pinned set, at the
|
|
133
|
+
# top level and inside each claim family.
|
|
134
|
+
extra = core.keys - CLAIM_CORE_KEYS
|
|
135
|
+
errors << "vocabulary/unknown-keys:#{extra.join(',')}" unless extra.empty?
|
|
136
|
+
missing = CLAIM_CORE_KEYS - core.keys
|
|
137
|
+
errors << "vocabulary/missing-keys:#{missing.join(',')}" unless missing.empty?
|
|
138
|
+
NESTED_KEYS.each do |family, keys|
|
|
139
|
+
fam = core[family]
|
|
140
|
+
unless fam.is_a?(Hash)
|
|
141
|
+
errors << "vocabulary/#{family}-not-a-mapping"
|
|
142
|
+
next
|
|
143
|
+
end
|
|
144
|
+
nested_extra = fam.keys - keys
|
|
145
|
+
errors << "vocabulary/#{family}-unknown-keys:#{nested_extra.join(',')}" unless nested_extra.empty?
|
|
146
|
+
nested_missing = keys - fam.keys
|
|
147
|
+
errors << "vocabulary/#{family}-missing-keys:#{nested_missing.join(',')}" unless nested_missing.empty?
|
|
148
|
+
end
|
|
149
|
+
# The span sub-mapping is likewise closed (CD-3 holds at every
|
|
150
|
+
# depth of the claim core — impl review R3).
|
|
151
|
+
span = core.is_a?(Hash) ? core.dig('derivation', 'span') : nil
|
|
152
|
+
if span.is_a?(Hash) && span.keys.sort != SPAN_KEYS
|
|
153
|
+
errors << "vocabulary/span-keys:#{(span.keys - SPAN_KEYS).join(',')}"
|
|
154
|
+
elsif !span.is_a?(Hash) && core['derivation'].is_a?(Hash)
|
|
155
|
+
errors << 'vocabulary/span-not-a-mapping'
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# CD-2 mislabeling defect, both directions: statuses must equal
|
|
159
|
+
# the pinned table exactly.
|
|
160
|
+
errors << 'statuses/mislabeled' unless core['statuses'] == STATUS_TABLE
|
|
161
|
+
|
|
162
|
+
# Openings only over released artifacts (claim core + distillate).
|
|
163
|
+
openings = cert['openings'].is_a?(Hash) ? cert['openings'] : {}
|
|
164
|
+
errors << 'openings/extra' unless (openings.keys - %w[claim_core_salt distillate_salt]).empty?
|
|
165
|
+
|
|
166
|
+
revoked = nil
|
|
167
|
+
if chain_entries
|
|
168
|
+
revoked, chain_errors = verify_against_chain(cert, core, openings, chain_entries,
|
|
169
|
+
chain_hashes: chain_hashes)
|
|
170
|
+
errors.concat(chain_errors)
|
|
171
|
+
# A revoked certificate does not verify (CD-6: revocation is
|
|
172
|
+
# the authoritative channel); callers checking only `valid`
|
|
173
|
+
# must not accept a chain-revoked certificate.
|
|
174
|
+
errors << 'revocation/revoked' if revoked
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if distillate_json
|
|
178
|
+
if openings['distillate_salt']
|
|
179
|
+
digest = core.dig('derivation', 'distillate_commitment')
|
|
180
|
+
unless Recorder.commitment_valid?(Recorder::ARTIFACT_DOMAIN, digest.to_s,
|
|
181
|
+
openings['distillate_salt'], distillate_json)
|
|
182
|
+
errors << 'distillate/commitment-mismatch'
|
|
183
|
+
end
|
|
184
|
+
else
|
|
185
|
+
# A supplied distillate that cannot be checked is a defect,
|
|
186
|
+
# not a silent pass (impl review R1).
|
|
187
|
+
errors << 'distillate/no-opening'
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
{ valid: errors.empty?, errors: errors, revoked: revoked }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Grounding and chain-witnessed checks (positive, never by
|
|
195
|
+
# exhaustion): locate the cited record, match commitments and
|
|
196
|
+
# identity, check revocation and the designation-overlap
|
|
197
|
+
# predecessor-citation obligation.
|
|
198
|
+
def verify_against_chain(cert, core, openings, chain_entries, chain_hashes: nil)
|
|
199
|
+
errors = []
|
|
200
|
+
citation = cert['record_citation'].is_a?(Hash) ? cert['record_citation'] : {}
|
|
201
|
+
record_index = normalize_index(citation['block_index'])
|
|
202
|
+
record = record_index ? chain_entries[record_index] : nil
|
|
203
|
+
unless record.is_a?(Hash) && record['type'] == 'cd_distillation'
|
|
204
|
+
return [nil, ['grounding/cited-record-absent']]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Positive grounding: claim core matches the cited record's
|
|
208
|
+
# claim-core commitment (via the disclosed opening), and every
|
|
209
|
+
# field the record and the core both carry must agree — identity,
|
|
210
|
+
# designation digest, guard policy version, distillate
|
|
211
|
+
# commitment, predecessors (impl review R1: grounding must bind
|
|
212
|
+
# the whole citation, not the index alone).
|
|
213
|
+
if openings['claim_core_salt']
|
|
214
|
+
unless Recorder.commitment_valid?(Recorder::CLAIM_CORE_DOMAIN,
|
|
215
|
+
record['claim_core_commitment'].to_s,
|
|
216
|
+
openings['claim_core_salt'], Canon.canonical(core))
|
|
217
|
+
errors << 'grounding/claim-core-commitment-mismatch'
|
|
218
|
+
end
|
|
219
|
+
else
|
|
220
|
+
errors << 'grounding/no-claim-core-opening'
|
|
221
|
+
end
|
|
222
|
+
errors << 'grounding/identity-mismatch' unless record['certificate_identity'] == core['certificate_identity']
|
|
223
|
+
errors << 'grounding/designation-mismatch' unless record['designation_digest'] == core.dig('derivation', 'designation_digest')
|
|
224
|
+
errors << 'grounding/policy-mismatch' unless record['guard_policy_sha256'] == core.dig('derivation', 'guard_policy_sha256')
|
|
225
|
+
errors << 'grounding/distillate-commitment-mismatch' unless record['distillate_commitment'] == core.dig('derivation', 'distillate_commitment')
|
|
226
|
+
errors << 'grounding/predecessors-mismatch' unless Array(record['predecessors']).sort == Array(core['predecessors']).sort
|
|
227
|
+
|
|
228
|
+
# With block hashes available, bind the citation to the block
|
|
229
|
+
# itself and check the identity family (identity.binding is a
|
|
230
|
+
# CHECKABLE claim and must actually be checked — impl review R1).
|
|
231
|
+
if chain_hashes.is_a?(Hash) && !chain_hashes.empty?
|
|
232
|
+
# Full-citation grounding (CD-2): the block hash is part of the
|
|
233
|
+
# citation, not an optional extra — a certificate stripped of
|
|
234
|
+
# it must not verify (impl review R3).
|
|
235
|
+
cited_hash = citation['block_hash']
|
|
236
|
+
if cited_hash.nil? || cited_hash.to_s.empty?
|
|
237
|
+
errors << 'grounding/no-block-hash'
|
|
238
|
+
elsif chain_hashes[record_index] != cited_hash
|
|
239
|
+
errors << 'grounding/block-hash-mismatch'
|
|
240
|
+
end
|
|
241
|
+
genesis_hash = chain_hashes[chain_hashes.keys.min]
|
|
242
|
+
claimed_identity = core.dig('identity', 'chain_identity')
|
|
243
|
+
if genesis_hash && claimed_identity != "block1-sha256:#{genesis_hash}"
|
|
244
|
+
errors << 'identity/chain-identity-mismatch'
|
|
245
|
+
end
|
|
246
|
+
head_index = normalize_index(core.dig('identity', 'chain_head_index'))
|
|
247
|
+
head_hash = core.dig('identity', 'chain_head_hash')
|
|
248
|
+
if head_index && head_hash && chain_hashes[head_index] != head_hash
|
|
249
|
+
errors << 'identity/head-binding-mismatch'
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Revocation: keyed to the certificate identity, chain
|
|
254
|
+
# authoritative (trusted-status for outside verifiers; checked
|
|
255
|
+
# here because chain access was provided).
|
|
256
|
+
revoked = chain_entries.values.any? do |e|
|
|
257
|
+
e.is_a?(Hash) && e['type'] == 'cd_revocation' &&
|
|
258
|
+
e['certificate_identity'] == core['certificate_identity']
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Designation-overlap predecessor-citation obligation (CD-6,
|
|
262
|
+
# evaluated at issuance): every certificate identity that was
|
|
263
|
+
# revoked before this record and whose distillation record's
|
|
264
|
+
# designation shares records with this designation must appear
|
|
265
|
+
# in predecessors.
|
|
266
|
+
required = revoked_overlapping_identities(chain_entries, core.dig('derivation', 'designation'), record_index)
|
|
267
|
+
cited = Array(core['predecessors'])
|
|
268
|
+
missing = required - cited
|
|
269
|
+
errors << "reissuance/uncited-revoked-predecessors:#{missing.join(',')}" unless missing.empty?
|
|
270
|
+
|
|
271
|
+
[revoked, errors]
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Identities of certificates revoked before `before_index` whose
|
|
275
|
+
# distillation designation overlaps `designation`. Both the
|
|
276
|
+
# revocation AND its grounding distillation record must precede
|
|
277
|
+
# `before_index` (impl review R1: records written after the
|
|
278
|
+
# evaluated record cannot ground an obligation on it).
|
|
279
|
+
def revoked_overlapping_identities(chain_entries, designation, before_index)
|
|
280
|
+
designated = Array(designation)
|
|
281
|
+
distillations = {}
|
|
282
|
+
revoked_before = []
|
|
283
|
+
chain_entries.each do |index, entry|
|
|
284
|
+
next unless entry.is_a?(Hash)
|
|
285
|
+
next if index.to_i >= before_index
|
|
286
|
+
case entry['type']
|
|
287
|
+
when 'cd_distillation'
|
|
288
|
+
distillations[entry['certificate_identity']] = Array(entry['designation'])
|
|
289
|
+
when 'cd_revocation'
|
|
290
|
+
revoked_before << entry['certificate_identity']
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
revoked_before.uniq.select do |identity|
|
|
294
|
+
overlap = distillations[identity]
|
|
295
|
+
overlap && !(overlap & designated).empty?
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Integer indices only; a JSON round-trip may deliver strings.
|
|
300
|
+
def normalize_index(value)
|
|
301
|
+
return value if value.is_a?(Integer)
|
|
302
|
+
return value.to_i if value.is_a?(String) && value.match?(/\A\d+\z/)
|
|
303
|
+
nil
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|