Digital signatures on JSON payloads — let’s call it ‘jsonsig’

I’m finding myself wanting to digitally sign JSON content, keeping the signature and metadata inside the JSON file, and I’m largely coming up empty looking for a solution that exists already. I only found:

  • Camlistore: it signs JSON documents but creates an “outer wrapper”. It’s also a bit too specific to Camlistore for my needs. They helpfully point to the next thing I found:
  • canonical-json: documented here in github. He changes the structure of the JSON by moving the payload into “signed-content” and is doing a bit more with canonicalization than I like.
  • The StackExchange community points out the recent JSON web signature draft by old buddies Mike Jones, John Bradley and Nat Sakimura: however, that’s only about representing the signature and metadata in JSON, not the payload.

So here are my thoughts. First the requirements, as I see them:

  • do not change the structure of the JSON payload. A parser that does not validate the signature of the JSON content should simply be able to ignore signature-related info, and still work on the original JSON without changes.
  • keep content and signature(s) in the same file. This allows single-operation network fetches, and also avoids having to come up with funny conventions for how to derive the name of signature files from the name of content files. (That usually works, if you assume a single signature file, like gpg does, but only then. See below.)
  • be usable with GPG, but extensible to other types of crypto
  • allow multiple parties to sign the same payload, without getting into each others’ way.

The latter point may need explaining. There are two distinct scenarios:

  1. A and B, both sign content X, independently of each other, and I’d like to be able to carry both signatures in the same JSON file that also has the content. Example use case: A and B agree to the same contract. Obviously that should work for any number of parties, not just two.
  2. A signs content X first, and then B signs ( content + A’s signature). MSalters on Stackexchange points out that there are use cases for that. Also, it’s so simple in the non-JSON world (cat content | gpg --sign | gpg --sign) that we should support this, too)

So here’s the basic idea, in the form of an example. Black is the payload. Green is the new jsonsig section. Red reflects that A and B signed the payload independently of each other, while blue adds that C signed the content-signed-by-A-and-B.

  "abc" : [ "def", "ghi" ],
  "jkl" : 5,
  "mno" : { "pqr" : "stu" },
  "jsonsig" : [
        "by"  : "<<identifier of signer A>>",
        "alg" : "gpg",
        "pubkey" : "<<public key of signer A>>",
        "sig" : "<<signature A>>"
        "by"  : "<<identifier of signer B>>",
        "alg" : "gpg",
        "pubkey" : "<<public key of signer B>>",
        "sig" : "<<signature B>>"
        "by" : "<<identifier of signer C>>",
        "alg" : "gpg",
        "pubkey" : "<<public key of signer C>>",
        "sig" : "<<signature C>>"

Note the two nested arrays:

  • The outer array (green) reflects the sequence in which signatures were made: the signatures in the first array element were made before the one in the second. In other words, C signed after A and B did, and C signed the document plus A’s and B’s signatures, not just the payload.
  • The inner array captures all signatures that were performed concurrently in this step: A and B signed the document independently of each other. Note that although A is listed before B in the array, the inner array (unlike the outer array) does not  imply sequence.

To sign JSON content using JSONSIG that does not contain any JSONSIG yet, follow this algorithm:

  1. Consider the JSON document to be a stream of bytes, and sign that stream of bytes.
  2. Add the “jsonsig” element to the top-level hash, with the appropriate values for signature and metadata. This might be easiest by simply looking for the first { or last } in the document, and inserting it there.

Signature validation is performed by first removing the bytes representing the “jsonsig” section. Care needs to be taken to do this without changing the byte sequence of the remaining content.

If the JSON content already contains a “jsonsig” element at the top level, the signer first has to decide whether they intend to sign the content “in parallel” or “sequentially”. This corresponds to B vs C in the example above.

If “sequentially”, like C, the signer processes the JSON content as byte stream without change as above, except that they add their signature and metadata into the existing “jsonsig” element as a last element in the outer array, instead of creating a new one. To validate, the validator first removes the last element in the outer array, and the preceding comma.

If “in parallel”, the signer first removes the last element in the outer array, and the preceding comma. If that makes the outer array empty, the signer removes the entire “jsonsig” element to turn the JSON content into the original content without any signature information. The signature is then created treating the remaining content as a byte stream, and the signature and metadata is added as the last element in the inner array that is the last element in the outer array.

So. What do you think?



Comments are closed.