如果要在 用户态 做消息签名的话。也就是说,不去复用TLS。
消息原本的序列化/反序列化是用protobuf做的。
可以选择把签名作为protobuf spec的一个字段,也可以手动在protobuf外面包一层,对整个序列化之后的数据进行签名/验证操作。
无论是哪个方案,首先都必须得把签名对应的身份也编码进消息里去。考虑验证quorum certification是否有效的场景,如果没有身份信息就无法知道其中的消息是从哪里收集来的,该怎么验证它。一般这里面的消息都会有replica id字段可以当作身份字段来用,不过不能完全通用所有情况,比如需要转发消息的场景。
如果身份和签名在protobuf里面的话,就要先解码protobuf再验证签名,更糟糕的是序列化消息时要序列化两次
- 签名字段留空,序列化消息得到消息数据
- 根据消息数据计算签名,填入签名字段
- 再序列化一次得到最终消息
如果签名作为protobuf的字段,那么如果后续想要对消息结构进行调整,添加/取消对消息的签名就必须手动改protobuf的定义文件。就算不改,大批的消息定义都要包含一个签名字段(以及身份字段),看起来十分冗余。
如果签名作为单独的一层,对于有嵌套结构的消息,其protobuf定义不能直接进行嵌套,而必须用bytes类型作为中间层。一个比较突出的例子是PBFT的preprepare消息,其中的
m
是。也就是说,这是一个本身不需要签名,但是嵌套了两个需要签名的子消息的消息。
就算preprepare不需要签名,它还是得被套在签名层里面,因为协议的其它消息都得签名,所以解码的时候必须统一从签名层开始解。只能再用别的方式告知 这个签名层实际上没有签名 。所以对preprepare的解码需要解三次
proto::PBFTMessage message;
PBMessage pb_layer(message);
SignedAdapter signed_layer(pb_layer, "");
signed_layer.Parse(owned_buffer.data(), owned_buffer.size());
if (!signed_layer.IsVerified()) {
RWarning("Receive message failed to verify");
return nullptr;
}
switch (message.sub_case()) {
case proto::PBFTMessage::SubCase::kRequest:
// ...
case proto::PBFTMessage::SubCase::kPreprepare: {
const string &prepare_buffer =
message.preprepare().signed_prepare();
proto::Prepare prepare_message;
PBMessage pb_prepare(prepare_message);
SignedAdapter signed_prepare(pb_prepare, "");
signed_prepare.Parse(
prepare_buffer.data(), prepare_buffer.size());
if (!signed_prepare.IsVerified()) {
RWarning("Failed to verify Preprepare (Prepare)");
return nullptr;
}
const string &request_buffer =
message.preprepare().signed_message();
proto::PBFTMessage request_message;
PBMessage pb_request(request_message);
SignedAdapter signed_request(pb_request, "");
signed_request.Parse(
request_buffer.data(), request_buffer.size());
if (!signed_request.IsVerified() ||
!request_message.has_request()) {
RWarning("Failed to verify Preprepare (Request)");
return nullptr;
}
return [ //
this, escaping_remote = remote.release(), message,
prepare_message, prepare_buffer,
request_message //
]() {
auto remote = unique_ptr<TransportAddress>(escaping_remote);
HandlePreprepare(
*remote, prepare_message, prepare_buffer,
request_message.request());
};
最后在传递两个子消息的同时,还得把其中一个的字节数据也传进去,因为要往quorum certification里放,放的话肯定得放有签名的。有签名就不能有protobuf消息结构,因为签名在protobuf外面。
每一层都要单独传,参数给人一种非常冗余的感觉。也可能应该把 传所有能传的 这种 冗余 形式定义为默认,通常的做法其实都是省略了很多参数的特殊情况吧。