code wiki / (root) / nx_ltv_lib.nx

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}