Files
mqtt-security-test/c/main.c
T
RyanandClaude Sonnet 5.5 bf40b92c52
Test and deploy enrollment demo / test (push) Successful in 7s
Test and deploy enrollment demo / deploy (push) Successful in 1m12s
Add MQTT enrollment demo with Gitea deploy pipeline
Go backend plus C (libsodium) and C++ (OpenSSL) simulated boards enroll
through a Mosquitto 2.1 broker with Dynamic Security using X25519/HKDF/
Ed25519; a web view shows the run live. The Gitea workflow validates and
builds the images, redeploys the stack on the server with the viewer on
port 8099 and fails unless both boards pass all checks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 21:25:14 +03:30

225 lines
7.9 KiB
C

/* Interop spike: X25519 + HKDF-SHA256 + Ed25519 with fixed inputs (C, OpenSSL 3 EVP).
* Prints the same key=value lines as the Go and C++ programs so the outputs can be diffed. */
#include <openssl/core_names.h>
#include <openssl/evp.h>
#include <openssl/kdf.h>
#include <openssl/params.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BOARD_PRIV "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
#define BACKEND_EPH "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"
#define SIGNING_SEED "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"
#define NONCE "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
#define CLIENT_ID "e1c9f0a2b3d4c5b6a79881726354453f"
#define FN "FN-000042"
#define SN "SN000123"
static void die(const char *msg)
{
fprintf(stderr, "FAIL: %s\n", msg);
exit(1);
}
static size_t unhex(const char *h, uint8_t *out)
{
size_t n = strlen(h) / 2;
for (size_t i = 0; i < n; i++) {
unsigned v;
if (sscanf(h + 2 * i, "%2x", &v) != 1)
die("hex");
out[i] = (uint8_t)v;
}
return n;
}
static void tohex(const uint8_t *b, size_t n, char *out)
{
for (size_t i = 0; i < n; i++)
sprintf(out + 2 * i, "%02x", b[i]);
out[2 * n] = 0;
}
static void phex(const char *label, const uint8_t *b, size_t n)
{
char *s = malloc(2 * n + 1);
tohex(b, n, s);
printf("%s=%s\n", label, s);
free(s);
}
static void expect(const char *name, const uint8_t *got, size_t n, const char *want)
{
char *s = malloc(2 * n + 1);
tohex(got, n, s);
if (strcmp(s, want) != 0) {
fprintf(stderr, "FAIL: %s\n got %s\n want %s\n", name, s, want);
exit(1);
}
free(s);
}
static void x25519_pub(const uint8_t *priv, uint8_t *pub)
{
EVP_PKEY *k = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, priv, 32);
size_t l = 32;
if (!k || EVP_PKEY_get_raw_public_key(k, pub, &l) != 1 || l != 32)
die("x25519 pub");
EVP_PKEY_free(k);
}
static void x25519_shared(const uint8_t *priv, const uint8_t *peer, uint8_t *out)
{
EVP_PKEY *k = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, priv, 32);
EVP_PKEY *p = EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL, peer, 32);
EVP_PKEY_CTX *c = k ? EVP_PKEY_CTX_new(k, NULL) : NULL;
size_t l = 32;
if (!k || !p || !c || EVP_PKEY_derive_init(c) != 1 || EVP_PKEY_derive_set_peer(c, p) != 1 ||
EVP_PKEY_derive(c, out, &l) != 1 || l != 32)
die("x25519 derive");
EVP_PKEY_CTX_free(c);
EVP_PKEY_free(p);
EVP_PKEY_free(k);
}
static void hkdf(const uint8_t *ikm, size_t ikml, const uint8_t *salt, size_t sl,
const uint8_t *info, size_t il, uint8_t *out, size_t ol)
{
EVP_KDF *kdf = EVP_KDF_fetch(NULL, "HKDF", NULL);
EVP_KDF_CTX *ctx = kdf ? EVP_KDF_CTX_new(kdf) : NULL;
char digest[] = "SHA256";
OSSL_PARAM p[5];
p[0] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, digest, 0);
p[1] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, (void *)ikm, ikml);
p[2] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, (void *)salt, sl);
p[3] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_INFO, (void *)info, il);
p[4] = OSSL_PARAM_construct_end();
if (!ctx || EVP_KDF_derive(ctx, out, ol, p) != 1)
die("hkdf");
EVP_KDF_CTX_free(ctx);
EVP_KDF_free(kdf);
}
static void ed_pub(const uint8_t *seed, uint8_t *pub)
{
EVP_PKEY *k = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, seed, 32);
size_t l = 32;
if (!k || EVP_PKEY_get_raw_public_key(k, pub, &l) != 1 || l != 32)
die("ed25519 pub");
EVP_PKEY_free(k);
}
static void ed_sign(const uint8_t *seed, const uint8_t *msg, size_t ml, uint8_t *sig)
{
EVP_PKEY *k = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, seed, 32);
EVP_MD_CTX *c = EVP_MD_CTX_new();
size_t sl = 64;
if (!k || !c || EVP_DigestSignInit(c, NULL, NULL, NULL, k) != 1 ||
EVP_DigestSign(c, sig, &sl, msg, ml) != 1 || sl != 64)
die("ed25519 sign");
EVP_MD_CTX_free(c);
EVP_PKEY_free(k);
}
static int ed_verify(const uint8_t *pub, const uint8_t *msg, size_t ml, const uint8_t *sig)
{
EVP_PKEY *k = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, pub, 32);
EVP_MD_CTX *c = EVP_MD_CTX_new();
int ok = k && c && EVP_DigestVerifyInit(c, NULL, NULL, NULL, k) == 1 &&
EVP_DigestVerify(c, sig, 64, msg, ml) == 1;
EVP_MD_CTX_free(c);
EVP_PKEY_free(k);
return ok;
}
static void kat(void)
{
uint8_t ap[32], bp[32], apub[32], bpub[32], sh[32];
unhex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", ap);
unhex("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb", bp);
x25519_pub(ap, apub);
x25519_pub(bp, bpub);
expect("rfc7748 alice pub", apub, 32, "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a");
expect("rfc7748 bob pub", bpub, 32, "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f");
x25519_shared(ap, bpub, sh);
expect("rfc7748 shared a", sh, 32, "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742");
x25519_shared(bp, apub, sh);
expect("rfc7748 shared b", sh, 32, "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742");
puts("kat.x25519=ok");
uint8_t ikm[22], salt[13], info[10], okm[42];
memset(ikm, 0x0b, sizeof ikm);
unhex("000102030405060708090a0b0c", salt);
unhex("f0f1f2f3f4f5f6f7f8f9", info);
hkdf(ikm, sizeof ikm, salt, sizeof salt, info, sizeof info, okm, sizeof okm);
expect("rfc5869 tc1", okm, 42,
"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865");
puts("kat.hkdf=ok");
uint8_t seed[32], pub[32], sig[64];
unhex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", seed);
ed_pub(seed, pub);
expect("rfc8032 pub", pub, 32, "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a");
ed_sign(seed, (const uint8_t *)"", 0, sig);
expect("rfc8032 sig", sig, 64,
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b");
puts("kat.ed25519=ok");
}
static void password(const uint8_t *shared, const uint8_t *nonce, const char *id, char *out)
{
char info[128];
uint8_t pw[32];
int il = snprintf(info, sizeof info, "robot-market/mqtt-pw/v1|%s", id);
hkdf(shared, 32, nonce, 16, (const uint8_t *)info, (size_t)il, pw, sizeof pw);
tohex(pw, sizeof pw, out);
}
int main(void)
{
kat();
uint8_t b[32], s[32], bpub[32], spub[32], shared[32], shared2[32], nonce[16], seed[32];
unhex(BOARD_PRIV, b);
unhex(BACKEND_EPH, s);
unhex(NONCE, nonce);
unhex(SIGNING_SEED, seed);
x25519_pub(b, bpub);
x25519_pub(s, spub);
phex("board_pub", bpub, 32);
phex("backend_eph_pub", spub, 32);
x25519_shared(s, bpub, shared); /* backend side */
x25519_shared(b, spub, shared2); /* board side */
phex("shared_secret", shared, 32);
if (memcmp(shared, shared2, 32) != 0)
die("shared secret mismatch");
puts("shared_secret_peer_match=ok");
char pw[65];
password(shared, nonce, FN, pw);
printf("password_fn=%s\n", pw);
password(shared, nonce, SN, pw);
printf("password_sn=%s\n", pw);
/* Backend signs S || B || nonce || clientID. */
uint8_t msg[32 + 32 + 16 + sizeof(CLIENT_ID) - 1], pub[32], sig[64];
size_t ml = 0;
memcpy(msg + ml, spub, 32), ml += 32;
memcpy(msg + ml, bpub, 32), ml += 32;
memcpy(msg + ml, nonce, 16), ml += 16;
memcpy(msg + ml, CLIENT_ID, sizeof(CLIENT_ID) - 1), ml += sizeof(CLIENT_ID) - 1;
ed_pub(seed, pub);
ed_sign(seed, msg, ml, sig);
phex("signing_pub", pub, 32);
phex("signed_message", msg, ml);
phex("signature", sig, 64);
if (!ed_verify(pub, msg, ml, sig))
die("signature verify");
puts("signature_verify=ok");
return 0;
}