Is there a simple method to "self-sign" contents of document with a QR code?

I’m wondering if it would be possible to include a QR code in the PDF that would somehow authenticate that the contents of the document have not been tampered with (example use: to sign a letter).

My initial thought was to encrypt a hash of the PDF using a private key ans include that signed hash as a QR. Of course I then realized the obvious chicken-and-egg problem that this would change the contents of the PDF, thus its hash, etc.

My second thought was to encrypt the main typst source code and then include that as a QR code. Here is a minimal example:

Assume the following template file (template.typ):

#let template(doc) = {
	set text(fill: red)
	doc
}

and the following document file (doc.typ):

#import "template.typ": *
#show: template

= Foo

Lorem ipsum foo bar baz.

The text to encrypt and QR-code would be the exact contents of doc.sty, including the #import statements.

To do this, there are three issues I would have to solve:

  • Is there a way to access the original contents of doc.typ from within template.typ, without hard-coding the file name doc.typ?
  • Is there a way to encrypt a string using a private-public encryption method from within typst?
  • Is there a way to generate a QR code from within typst?

I’m assuming that the answer to the two last questions is yes, and I’ll eventually find a suitable package. But if you have specific recommendations, I’m listening.

But perhaps the first issue is just not possible? Does the parser have any way to know about the source documents’ filenames or contents?

1 Like

Typst doesn’t natively support encryption or QR code generation, but you can handle this externally. Use a script to hash and sign the document, generate a QR code, and then embed it in Typst using image(). This ensures document integrity without altering its contents.

1 Like

Thanks for your suggestion. Yes, this can obviously be achieved with a makefile or such.

I was wondering whether there are hard limitations to doing so within typst itself, or if it’s just a matter of finding/writing suitable packages.

There are packages that do allow QR code generation, for example tiaoma or cades, so if you have the data you want to encode in the QR code available, that is possible entirely within Typst.

However, as of now, there is no package (that I know of) for asymmetric encryption, and no way to get the name of the document’s .typ file. For the latter, you can follow this related feature request on GitHub.

1 Like

(shameless plug)
Although there is no package for asymmetric encryption yet, jumble provides some hashing functions (MD5, SHA1, etc.)

Would it be sufficient for the template to hash the doc content passed to it? It would no longer be the plaintext of the document that is hashed, but what would be hashed is based directly on the text in the document.

In your example this would be what gets hashed:

sequence(
  parbreak(),
  heading(depth: 1, body: [Foo]),
  parbreak(),
  [Lorem ipsum foo bar baz.],
)

Changing anything* in the document would result in a change here.

This also does not have the document file name.

*Using syntactic sugar vs calling the function directly would not change the doc content, but would be a change in the source. I’m not sure if this is a problem, however.

I think this fails for anything that uses styles or context, as in this case when you try to get a string representation of the content you just get .. for the styles and context() for the context:

#show: repr

#set text(red)
Text.
#context text.size

image

You’d get the same hash when using different styles or different content in the context block…

To be honest, I think any attempt to do this from within Typst is doomed to fail. First to the technical side discussed so far: as @sijo said, the document tree is not enough, and if you take the source code, you’d also have to take all imported source files into account - which you can’t for packages, since Typst sandboxes the filesystem accesses.

If you did have that, hashing and encrypting would be simple in theory, just a matter of writing a plugin. And generating QR codes is already supported.

However…

The real problem is in my opinion verification: a signature is only as good as the reader’s ability to verify it, and here the reader would need to access the Typst source code (or, for the sake of argument, the document tree). How do they get it? Are the source files distributed along the PDF? Are they embedded in the PDF using the new attachment feature?

If so, how do they know that the attachments match the visual appearance of the document? I think the only way to verify such a signature would be to regenerate the PDF from sources and compare visually. At that point, it would be easier to

  • skip the PDF and only send source files in a signed zip archive, and expect the receiver to compile the document
  • or even better: send a regular PDF inside a signed zip archive.

The critical difference is tool support: signed zips are already a thing, and as long as the receiver uses an archiving tool that supports it, the signature check will be largely automatic (you’d probably have to provide the sender’s public key, but that’s it).

The third way, of course, is to sign the PDF after the fact. Electronically signed PDFs are already a thing, so use that mechanism. Like the other two mechanisms, that’s not possible to do as a Typst package, but Typst itself could eventually support it (I think) and third-party tools already exist.

Thanks for the feedback, everybody. My original goal was to include some level of authentification in the printed version of a letter. At work I sometimes have to put some kind of commitment in writing, and it always bugged me that this is essentially a pdf with a jpg of my signature, which is hardly bullet-proof security. I know the proper way to do this is to sign the pdf file itself, but I was thinking of ways to authenticate the contents (the statement of commitment, as opposed to the styling of the pdf) that would survive printing on piece of paper.

So far here is what I do:

  1. gpg --armor --sign doc.typ, which outputs doc.typ.asc
  2. include the contents of doc.typ.asc in the PDF next to my signature.jpg
  3. also mention that my public key can be found at https://myowndomain.com/pgp

Because myowndomain.com has been linked to me for the past 20 years, readers can generally trust that the’ll be getting my true public key from that url (barring man-in-the-middle shenanigans, of course). So this means that someone presented with a printed version of this letter can decrypt the PGP-encrypted contents of doc.typ and get a time-stamped, authenticated version of my statement, e.g.:

#import "signed-letter.typ": template
#show: template

To whom it may concern:

I solemnly swear that I am up to no good.

#sign()

Any comments or alternative suggestions are welcome. Thanks again for the feedback.

if that level of attestation is enough, I think you have options. The open problem is someone implementing the signature algorithm in a WASM plugin, but other than that you can do something like this.

(The plain-text() function is from this example from the Typstonomicon)

#let sign(body) = {
  import "@preview/tiaoma:0.2.1"

  body

  let text = plain-text(body)
  tiaoma.qrcode(text)
}

#sign[
  To whom it may concern:
  
  I solemnly swear that I am up to no good.
]

or if you want to sign the whole rest of the document anyway:

#show: sign

To whom it may concern:
  
I solemnly swear that I am up to no good.

Note that this is vulnerable to attacks like #show "no good": "good".


If you want to use the whole source code (although I’d argue signing the non-content part of the source code doesn’t add any real authentication), I’d probably just read it from within doc.typ.

Is there a way to access the original contents of doc.typ from within template.typ, without hard-coding the file name doc.typ?

Currently not. The input file name is not exposed, and neither can the template file reliably read the input file if we assume you may want to extract template.typ into a package. So the best you can currently do is the input file explicitly referring to itself:

#let sign(source) = {
  import "@preview/tiaoma:0.2.1"

  tiaoma.qrcode(source)
}

...

#sign(read("doc.typ"))
1 Like

Why so complicated?

gpg sign the PDF and enclose that with the document.

Why so complicated?

Because not everything stays digital. It seems useful to be able to authenticate a printed document. YMMV, of course.

If it doesn’t stay digital, why try a digital solution?

The saying “don’t search for a X (digital) solution for a Y (analog) problem” only works if it’s a Y problem. Wanting documents to be authenticated is not an exclusively analog or digital problem. I think it’s totally sensible to want to sign something digitally (because it doesn’t necessarily become analog) and want the signature to carry over in the event it becomes analog.

1 Like