code wiki / (root) / nx_esign_lib.nx

nx_esign_lib.nx source

↩ module page · 268 lines · 12359 B

1// nx_esign_lib.nx -- E-SIGNATURE LEGAL VALIDITY: the ESIGN/UETA four-element test + the eIDAS tier 2// ladder (SES/AES/QES). The gap this closes: a tamper-evident hash chain (nx_sign_envelope) proves a 3// document was NOT ALTERED. It does not answer the only two questions a court asks -- was this signature 4// legally executed, and what legal effect does it carry. DocuSign/Adobe-class products answer both; 5// a bare hash chain answers neither. 6// 7// US (ESIGN 15 USC 7001 + UETA s2(8)/s7): enforceable ONLY on all FOUR of -- 8// 1 INTENT the signer performed an affirmative act intending to sign 9// 2 CONSENT the parties agreed to transact electronically 10// 3 ASSOCIATION the signature is logically/cryptographically bound to THAT record 11// 4 RETENTION the record is tamper-evident, accurately reproducible, and accessible 12// CONSUMER OVERLAY (ESIGN 101(c)) -- a consumer signer additionally requires paper-copy right, 13// withdrawal right, hardware/software disclosure, and a DEMONSTRATED ability to access the record. 14// A consumer transaction missing these is unenforceable even with all four base elements present. 15// 16// EU (eIDAS Art 3/25/26): SES < AES < QES. THE DISTINCTION MOST IMPLEMENTATIONS BLUR: Art 25(1) says no 17// electronic signature may be denied legal effect merely for being electronic -- that is ADMISSIBILITY, 18// and it reaches every tier. Art 25(2) grants EQUIVALENCE TO A HANDWRITTEN SIGNATURE to QES *alone*. 19// Conflating them is the error that loses cases, so they are SEPARATE predicates that never collapse. 20// 21// STRUCTURE (two layers, deliberately): a PURE DECISION CORE holding the statutory rules as total 22// functions over resolved flags, and REGISTRY ADAPTERS that fetch facts and delegate to it. Every rule 23// exists exactly once, so the stored-fact path can never disagree with the rule. See the core's own 24// note for why this split is load-bearing rather than cosmetic. 25// 26// FAIL-CLOSED BY CONSTRUCTION: only the exact string "yes" asserts a stored element, and only the 27// integer 1 asserts a resolved one. Absent, empty, "true", "1", "YES" all leave it UNSATISFIED. An 28// element nobody recorded is an element nobody performed; a signature is never valid by omission. 29// 30// SCALE ENVELOPE (declared, measured): the pure core is O(1) and allocation-free. Each adapter costs one 31// reg_get per element (~70us). reg_put costs ~678ms and is the binding cost of WRITING facts, never of 32// evaluating them -- see debt 1785519597. license_tier: ORIGINAL LIB. 33 34import "nx_matter_lib.nx" 35 36const ES_NONE: i64 = 0 37const ES_SES: i64 = 1 38const ES_AES: i64 = 2 39const ES_QES: i64 = 3 40 41const ES_RECBUF: i64 = 512 42const ES_IDBUF: i64 = 256 43 44// ============================================================================ 45// PURE DECISION CORE -- the legal rules, with NO storage and NO I/O. 46// 47// WHY THIS EXISTS (learned the hard way, debt 1785519597): the first cut of this lib put every rule 48// BEHIND a registry read, so when the seg_store write path stalled ecosystem-wide the RULES became 49// untestable -- a storage outage was silently also a correctness-verification outage. Those are 50// different concerns and must be able to fail independently. The statutory logic does not depend on 51// WHERE the facts are kept, so it lives here as total functions over already-resolved flags. 52// 53// Consequences: the rules gate with ZERO writes and run in microseconds; the adapters below cannot 54// drift from the rules because there is only one copy of each rule; and a caller holding facts from 55// another source (an imported audit trail, a counterparty's evidence pack) can evaluate them directly. 56// Every flag argument is 1 for asserted, anything else for not-asserted -- fail-closed on garbage. 57// ============================================================================ 58 59func es_is1(v: i64) -> i64 { 60 if v == 1 { return 1 } 61 return 0 62} 63 64func es_elements_pure(intent: i64, consent: i64, assoc: i64, retention: i64) -> i64 { 65 var n: i64 = 0 66 n = n + es_is1(intent) 67 n = n + es_is1(consent) 68 n = n + es_is1(assoc) 69 n = n + es_is1(retention) 70 return n 71} 72 73// the base UETA test: ALL FOUR or void. Three of four is not "mostly enforceable". 74func es_ueta_pure(intent: i64, consent: i64, assoc: i64, retention: i64) -> i64 { 75 if es_elements_pure(intent, consent, assoc, retention) == 4 { return 1 } 76 return 0 77} 78 79// ESIGN 101(c): non-consumers pass vacuously; consumers need all four disclosures. 80func es_consumer_pure(is_consumer: i64, paper: i64, withdrawal: i64, hwsw: i64, access: i64) -> i64 { 81 if es_is1(is_consumer) == 0 { return 1 } 82 if es_is1(paper) == 0 { return 0 } 83 if es_is1(withdrawal) == 0 { return 0 } 84 if es_is1(hwsw) == 0 { return 0 } 85 if es_is1(access) == 0 { return 0 } 86 return 1 87} 88 89// eIDAS Art 26: all four advanced properties are required. 90func es_aes_pure(unique: i64, sigid: i64, sole: i64, tamper: i64) -> i64 { 91 if es_is1(unique) == 0 { return 0 } 92 if es_is1(sigid) == 0 { return 0 } 93 if es_is1(sole) == 0 { return 0 } 94 if es_is1(tamper) == 0 { return 0 } 95 return 1 96} 97 98// the ladder, highest-first. Absence of signature data is ES_NONE -- "nothing" is not a tier. 99// QES needs BOTH the QSCD and the qualified certificate; either alone leaves it at AES. 100func es_tier_pure(sig_data: i64, unique: i64, sigid: i64, sole: i64, tamper: i64, qscd: i64, qcert: i64) -> i64 { 101 if es_aes_pure(unique, sigid, sole, tamper) == 1 { 102 if es_is1(qscd) == 1 { 103 if es_is1(qcert) == 1 { return ES_QES } 104 } 105 return ES_AES 106 } 107 if es_is1(sig_data) == 1 { return ES_SES } 108 return ES_NONE 109} 110 111// Art 25(1) non-discrimination -- deliberately weak, reaches every real tier. 112func es_admissible_pure(tier: i64) -> i64 { 113 if tier >= ES_SES { return 1 } 114 return 0 115} 116 117// Art 25(2) equivalence -- QES ALONE. Widening this to AES is the costliest mistake in the field. 118func es_handwritten_pure(tier: i64) -> i64 { 119 if tier == ES_QES { return 1 } 120 return 0 121} 122 123// ============================================================================ 124// STORAGE LAYER -- one append-only, newest-wins record per element. 125// Storing elements separately is what makes an UNRECORDED element structurally indistinguishable 126// from an absent one: the fail-closed default is a property of the layout, not of a branch. 127// ============================================================================ 128 129func es_key(sig: *u8, field: *u8, out: *u8) -> i64 { 130 var o: i64 = mt_catcopy(out, 0, sig) 131 out[o] = 124 as u8 132 o = o + 1 133 o = mt_catcopy(out, o, field) 134 out[o] = 0 as u8 135 return o 136} 137 138func es_set(prefix: *u8, sig: *u8, field: *u8, val: *u8) -> i64 { 139 let id: *u8 = sys_mmap(ES_IDBUF) 140 es_key(sig, field, id) 141 let k: *i64 = sys_mmap(8 * 1) as *i64 142 let v: *i64 = sys_mmap(8 * 1) as *i64 143 k[0] = ("v" as *u8) as i64 144 v[0] = val as i64 145 let rec: *u8 = sys_mmap(ES_RECBUF) 146 let rl: i64 = canon_encode(k, v, 1, rec) 147 return reg_put(prefix, "es:" as *u8, "es:__idx__" as *u8, id, rec, rl) 148} 149 150func es_get(prefix: *u8, sig: *u8, field: *u8, out: *u8) -> i64 { 151 let id: *u8 = sys_mmap(ES_IDBUF) 152 es_key(sig, field, id) 153 let po: *i64 = sys_mmap(16) as *i64 154 let lo: *i64 = sys_mmap(16) as *i64 155 if reg_get(prefix, "es:" as *u8, id, po, lo) != 1 { 156 out[0] = 0 as u8 157 return 0 158 } 159 mt_field(po[0] as *u8, lo[0], "v" as *u8, 1, out) 160 return 1 161} 162 163// the single place that decides what a STORED assertion looks like -- exactly one string. 164func es_yes(prefix: *u8, sig: *u8, field: *u8) -> i64 { 165 let b: *u8 = sys_mmap(ES_IDBUF) 166 es_get(prefix, sig, field, b) 167 if mt_streq(b, "yes" as *u8) == 1 { return 1 } 168 return 0 169} 170 171// ============================================================================ 172// REGISTRY ADAPTERS -- resolve stored facts, then DELEGATE to the pure core. 173// No rule is restated here; that is the point. 174// ============================================================================ 175 176func es_intent(prefix: *u8, sig: *u8) -> i64 { return es_yes(prefix, sig, "intent" as *u8) } 177func es_consent(prefix: *u8, sig: *u8) -> i64 { return es_yes(prefix, sig, "consent" as *u8) } 178func es_association(prefix: *u8, sig: *u8) -> i64 { return es_yes(prefix, sig, "association" as *u8) } 179func es_retention(prefix: *u8, sig: *u8) -> i64 { return es_yes(prefix, sig, "retention" as *u8) } 180 181func es_elements(prefix: *u8, sig: *u8) -> i64 { 182 return es_elements_pure(es_intent(prefix, sig), es_consent(prefix, sig), es_association(prefix, sig), es_retention(prefix, sig)) 183} 184 185func es_ueta_valid(prefix: *u8, sig: *u8) -> i64 { 186 return es_ueta_pure(es_intent(prefix, sig), es_consent(prefix, sig), es_association(prefix, sig), es_retention(prefix, sig)) 187} 188 189func es_is_consumer(prefix: *u8, sig: *u8) -> i64 { 190 let b: *u8 = sys_mmap(ES_IDBUF) 191 es_get(prefix, sig, "signer_type" as *u8, b) 192 if mt_streq(b, "consumer" as *u8) == 1 { return 1 } 193 return 0 194} 195 196func es_consumer_ok(prefix: *u8, sig: *u8) -> i64 { 197 return es_consumer_pure(es_is_consumer(prefix, sig), es_yes(prefix, sig, "paper_copy_right" as *u8), es_yes(prefix, sig, "withdrawal_right" as *u8), es_yes(prefix, sig, "hw_sw_disclosure" as *u8), es_yes(prefix, sig, "access_demonstrated" as *u8)) 198} 199 200func es_esign_valid(prefix: *u8, sig: *u8) -> i64 { 201 if es_ueta_valid(prefix, sig) == 0 { return 0 } 202 return es_consumer_ok(prefix, sig) 203} 204 205func es_aes_ok(prefix: *u8, sig: *u8) -> i64 { 206 return es_aes_pure(es_yes(prefix, sig, "unique_link" as *u8), es_yes(prefix, sig, "signatory_id" as *u8), es_yes(prefix, sig, "sole_control" as *u8), es_yes(prefix, sig, "tamper_detect" as *u8)) 207} 208 209func es_eidas_tier(prefix: *u8, sig: *u8) -> i64 { 210 return es_tier_pure(es_yes(prefix, sig, "sig_data" as *u8), es_yes(prefix, sig, "unique_link" as *u8), es_yes(prefix, sig, "signatory_id" as *u8), es_yes(prefix, sig, "sole_control" as *u8), es_yes(prefix, sig, "tamper_detect" as *u8), es_yes(prefix, sig, "qscd" as *u8), es_yes(prefix, sig, "qualified_cert" as *u8)) 211} 212 213func es_qes_ok(prefix: *u8, sig: *u8) -> i64 { 214 if es_eidas_tier(prefix, sig) == ES_QES { return 1 } 215 return 0 216} 217 218func es_tier_label(tier: i64, out: *u8) -> i64 { 219 if tier == ES_QES { mt_catcopy(out, 0, "QES" as *u8); out[3] = 0 as u8; return 3 } 220 if tier == ES_AES { mt_catcopy(out, 0, "AES" as *u8); out[3] = 0 as u8; return 3 } 221 if tier == ES_SES { mt_catcopy(out, 0, "SES" as *u8); out[3] = 0 as u8; return 3 } 222 mt_catcopy(out, 0, "NONE" as *u8) 223 out[4] = 0 as u8 224 return 4 225} 226 227func es_admissible(prefix: *u8, sig: *u8) -> i64 { 228 return es_admissible_pure(es_eidas_tier(prefix, sig)) 229} 230 231func es_handwritten_equivalent(prefix: *u8, sig: *u8) -> i64 { 232 return es_handwritten_pure(es_eidas_tier(prefix, sig)) 233} 234 235// ---- the Certificate of Completion (the admissible audit record) ---- 236 237func es_audit_missing_pure(identity: i64, auth: i64, ts: i64, hash: i64, consent_rec: i64, seq: i64) -> i64 { 238 var miss: i64 = 0 239 if es_is1(identity) == 0 { miss = miss + 1 } 240 if es_is1(auth) == 0 { miss = miss + 1 } 241 if es_is1(ts) == 0 { miss = miss + 1 } 242 if es_is1(hash) == 0 { miss = miss + 1 } 243 if es_is1(consent_rec) == 0 { miss = miss + 1 } 244 if es_is1(seq) == 0 { miss = miss + 1 } 245 return miss 246} 247 248func es_audit_missing(prefix: *u8, sig: *u8) -> i64 { 249 return es_audit_missing_pure(es_yes(prefix, sig, "signer_identity" as *u8), es_yes(prefix, sig, "auth_method" as *u8), es_yes(prefix, sig, "timestamp" as *u8), es_yes(prefix, sig, "doc_hash" as *u8), es_yes(prefix, sig, "consent_record" as *u8), es_yes(prefix, sig, "event_sequence" as *u8)) 250} 251 252func es_audit_complete(prefix: *u8, sig: *u8) -> i64 { 253 if es_audit_missing(prefix, sig) == 0 { return 1 } 254 return 0 255} 256 257// THE PRODUCT-SURFACE VERDICT: rely on a signature only when the US statutory test passes, it reaches 258// a real eIDAS tier, and the audit record is complete. Any one missing blocks reliance. 259func es_ready_pure(esign_ok: i64, tier: i64, audit_miss: i64) -> i64 { 260 if es_is1(esign_ok) == 0 { return 0 } 261 if es_admissible_pure(tier) == 0 { return 0 } 262 if audit_miss != 0 { return 0 } 263 return 1 264} 265 266func es_ready(prefix: *u8, sig: *u8) -> i64 { 267 return es_ready_pure(es_esign_valid(prefix, sig), es_eidas_tier(prefix, sig), es_audit_missing(prefix, sig)) 268}