SSB Message Signing is Fun!
Turns out there is a whole new class of strings for which JSON.stringify(JSON.parse(str)) === str
does not hold: Node does not preserve object key order in all of these cases. The one I (or rather the fuzzer) found is JSON.stringify(JSON.parse('{"!":2,"66[[":2,"6[[":2,"":3,"0[al|":3,"6[":2,"6":266}'), null, 2)
, which yields (cleaned up escaped quotes for readability)
{
"6": 266,
"!": 2,
"66[[": 2,
"6[[": 2,
"": 3,
"0[al|": 3,
"6[": 2
}
instead of the expected
rs: {
"!": 2,
"66[[": 2,
"6[[": 2,
"": 3,
"0[al|": 3,
"6[": 2,
"6": 266
}
If a non-js implementation used the order-preserving signing encoding to compute the signature, and then sent the message over the wire to sbot, then sbot would reject it as invalid. This is not good.
I see three ways out:
- burn everything to the ground
- as much as I want to do this, it's not really an option
- reverse-engineer the js behavior and make it part of the spec by disallowing to send messages over the wire where the object order might change upon the
JSON.parse -> JSON.stringify
roundtrip.- this is horrible for obvious reasons
- change the behavior of the js implementation to use a custom, order preserving parser when decoding json messages received over the wire
- since this only affects the transport encoding, it is not a breaking change to the core protocol
- I've heard rumours of a rust implementation that could serve as a drop-in replacement for
JSON.parse
andJSON.stringify
once js bindings are written
CC @Dominic @dinosaur @Piet @arj
@cryptix, @keks, @cel: Could you check how your implementations deal with this message?
A bunch of test data for implementors:
Input:
{"!":2,"66[[":2,"6[[":2,"":3,"0[al|":3,"6[":2,"6":266}
Input as hex byte array:
[7b, 22, 21, 22, 3a, 32, 2c, 22, 36, 36, 5b, 5b, 22, 3a, 32, 2c, 22, 36, 5b, 5b, 22, 3a, 32, 2c, 22, 22, 3a, 33, 2c, 22, 30, 5b, 61, 6c, 7c, 22, 3a, 33, 2c, 22, 36, 5b, 22, 3a, 32, 2c, 22, 36, 22, 3a, 32, 36, 36, 7d]
Nodejs signing output:
{
"6": 266,
"!": 2,
"66[[": 2,
"6[[": 2,
"": 3,
"0[al|": 3,
"6[": 2
}
Nodejs signing output as hex byte array:
[7b, a, 20, 20, 22, 36, 22, 3a, 20, 32, 36, 36, 2c, a, 20, 20, 22, 21, 22, 3a, 20, 32, 2c, a, 20, 20, 22, 36, 36, 5b, 5b, 22, 3a, 20, 32, 2c, a, 20, 20, 22, 36, 5b, 5b, 22, 3a, 20, 32, 2c, a, 20, 20, 22, 22, 3a, 20, 33, 2c, a, 20, 20, 22, 30, 5b, 61, 6c, 7c, 22, 3a, 20, 33, 2c, a, 20, 20, 22, 36, 5b, 22, 3a, 20, 32, a, 7d]
Order-preserving output:
rs: {
"!": 2,
"66[[": 2,
"6[[": 2,
"": 3,
"0[al|": 3,
"6[": 2,
"6": 266
}
Order-preserving output as hex byte array:
[7b, a, 20, 20, 22, 21, 22, 3a, 20, 32, 2c, a, 20, 20, 22, 36, 36, 5b, 5b, 22, 3a, 20, 32, 2c, a, 20, 20, 22, 36, 5b, 5b, 22, 3a, 20, 32, 2c, a, 20, 20, 22, 22, 3a, 20, 33, 2c, a, 20, 20, 22, 30, 5b, 61, 6c, 7c, 22, 3a, 20, 33, 2c, a, 20, 20, 22, 36, 5b, 22, 3a, 20, 32, 2c, a, 20, 20, 22, 36, 22, 3a, 20, 32, 36, 36, a, 7d]