// Sign a Plynth robot API call in C++17 with OpenSSL 3's HMAC, and send it with libcurl. // Linux: g++ -std=c++17 sign.cpp -lcrypto -lcurl -o sign (libssl-dev, libcurl4-openssl-dev) // macOS: clang++ -std=c++17 sign.cpp -I/opt/homebrew/opt/openssl@3/include -L/opt/homebrew/opt/openssl@3/lib \ // -lcrypto -lcurl -o sign // GATEWAY_URL=https://... GATEWAY_ROBOT_ID=... GATEWAY_ROBOT_SECRET=... ./sign [METHOD PATH [BODY]] // ./sign --vector # the test vectors' signatures, one per line #include #include #include #include #include #include #include #include // Hex HMAC-SHA256 keyed with the secret's own characters, over METHOD \n PATH \n TIMESTAMP \n and then the body. std::string sign(const std::string& secret, const std::string& method, const std::string& path, const std::string& timestamp, const std::string& body = "") { const std::string message = method + "\n" + path + "\n" + timestamp + "\n" + body; unsigned char mac[EVP_MAX_MD_SIZE]; unsigned int size = 0; if (!HMAC(EVP_sha256(), secret.data(), static_cast(secret.size()), reinterpret_cast(message.data()), message.size(), mac, &size)) { throw std::runtime_error("HMAC-SHA256 failed"); } static const char digits[] = "0123456789abcdef"; std::string hex; for (unsigned int i = 0; i < size; ++i) { hex += digits[mac[i] >> 4]; hex += digits[mac[i] & 0x0f]; } return hex; } static size_t keep(char* data, size_t size, size_t count, void* answer) { static_cast(answer)->append(data, size * count); return size * count; } // The body goes to curl with its length, as the same bytes that were signed. int call(const std::string& method, const std::string& path, const std::string& body) { const char* url = std::getenv("GATEWAY_URL"); const char* robot = std::getenv("GATEWAY_ROBOT_ID"); const char* secret = std::getenv("GATEWAY_ROBOT_SECRET"); if (!url || !robot || !secret) { std::cerr << "set GATEWAY_URL, GATEWAY_ROBOT_ID and GATEWAY_ROBOT_SECRET\n"; return 2; } std::string base = url; while (!base.empty() && base.back() == '/') base.pop_back(); const std::string timestamp = std::to_string(std::time(nullptr)); CURL* curl = curl_easy_init(); if (!curl) return 1; curl_slist* headers = nullptr; headers = curl_slist_append(headers, ("X-Robot-Id: " + std::string(robot)).c_str()); headers = curl_slist_append(headers, ("X-Timestamp: " + timestamp).c_str()); headers = curl_slist_append(headers, ("X-Signature: " + sign(secret, method, path, timestamp, body)).c_str()); if (!body.empty()) headers = curl_slist_append(headers, "Content-Type: application/json"); std::string answer; curl_easy_setopt(curl, CURLOPT_URL, (base + path).c_str()); if (method == "POST") { curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.data()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, keep); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &answer); const CURLcode sent = curl_easy_perform(curl); long status = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); curl_slist_free_all(headers); curl_easy_cleanup(curl); if (sent != CURLE_OK) { std::cerr << curl_easy_strerror(sent) << "\n"; return 1; } if (status >= 400) { std::cerr << "HTTP " << status << ": " << answer << "\n"; return 1; } std::cout << answer << "\n"; return 0; } int main(int argc, char** argv) { if (argc == 2 && std::string(argv[1]) == "--vector") { const std::string key = "test-secret-for-docs-only-0123456789abcdef"; std::cout << sign(key, "GET", "/v1/me", "1767225600") << "\n" << sign(key, "POST", "/v1/passages", "1767225600", R"({"passage_id":"3f2b9c1e-6a4d-4e8b-9f1a-2c7d5e0b8a14","resource_id":"door"})") << "\n"; return 0; } return call(argc > 1 ? argv[1] : "GET", argc > 2 ? argv[2] : "/v1/me", argc > 3 ? argv[3] : ""); }