You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Protobuf反射实现带加密签名的确定性序列化?

如何通过Protobuf反射实现确定性序列化以支持加密签名?

我需要实现Protobuf消息的确定性序列化,这样才能给消息附加加密签名——因为不同序列化器生成的二进制数据可能不一样,会导致签名失效。

Proto3发布说明里提到:

不过,确定性序列化在不同语言间不具备规范性;且在存在未知字段的架构变更后,不同构建版本间也不稳定。需要规范性序列化的用户(例如以规范形式持久化存储、指纹识别等场景)应自行定义规范化规范,并使用反射API实现序列化器,而非依赖此API。

我的Proto定义如下:

package Something;
message Request {
  Payload payload = 1;
  // 序列化后payload的字节签名
  bytes signature = 2;
  message Payload {
    string user_id_from = 1;
    uint64 amount = 2;
    string user_id_to = 3;
  }
}

请问怎么用Protobuf反射来实现这个需求?


解决方案

要实现确定性序列化,核心是自定义一套严格无歧义的序列化规则,再通过Protobuf反射API严格遵循规则序列化字段,确保相同消息内容在任何环境下都生成完全一致的二进制字节流。下面以Java为例(其他语言思路一致,仅反射API细节不同),一步步说明实现方式:

1. 先明确你的确定性序列化规则

这是最关键的一步,规则必须清晰可落地,比如:

  • 只序列化已知字段(忽略未知字段,避免架构变更影响签名一致性)
  • 字段必须按字段编号从小到大的顺序序列化
  • 基础类型的编码严格对齐Protobuf规范:
    • 字符串强制用UTF-8编码,保留原始内容(不做额外空白处理)
    • 数值类型(如uint64)按Protobuf Varint编码规则处理
    • 嵌套消息递归应用相同的确定性规则
  • 明确是否序列化默认值:建议序列化所有字段(包括默认值),避免不同语言对默认值的处理差异

2. 利用Protobuf反射API实现序列化

我们需要单独序列化Request.Payload来生成签名,步骤如下:

步骤1:获取消息的描述符与反射入口

先拿到Payload的元数据和反射操作对象:

import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import com.google.protobuf.WireFormat;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

// 假设已有一个构造好的Payload实例
Something.Request.Payload payload = Something.Request.Payload.newBuilder()
    .setUserIdFrom("user_001")
    .setAmount(5000)
    .setUserIdTo("user_002")
    .build();

// 获取消息描述符和反射对象
Descriptors.Descriptor payloadDescriptor = payload.getDescriptorForType();
Message.Reflection reflection = payload.getReflection();

步骤2:按字段编号排序字段

从描述符中取出所有字段,按编号从小到大排序,确保序列化顺序固定:

List<Descriptors.FieldDescriptor> sortedFields = new ArrayList<>(payloadDescriptor.getFields());
sortedFields.sort(Comparator.comparingInt(Descriptors.FieldDescriptor::getNumber));

步骤3:逐个字段序列化,拼接字节流

遍历排序后的字段,按规则序列化每个字段并写入输出流:

ByteArrayOutputStream baos = new ByteArrayOutputStream();

for (Descriptors.FieldDescriptor field : sortedFields) {
    // 这里选择跳过无值字段,如果需要序列化默认值,可改为处理默认值逻辑
    if (!payload.hasField(field)) {
        continue;
    }
    Object value = reflection.getField(payload, field);

    // 根据字段类型做针对性序列化
    switch (field.getType()) {
        case STRING:
            String strVal = (String) value;
            byte[] strBytes = strVal.getBytes(StandardCharsets.UTF_8);
            // 写入字段标签(编号+类型)
            writeVarint(baos, makeTag(field.getNumber(), WireFormat.WIRETYPE_LENGTH_DELIMITED));
            // 写入字符串长度和字节内容
            writeVarint(baos, strBytes.length);
            baos.write(strBytes);
            break;
        case UINT64:
            long uintVal = (Long) value;
            writeVarint(baos, makeTag(field.getNumber(), WireFormat.WIRETYPE_VARINT));
            writeVarint(baos, uintVal);
            break;
        case MESSAGE:
            // 嵌套消息递归调用确定性序列化方法
            Message nestedMsg = (Message) value;
            byte[] nestedBytes = deterministicSerialize(nestedMsg);
            writeVarint(baos, makeTag(field.getNumber(), WireFormat.WIRETYPE_LENGTH_DELIMITED));
            writeVarint(baos, nestedBytes.length);
            baos.write(nestedBytes);
            break;
        // 其他类型可根据需求补充
    }
}

// 最终得到确定性序列化后的payload字节流
byte[] deterministicPayloadBytes = baos.toByteArray();

辅助方法实现

上面用到的makeTag和writeVarint是Protobuf底层编码逻辑,可直接复用或自行实现:

// 生成Protobuf字段标签:(字段编号 << 3) |  wireType
private static int makeTag(int fieldNumber, int wireType) {
    return (fieldNumber << 3) | wireType;
}

// 写入Varint编码的数值
private static void writeVarint(OutputStream out, long value) throws IOException {
    while (true) {
        if ((value & ~0x7FL) == 0) {
            out.write((int) value);
            return;
        } else {
            out.write(((int) value & 0x7F) | 0x80);
            value >>>= 7;
        }
    }
}

// 递归的确定性序列化方法,用于嵌套消息
private static byte[] deterministicSerialize(Message msg) throws IOException {
    Descriptors.Descriptor descriptor = msg.getDescriptorForType();
    Message.Reflection reflection = msg.getReflection();
    List<Descriptors.FieldDescriptor> sortedFields = new ArrayList<>(descriptor.getFields());
    sortedFields.sort(Comparator.comparingInt(Descriptors.FieldDescriptor::getNumber));

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    for (Descriptors.FieldDescriptor field : sortedFields) {
        if (!msg.hasField(field)) continue;
        Object value = reflection.getField(msg, field);
        // 重复上面的类型判断与序列化逻辑,或封装成通用方法
    }
    return baos.toByteArray();
}

3. 生成签名并组装完整消息

拿到确定性序列化的payload字节流后,用你的加密算法(如RSA、ECDSA)生成签名,再组装成Request:

// 假设你有现成的签名工具方法
byte[] signature = sign(deterministicPayloadBytes, privateKey);

Something.Request finalRequest = Something.Request.newBuilder()
    .setPayload(payload)
    .setSignature(com.google.protobuf.ByteString.copyFrom(signature))
    .build();

4. 签名验证逻辑

验证时,需要从收到的Request中取出payload,用同样的规则序列化后,再用公钥验证签名:

// 假设收到了请求消息
Something.Request receivedRequest = ...;
Something.Request.Payload receivedPayload = receivedRequest.getPayload();
byte[] receivedSignature = receivedRequest.getSignature().toByteArray();

// 对收到的payload做确定性序列化
byte[] receivedPayloadBytes = deterministicSerialize(receivedPayload);

// 验证签名合法性
boolean isSignatureValid = verify(receivedPayloadBytes, receivedSignature, publicKey);

关键注意事项

  • 跨语言一致性:如果系统涉及多语言,必须在所有语言中严格实现相同的序列化规则(字段顺序、默认值处理、编码方式等),否则不同语言生成的字节流会不一致
  • 未知字段处理:建议忽略未知字段,避免Proto版本迭代时新增字段破坏旧版本的签名验证
  • 性能权衡:反射序列化比原生序列化慢,若对性能要求极高,可考虑用Protobuf插件生成确定性序列化代码,但反射的优势是无需修改Proto定义,灵活性更强

内容的提问来源于stack exchange,提问作者ischepin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 20:42:29