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

QuickFix C++对接Binance FIX API:RawData签名无效问题排查

问题描述

使用QuickFix C库对接Binance FIX API时,按照文档提供的Python代码生成Logon消息的RawData(Tag96)签名,手动复制到C代码的Logon消息中,收到「Signature for this request is not valid」的登录拒绝响应。已排除时间戳过期问题(该场景报错为「Timestamp for this request is outside of the recvWindow」),咨询是否为C特有问题,或自行在C中实现签名算法即可解决?

文档提供的Python签名代码

import base64

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import load_pem_private_key

def logon_raw_data(private_key: Ed25519PrivateKey,
                   sender_comp_id: str,
                   target_comp_id: str,
                   msg_seq_num: str,
                   sending_time: str):
    """
    Computes the value of RawData (96) field in Logon<A> message.
    """
    payload = chr(1).join([
        'A',
        sender_comp_id,
        target_comp_id,
        msg_seq_num,
        sending_time,
    ])
    signature = private_key.sign(payload.encode('ASCII'))
    return base64.b64encode(signature).decode('ASCII')


with open('private_key.pem', 'rb') as f:
    private_key = load_pem_private_key(data=f.read(),
                                       password=None)

raw_data = logon_raw_data(private_key,
                          sender_comp_id='5JQmUOsm',
                          target_comp_id='SPOT',
                          msg_seq_num='1',
                          sending_time='20240612-08:52:21.613')

C++代码片段

void Application::toAdmin(FIX::Message &message, const FIX::SessionID &sessionID)
EXCEPT(FIX::DoNotSend) {
        if (FIX::MsgType_Logon == message.getHeader().getField(FIX::FIELD::MsgType)) {
           std::string rawData = "CkY08Cf3xrN4MwAdl/Kblj2aHrGxT2sS1URZ/VM0mksR1tQd6hHQ5bda0oNSIeD/JI9cwPBJ4y2R2ZcVgy7oDQ==";
            message.setField(FIX::RawDataLength(88));
            message.setField(FIX::RawData(rawData));
            message.setField(FIX::Username("myapikey"));
            message.setField(FIX::StringField(25035, "1"));
        }
}

错误日志

发送消息:(8=FIX.4.4|9=248|35=A|34=1|49=sendercompid|52=20250102-15:38:55.546|56=SPOT|95=88|96=CkY08Cf3xrN4MwAdl/Kblj2aHrGxT2sS1URZ/VM0mksR1tQd6hHQ5bda0oNSIeD/JI9cwPBJ4y2R2ZcVgy7oDQ==|98=0|108=30|141=Y|553=apikey|25035=1|10=149|)
响应消息:(8=FIX.4.4|9=0000131|35=3|49=SPOT|56=ryantest|34=1|52=20250102-15:38:55.774913|45=1|372=A|373=8|25016=-1022|58=Signature for this request is not valid.|10=238|)

解决方案

核心原因

这不是C++特有的问题,根本原因是手动复制的签名与实际发送的Logon消息中关键字段不匹配:

  • Python生成签名时使用的sender_comp_id为5JQmUOsm,但C++发送消息的SenderCompID(Tag49)是sendercompid
  • Python生成签名时的sending_time为20240612-08:52:21.613,而C++发送消息的SendingTime(Tag52)是20250102-15:38:55.546

Binance的Ed25519签名是基于MsgType(A)、SenderCompID、TargetCompID、MsgSeqNum、SendingTime这几个字段用SOH字符(ASCII码1,即\x01)拼接后的payload生成的,只要其中任何一个字段和签名生成时的参数不一致,就会判定签名无效。

解决步骤与代码示例

必须在C++代码中实时生成签名,而非手动复制。以下是基于OpenSSL库的实现示例:

#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/ed25519.h>
#include <string>
#include <cstdio>

// Base64编码实现(基于OpenSSL)
std::string base64_encode(const uint8_t* data, size_t len) {
    BIO *bio = BIO_new(BIO_s_mem());
    BIO *b64 = BIO_new(BIO_f_base64());
    bio = BIO_push(b64, bio);
    BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); // 不添加换行符
    BIO_write(bio, data, len);
    BIO_flush(bio);
    BUF_MEM *buf;
    BIO_get_mem_ptr(bio, &buf);
    std::string result(buf->data, buf->length);
    BIO_free_all(bio);
    return result;
}

void Application::toAdmin(FIX::Message &message, const FIX::SessionID &sessionID)
EXCEPT(FIX::DoNotSend) {
    if (FIX::MsgType_Logon == message.getHeader().getField(FIX::FIELD::MsgType)) {
        // 从当前Logon消息中获取真实字段值
        const std::string msgType = "A";
        const std::string senderCompID = message.getHeader().getField(FIX::FIELD::SenderCompID);
        const std::string targetCompID = message.getHeader().getField(FIX::FIELD::TargetCompID);
        const std::string msgSeqNum = message.getHeader().getField(FIX::FIELD::MsgSeqNum);
        const std::string sendingTime = message.getHeader().getField(FIX::FIELD::SendingTime);

        // 拼接签名用的payload,使用SOH字符(\x01)分隔
        const std::string payload = msgType + "\x01" + senderCompID + "\x01" + targetCompID + "\x01" + msgSeqNum + "\x01" + sendingTime;

        // 加载Ed25519私钥
        FILE* keyFile = fopen("private_key.pem", "rb");
        if (!keyFile) {
            throw FIX::DoNotSend("无法打开私钥文件");
        }
        EVP_PKEY* privateKey = PEM_read_PrivateKey(keyFile, nullptr, nullptr, nullptr);
        fclose(keyFile);
        if (!privateKey || EVP_PKEY_id(privateKey) != EVP_PKEY_ED25519) {
            throw FIX::DoNotSend("无效的Ed25519私钥");
        }

        // 生成Ed25519签名
        uint8_t signature[ED25519_SIGNATURE_LEN];
        size_t signatureLen;
        if (!EVP_DigestSign(privateKey, signature, &signatureLen, reinterpret_cast<const uint8_t*>(payload.c_str()), payload.size())) {
            EVP_PKEY_free(privateKey);
            throw FIX::DoNotSend("签名生成失败");
        }

        // 对签名进行Base64编码
        const std::string rawData = base64_encode(signature, signatureLen);
        EVP_PKEY_free(privateKey);

        // 更新Logon消息的相关字段
        message.setField(FIX::RawDataLength(rawData.size()));
        message.setField(FIX::RawData(rawData));
        message.setField(FIX::Username("myapikey"));
        message.setField(FIX::StringField(25035, "1"));
    }
}

注意事项

  • 确保项目链接OpenSSL库,编译时添加对应的链接参数
  • 私钥文件路径需与实际部署路径一致,且文件权限设置正确
  • 所有从消息中获取的字段值必须与最终发送的内容完全一致,不能有修改或额外空格

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 05:34:55