WEDOS×ENISA

Alternative 1 — L3/L4 DDoS Scrubbing + L7 WAF Access Pattern

Sensitive body — sealed end-to-end (HPKE · RFC 9180) Browser seals the body WGP edge full L7 on metadata Origin decrypts · holds the key TLS TLS sees: metadata · JA4 / JA4T / JA4H · headers · cookies · URL does NOT see: the sealed body (content)

<!-- CLIENT (browser) — include the SDK with SRI, then one configure() -->
<script src="https://your-origin.example/wgp-seal.js"
        integrity="sha384-…" crossorigin="anonymous"></script>
<script>
  WGPSeal.configure({ endpoints: ["/api/submit"], keyUrl: "/wgp-key.json" });
  // normal fetch POST is sealed automatically:
  await fetch("/api/submit", { method:"POST", body: JSON.stringify(data) });
</script>
/*!
 * wgp-seal.js — WGP end-to-end body sealing (HPKE, RFC 9180)
 * Version: 1.0.0  |  Released: 2026-07-02  |  Status: first public release
 * Suite: DHKEM(P-256, HKDF-SHA256) + HKDF-SHA256 + AES-256-GCM
 * Pure WebCrypto. No dependencies. Runs in the browser and in Node.
 * SemVer: MAJOR = wire/format break, MINOR = compatible feature, PATCH = fix.
 *
 * Small on purpose: the whole trust of the scheme rests on this file,
 * so it is meant to be read, hashed (SRI) and audited.
 */
(function (root) {
  "use strict";
  var subtle = (root.crypto || (typeof require === "function" ? require("node:crypto").webcrypto : null)).subtle;

  // ---- byte helpers -------------------------------------------------------
  var TE = new TextEncoder();
  function u8(a) { return a instanceof Uint8Array ? a : new Uint8Array(a); }
  function cat() {
    var t = 0, i, arr = arguments;
    for (i = 0; i < arr.length; i++) t += arr[i].length;
    var out = new Uint8Array(t), o = 0;
    for (i = 0; i < arr.length; i++) { out.set(arr[i], o); o += arr[i].length; }
    return out;
  }
  function i2osp2(n) { return new Uint8Array([(n >> 8) & 0xff, n & 0xff]); }
  function b64e(bytes) {
    var s = "", b = u8(bytes), i;
    for (i = 0; i < b.length; i++) s += String.fromCharCode(b[i]);
    return (root.btoa ? root.btoa(s) : Buffer.from(b).toString("base64"));
  }
  function b64d(str) {
    if (root.atob) { var s = root.atob(str), a = new Uint8Array(s.length), i; for (i = 0; i < s.length; i++) a[i] = s.charCodeAt(i); return a; }
    return u8(Buffer.from(str, "base64"));
  }

  // ---- suite constants ----------------------------------------------------
  var KEM_ID = 0x0010, KDF_ID = 0x0001, AEAD_ID = 0x0002;
  var Nsecret = 32, Nk = 32, Nn = 12, Nh = 32;
  var SUITE_KEM = cat(TE.encode("KEM"), i2osp2(KEM_ID));
  var SUITE_HPKE = cat(TE.encode("HPKE"), i2osp2(KEM_ID), i2osp2(KDF_ID), i2osp2(AEAD_ID));
  var HPKE_V1 = TE.encode("HPKE-v1");
  var SUITE_STR = "p256-hkdf-sha256-aes256gcm";

  // ---- HMAC / HKDF (extract + expand separately) --------------------------
  async function hmac(key, data) {
    var k = await subtle.importKey("raw", key.length ? key : new Uint8Array(32), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
    return u8(await subtle.sign("HMAC", k, data));
  }
  async function extract(salt, ikm) { return hmac(salt && salt.length ? salt : new Uint8Array(32), ikm); }
  async function expand(prk, info, L) {
    var out = new Uint8Array(L), t = new Uint8Array(0), n = Math.ceil(L / 32), o = 0, i;
    for (i = 1; i <= n; i++) {
      t = await hmac(prk, cat(t, info, new Uint8Array([i])));
      out.set(t.subarray(0, Math.min(32, L - o)), o); o += 32;
    }
    return out;
  }
  function labeledExtract(salt, label, ikm, suite) { return extract(salt, cat(HPKE_V1, suite, TE.encode(label), ikm)); }
  function labeledExpand(prk, label, info, L, suite) { return expand(prk, cat(i2osp2(L), HPKE_V1, suite, TE.encode(label), info), L); }

  // ---- DHKEM(P-256) -------------------------------------------------------
  async function importPub(raw) { return subtle.importKey("raw", raw, { name: "ECDH", namedCurve: "P-256" }, false, []); }
  async function dh(privKey, pubRaw) {
    var pub = await importPub(pubRaw);
    return u8(await subtle.deriveBits({ name: "ECDH", public: pub }, privKey, 256)); // X coordinate (32 bytes)
  }
  async function extractAndExpand(dhBytes, kemCtx) {
    var prk = await labeledExtract(new Uint8Array(0), "eae_prk", dhBytes, SUITE_KEM);
    return labeledExpand(prk, "shared_secret", kemCtx, Nsecret, SUITE_KEM);
  }
  async function genKeyPair() {
    var kp = await subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
    var pub = u8(await subtle.exportKey("raw", kp.publicKey));
    return { privateKey: kp.privateKey, publicKey: kp.publicKey, publicRaw: pub };
  }
  async function encap(pkRraw) {
    var e = await subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
    var enc = u8(await subtle.exportKey("raw", e.publicKey));
    var dhb = await dh(e.privateKey, pkRraw);
    var shared = await extractAndExpand(dhb, cat(enc, pkRraw));
    return { shared: shared, enc: enc };
  }
  async function decap(enc, skR, pkRraw) {
    var dhb = await dh(skR, enc);
    return extractAndExpand(dhb, cat(enc, pkRraw));
  }

  // ---- key schedule (base mode) ------------------------------------------
  async function keySchedule(shared, info) {
    var pskIdHash = await labeledExtract(new Uint8Array(0), "psk_id_hash", new Uint8Array(0), SUITE_HPKE);
    var infoHash = await labeledExtract(new Uint8Array(0), "info_hash", info, SUITE_HPKE);
    var ksc = cat(new Uint8Array([0x00]), pskIdHash, infoHash);
    var secret = await labeledExtract(shared, "secret", new Uint8Array(0), SUITE_HPKE);
    var key = await labeledExpand(secret, "key", ksc, Nk, SUITE_HPKE);
    var baseNonce = await labeledExpand(secret, "base_nonce", ksc, Nn, SUITE_HPKE);
    var exp = await labeledExpand(secret, "exp", ksc, Nh, SUITE_HPKE);
    return { key: key, baseNonce: baseNonce, exp: exp };
  }
  async function exportSecret(ctx, context, L) { return labeledExpand(ctx.exp, "sec", context, L, SUITE_HPKE); }

  // ---- AEAD (AES-256-GCM) -------------------------------------------------
  async function aeadEnc(keyBytes, nonce, aad, pt) {
    var k = await subtle.importKey("raw", keyBytes, "AES-GCM", false, ["encrypt"]);
    return u8(await subtle.encrypt({ name: "AES-GCM", iv: nonce, additionalData: aad, tagLength: 128 }, k, pt));
  }
  async function aeadDec(keyBytes, nonce, aad, ct) {
    var k = await subtle.importKey("raw", keyBytes, "AES-GCM", false, ["decrypt"]);
    return u8(await subtle.decrypt({ name: "AES-GCM", iv: nonce, additionalData: aad, tagLength: 128 }, k, ct));
  }

  // ---- high-level seal / open (single-shot) -------------------------------
  var INFO = TE.encode("wgp-sealed-body/v1");
  var RESP_CTX = TE.encode("wgp-response");

  async function sealToServer(pkRraw, aad, plaintext) {
    var e = await encap(pkRraw);
    var ctx = await keySchedule(e.shared, INFO);
    var ct = await aeadEnc(ctx.key, ctx.baseNonce, aad, plaintext);
    var respKey = await exportSecret(ctx, RESP_CTX, 32);
    return { enc: e.enc, ct: ct, respKey: respKey };
  }
  async function openFromClient(enc, skR, pkRraw, aad, ct) {
    var shared = await decap(enc, skR, pkRraw);
    var ctx = await keySchedule(shared, INFO);
    var pt = await aeadDec(ctx.key, ctx.baseNonce, aad, ct);
    var respKey = await exportSecret(ctx, RESP_CTX, 32);
    return { pt: pt, respKey: respKey };
  }

  // ---- envelope (JSON wire format for the demo; CBOR in production) --------
  function aadBytes(h) { return TE.encode([h.v, h.suite, h.key_id, h.ts, h.rid, h.method, h.path].join("|")); }

  async function sealRequest(pkRraw, keyId, method, path, obj) {
    var h = { v: 1, suite: SUITE_STR, key_id: keyId, ts: Date.now(), rid: b64e(root.crypto.getRandomValues(new Uint8Array(12))), method: method, path: path };
    var pt = TE.encode(JSON.stringify(obj));
    var s = await sealToServer(pkRraw, aadBytes(h), pt);
    return { envelope: { h: h, enc: b64e(s.enc), ct: b64e(s.ct) }, respKey: s.respKey };
  }
  async function openRequest(envelope, skR, pkRraw) {
    var h = envelope.h;
    var r = await openFromClient(b64d(envelope.enc), skR, pkRraw, aadBytes(h), b64d(envelope.ct));
    return { obj: JSON.parse(new TextDecoder().decode(r.pt)), respKey: r.respKey, header: h };
  }
  async function sealResponse(respKey, reqHeader, obj) {
    var iv = root.crypto.getRandomValues(new Uint8Array(12));
    var h = { v: 1, resp: true, ts: Date.now(), rid: reqHeader.rid };
    var aad = TE.encode([h.v, "resp", h.ts, h.rid].join("|"));
    var ct = await aeadEnc(respKey, iv, aad, TE.encode(JSON.stringify(obj)));
    return { h: h, iv: b64e(iv), ct: b64e(ct) };
  }
  async function openResponse(respEnvelope, respKey) {
    var h = respEnvelope.h;
    var aad = TE.encode([h.v, "resp", h.ts, h.rid].join("|"));
    var pt = await aeadDec(respKey, b64d(respEnvelope.iv), aad, b64d(respEnvelope.ct));
    return JSON.parse(new TextDecoder().decode(pt));
  }

  // ---- browser SDK: transparent fetch wrapper -----------------------------
  var cfg = { keyUrl: "/.well-known/wgp-key.json", endpoints: [], pk: null, keyId: null, onSeal: null };
  async function loadKey() {
    var r = await (WGP._origFetch || root.fetch)(cfg.keyUrl); var j = await r.json();
    cfg.pk = b64d(j.pk); cfg.keyId = j.key_id; return j;
  }
  function matches(url) {
    try { var p = new URL(url, root.location ? root.location.href : "http://x").pathname; return cfg.endpoints.indexOf(p) >= 0; }
    catch (e) { return false; }
  }
  async function sealedFetch(url, opts) {
    opts = opts || {};
    if (!cfg.pk) await loadKey();
    var method = (opts.method || "GET").toUpperCase();
    var path = new URL(url, root.location.href).pathname;
    var body = opts.body ? (typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body) : {};
    var sealed = await sealRequest(cfg.pk, cfg.keyId, method, path, body);
    if (cfg.onSeal) try { cfg.onSeal(sealed.envelope); } catch (e) {}
    var res = await (WGP._origFetch || root.fetch)(url, { method: method, headers: { "content-type": "application/wgp-sealed+json" }, body: JSON.stringify(sealed.envelope) });
    var respEnv; try { respEnv = await res.json(); } catch (e) { respEnv = null; }
    // Guard: only decrypt if the server actually returned a sealed reply.
    if (!res.ok || !respEnv || !respEnv.iv || !respEnv.ct) {
      return { ok: res.ok, status: res.status, json: function () { return Promise.resolve(respEnv); }, _plain: respEnv, _sealedResponse: null, error: (respEnv && respEnv.error) || ("HTTP " + res.status) };
    }
    var plain = await openResponse(respEnv, sealed.respKey);
    return { ok: res.ok, status: res.status, json: function () { return Promise.resolve(plain); }, _plain: plain, _sealedResponse: respEnv };
  }
  function configure(o) {
    for (var k in o) cfg[k] = o[k];
    if (root.fetch && !root.__wgpWrapped) {
      var orig = root.fetch.bind(root);
      root.fetch = function (url, opts) {
        if (opts && (opts.method || "GET").toUpperCase() === "POST" && matches(url)) return sealedFetch(url, opts);
        return orig(url, opts);
      };
      root.__wgpWrapped = true;
      WGP._origFetch = orig;
    }
  }

  var WGP = {
    VERSION: "1.0.0",
    SUITE: SUITE_STR,
    genKeyPair: genKeyPair,
    sealRequest: sealRequest, openRequest: openRequest,
    sealResponse: sealResponse, openResponse: openResponse,
    // low-level for tests
    _encap: encap, _decap: decap, _keySchedule: keySchedule, _b64e: b64e, _b64d: b64d,
    // browser SDK
    configure: configure, loadKey: loadKey, sealedFetch: sealedFetch
  };

  if (typeof module !== "undefined" && module.exports) module.exports = WGP;
  if (root.window) root.window.WGPSeal = WGP;
  root.WGPSeal = WGP;
})(typeof self !== "undefined" ? self : (typeof globalThis !== "undefined" ? globalThis : this));

// SERVER (Node origin) — one decrypt step, then your logic
const WGP = require("./wgp-seal.js");
// SRV = { privateKey, publicRaw }   // in production from HSM/KMS
app.post("/api/submit", express.text({type:"*/*"}), async (req, res) => {
  const env    = JSON.parse(req.body);
  const opened = await WGP.openRequest(env, SRV.privateKey, SRV.publicRaw); // decrypt
  // ... your logic uses opened.obj ...
  const resp = await WGP.sealResponse(opened.respKey, opened.header, { status: "ok" });
  res.type("application/wgp-sealed+json").send(JSON.stringify(resp));
});
/*
 * server.js — WGP E2EE origin (Node.js sample).
 *
 * Runs the *decrypt/encrypt* side of the sealed body. Zero dependencies —
 * only Node's built-ins. Uses the SAME wgp-seal.js as the browser, so client
 * and server are guaranteed to interoperate.
 *
 *   node server.js         # then open http://localhost:8080
 *   PORT=3000 node server.js
 *
 * Requires Node.js 18 or newer (built-in WebCrypto).
 *
 * In production, put the private key in an HSM/KMS instead of a file, and
 * mount the two crypto calls (openRequest / sealResponse) as middleware on
 * your own endpoints. See README.md.
 */
"use strict";
const http = require("http");
const fs = require("fs");
const path = require("path");
const { webcrypto } = require("node:crypto");
const WGP = require("./wgp-seal.js");

const PORT = process.env.PORT || 8080;
const HERE = __dirname;
const KEY_FILE = path.join(HERE, "server-key.json");
const KEY_ID = "enisa-origin-key-1";

/* ---- key material: persisted so the public key is stable across restarts ----
   PRODUCTION: replace this whole block with an HSM/KMS handle. */
async function loadOrCreateKey() {
  const subtle = webcrypto.subtle;
  if (fs.existsSync(KEY_FILE)) {
    const j = JSON.parse(fs.readFileSync(KEY_FILE, "utf8"));
    const priv = await subtle.importKey("pkcs8", Buffer.from(j.pkcs8, "base64"),
      { name: "ECDH", namedCurve: "P-256" }, false, ["deriveBits"]);
    return { privateKey: priv, publicRaw: new Uint8Array(Buffer.from(j.pubRaw, "base64")) };
  }
  const kp = await subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
  const pkcs8 = Buffer.from(await subtle.exportKey("pkcs8", kp.privateKey)).toString("base64");
  const pubRaw = Buffer.from(await subtle.exportKey("raw", kp.publicKey)).toString("base64");
  fs.writeFileSync(KEY_FILE, JSON.stringify({ pkcs8, pubRaw }), { mode: 0o600 });
  const priv = await subtle.importKey("pkcs8", Buffer.from(pkcs8, "base64"),
    { name: "ECDH", namedCurve: "P-256" }, false, ["deriveBits"]);
  return { privateKey: priv, publicRaw: new Uint8Array(Buffer.from(pubRaw, "base64")) };
}

let SRV = null;
const seen = new Map(); // rid -> ts (replay guard)

function send(res, code, type, body, extra) {
  res.writeHead(code, Object.assign({ "content-type": type, "cache-control": "no-store" }, extra || {}));
  res.end(body);
}
function serveStatic(res, rel) {
  const p = path.join(HERE, rel);
  if (!p.startsWith(HERE) || !fs.existsSync(p)) return send(res, 404, "text/plain", "not found");
  const t = rel.endsWith(".js") ? "application/javascript; charset=utf-8"
    : rel.endsWith(".html") ? "text/html; charset=utf-8" : "text/plain";
  send(res, 200, t, fs.readFileSync(p));
}
function readBody(req) {
  return new Promise((resolve) => { let d = ""; req.on("data", c => { d += c; if (d.length > 1e6) req.destroy(); }); req.on("end", () => resolve(d)); });
}

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, "http://x");

  if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/index.html")) return serveStatic(res, "index.html");
  if (req.method === "GET" && url.pathname === "/wgp-seal.js") return serveStatic(res, "wgp-seal.js");

  // publish the origin's public key (the browser fetches this)
  if (req.method === "GET" && url.pathname === "/wgp-key.json") {
    return send(res, 200, "application/json; charset=utf-8",
      JSON.stringify({ key_id: KEY_ID, suite: WGP.SUITE, pk: WGP._b64e(SRV.publicRaw) }));
  }

  // the sealed endpoint
  if (req.method === "POST" && url.pathname === "/api/submit") {
    let env; try { env = JSON.parse(await readBody(req)); } catch { return send(res, 400, "application/json", '{"error":"bad envelope"}'); }
    if (!env || !env.h || !env.enc || !env.ct) return send(res, 400, "application/json", '{"error":"bad envelope"}');

    // freshness + replay (header is bound in AAD, so tampering it also fails to decrypt)
    const now = Date.now(), h = env.h;
    if (!h.ts || Math.abs(now - h.ts) > 30000) return send(res, 400, "application/json", '{"error":"stale"}');
    if (seen.has(h.rid)) return send(res, 409, "application/json", '{"error":"replay"}');
    seen.set(h.rid, now); for (const [k, t] of seen) if (now - t > 30000) seen.delete(k);

    // origin decrypts with its private key
    let opened;
    try { opened = await WGP.openRequest(env, SRV.privateKey, SRV.publicRaw); }
    catch { return send(res, 400, "application/json", '{"error":"decrypt failed"}'); }

    // ---- your application logic (this is the only place the plaintext exists) ----
    const reply = {
      status: "received",
      ref: "REQ-" + Date.now().toString(36),
      echo: opened.obj,
      note: "Decrypted at the origin only."
    };

    // seal the reply back to the same session and return it
    const respEnv = await WGP.sealResponse(opened.respKey, opened.header, reply);
    return send(res, 200, "application/wgp-sealed+json; charset=utf-8", JSON.stringify(respEnv));
  }

  send(res, 404, "text/plain", "not found");
});

loadOrCreateKey().then((k) => {
  SRV = k;
  server.listen(PORT, () => {
    console.log("WGP E2EE origin (Node) on http://localhost:" + PORT);
    console.log("key_id=" + KEY_ID + "  public key (first bytes): " + WGP._b64e(k.publicRaw).slice(0, 20) + "…");
    console.log("private key persisted in server-key.json (use an HSM in production).");
  });
});

✓
ISO 9001
TÜV SÜD
✓
ISO 14001
TÜV SÜD
✓
ISO 27001
TÜV SÜD
✓
ISO 27017
TÜV SÜD
✓
ISO 27018
TÜV SÜD
✓
NIS2 Essential Entity
EU
✓
Allianz für Cyber-Sicherheit
BSI