from Hacker News

How (not) to sign a JSON object (2019)

by maple3142 on 2/9/25, 2:38 PM with 42 comments

  • by codeflo on 2/9/25, 7:45 PM

    I have a feeling that idea 2 is a recipe for disaster:

    > Add the tag and the exact string you signed to the object, validate the signature and then validate that the JSON object is the same as the one you got.

    In cryptographic practice, redundant information usually spells disaster, because inevitably, someone will use the copy that wasn't verified.

    But let's dig into it. If I understand this correctly, the suggestion is to have something like this:

        {
            "object": "with",
            "some": "properties",
    
            "signatureInfo": {
                "signedString": "{\"object\":\"with\",\"some\":\"properties\"}",
                "signature": "... base64-encoded-signature ..."
            }
        }
    
    It's mentioned that "the downside is your messages are about twice the size that they need to be". In my opinion, this scheme is pointless. To verify "the JSON object is the same as the one you got", you have to do what?

    1. Parse the outer object as JSON, extract and remove signatureInfo.

    2. Verify the signature.

    3. Parse the signedString as JSON.

    4. Verify that the object you got in step 1 equal to object you got in step 3 using a some kind of deep equality.

    First of all, this is error prone, and as underspecified as JSON is, there are potential exploits if the comparison isn't done carefully. But even worse, if you think about it, the outer JSON is entirely useless, since you need to parse the inner JSON anyway -- so why not just use it directly?

    It seems to me that this suggestion is strictly worse than just sending the inner part:

        {
            "signedString": "{\"object\":\"with\",\"some\":\"properties\"}",
            "signature": "... base64-encoded-signature ..."
        }
    
    Yes, it's no longer "in-band", but I don't think it was really in-band before, it was just out-of-band with an outer layer of redundant information.
  • by treyd on 2/9/25, 5:18 PM

    This is one of the deeply dissatisfying parts of the Matrix spec. They didn't have any constraints forcing them to embed signatures within the json objects, but they elected to invent their own signing scheme and do it anyways, despite being a greenfield. It also includes support for a special "unsigned" portion for extra data that comes along (which is often used for the server to inject the age of an event).

    I don't think the protocol still injects the signature into event structures, but this weird "unsigned" field is still there looking at the source json for a message I sent today, but it's possible it's removed after processing and Fluffychat is just removing it.

  • by DarkUranium on 2/9/25, 4:58 PM

    Not sure if I'm just misunderstanding the article or not, but it feels like an overengineered solution, reminescent of SAML's replacement instructions (just a hardcoded and admittedly way better option --- but still in a similar vein of "text replacement hacks").

    I know it's not the most elegant thing ever, but if it needs to be JSON at the post-signing level, why not just something like `["75cj8hgmRg+v8AQq3OvTDaf8pEWEOelNHP2x99yiu3Y","{\"foo\":\"bar\"}"]`, in other words, encode the JSON being signed as a string. This would then ensure that, even if the "outer" JSON is parsed and re-encoded, the string is unmodified. It'll even survive weird parsing and re-encoding, which the regex replacement option might not (unless it's tolerant of whitespace changes).

    (or, for the extra paranoid: encode the latter to base64 first and then as a string, yielding something like `["75cj8hgmRg+v8AQq3OvTDaf8pEWEOelNHP2x99yiu3Y","eyJmb28iOiJiYXIifQ"]` --- this way, it doesn't look like JSON anymore, for any parsers that try to be too smart)

    If the outer needs to be an object (as opposed to array), this is also trivially adapted, of course: `{"hmac":"75cj8hgmRg+v8AQq3OvTDaf8pEWEOelNHP2x99yiu3Y","json":"{\"foo\":\"bar\"}"}`.

  • by askvictor on 2/9/25, 8:28 PM

    Another problem with signing JSON: you can have two different json objects that mean the same thing, and will do exactly the same thing in your code e.g. {"a": "foo", "b": "bar"} vs {"b": "bar", "a": "foo"}. Also, whitespace. Are there any standards for normalising json, so that two equivalent, but differently written JSON files will have the same signature?
  • by lxgr on 2/9/25, 8:42 PM

    > Unless you have a good reason why you need an (asymmetric) signature, you want a MAC.

    Is "I want the server/validating side to be safe even against server-side attackers with read-only permissions" not a good reason? Because that's one thing that asymmetric signatures provide out of the box compared to MACs.

  • by monocasa on 2/11/25, 7:04 PM

    I really wish that XMLDSig wasn't such an awful standard that it turned a good third of the security industry against canonicalization in general.

    Saying there's "sure there's lots of ways to serialize, but these specific rules get you the same octet and you sign that" is key to sanity in such situations.

    For all of ASN.1's many sins, they got that part absolutely right.

  • by saurik on 2/9/25, 8:22 PM

    Supposedly, this is the non-code documentation for AWS Version 3 signing.

    https://docs.aws.amazon.com/amazonswf/latest/developerguide/...

  • by Zamicol on 2/9/25, 5:58 PM

    We addressed these concerns while developing Coze, a cryptographic JSON messaging specification. The specification details how we chose to address these concerns.

    https://github.com/Cyphrme/Coze

  • by dang on 2/11/25, 10:45 PM

    Discussed at the time:

    How not to sign a JSON object - https://news.ycombinator.com/item?id=20516489 - July 2019 (151 comments)

  • by hughes on 2/11/25, 5:58 PM

    What does JSON object signing provide that TLS doesn't?

    Does this imply that the application doesn't trust the transport/presentation layers?

  • by tgsovlerkhgsel on 2/11/25, 7:00 PM

    "it’s OK to sign the exact byte sequence."

    Not just "OK". It's the only sane way to do it.

  • by Muromec on 2/9/25, 9:45 PM

    Or just use asn1 like normal people.