Node.js daemon: Ed25519 identity + self-signed TLS, signed registration/ renewal with the central registry, mDNS advertising, disk-backed fetch-through asset cache with an origin allowlist. Includes a local dev-registry stand-in for the real oxmc.me endpoints and an end-to-end smoke test (register -> token -> cache miss/hit -> SSRF rejection). See README.md for the full protocol and trust model.
56 lines
2.1 KiB
JavaScript
56 lines
2.1 KiB
JavaScript
const { generateKeyPairSync, sign: edSign, createPrivateKey } = require("node:crypto");
|
|
const { mkdirSync, existsSync, readFileSync, writeFileSync } = require("node:fs");
|
|
const { join } = require("node:path");
|
|
|
|
/**
|
|
* The dev registry's own root Ed25519 identity — stands in for the real
|
|
* oxmc.me signing key. Generated once, persisted, printed at startup so you
|
|
* can paste the public half into Constants.CACHE_TRUST_ROOT_PUBKEY_BASE64
|
|
* for local device/emulator testing. The private half never leaves this
|
|
* process — same shape as the real deployment, just self-issued.
|
|
*/
|
|
function loadOrCreateRootKey(dir) {
|
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
const pubPath = join(dir, "root_ed25519.pub.pem");
|
|
const privPath = join(dir, "root_ed25519.priv.pem");
|
|
|
|
if (existsSync(pubPath) && existsSync(privPath)) {
|
|
return {
|
|
publicKeyPem: readFileSync(pubPath, "utf8"),
|
|
privateKeyPem: readFileSync(privPath, "utf8"),
|
|
};
|
|
}
|
|
|
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
});
|
|
writeFileSync(pubPath, publicKey, { mode: 0o644 });
|
|
writeFileSync(privPath, privateKey, { mode: 0o600 });
|
|
return { publicKeyPem: publicKey, privateKeyPem: privateKey };
|
|
}
|
|
|
|
/** Deterministic key order — must match CacheTokenVerifier.kt's canonicalize(). */
|
|
function canonicalize(obj) {
|
|
return JSON.stringify(obj, Object.keys(obj).sort());
|
|
}
|
|
|
|
/**
|
|
* Signs a CacheToken payload and wraps it in the wire envelope:
|
|
* base64( JSON.stringify({ payload, signature }) ).
|
|
*/
|
|
function signCacheToken(payload, rootPrivateKeyPem) {
|
|
const signature = edSign(null, Buffer.from(canonicalize(payload)), createPrivateKey(rootPrivateKeyPem));
|
|
const envelope = { payload, signature: signature.toString("base64") };
|
|
return Buffer.from(JSON.stringify(envelope), "utf8").toString("base64");
|
|
}
|
|
|
|
function pemToBase64Spki(pem) {
|
|
return pem
|
|
.replace(/-----BEGIN PUBLIC KEY-----/, "")
|
|
.replace(/-----END PUBLIC KEY-----/, "")
|
|
.replace(/\s+/g, "");
|
|
}
|
|
|
|
module.exports = { loadOrCreateRootKey, signCacheToken, canonicalize, pemToBase64Spki };
|