n8n-nodes-nilai 0.1.1 → 0.1.3
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.
|
@@ -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');
|
|
@@ -37,6 +38,7 @@ class NilAi {
|
|
|
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
|
},
|
|
@@ -50,14 +52,14 @@ class NilAi {
|
|
|
50
52
|
],
|
|
51
53
|
properties: [
|
|
52
54
|
{
|
|
53
|
-
displayName: 'Model',
|
|
55
|
+
displayName: 'Model Name or ID',
|
|
54
56
|
name: 'model',
|
|
55
57
|
type: 'options',
|
|
56
58
|
typeOptions: {
|
|
57
59
|
loadOptionsMethod: 'getModels',
|
|
58
60
|
},
|
|
59
|
-
default: '
|
|
60
|
-
description: 'The model to run inside the nilAI TEE. Loaded live from your nilAI endpoint.',
|
|
61
|
+
default: '',
|
|
62
|
+
description: 'The model to run inside the nilAI TEE. Loaded live from your nilAI endpoint. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
61
63
|
},
|
|
62
64
|
{
|
|
63
65
|
displayName: 'Instructions',
|
|
@@ -67,7 +69,7 @@ class NilAi {
|
|
|
67
69
|
rows: 2,
|
|
68
70
|
},
|
|
69
71
|
default: '',
|
|
70
|
-
description:
|
|
72
|
+
description: 'What you want the model to do, e.g. \'Summarise this in 3 sentences and 3-5 key bullet points\'',
|
|
71
73
|
hint: 'Plain text — the task only. Keep your data out of here (put that in Input). Leave empty to send just the Input as-is.',
|
|
72
74
|
},
|
|
73
75
|
{
|
|
@@ -87,7 +89,7 @@ class NilAi {
|
|
|
87
89
|
name: 'simplify',
|
|
88
90
|
type: 'boolean',
|
|
89
91
|
default: true,
|
|
90
|
-
description:
|
|
92
|
+
description: 'Whether to return a clean result instead of nilAI\'s full raw response object',
|
|
91
93
|
hint: 'On: tidy output — just the answer, the verification results and token usage. Off: the complete raw nilAI response (every field), handy for debugging.',
|
|
92
94
|
},
|
|
93
95
|
{
|
|
@@ -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.
|
|
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);
|
|
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,
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|