nx_ltv_lib.nx source
↩ module page · 221 lines · 9084 B
1// nx_ltv_lib.nx -- PAdES LONG-TERM VALIDATION (ETSI EN 319 142 baseline levels B-B/B-T/B-LT/B-LTA).
2// The gap this closes: a signature that verifies TODAY is worthless if it cannot be verified in the year
3// the dispute actually reaches a court. Signing certificates expire in 1-3 years; litigation and record
4// retention run 7-30. Once the certificate expires -- or the issuing CA's OCSP responder goes dark, or
5// the CA dissolves -- a verifier can no longer fetch the revocation status the signature depends on.
6//
7// THE LEVELS, each strictly containing the last:
8// B-B signed attributes + the signer certificate (verifiable only while the cert lives)
9// B-T + a trusted timestamp token (proves WHEN it was signed)
10// B-LT + the certificate chain AND revocation data EMBEDDED (carries its own proof; no live CA)
11// B-LTA + an archive timestamp over the whole structure (survives crypto obsolescence)
12//
13// THE TOOTH EVERY NAIVE VERIFIER GETS WRONG: when a signature can no longer be checked the answer is
14// INDETERMINATE, *not* INVALID. Those are opposite legal outcomes -- INVALID says forged or tampered;
15// INDETERMINATE says our evidence decayed. Reporting an expired-cert B-level signature as "invalid"
16// impeaches a document that was perfectly validly signed. The verdict is THREE-valued so the
17// distinction cannot be collapsed into a boolean, which is exactly how it gets lost.
18//
19// THE SECOND TOOTH: long-term validation is NOT a laundry. A certificate already REVOKED at signing
20// time stays INVALID at B-LTA -- embedding proof of a bad status proves the status was bad.
21//
22// STRUCTURE (two layers, deliberately): a PURE DECISION CORE holding the level and verdict rules as
23// total functions, and REGISTRY ADAPTERS that fetch components and delegate. The rules therefore gate
24// with ZERO writes and cannot be held hostage by a storage stall (debt 1785519597), and there is
25// exactly one copy of each rule so the two layers can never disagree.
26//
27// SCALE ENVELOPE (declared): pure core O(1), allocation-free; adapters one reg_get per component.
28// DRY: composes nx_matter_lib. license_tier: ORIGINAL LIB.
29
30import "nx_matter_lib.nx"
31
32const LTV_NONE: i64 = 0
33const LTV_B: i64 = 1
34const LTV_T: i64 = 2
35const LTV_LT: i64 = 3
36const LTV_LTA: i64 = 4
37
38const LTV_INDETERMINATE: i64 = 0
39const LTV_VALID: i64 = 1
40
41const LTV_UNSET: i64 = 0 - 2000000002
42const LTV_INVALID: i64 = 0 - 1
43
44const LTV_RECBUF: i64 = 512
45const LTV_IDBUF: i64 = 256
46
47// ============================================================================
48// PURE DECISION CORE -- levels and verdicts, no storage, no I/O.
49// Flags are 1 for present, anything else for absent (fail-closed on garbage).
50// ============================================================================
51
52func ltv_is1(v: i64) -> i64 {
53 if v == 1 { return 1 }
54 return 0
55}
56
57// the ladder. A missing component DEMOTES rather than disqualifies: a signature with a chain but no
58// revocation data is a perfectly good B-T signature, not a broken B-LT one.
59func ltv_level_pure(signed_attrs: i64, signer_cert: i64, ts: i64, chain: i64, revoc: i64, archive: i64) -> i64 {
60 if ltv_is1(signed_attrs) == 0 { return LTV_NONE }
61 if ltv_is1(signer_cert) == 0 { return LTV_NONE }
62 if ltv_is1(ts) == 0 { return LTV_B }
63 if ltv_is1(chain) == 0 { return LTV_T }
64 if ltv_is1(revoc) == 0 { return LTV_T }
65 if ltv_is1(archive) == 0 { return LTV_LT }
66 return LTV_LTA
67}
68
69// 1 only if the document can be checked with NO network and NO live CA.
70func ltv_self_contained_pure(level: i64) -> i64 {
71 if level >= LTV_LT { return 1 }
72 return 0
73}
74
75// THE VERDICT, three-valued. Revocation is checked FIRST and is terminal at every level.
76func ltv_verify_pure(level: i64, revoked: i64, asof: i64, expiry: i64) -> i64 {
77 if ltv_is1(revoked) == 1 { return LTV_INVALID }
78 if level == LTV_NONE { return LTV_INDETERMINATE }
79 if asof == LTV_UNSET { return LTV_INDETERMINATE }
80 if expiry == LTV_UNSET { return LTV_INDETERMINATE }
81 if asof <= expiry { return LTV_VALID }
82 if level >= LTV_LT { return LTV_VALID }
83 return LTV_INDETERMINATE
84}
85
86// Only a B-LTA ever made the archival promise, so only a B-LTA can fall behind on it.
87func ltv_needs_renewal_pure(level: i64, asof: i64, sunset: i64, renewed: i64) -> i64 {
88 if level < LTV_LTA { return 0 }
89 if asof == LTV_UNSET { return 0 }
90 if sunset == LTV_UNSET { return 0 }
91 if renewed != LTV_UNSET {
92 if renewed >= sunset { return 0 }
93 }
94 if asof >= sunset { return 1 }
95 return 0
96}
97
98// ============================================================================
99// STORAGE LAYER
100// ============================================================================
101
102func ltv_key(sig: *u8, field: *u8, out: *u8) -> i64 {
103 var o: i64 = mt_catcopy(out, 0, sig)
104 out[o] = 124 as u8
105 o = o + 1
106 o = mt_catcopy(out, o, field)
107 out[o] = 0 as u8
108 return o
109}
110
111func ltv_set(prefix: *u8, sig: *u8, field: *u8, val: *u8) -> i64 {
112 let id: *u8 = sys_mmap(LTV_IDBUF)
113 ltv_key(sig, field, id)
114 let k: *i64 = sys_mmap(8 * 1) as *i64
115 let v: *i64 = sys_mmap(8 * 1) as *i64
116 k[0] = ("v" as *u8) as i64
117 v[0] = val as i64
118 let rec: *u8 = sys_mmap(LTV_RECBUF)
119 let rl: i64 = canon_encode(k, v, 1, rec)
120 return reg_put(prefix, "lt:" as *u8, "lt:__idx__" as *u8, id, rec, rl)
121}
122
123func ltv_get(prefix: *u8, sig: *u8, field: *u8, out: *u8) -> i64 {
124 let id: *u8 = sys_mmap(LTV_IDBUF)
125 ltv_key(sig, field, id)
126 let po: *i64 = sys_mmap(16) as *i64
127 let lo: *i64 = sys_mmap(16) as *i64
128 if reg_get(prefix, "lt:" as *u8, id, po, lo) != 1 {
129 out[0] = 0 as u8
130 return 0
131 }
132 mt_field(po[0] as *u8, lo[0], "v" as *u8, 1, out)
133 return 1
134}
135
136func ltv_yes(prefix: *u8, sig: *u8, field: *u8) -> i64 {
137 let b: *u8 = sys_mmap(LTV_IDBUF)
138 ltv_get(prefix, sig, field, b)
139 if mt_streq(b, "yes" as *u8) == 1 { return 1 }
140 return 0
141}
142
143// absent or garbage yields LTV_UNSET rather than 0, so "no date" never reads as the epoch.
144func ltv_atoi(s: *u8) -> i64 {
145 var i: i64 = 0
146 var neg: i64 = 0
147 if s[0] == (45 as u8) { neg = 1; i = 1 }
148 var v: i64 = 0
149 var any: i64 = 0
150 while s[i] != (0 as u8) {
151 let c: i64 = s[i]
152 if c < 48 { return LTV_UNSET }
153 if c > 57 { return LTV_UNSET }
154 v = (v * 10) + (c - 48)
155 any = 1
156 i = i + 1
157 }
158 if any == 0 { return LTV_UNSET }
159 if neg == 1 { return 0 - v }
160 return v
161}
162
163func ltv_setn(prefix: *u8, sig: *u8, field: *u8, n: i64) -> i64 {
164 let b: *u8 = sys_mmap(64)
165 var o: i64 = 0
166 if n < 0 {
167 b[0] = 45 as u8
168 o = mt_catn(b, 1, 0 - n)
169 }
170 if n >= 0 { o = mt_catn(b, 0, n) }
171 b[o] = 0 as u8
172 return ltv_set(prefix, sig, field, b)
173}
174
175func ltv_getn(prefix: *u8, sig: *u8, field: *u8) -> i64 {
176 let b: *u8 = sys_mmap(LTV_IDBUF)
177 if ltv_get(prefix, sig, field, b) == 0 { return LTV_UNSET }
178 return ltv_atoi(b)
179}
180
181// ============================================================================
182// REGISTRY ADAPTERS -- resolve components, then DELEGATE to the pure core.
183// ============================================================================
184
185func ltv_level(prefix: *u8, sig: *u8) -> i64 {
186 return ltv_level_pure(ltv_yes(prefix, sig, "signed_attrs" as *u8), ltv_yes(prefix, sig, "signer_cert" as *u8), ltv_yes(prefix, sig, "timestamp_token" as *u8), ltv_yes(prefix, sig, "cert_chain" as *u8), ltv_yes(prefix, sig, "revocation_data" as *u8), ltv_yes(prefix, sig, "archive_timestamp" as *u8))
187}
188
189func ltv_self_contained(prefix: *u8, sig: *u8) -> i64 {
190 return ltv_self_contained_pure(ltv_level(prefix, sig))
191}
192
193func ltv_verify_at(prefix: *u8, sig: *u8, asof_day: i64, cert_expiry_day: i64) -> i64 {
194 return ltv_verify_pure(ltv_level(prefix, sig), ltv_yes(prefix, sig, "revoked_at_signing" as *u8), asof_day, cert_expiry_day)
195}
196
197func ltv_needs_renewal(prefix: *u8, sig: *u8, asof_day: i64, algo_sunset_day: i64) -> i64 {
198 return ltv_needs_renewal_pure(ltv_level(prefix, sig), asof_day, algo_sunset_day, ltv_getn(prefix, sig, "archive_renewed_day" as *u8))
199}
200
201func ltv_renew(prefix: *u8, sig: *u8, day: i64) -> i64 {
202 return ltv_setn(prefix, sig, "archive_renewed_day" as *u8, day)
203}
204
205func ltv_level_label(lvl: i64, out: *u8) -> i64 {
206 if lvl == LTV_LTA { mt_catcopy(out, 0, "B-LTA" as *u8); out[5] = 0 as u8; return 5 }
207 if lvl == LTV_LT { mt_catcopy(out, 0, "B-LT" as *u8); out[4] = 0 as u8; return 4 }
208 if lvl == LTV_T { mt_catcopy(out, 0, "B-T" as *u8); out[3] = 0 as u8; return 3 }
209 if lvl == LTV_B { mt_catcopy(out, 0, "B-B" as *u8); out[3] = 0 as u8; return 3 }
210 mt_catcopy(out, 0, "NONE" as *u8)
211 out[4] = 0 as u8
212 return 4
213}
214
215func ltv_verdict_label(v: i64, out: *u8) -> i64 {
216 if v == LTV_VALID { mt_catcopy(out, 0, "VALID" as *u8); out[5] = 0 as u8; return 5 }
217 if v == LTV_INVALID { mt_catcopy(out, 0, "INVALID" as *u8); out[7] = 0 as u8; return 7 }
218 mt_catcopy(out, 0, "INDETERMINATE" as *u8)
219 out[13] = 0 as u8
220 return 13
221}