blob: 88b1bb4fd95619d3e7ed643147e6c2829a950979 [file] [log] [blame]
  1. JSON claim objects need to be signed. If I want to distribute a Camli
  2. blob object publicly, declaring that I "favorite" or "star" a named
  3. entity, it should be verifiable.
  4. The properties we want in the JSON file, ideally, include:
  5. GOAL #1) it's still a valid JSON file in its entirety.
  6. This means no non-JSON compliant header or footer.
  7. This implies that the data structure to be signed and the signature
  8. metadata be separate, in an outer JSON wrapper.
  9. This has been discussed and implemented in various ways. For example,
  10. in jchris's canonical-json project,
  12. ... the "signed-content" and the "signature" are parallel objects under the
  13. same outer JSON object.
  14. The problem then becomes that the verifier, after parsing the JSON
  15. blob, needs to re-serialize the JSON "signed-content" object,
  16. byte-for-byte as in the original, in order to verify the signature.
  17. In jchris' strategy, the canonicalization is implemented by
  18. referencing JavaScript code that serializes it. This has the
  19. advantage that the serialization could change over time, but the
  20. disadvantage that you have to embed a Rhino, V8, SpiderMonkey, or
  21. similar into your parser, which is somewhat heavy. Considering that
  22. canonical JSON serialization is something that should be relatively
  23. static and could be defined once, I'm not sure that the flexibility is
  24. worth the cost.
  25. Overall, though, the jchris approach's structure of the JSON file is
  26. good.
  27. Notably, it satisfies on of my other goals:
  28. GOAL #2) The document still be human-readable.
  29. For instance, the project is proposing this Canonical JSON
  30. format:
  32. .. unfortunately, all whitespace is stripped. It's not a deal
  33. breaker, but lacks human readableness.
  34. You might say, "Bring your own serialization! Wrap the signed-content
  35. in a string!"
  36. But then you're back to the readable problem, because JSON strings
  37. can't have embedded newline literals.
  38. Further, the proposal requires the use of a new JSON
  39. serialization library and parser for each language which wants to
  40. produce camli documents. This isn't a huge deal, but considering that
  41. JSON libraries already exist and people are oddly passionate about
  42. their favorites and inertia's not to be ignored, I state the next
  43. goal:
  44. GOAL #3) Don't require a new JSON library for parsing/serialization.
  45. With the above goals in mind, Camli uses the following scheme to sign
  46. and verify JSON documents:
  48. =======
  49. -- Start with a JSON object (not an array) to be encoded and signed.
  50. We'll call this data structure 'O'. While this signing technique
  51. could be used for applications other than Camlistore, this document
  52. is specifically about Camlistore, which requires that the JSON
  53. object 'O' contain the following two key/value pairs:
  54. "camliVersion": "1"
  55. "camliSigner": "hashalg-xxxxxxxxxxx" (blobref of ASCII-armored public key)
  56. -- To find your camliSigner value, you could use GPG like:
  57. $ gpg --no-default-keyring --keyring=example/test-keyring.gpg --secret-keyring=example/test-secring.gpg \
  58. --export --armor 26F5ABDA > example/public-key.txt
  59. $ sha1sum example/public-key.txt
  60. 8616ebc5143efe038528c2ab8fa6582353805a7a
  61. ... so the blobref value for camliSigner is "sha1-8616ebc5143efe038528c2ab8fa6582353805a7a".
  62. Clients will use this value in the future to find the public key to verify
  63. signtures.
  64. -- Serialize in-memory JSON object 'O' with whatever JSON
  65. serialization library you have available. internal or trailing
  66. whitespace doesn't matter. We'll call the JSON serialization of
  67. 'O' (defined in earlier step) 'J'
  68. (e.g. doc/example/signing-before-J.camli)
  69. -- Now remove any trailing whitespace and exactly and only one '}'
  70. character from the end of string 'J'. We'll call this truncated,
  71. trimmed string 'T'.
  72. (e.g. doc/example/signing-before.camli)
  73. -- Create an ASCII-armored detached signature of this document,
  74. e.g.:
  75. gpg --detach-sign --local-user=54F8A914 --armor \
  76. -o signing-before.camli.detachsig signing-before.camli
  77. (The output file is in doc/example/signing-before.camli.detachsig)
  78. -- Take just the base64 part of that ASCII detached signature
  79. into a single line, and call that 'S'.
  80. -- Append the following to 'T' above:
  81. ,"camliSig":"<S>"}\n
  82. ... where <S> is the single-line ASCII base64 detached signature.
  83. Note that there are exactly 13 bytes before <S> and exactly
  84. 3 bytes after <S>. Those must match exactly.
  85. -- The resulting string is 'C', the camli-signed JSON document.
  86. (The output file is in doc/example/signing-after.camli)
  87. In review:
  88. O == the object to be signed
  89. J == any valid JSON serialization of O
  90. T == J, with 0+ trailing whitespace removed, and then 1 '}' character
  91. removed
  92. S == ascii-armored detached signature of T
  93. C == CONCAT(T, ',"camliSig":"', S, '"}', '\n')
  94. (strictly, the trailing newline and the exact JSON serialziation of
  95. the camlisig element doesn't matter, but it'd be advised to follow
  96. this recommendation for compatibility with other verification code)
  98. =========
  99. -- start with a byte array representing the JSON to be verified.
  100. call this 'BA' ("bytes all")
  101. -- given the byte array, find the last index in 'BA' of the 13 byte
  102. substring:
  103. ,"camliSig":"
  104. Let's call the bytes before that 'BP' ("bytes payload") and the bytes
  105. starting at that substring 'BS' ("bytes signature")
  106. -- define 'BPJ' ("bytes payload JSON") as 'BP' + the single byte '}'.
  107. -- parse 'BPJ', verifying that it's valid JSON object (dictionary).
  108. verify that the object has a 'camliSigner' key with a string key
  109. that's a valid blobref (e.g. "sha1-xxxxxxx") note the camliSigner.
  110. -- replace the first byte of 'BS' (the ',') with an open brace ('{')
  111. and parse it as JSON. verify that it's a valid JSON object with
  112. exactly one key: "camliSig"
  113. -- using 'camliSigner', a camli blobref, find the blob (cached, via
  114. camli/web lookup, etc) that represents a GPG public key.
  115. -- use GnuPG or equivalent libraries to verify that the ASCII-armored
  116. GPG signature in "camliSig" signs the bytes in 'BP' using the
  117. GPG public key found via the 'camliSigner' blobref