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.
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.
- 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
- 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
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 is defined as a ECDSA signature of SHA256 hash of serialized payload. At the moment only SECP256K1 curve is supported.
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.
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_pem
andprivate_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_type
encoded in base64signable_serialized_b64
: Same message serialized using signable library, encoded in base64signable_signature_b64
: Signature of sha256 hash of binary data generated bysignable
serializer from this protobuf message, encoded in base64
For each test case in testcases
array:
- Deserialize
proto_serialized_b64
into protobuf messageproto_message_type
using protobuf decoder, store result inmessage
- Serialize
message
using yoursignable
implementation, store result inmessage_serialized
- Encode
message_serialized
into base64 and compare withsignable_serialized_b64
. Test is failed if they are different - Verify a EC signature of sha256 hash of payload
message_serialized
with signaturesignable_signature_b64
(decode from base64 first), public key ispublic_key_pem
from json root. If signature verification failed, test is failed as well