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