1if not modules then modules = { } end modules ['lpdf-sig'] = {
2 version = 1.001,
3 optimize = true,
4 comment = "companion to back-imp-pdf.mkxl",
5 author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6 copyright = "PRAGMA ADE / ConTeXt Development Team",
7 license = "see context related readme files"
8}
9
10
11
12local find, gsub, rep, format = string.find, string.gsub, string.rep, string.format
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27local function updatesignature(signature,...)
28 local updated = gsub(signature,
29
30 "/ByteRange %[ .......... .......... .......... .......... %]",
31 format("/ByteRange [ %010i %010i %010i %010i ]",...)
32 )
33 return updated ~= signature and updated or false
34end
35
36local function getbyteranges(signature,position,filesize)
37 local leftangle = find(signature,"<0")
38 local rightangle = find(signature,"0>")
39 local b1 = 0
40
41 local n1 = position + leftangle - 1
42 local b2 = position + rightangle + 1
43 local n2 = filesize - b2
44 return { b1, n1, b2, n2 }
45end
46
47if lpdf and context then
48
49 local pdfreserveobject = lpdf.reserveobject
50 local pdfflushobject = lpdf.flushobject
51 local pdfreference = lpdf.reference
52 local pdfconstant = lpdf.constant
53 local pdfdictionary = lpdf.dictionary
54 local pdfarray = lpdf.array
55 local pdfliteral = lpdf.literal
56 local pdfunicode = lpdf.unicode
57
58 local status = {
59 signature = false,
60 position = false,
61 filename = false,
62 filesize = false,
63 }
64
65 local reference = nil
66
67 function lpdf.registersignature(value)
68 if value == "pkcs7" then
69 local signature = pdfreserveobject()
70 status.signature = signature
71
72
73
74 return pdfreference(signature)
75 end
76 end
77
78 function lpdf.preparesignature(flush,f,offset,objects)
79 local signature = status.signature
80 if signature then
81 local d = pdfdictionary {
82 ByteRange = pdfarray { 2000000000, 2000000000, 2000000000, 2000000000 },
83 Contents = pdfliteral(rep("0",4096),true),
84 Filter = pdfconstant("Adobe.PPKLite"),
85 SubFilter = pdfconstant("adbe.pkcs7.detached"),
86 Type = pdfconstant("Sig"),
87
88 Reference = reference,
89 }
90 objects[signature] = offset
91 local object = signature .. " 0 obj\n" .. tostring(d) .. "\nendobj\n"
92 status.reference = signature
93 status.position = offset
94 status.signature = object
95 flush(f,object)
96 offset = offset + #object
97 end
98 return offset
99 end
100
101 function lpdf.finalizesignature(filename,usedsize)
102 if not filename then
103 return
104 end
105 local signature = status.signature
106 if not signature then
107 return
108 end
109 local signame = file.replacesuffix(filename,"sig")
110 local filesize = file.size(filename)
111 local samesize = usedsize ~= filesize
112 local handle = samesize and io.open(filename,"r+b")
113 local position = status.position
114 status.ranges = getbyteranges(signature,position,filesize)
115 status.signature = signature
116 status.length = #signature
117 status.filename = filename
118 status.filesize = filesize
119 table.save(signame, status)
120 if handle then
121 local updated = updatesignature(signature,b1,n1,b2,n2)
122 if updated then
123 handle:seek("set",position)
124 handle:write(updated)
125 handle:close()
126 status.updated = true
127 end
128 end
129 end
130
131else
132
133
134
135
136 lpdf = lpdf or { }
137 local report = logs.reporter("sign pdf")
138
139 local openssl = nil
140
141 local runner = sandbox and sandbox.registerrunner {
142 name = "openssl",
143 program = "openssl",
144 template = "cms -sign -binary -passin pass:%password% -in %datafile% -out %signature% -outform der -signer %certificate%",
145 reporter = report,
146 }
147
148 local function checklibrary(uselibrary)
149 if uselibrary and openssl == nil then
150 dofile(resolvers.findfile("libs-imp-openssl.lmt","tex"))
151 openssl = require("openssl")
152 if not openssl then
153 openssl = false
154 end
155 end
156 return openssl
157 end
158
159
160
161
162 local function getsigdata(pdffile,f)
163 if not lpdf.epdf then
164 report("epdf support is not loaded")
165 return
166 end
167 local pdf = lpdf.epdf.load(pdffile)
168 if not pdf then
169 report("file %a can't be loaded",pdffile)
170 return
171 end
172 local widgets = pdf.widgets
173 if not widgets then
174 lpdf.close(pdf)
175 return
176 end
177 local position = nil
178 local length = nil
179 for i=1,#widgets do
180 local annotation = widgets[i]
181 local parent = annotation.Parent or { }
182 local what = annotation.FT or parent.FT
183 if what == "Sig" then
184 local obj = annotation.__raw__.V
185 if obj and obj[1]== 10 then
186 position, length = lpdf.epdf.objectrange(pdf,obj[3])
187 break
188 end
189 end
190 end
191 lpdf.close(pdf)
192 if position then
193 local p = f:seek()
194 f:seek("set",position)
195 local signature = f:read(length),
196 f:seek("set",p)
197 local filesize = file.size(pdffile)
198 return {
199 position = position,
200 length = length,
201 signature = signature,
202 filename = pdffile,
203 filesize = filesize,
204 ranges = getbyteranges(signature,position,filesize)
205 }
206 end
207 end
208
209 function lpdf.sign(specification)
210 local report = specification.report or report
211 local pdffile = specification.filename or ""
212 local certificate = specification.certificate or ""
213 local password = specification.password or ""
214 if pdffile == "" or certificate == "" or password == "" then
215 report("invalid specification")
216 return
217 end
218
219 local openssl = checklibrary(specification.uselibrary or false)
220
221 local sigfile = file.replacesuffix(pdffile,"sig")
222 local tmpfile = file.replacesuffix(pdffile,"tmp")
223 local binfile = file.replacesuffix(pdffile,"bin")
224
225 local f = io.open(pdffile,"r+b")
226 if not f then
227 report("unable to open %a for reading and writing",pdffile)
228 return
229 end
230
231 local data = table.load(sigfile)
232 if not data then
233 data = getsigdata(pdffile,f)
234 end
235 if not data then
236 report("unable to open %a",sigfile)
237 return
238 end
239 local filename = data.filename
240 if filename ~= pdffile then
241 report("file mismatch %a",pdffile)
242 return
243 end
244
245 local signature = data.signature
246 local position = data.position
247 local ranges = data.ranges
248 local filesize = data.filesize
249 statistics.starttiming("sign pdf")
250 local before = f:read(position)
251 local insert = f:seek()
252 local middle = f:read(#signature)
253 local after = f:read(filesize)
254 if not before or not after then
255 report("invalid status")
256 return
257 end
258 if middle ~= signature then
259 report("no pristine signature")
260 return
261 end
262
263
264 signature = updatesignature(signature,unpack(ranges)) or signature
265
266
267 middle = string.gsub(signature," <0.-0> "," ")
268 local data = before .. middle .. after
269 statistics.starttiming("sign ssh")
270 report("signing %a",pdffile)
271 local digest = nil
272 if openssl then
273 local result, message = openssl.sign {
274 data = data,
275 certfile = certificate,
276 password = password,
277 }
278 if result then
279 digest = message
280 else
281 report("ssh error: %a",message)
282 end
283 else
284 io.savedata(tmpfile,data)
285 runner {
286 password = password,
287 datafile = tmpfile,
288 signature = binfile,
289 certificate = certificate,
290 }
291 statistics.stoptiming("sign ssh")
292 digest = io.loaddata(binfile)
293 if not digest or digest == "" then
294 report("invalid digest from ssh")
295 return
296 end
297 if status.purge then
298 os.remove(tmpfile)
299 os.remove(binfile)
300 end
301 end
302 if digest then
303 digest = string.tohex(digest)
304 signature = string.gsub(signature,
305 "(/Contents <)([^>]+)(>)",
306 function(b,s,e) return b .. digest .. string.rep("0",#s-#digest) .. e end
307 )
308 f:seek("set",insert)
309 f:write(signature)
310 f:close()
311 end
312 if status.purge then
313 os.remove(sigfile)
314 end
315 statistics.stoptiming("sign pdf")
316 status.sshtime = statistics.elapsedtime("sign ssh")
317 status.pdftime = statistics.elapsedtime("sign pdf")
318 report("filesize %i bytes, ssh time %0.3f seconds, total time %0.3f seconds",
319 filesize,
320 statistics.elapsedtime("sign ssh"),
321 statistics.elapsedtime("sign pdf")
322 )
323 return status
324 end
325
326
327
328
329
330
331
332
333
334
335
336 function lpdf.verify(specification)
337 if not lpdf.epdf then
338 report("epdf support is not loaded")
339 return
340 end
341
342 local report = specification.report or report
343 local pdffile = specification.filename or ""
344 local certificate = specification.certificate or ""
345 local password = specification.password or ""
346 if pdffile == "" or certificate == "" or password == "" then
347 report("invalid specification")
348 return
349 end
350
351 local openssl = checklibrary(specification.uselibrary or false)
352
353 local pdf = lpdf.epdf.load(pdffile)
354 if not pdf then
355 report("file %a can't be loaded",pdffile)
356 return
357 end
358
359 local widgets = pdf.widgets
360 if not widgets then
361 report("file %a has no signature widgets",pdffile)
362 lpdf.close(pdf)
363 return
364 end
365 local signature = nil
366 local byterange = nil
367 for i=1,#widgets do
368 local annotation = widgets[i]
369 local parent = annotation.Parent or { }
370 local what = annotation.FT or parent.FT
371 if what == "Sig" then
372 local value = annotation.V
373 if value then
374 signature = string.tobytes(tostring(value.Contents) or "")
375 byterange = value.ByteRange
376 end
377 end
378 end
379 lpdf.close(pdf)
380 if not signature or signature == "" or not byterange then
381 report("file %a has no signature",pdffile)
382 return
383 end
384
385 local f = io.open(pdffile,"rb")
386 if not f then
387 report("unable to open %a for reading",pdffile)
388 return
389 end
390 f:seek("set",byterange[1])
391 local before = f:read(byterange[2] + 1)
392
393
394
395
396
397 f:seek("set",byterange[3])
398 local after = f:read(byterange[4])
399
400 if not before or not after then
401 report("invalid byteranges in %a",pdffile)
402 return
403 end
404 local okay = false
405 local data = before .. after
406 if openssl then
407 okay = openssl.verify {
408 data = data,
409 certfile = certificate,
410 password = password,
411 signature = signature,
412 }
413 else
414 report("verifying with the binary is not yet implemented")
415
416
417
418
419
420
421
422
423
424
425
426 end
427 if okay then
428 report("signature in file %a matches the content",pdffile)
429 else
430 report("signature in file %a doesn't match the content",pdffile)
431 end
432 end
433
434end
435 |