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}