Binance Spot FIX API Logon Signature in C++

Computing the Binance Spot FIX API Logon Signature with Ed25519 in C++ Link to heading

The Binance Spot FIX API provides a low-latency way to interact with the spot exchange using the Financial Information eXchange (FIX) protocol. Authentication for the connection happens during the initial Logon (MsgType=A) message, where you must provide a cryptographic signature in the RawData (96) field.

According to the official Binance documentation, the signature is computed as follows:

Signature Computation Steps Link to heading

  1. Construct the payload: Concatenate these field values in exact order, separated by the SOH character (\x01 or ASCII 1):
    • MsgType (35): "A"
    • SenderCompID (49): Your chosen sender identifier
    • TargetCompID (56): "SPOT"
    • MsgSeqNum (34): Usually "1" for the logon
    • SendingTime (52): UTC timestamp in format YYYYMMDD-HH:MM:SS (or with milliseconds)
  2. Sign the payload using your Ed25519 private key (pure Ed25519, no hashing required).
  3. Base64-encode the resulting 64-byte signature.
  4. Place the base64 string in RawData (96) and its length in RawDataLength (95) of the Logon message.

Binance only supports Ed25519 keys for FIX sessions.

C++ Implementation Using OpenSSL’s high-level EVP API Link to heading

First let’s define the following:

 1constexpr auto ED25519_SIGSIZE = 64;
 2constexpr std::string_view SOH = "\x01";
 3
 4enum class sign_error : uint8_t
 5{
 6    invalid_input,
 7    close_pkey,
 8    mdctx,
 9    digest_sign_init,
10    digest_sign,
11};

Loading the Private Key (PEM format) Link to heading

 1auto load_private_key(const std::string &path) -> std::expected<EVP_PKEY *, sign_error>
 2{
 3    FILE *fp = fopen(path.c_str(), "r");
 4    if (fp == nullptr)
 5    {
 6        return std::unexpected(sign_error::invalid_input);
 7    }
 8
 9    EVP_PKEY *private_key = PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr);
10    if (fclose(fp) != 0)
11    {
12        return std::unexpected(sign_error::close_pkey);
13    }
14
15    return private_key;
16}

This reads a standard PEM-encoded Ed25519 private key file.

Signing with Ed25519 (Pure Signature) Link to heading

 1auto sign_ed25519(EVP_PKEY *pkey, std::string_view &&payload, std::array<unsigned char, ED25519_SIGSIZE> &signature) -> std::expected<void, sign_error>
 2{
 3    std::expected<void, sign_error> ret;
 4    std::size_t signature_length = signature.size();
 5
 6    EVP_MD_CTX *mdctx = EVP_MD_CTX_create();
 7    if (mdctx == nullptr)
 8    {
 9        ret = std::unexpected(sign_error::mdctx);
10        goto err_md_ctx;
11    }
12
13    if (EVP_DigestSignInit(mdctx, nullptr, nullptr, nullptr, pkey) != 1)
14    {
15        ret = std::unexpected(sign_error::digest_sign_init);
16        goto err_digest;
17    }
18
19    if (EVP_DigestSign(mdctx, signature.data(), &signature_length, reinterpret_cast<const unsigned char *>(payload.data()), payload.size()) != 1)
20    {
21        ret = std::unexpected(sign_error::digest_sign);
22        goto err_digest;
23    }
24
25    return ret;
26
27err_digest:
28    EVP_MD_CTX_destroy(mdctx);
29
30err_md_ctx:
31    return ret;
32}

Base64 Encoding Link to heading

1auto base64_encode(std::array<unsigned char, ED25519_SIGSIZE> signature) -> std::string
2{
3    // 3 input bytes produces 4 output bytes (padding included) so we ceil(siglen / 3) * 4 and add 1 for NUL
4    constexpr auto size = ((ED25519_SIGSIZE / 3) + 1) * 4;
5    std::string output(size, '\0');
6    EVP_EncodeBlock(reinterpret_cast<unsigned char *>(output.data()), signature.data(), signature.size());
7    return output;
8}

Uses OpenSSL’s EVP_EncodeBlock for standard base64 (with padding).

5. Main Function Link to heading

Now that we have all foundation blocks, we can write a signature generation function

 1auto generate_signature(const std::string &key_path, const std::string &sender_comp_id, const std::string &sending_time) -> std::string
 2{
 3    auto private_key = load_private_key(key_path);
 4    if (not private_key.has_value())
 5    {
 6        return {};
 7    }
 8
 9    auto payload = ("A" +                               // 35: MsgType
10                    std::string(SOH) + sender_comp_id + // 49: SenderCompId
11                    std::string(SOH) + "SPOT" +         // 56: TargetCompId
12                    std::string(SOH) + "1" +            // 34: MsgSeqNum
13                    std::string(SOH) + sending_time     // 52: SendingTime
14    );
15
16    std::array<unsigned char, ED25519_SIGSIZE> signature;
17
18    if (not sign_ed25519(*private_key, std::move(payload), signature).has_value())
19    {
20        libmarket::log::error("failed to generate signature");
21        EVP_PKEY_free(*private_key);
22        return {};
23    }
24
25    EVP_PKEY_free(*private_key);
26    return base64_encode(signature);
27}

Validation Link to heading

Binance provides test values in the docs:

  • Payload fields: A, EXAMPLE, SPOT, 1, 20240627-11:17:25.223
  • Test private key (do not use in production!)
  • Expected base64 signature:
    4MHXelVVcpkdwuLbl6n73HQUXUf1dse2PCgT1DYqW9w8AVZ1RACFGM+5UdlGPrQHrgtS3CvsRURC1oj73j8gCA==

You can plug these into the function to verify correctness or write a unit test like:

 1#include <gtest/gtest.h>
 2
 3#include <libmarket/binance.h>
 4
 5TEST(signature, sample_signature)
 6{
 7    // See https://developers.binance.com/docs/binance-spot-api-docs/testnet/fix-api#how-to-sign-logona-request
 8    const auto signature = libmarket::binance::signature::generate_signature(
 9        "libmarket-utest/res/sample.pem",
10        "EXAMPLE",
11        "20240627-11:17:25.223");
12    const auto expected = std::string("4MHXelVVcpkdwuLbl6n73HQUXUf1dse2PCgT1DYqW9w8AVZ1RACFGM+5UdlGPrQHrgtS3CvsRURC1oj73j8gCA==");
13    EXPECT_EQ(signature, expected);
14}

Usage with the QuickFIX engine Link to heading

When integrating into a full FIX client, call generate_signature() just before sending the Logon message, populate RawData (96) with the result, and ensure SendingTime (52) is accurate.

Wiht QuickFIX, you’ll have to craft your outgoing admin logon message like this:

 1void toAdmin(FIX::Message &message, const FIX::SessionID &session_id) override
 2{
 3    if (message.getHeader().getField(FIX::FIELD::MsgType) == FIX::MsgType_Logon)
 4    {
 5        std::string sender_comp_id = session_id.getSenderCompID();
 6        std::string sending_time = message.getHeader().getField(FIX::FIELD::SendingTime);
 7        std::string signature = libmarket::binance::signature::generate_signature(config_.auth.private_key_path, sender_comp_id, sending_time);
 8        message.setField(FIX::RawDataLength(static_cast<FIX::LENGTH>(signature.size()))); // narrowing is OK
 9        message.setField(FIX::RawData(signature));
10        message.setField(FIX::Username(config_.auth.api_key));
11        message.setField(FIX::StringField(MessageHandling, "1"));
12    }
13}

Summary Link to heading

This C++ code offers a clean, robust way to generate the required Logon signature using OpenSSL. It handles key loading, payload construction, pure Ed25519 signing, and base64 encoding—all exactly as specified by Binance.