Hacker News new | ask | show | jobs
by blattimwind 2932 days ago
> Note that there is a complex canonicalisation procedure for the JSON object, and that the sender must mutate the signed object;

This is a big no-no and actual source of vulnerabilities. If you sign something, the signature goes around what you want to sign.[1] Doing "in-line" signatures is excessively more complex and error-prone. The easiest and most secure scheme is actually "sign a blob of bytes", i.e. signing a packed representation of a message. That way, you get zero ambiguity issues as far as signature-content interactions go [2], and you don't actually need a canonicalized message representation any more (which is not a common feature of serialization formats outside ASN.1 encodings).

There might be other reasons to not use UMF, but this one is completely sufficient to avoid it.

(Also calling HMAC tags "signatures" is confusing as heck and should be avoided.)

(Also the actual method of how the MAC is calculated is not specified; so clearly UMF is not a format, it is a meta-format.)

[1] Even JWT got that right.

[2] Context ambiguity AKA The Horton Principle remains, because that's not something a format solves.

3 comments

Thanks for this. The need for canonical JSON is perhaps the best reason for dropping the signature field from future versions of the spec. However, because only the `to` `from` and `body` fields are required there's no need avoid the format - just don't use the signature field unless the body field contains a single field with serialized data. Certainly, that use would need to be clearly documented.

Again your point is valid and will likely result in the depreciation of the signature field.

Thanks for taking the time to offer feedback.

Would you be able to elaborate on this?

Why is doing "in-line" signatures a worse design or a source of vulnerabilities? Are there any benefits for providing an in-line signature?

Any examples or additional information would be appreciated. Trying to better understand the issue at hand.

You have to completely parse the message to extract the signature and then re-serialize the message before you're able to validate the message. Consider a situation where you have a defect in your parser:

- in-line signature: you're applying your parser and serializer to the untrusted body of the message, and then verifying the signature. If this is a malicious payload, you've just run it through your parser and serialzer.

- out-of-message signature: you have the full signature and can verify the message without running a potentially-malicious message through anything other than your signature-verification code.

Option1: json.inlineSig: '{ "a": 1, "b": 2, "signature": "ff1341234..." }'

Option2: json.outOfBandSig: '?????'

Option2: json.signature: 'File=json.outOfBandSig; Signature=ff12341234...'

Basically if you try and do option #1 you actually need to parse the content, and THEN find out it's untrusted (which means you need to _execute_ your parser on the potentially unknown / hostile bytes), and then pretend you never processed them in the first place (discard) unknown / hostile bytes.

If you do option #2 then you blindly process the bytes with the signature algorithm, verify they are trusted and THEN run your parser on bytes of a signed / known origin.

Compare:

signedParseInlineSig( '{ "a": 1, "signature": "<<INVALID>>" }' );

signedParseOutOfBandSig( '{ "a": 1 }', "<<INVALID>>" );

...with #1 you have to run isValid( input, JSON.parse(input).signature )

...with #2 you run isValid( input, signature ) && JSON.parse( input )

> Basically if you try and do option #1 you actually need to parse the content, and THEN find out it's untrusted (which means you need to _execute_ your parser on the potentially unknown / hostile bytes), and then pretend you never processed them in the first place (discard) unknown / hostile bytes.

And you need to remove the signature and reassemble the modified data structure back to bytes in EXACTLY the same way as the signer did. This is more work (for larger data structures) and way harder to get right.

Re-normalization of the message also has some other issues, e.g. you need to make sure that you are parsing and processing the re-assembled version (what the signature was checked against), not the message you received; otherwise your signature might be completely useless (think about an attacker inserting duplicate keys: the re-normalization might remove them, but your parser might normally not. Signature validates, but you're not processing what was signed! Oops.)

If you do this the best case scenario is that it kinda seems to work, and if you're lucky it's even secure, but it actually doesn't work or silently stops working for some messages after you update a parser somewhere in the system, because suddenly they disagree about some edge case, and your system breaks.

+1e6

Never design a protocol where you must re-encode (and canonicalize! ouch!) in order to verify signatures. Instead you should wrap the thing to sign (and the signature) as an octet string. E.g.,

    {"thing-to-sign":"<base64-encoded-thing>",
     "signer-info":...,
     "signature":"<base64-encoded-signature-of-the-base64-encoded-thing>"}
This basically kills any joy of using JSON...

This, for example, does not work:

    {"thing":<thing-object>,
     "signer-info":...,
     "signature":"<base64-encoded-signature-of-thing>"}
because you'd have to have a JSON text parser that lets you get at the as-originally-encoded <thing> part of the above JSON text. This is not a common JSON text parser feature! So implementors would tend to re-encode <thing-object> in order to verify the signature.

This also doesn't work, for the same reason and because it's even harder to deal with a signature that's in the middle of the <thing>:

    {"thing-field0":...,
     "thing-field1":...,
     "signer-info":...,
     "signature":"<base64-encoded-signature-of-the-base64-encoded-thing>",
     "thing-fieldN":...}
Even if you promise and keep the promise to put the signature fields first or last, it's still super difficult to make this work well for other implementors, and is difficult even for yourself: you'll probably end up writing a new JSON parser from scratch to deal with this mess without having to re-encode, and most likely you'll opt to re-encode.

Re-encoding for signature verification requires canonicalization. For JSON canonicalization means:

- you must specify object key ("name") order - you must specify what if any interstitial whitespace to have - you must specify a canonical string representation (e.g., all Unicode escaped, or all Unicode not escaped, ...) - you must specify a canonical number representation (oof!)

Numbers make canonicalization really tricky! You'd better limit yourself to 52-bit signed integers. If you use real numbers you'll quickly get into an IEE754 mess.

But again, no one will think this is useful:

    {"thing-to-sign":"<base64-encoded-thing>",
     "signer-info":...,
     "signature":"<base64-encoded-signature-of-the-base64-encoded-thing>"}
if the whole point of using JSON was to make this sort of thing close to human readable.

Now, of course, it's trivial to write some jq code to decode the <thing> and pretty-print it, so it's not the end of the world. But still, people will resist this approach and we'll be right back to defining a canonical JSON encoding.