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

