如果要在 用户态 做消息签名的话。也就是说,不去复用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外面。

每一层都要单独传,参数给人一种非常冗余的感觉。也可能应该把 传所有能传的 这种 冗余 形式定义为默认,通常的做法其实都是省略了很多参数的特殊情况吧。