Deterministic serialization and signatures with proto-lens and protobuf-elixir support. Haskell version is based on generic type class, which means it can be implemented for any data type. There is signable-haskell-protoc plugin which generates implementation for proto-lens types. Elixir version is protobuf-elixir specific.
Serialization
Protobuf standard doesn't guarantee deterministic serialization. In order to deterministically serialize and sign a protobuf message specific serialization protocol should be followed:
- Sort all message fields by index (ASC order).
- Serialize every field value (take a look to type-specific notes below).
- If field value is unset (only for message/oneof) or is empty list (repeated) then leave serialized value as it is (empty bytestring), otherwise prepend it with serialized field index (as uint32 4 bytes).
- Concatenate resulting bytestrings.
Please distinguish unset value (it might be called nil, null or Nothing - depends on programming language you are using) and default value (for example default message with unset fields). Serialized default value still might be empty bytestring, but it should be prepended with serialized field index (unlike unset value which is just empty bytestring without any index). Also notice that value might be unset only for message/oneof. Scalars (types like uint32, bool or string) are always set (even in cases where serialized value is empty bytestring) and always should be prepended with serialized field index.
Type-specific notes
- Integers are serialized in big endian format
- Enums are serialized as uint32
- Unset message values are serialized as empty bytestring
- Unset oneof values are serialized as empty bytestring
- Repeated values are serialized and concatenated according order in payload
- Serialization protocol don't support maps and floats
Protobuf schema upgrade
- Add new optional (message/repeated/oneof) field to protobuf schema
- Upgrade receiver side (signatures of unupgraded sender still will be valid in case where optional field has been added)
- Upgrade sender side
Explanation
Suppose we have upgraded proto message by adding a new required field. Signature verification is going to work like this:
| Sender | Receiver | Signature status |
|---|---|---|
| aware | aware | signatures match |
| aware | not aware | signatures mismatch |
| not aware | aware | signatures mismatch |
| not aware | not aware | signatures match |
From this table its clear that situation when one party is unaware of changes leads to potential signature mismatch. To avoid this issue, all newly added scalar fields must be added in wrapper type (Google.Protobuf.StringValue, Google.Protobuf.BytesValue, etc.), allowing them to be unset. For unset fields no data is added to signable serialized binary, so there will be no signature mismatch.
Signature
Signature is defined as a ECDSA signature of SHA256 hash of serialized payload. At the moment only SECP256K1 curve is supported.
Testing
Every implementation must comply all test cases provided in test-case directory. If new implementation is written, it should provide generated test cases as well, for compatibility testing in already existing implementations.
Test-case file description
Each test case file is a json object with following fields:
public_key_pem: EC public key that can be used to verify signatures in test cases, PEM formatprivate_key_pem: EC private key that can be used to generate new signatures (generate new test cases), PEM formatcurve: Elliptic curve type ofpublic_key_pemandprivate_key_pem, lowercasetestcases: json array of test case objects:test_description: Test descriptionproto_message_type: Protobuf message used in this test caseproto_serialized_b64: Protobuf message of typeproto_message_typeencoded in base64signable_serialized_b64: Same message serialized using signable library, encoded in base64signable_signature_b64: Signature of sha256 hash of binary data generated bysignableserializer from this protobuf message, encoded in base64
Test cases execution
For each test case in testcases array:
- Deserialize
proto_serialized_b64into protobuf messageproto_message_typeusing protobuf decoder, store result inmessage - Serialize
messageusing yoursignableimplementation, store result inmessage_serialized - Encode
message_serializedinto base64 and compare withsignable_serialized_b64. Test is failed if they are different - Verify a EC signature of sha256 hash of payload
message_serializedwith signaturesignable_signature_b64(decode from base64 first), public key ispublic_key_pemfrom json root. If signature verification failed, test is failed as well