// Interop spike: X25519 + HKDF-SHA256 + Ed25519 with fixed inputs (C++17, OpenSSL 3 EVP, RAII). // Prints the same key=value lines as the Go and C programs so the outputs can be diffed. #include #include #include #include #include #include #include #include #include #include #include #include using Bytes = std::vector; namespace { constexpr const char *kBoardPriv = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; constexpr const char *kBackendEph = "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"; constexpr const char *kSigningSeed = "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"; constexpr const char *kNonce = "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"; constexpr const char *kClientId = "e1c9f0a2b3d4c5b6a79881726354453f"; constexpr const char *kFn = "FN-000042"; constexpr const char *kSn = "SN000123"; struct PkeyDel { void operator()(EVP_PKEY *p) const { EVP_PKEY_free(p); } }; struct PctxDel { void operator()(EVP_PKEY_CTX *p) const { EVP_PKEY_CTX_free(p); } }; struct MdDel { void operator()(EVP_MD_CTX *p) const { EVP_MD_CTX_free(p); } }; struct KdfDel { void operator()(EVP_KDF *p) const { EVP_KDF_free(p); } }; struct KdfCtxDel { void operator()(EVP_KDF_CTX *p) const { EVP_KDF_CTX_free(p); } }; using Pkey = std::unique_ptr; using Pctx = std::unique_ptr; using MdCtx = std::unique_ptr; [[noreturn]] void die(const std::string &m) { throw std::runtime_error(m); } Bytes unhex(const std::string &h) { Bytes out; for (size_t i = 0; i + 1 < h.size(); i += 2) out.push_back(static_cast(std::stoi(h.substr(i, 2), nullptr, 16))); return out; } std::string tohex(const Bytes &b) { std::string s; char buf[3]; for (uint8_t c : b) { std::snprintf(buf, sizeof buf, "%02x", c); s += buf; } return s; } void expect(const std::string &name, const Bytes &got, const std::string &want) { if (tohex(got) != want) die(name + "\n got " + tohex(got) + "\n want " + want); } Bytes x25519Pub(const Bytes &priv) { Pkey k(EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, nullptr, priv.data(), priv.size())); Bytes pub(32); size_t l = pub.size(); if (!k || EVP_PKEY_get_raw_public_key(k.get(), pub.data(), &l) != 1 || l != 32) die("x25519 pub"); return pub; } Bytes x25519Shared(const Bytes &priv, const Bytes &peer) { Pkey k(EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, nullptr, priv.data(), priv.size())); Pkey p(EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, nullptr, peer.data(), peer.size())); if (!k || !p) die("x25519 keys"); Pctx c(EVP_PKEY_CTX_new(k.get(), nullptr)); Bytes out(32); size_t l = out.size(); if (!c || EVP_PKEY_derive_init(c.get()) != 1 || EVP_PKEY_derive_set_peer(c.get(), p.get()) != 1 || EVP_PKEY_derive(c.get(), out.data(), &l) != 1 || l != 32) die("x25519 derive"); return out; } Bytes hkdf(const Bytes &ikm, const Bytes &salt, const Bytes &info, size_t n) { std::unique_ptr kdf(EVP_KDF_fetch(nullptr, "HKDF", nullptr)); if (!kdf) die("hkdf fetch"); std::unique_ptr ctx(EVP_KDF_CTX_new(kdf.get())); std::string digest = "SHA256"; OSSL_PARAM p[5]; p[0] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, digest.data(), 0); p[1] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, const_cast(ikm.data()), ikm.size()); p[2] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, const_cast(salt.data()), salt.size()); p[3] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_INFO, const_cast(info.data()), info.size()); p[4] = OSSL_PARAM_construct_end(); Bytes out(n); if (!ctx || EVP_KDF_derive(ctx.get(), out.data(), out.size(), p) != 1) die("hkdf"); return out; } Bytes edPub(const Bytes &seed) { Pkey k(EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, seed.data(), seed.size())); Bytes pub(32); size_t l = pub.size(); if (!k || EVP_PKEY_get_raw_public_key(k.get(), pub.data(), &l) != 1 || l != 32) die("ed25519 pub"); return pub; } Bytes edSign(const Bytes &seed, const Bytes &msg) { Pkey k(EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, seed.data(), seed.size())); MdCtx c(EVP_MD_CTX_new()); Bytes sig(64); size_t sl = sig.size(); static const uint8_t empty = 0; const uint8_t *m = msg.empty() ? &empty : msg.data(); if (!k || !c || EVP_DigestSignInit(c.get(), nullptr, nullptr, nullptr, k.get()) != 1 || EVP_DigestSign(c.get(), sig.data(), &sl, m, msg.size()) != 1 || sl != 64) die("ed25519 sign"); return sig; } bool edVerify(const Bytes &pub, const Bytes &msg, const Bytes &sig) { Pkey k(EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, pub.data(), pub.size())); MdCtx c(EVP_MD_CTX_new()); return k && c && EVP_DigestVerifyInit(c.get(), nullptr, nullptr, nullptr, k.get()) == 1 && EVP_DigestVerify(c.get(), sig.data(), sig.size(), msg.data(), msg.size()) == 1; } void kat() { // RFC 7748 section 6.1 Bytes ap = unhex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"); Bytes bp = unhex("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"); Bytes apub = x25519Pub(ap), bpub = x25519Pub(bp); expect("rfc7748 alice pub", apub, "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"); expect("rfc7748 bob pub", bpub, "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"); expect("rfc7748 shared a", x25519Shared(ap, bpub), "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"); expect("rfc7748 shared b", x25519Shared(bp, apub), "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"); std::cout << "kat.x25519=ok\n"; // RFC 5869 test case 1 expect("rfc5869 tc1", hkdf(Bytes(22, 0x0b), unhex("000102030405060708090a0b0c"), unhex("f0f1f2f3f4f5f6f7f8f9"), 42), "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"); std::cout << "kat.hkdf=ok\n"; // RFC 8032 section 7.1, test 1 (empty message) Bytes seed = unhex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"); expect("rfc8032 pub", edPub(seed), "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"); expect("rfc8032 sig", edSign(seed, {}), "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"); std::cout << "kat.ed25519=ok\n"; } std::string password(const Bytes &shared, const Bytes &nonce, const std::string &id) { std::string info = "robot-market/mqtt-pw/v1|" + id; return tohex(hkdf(shared, nonce, Bytes(info.begin(), info.end()), 32)); } } // namespace int main() try { kat(); Bytes b = unhex(kBoardPriv), s = unhex(kBackendEph), nonce = unhex(kNonce), seed = unhex(kSigningSeed); Bytes bpub = x25519Pub(b), spub = x25519Pub(s); std::cout << "board_pub=" << tohex(bpub) << "\n"; std::cout << "backend_eph_pub=" << tohex(spub) << "\n"; Bytes shared = x25519Shared(s, bpub); // backend side Bytes shared2 = x25519Shared(b, spub); // board side std::cout << "shared_secret=" << tohex(shared) << "\n"; if (shared != shared2) die("shared secret mismatch"); std::cout << "shared_secret_peer_match=ok\n"; std::cout << "password_fn=" << password(shared, nonce, kFn) << "\n"; std::cout << "password_sn=" << password(shared, nonce, kSn) << "\n"; // Backend signs S || B || nonce || clientID. Bytes msg; msg.insert(msg.end(), spub.begin(), spub.end()); msg.insert(msg.end(), bpub.begin(), bpub.end()); msg.insert(msg.end(), nonce.begin(), nonce.end()); std::string cid = kClientId; msg.insert(msg.end(), cid.begin(), cid.end()); Bytes pub = edPub(seed), sig = edSign(seed, msg); std::cout << "signing_pub=" << tohex(pub) << "\n"; std::cout << "signed_message=" << tohex(msg) << "\n"; std::cout << "signature=" << tohex(sig) << "\n"; if (!edVerify(pub, msg, sig)) die("signature verify"); std::cout << "signature_verify=ok\n"; return 0; } catch (const std::exception &e) { std::cerr << "FAIL: " << e.what() << "\n"; return 1; }