n8n-nodes-nilai 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,7 @@ Turn off **Simplify Output** to receive the full raw nilAI response.
45
45
  Two independent checks, both run locally with no external dependencies:
46
46
 
47
47
  - **Response signature (`tee_verified`).** nilAI signs each response inside the enclave (secp256k1 ECDSA over SHA-256). The node fetches the enclave's public key, reconstructs the exact signed bytes from the raw response, and verifies the signature.
48
- - **Enclave attestation (`attestation_verified`).** The node fetches the enclave's AMD SEV-SNP attestation report, fetches the chip's certificate and AMD's certificate chain, and verifies the chain up to AMD's root, the report signature, the TCB values, that debug mode is off, the launch measurement against a known-good build, and that the report binds the live TLS session.
48
+ - **Enclave attestation (`attestation_verified`).** The node fetches the enclave's AMD SEV-SNP attestation report, fetches the chip's certificate and AMD's certificate chain, and verifies the chain up to AMD's root, the report signature, the TCB values, that debug mode is off, and the launch measurement against a known-good build. The signed report also carries a `report_data` value that binds the serving TLS certificate; the node surfaces it in the output for out-of-band checking, but does not open a raw TLS connection to verify that binding live, because n8n's sandbox does not permit community nodes to make TLS connections.
49
49
 
50
50
  ## Resources
51
51
 
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NilAi = void 0;
4
4
  exports.verifyNilaiSignature = verifyNilaiSignature;
5
+ const n8n_workflow_1 = require("n8n-workflow");
5
6
  const crypto_1 = require("crypto");
6
7
  const attestation_1 = require("./attestation");
7
8
  const SPKI_SECP256K1_COMPRESSED_PREFIX = Buffer.from('3036301006072a8648ce3d020106052b8104000a032200', 'hex');
@@ -32,11 +33,12 @@ class NilAi {
32
33
  this.description = {
33
34
  displayName: 'nilAI',
34
35
  name: 'nilAi',
35
- icon: 'file:nilai.svg',
36
+ icon: { light: 'file:nilai.svg', dark: 'file:nilai.svg' },
36
37
  group: ['transform'],
37
38
  version: 1,
38
39
  subtitle: '={{$parameter["model"]}}',
39
40
  description: 'Private, verifiable AI inference inside a Trusted Execution Environment (TEE), powered by Nillion nilAI.',
41
+ usableAsTool: true,
40
42
  defaults: {
41
43
  name: 'nilAI',
42
44
  },
@@ -158,7 +160,7 @@ class NilAi {
158
160
  let teeVerified = null;
159
161
  if (doVerify) {
160
162
  if (publicKeyB64 === undefined) {
161
- publicKeyB64 = (await this.helpers.httpRequest({
163
+ publicKeyB64 = (await this.helpers.httpRequestWithAuthentication.call(this, 'nilAiApi', {
162
164
  method: 'GET',
163
165
  url: `${baseUrl}/v1/public_key`,
164
166
  json: true,
@@ -200,7 +202,7 @@ class NilAi {
200
202
  });
201
203
  continue;
202
204
  }
203
- throw error;
205
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex: i });
204
206
  }
205
207
  }
206
208
  return [returnData];
@@ -5,44 +5,12 @@ exports.verifyTcbExtensions = verifyTcbExtensions;
5
5
  exports.checkReportDataBinding = checkReportDataBinding;
6
6
  exports.verifyEnclaveAttestation = verifyEnclaveAttestation;
7
7
  const crypto_1 = require("crypto");
8
- const tls_1 = require("tls");
9
- const fs_1 = require("fs");
10
- const path_1 = require("path");
11
- const os_1 = require("os");
12
8
  const KDS_DOMAIN = 'kdsintf.amd.com';
13
9
  const KNOWN_MEASUREMENTS = {
14
10
  '0.3.0': '981bcaa62bcd63cb03c7b93a67d1a8c17ff63e45e6d16323a8784bacbfcb254313741bcf32d0bccc795d6ea0e6ac1481',
15
11
  };
16
12
  const certChainCache = {};
17
13
  const vcekCache = {};
18
- function certCacheDir() {
19
- for (const base of [(0, path_1.join)((0, os_1.homedir)(), '.n8n'), (0, os_1.tmpdir)()]) {
20
- try {
21
- const dir = (0, path_1.join)(base, '.nilai-cert-cache');
22
- (0, fs_1.mkdirSync)(dir, { recursive: true });
23
- return dir;
24
- }
25
- catch {
26
- }
27
- }
28
- return (0, os_1.tmpdir)();
29
- }
30
- function readCertCache(name) {
31
- try {
32
- const p = (0, path_1.join)(certCacheDir(), name);
33
- return (0, fs_1.existsSync)(p) ? (0, fs_1.readFileSync)(p) : null;
34
- }
35
- catch {
36
- return null;
37
- }
38
- }
39
- function writeCertCache(name, data) {
40
- try {
41
- (0, fs_1.writeFileSync)((0, path_1.join)(certCacheDir(), name), data);
42
- }
43
- catch {
44
- }
45
- }
46
14
  function detectProcessor(family, model) {
47
15
  if (family === 0x19) {
48
16
  if (model <= 0x0f)
@@ -125,40 +93,6 @@ function parseCertChain(pem) {
125
93
  return null;
126
94
  }
127
95
  }
128
- async function getServerCertDer(host) {
129
- return new Promise((resolve) => {
130
- let settled = false;
131
- let socket;
132
- const finish = (v) => {
133
- if (settled)
134
- return;
135
- settled = true;
136
- clearTimeout(timer);
137
- try {
138
- socket?.destroy();
139
- }
140
- catch {
141
- }
142
- resolve(v);
143
- };
144
- const timer = setTimeout(() => finish(null), 7000);
145
- try {
146
- socket = (0, tls_1.connect)({ host, port: 443, servername: host, rejectUnauthorized: true }, () => {
147
- try {
148
- const peer = socket.getPeerCertificate();
149
- finish(peer && peer.raw ? peer.raw : null);
150
- }
151
- catch {
152
- finish(null);
153
- }
154
- });
155
- socket.on('error', () => finish(null));
156
- }
157
- catch {
158
- finish(null);
159
- }
160
- });
161
- }
162
96
  function checkReportDataBinding(reportDataHex, certDer) {
163
97
  try {
164
98
  const spki = new crypto_1.X509Certificate(certDer).publicKey.export({ type: 'spki', format: 'der' });
@@ -177,7 +111,8 @@ async function verifyEnclaveAttestation(ctx, baseUrl) {
177
111
  const reportResp = (await ctx.helpers.httpRequest({
178
112
  method: 'GET',
179
113
  url: `${baseUrl}/nilcc/api/v2/report`,
180
- json: true, timeout: 15000,
114
+ json: true,
115
+ timeout: 15000,
181
116
  }));
182
117
  const report = reportResp.report;
183
118
  const raw = Buffer.from(reportResp.raw_report, 'hex');
@@ -203,50 +138,28 @@ async function verifyEnclaveAttestation(ctx, baseUrl) {
203
138
  }
204
139
  const pad2 = (n) => String(n).padStart(2, '0');
205
140
  if (!certChainCache[processor]) {
206
- const chainCacheName = `certchain_${processor}.pem`;
207
- const cachedChain = readCertCache(chainCacheName);
208
- let chain = cachedChain ? parseCertChain(cachedChain.toString('utf8')) : null;
209
- if (!chain) {
210
- const chainPem = (await ctx.helpers.httpRequest({
211
- method: 'GET',
212
- url: `https://${KDS_DOMAIN}/vcek/v1/${processor}/cert_chain`,
213
- encoding: 'text',
214
- timeout: 15000,
215
- }));
216
- chain = parseCertChain(chainPem);
217
- if (!chain)
218
- throw new Error('AMD KDS returned an unparseable certificate chain');
219
- writeCertCache(chainCacheName, Buffer.from(chainPem, 'utf8'));
220
- }
141
+ const chainPem = (await ctx.helpers.httpRequest({
142
+ method: 'GET',
143
+ url: `https://${KDS_DOMAIN}/vcek/v1/${processor}/cert_chain`,
144
+ encoding: 'text',
145
+ timeout: 15000,
146
+ }));
147
+ const chain = parseCertChain(chainPem);
148
+ if (!chain)
149
+ throw new Error('AMD KDS returned an unparseable certificate chain');
221
150
  certChainCache[processor] = chain;
222
151
  }
223
152
  const { ark, ask } = certChainCache[processor];
224
153
  const vcekUrl = `https://${KDS_DOMAIN}/vcek/v1/${processor}/${chipIdHex}` +
225
154
  `?blSPL=${pad2(tcb.bootloader)}&teeSPL=${pad2(tcb.tee)}&snpSPL=${pad2(tcb.snp)}&ucodeSPL=${pad2(tcb.microcode)}`;
226
155
  if (!vcekCache[vcekUrl]) {
227
- const vcekCacheName = `vcek_${chipIdHex}_${tcb.bootloader}_${tcb.tee}_${tcb.snp}_${tcb.microcode}.der`;
228
- const cachedDer = readCertCache(vcekCacheName);
229
- let vcekCert = null;
230
- if (cachedDer) {
231
- try {
232
- vcekCert = new crypto_1.X509Certificate(cachedDer);
233
- }
234
- catch {
235
- vcekCert = null;
236
- }
237
- }
238
- if (!vcekCert) {
239
- const fetched = (await ctx.helpers.httpRequest({
240
- method: 'GET',
241
- url: vcekUrl,
242
- encoding: 'arraybuffer',
243
- timeout: 15000,
244
- }));
245
- const der = Buffer.from(fetched);
246
- vcekCert = new crypto_1.X509Certificate(der);
247
- writeCertCache(vcekCacheName, der);
248
- }
249
- vcekCache[vcekUrl] = vcekCert;
156
+ const fetched = (await ctx.helpers.httpRequest({
157
+ method: 'GET',
158
+ url: vcekUrl,
159
+ encoding: 'arraybuffer',
160
+ timeout: 15000,
161
+ }));
162
+ vcekCache[vcekUrl] = new crypto_1.X509Certificate(Buffer.from(fetched));
250
163
  }
251
164
  const vcek = vcekCache[vcekUrl];
252
165
  const arkSelfSigned = ark.verify(ark.publicKey);
@@ -269,23 +182,12 @@ async function verifyEnclaveAttestation(ctx, baseUrl) {
269
182
  const measurementMatches = known ? measurement === known : null;
270
183
  const reportData = raw.subarray(0x50, 0x50 + 64).toString('hex');
271
184
  const tcbOk = verifyTcbExtensions(vcek.raw, tcb, chipIdHex);
272
- const host = (() => {
273
- try {
274
- return new URL(baseUrl).hostname;
275
- }
276
- catch {
277
- return '';
278
- }
279
- })();
280
- const serverCertDer = host ? await getServerCertDer(host) : null;
281
- const tlsBound = serverCertDer ? checkReportDataBinding(reportData, serverCertDer) : false;
282
185
  const checks = {
283
186
  ark_self_signed: arkSelfSigned,
284
187
  ask_signed_by_ark: askByArk,
285
188
  vcek_signed_by_ask: vcekByAsk,
286
189
  report_signature_valid: sigValid,
287
190
  vcek_tcb_matches_report: tcbOk,
288
- tls_session_bound: tlsBound,
289
191
  debug_disabled: !debugAllowed,
290
192
  };
291
193
  const attestation_verified = Object.values(checks).every(Boolean) && measurementMatches !== false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-nilai",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Use Nillion nilAI — private LLM inference in a TEE, OpenAI-compatible — directly in n8n, with built-in cryptographic verification of every response.",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",