@nanogpt/private-mode 0.2.7 → 0.2.9
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/lib/requestTransforms.js +15 -6
- package/lib/secureClientLifecycle.js +114 -4
- package/lib/server.js +9 -7
- package/lib/statusContract.js +79 -1
- package/lib/verifyReceipt.js +39 -1
- package/models/private-tee.json +10 -1
- package/package.json +1 -1
package/lib/requestTransforms.js
CHANGED
|
@@ -14,8 +14,7 @@ const MAX_TOKEN_ALIAS_FIELDS = [
|
|
|
14
14
|
];
|
|
15
15
|
const KIMI_K3_STANDARD_DEFAULT_MAX_COMPLETION_TOKENS = 16_384;
|
|
16
16
|
const KIMI_K3_DEFAULT_MAX_COMPLETION_TOKENS = 65_536;
|
|
17
|
-
const
|
|
18
|
-
const KIMI_K3_CONTEXT_WINDOW_TOKENS = 1_048_576;
|
|
17
|
+
const KIMI_K3_PRIVATE_CONTEXT_WINDOW_TOKENS = 256_000;
|
|
19
18
|
const KIMI_K3_CONTEXT_SAFETY_MARGIN_TOKENS = 10;
|
|
20
19
|
const KIMI_K3_ESTIMATED_TEXT_BYTES_PER_TOKEN = 3;
|
|
21
20
|
const KIMI_K3_HIGH_ENTROPY_TEXT_MIN_LENGTH = 2_048;
|
|
@@ -549,8 +548,18 @@ function resolvePrivateModeKimiK3ReasoningEffort(body) {
|
|
|
549
548
|
return excludesReasoning ? normalizedEffort ?? 'low' : normalizedEffort ?? 'max';
|
|
550
549
|
}
|
|
551
550
|
|
|
552
|
-
function applyPrivateModeKimiK3RequestParams(body) {
|
|
551
|
+
function applyPrivateModeKimiK3RequestParams(body, model) {
|
|
553
552
|
const reasoningEffort = resolvePrivateModeKimiK3ReasoningEffort(body);
|
|
553
|
+
const contextWindowTokens = typeof model.maxInputTokens === 'number'
|
|
554
|
+
&& Number.isFinite(model.maxInputTokens)
|
|
555
|
+
&& model.maxInputTokens > 0
|
|
556
|
+
? Math.floor(model.maxInputTokens)
|
|
557
|
+
: KIMI_K3_PRIVATE_CONTEXT_WINDOW_TOKENS;
|
|
558
|
+
const maxCompletionTokens = typeof model.maxOutputTokens === 'number'
|
|
559
|
+
&& Number.isFinite(model.maxOutputTokens)
|
|
560
|
+
&& model.maxOutputTokens > 0
|
|
561
|
+
? Math.floor(model.maxOutputTokens)
|
|
562
|
+
: contextWindowTokens;
|
|
554
563
|
const requestedMaxTokens = typeof body.max_tokens === 'number' && Number.isFinite(body.max_tokens)
|
|
555
564
|
? body.max_tokens
|
|
556
565
|
: undefined;
|
|
@@ -559,13 +568,13 @@ function applyPrivateModeKimiK3RequestParams(body) {
|
|
|
559
568
|
? KIMI_K3_DEFAULT_MAX_COMPLETION_TOKENS
|
|
560
569
|
: KIMI_K3_STANDARD_DEFAULT_MAX_COMPLETION_TOKENS
|
|
561
570
|
: requestedMaxTokens < 0
|
|
562
|
-
?
|
|
571
|
+
? maxCompletionTokens
|
|
563
572
|
: requestedMaxTokens;
|
|
564
573
|
normalizePrivateModeKimiK3NamedToolChoice(body);
|
|
565
574
|
const promptTokenEstimate = estimatePrivateModeKimiK3PromptTokens(body);
|
|
566
575
|
const remainingContext = Math.max(
|
|
567
576
|
1,
|
|
568
|
-
|
|
577
|
+
contextWindowTokens -
|
|
569
578
|
promptTokenEstimate -
|
|
570
579
|
KIMI_K3_CONTEXT_SAFETY_MARGIN_TOKENS,
|
|
571
580
|
);
|
|
@@ -899,7 +908,7 @@ export function applyPrivateModelRequestMutations(body, model) {
|
|
|
899
908
|
normalizeStreamOptions(body);
|
|
900
909
|
applyTinfoilCompatibilityMutations(body, model);
|
|
901
910
|
if (isPrivateModeKimiK3Model(model)) {
|
|
902
|
-
applyPrivateModeKimiK3RequestParams(body);
|
|
911
|
+
applyPrivateModeKimiK3RequestParams(body, model);
|
|
903
912
|
} else if (model.thinkingMode === 'deepseek-v4') {
|
|
904
913
|
clampPrivateModeDeepSeekV4Output(body);
|
|
905
914
|
}
|
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
const DEFAULT_SECURE_CLIENT_IDLE_RESET_MS = 5 * 60 * 1000;
|
|
2
|
+
const DEFAULT_VERIFICATION_MAX_AGE_MS = 5 * 60 * 1000;
|
|
2
3
|
const EHBP_RESPONSE_NONCE_HEADER = 'ehbp-response-nonce';
|
|
4
|
+
const REQUIRED_VERIFICATION_STEPS = [
|
|
5
|
+
'fetchDigest',
|
|
6
|
+
'verifyCode',
|
|
7
|
+
'verifyEnclave',
|
|
8
|
+
'compareMeasurements',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function isNonEmptyString(value) {
|
|
12
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasCompleteVerificationEvidence(document) {
|
|
16
|
+
return document?.securityVerified === true
|
|
17
|
+
&& isNonEmptyString(document.codeFingerprint)
|
|
18
|
+
&& isNonEmptyString(document.enclaveFingerprint)
|
|
19
|
+
&& isNonEmptyString(document.releaseDigest)
|
|
20
|
+
&& isNonEmptyString(document.hpkePublicKey)
|
|
21
|
+
&& REQUIRED_VERIFICATION_STEPS.every(
|
|
22
|
+
(step) => document.steps?.[step]?.status === 'success',
|
|
23
|
+
)
|
|
24
|
+
&& !Object.values(document.steps || {}).some((step) => step?.status === 'failed');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isMeasurementMismatchError(error) {
|
|
28
|
+
let current = error;
|
|
29
|
+
for (let depth = 0; depth < 5 && current; depth += 1) {
|
|
30
|
+
const message = String(current.message || current);
|
|
31
|
+
if (/\b(?:code\s+)?measurement mismatch\b|\bcannot compare measurements\b/i.test(message)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
current = current.cause;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
3
38
|
|
|
4
39
|
function readSecureClientIdleResetMs() {
|
|
5
40
|
const configured = process.env.NANOGPT_PRIVATE_CLIENT_IDLE_RESET_MS?.trim();
|
|
@@ -14,9 +49,14 @@ export function createSecureState(apiBase, options = {}) {
|
|
|
14
49
|
: readSecureClientIdleResetMs();
|
|
15
50
|
const loadTinfoil = options.loadTinfoil || (() => import('tinfoil'));
|
|
16
51
|
const now = options.now || Date.now;
|
|
52
|
+
const verificationMaxAgeMs = Number.isFinite(options.verificationMaxAgeMs)
|
|
53
|
+
&& options.verificationMaxAgeMs >= 0
|
|
54
|
+
? Math.floor(options.verificationMaxAgeMs)
|
|
55
|
+
: DEFAULT_VERIFICATION_MAX_AGE_MS;
|
|
17
56
|
let clientState = null;
|
|
18
57
|
let verificationDocument = null;
|
|
19
58
|
let verificationError = null;
|
|
59
|
+
let verifiedAtMs = null;
|
|
20
60
|
|
|
21
61
|
async function getClient(userCacheSecret = clientState?.userCacheSecret) {
|
|
22
62
|
if (!userCacheSecret) {
|
|
@@ -24,12 +64,18 @@ export function createSecureState(apiBase, options = {}) {
|
|
|
24
64
|
}
|
|
25
65
|
const nowMs = now();
|
|
26
66
|
const idleMs = clientState ? nowMs - clientState.lastUsedAtMs : 0;
|
|
67
|
+
const verificationAgeMs = verifiedAtMs === null ? 0 : nowMs - verifiedAtMs;
|
|
27
68
|
if (
|
|
28
69
|
clientState?.userCacheSecret === userCacheSecret
|
|
29
70
|
&& (
|
|
30
71
|
idleResetMs === 0
|
|
31
72
|
|| (idleMs >= 0 && idleMs < idleResetMs)
|
|
32
73
|
)
|
|
74
|
+
&& (
|
|
75
|
+
verifiedAtMs === null
|
|
76
|
+
|| verificationMaxAgeMs === 0
|
|
77
|
+
|| (verificationAgeMs >= 0 && verificationAgeMs < verificationMaxAgeMs)
|
|
78
|
+
)
|
|
33
79
|
) {
|
|
34
80
|
clientState.lastUsedAtMs = nowMs;
|
|
35
81
|
return clientState.promise;
|
|
@@ -40,6 +86,7 @@ export function createSecureState(apiBase, options = {}) {
|
|
|
40
86
|
// still be finishing an older in-flight request.
|
|
41
87
|
verificationDocument = null;
|
|
42
88
|
verificationError = null;
|
|
89
|
+
verifiedAtMs = null;
|
|
43
90
|
const nextState = {
|
|
44
91
|
client: null,
|
|
45
92
|
lastUsedAtMs: nowMs,
|
|
@@ -54,10 +101,19 @@ export function createSecureState(apiBase, options = {}) {
|
|
|
54
101
|
transport: 'ehbp',
|
|
55
102
|
userCacheSecret,
|
|
56
103
|
});
|
|
57
|
-
await client.ready();
|
|
58
104
|
nextState.client = client;
|
|
105
|
+
try {
|
|
106
|
+
await client.ready();
|
|
107
|
+
} finally {
|
|
108
|
+
if (clientState === nextState) {
|
|
109
|
+
verificationDocument = client.getVerificationDocument();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!hasCompleteVerificationEvidence(verificationDocument)) {
|
|
113
|
+
throw new Error('Private Mode attestation evidence was incomplete.');
|
|
114
|
+
}
|
|
59
115
|
if (clientState === nextState) {
|
|
60
|
-
|
|
116
|
+
verifiedAtMs = now();
|
|
61
117
|
verificationError = null;
|
|
62
118
|
}
|
|
63
119
|
return client;
|
|
@@ -80,7 +136,23 @@ export function createSecureState(apiBase, options = {}) {
|
|
|
80
136
|
getClient,
|
|
81
137
|
markClientUsed(client) {
|
|
82
138
|
if (!clientState || clientState.client !== client) return false;
|
|
83
|
-
|
|
139
|
+
const nowMs = now();
|
|
140
|
+
const currentDocument = client.getVerificationDocument();
|
|
141
|
+
if (currentDocument !== verificationDocument) {
|
|
142
|
+
verificationDocument = currentDocument;
|
|
143
|
+
if (hasCompleteVerificationEvidence(currentDocument)) {
|
|
144
|
+
verifiedAtMs = nowMs;
|
|
145
|
+
verificationError = null;
|
|
146
|
+
} else {
|
|
147
|
+
verifiedAtMs = null;
|
|
148
|
+
// Preserve the incomplete document for status visibility, but never
|
|
149
|
+
// reuse a transport after the SDK has invalidated its evidence.
|
|
150
|
+
clientState.lastUsedAtMs = nowMs;
|
|
151
|
+
clientState = null;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
clientState.lastUsedAtMs = nowMs;
|
|
84
156
|
return true;
|
|
85
157
|
},
|
|
86
158
|
invalidateClient(client) {
|
|
@@ -89,11 +161,49 @@ export function createSecureState(apiBase, options = {}) {
|
|
|
89
161
|
clientState = null;
|
|
90
162
|
verificationDocument = null;
|
|
91
163
|
verificationError = null;
|
|
164
|
+
verifiedAtMs = null;
|
|
92
165
|
return true;
|
|
93
166
|
},
|
|
94
167
|
getVerificationState() {
|
|
168
|
+
const nowMs = now();
|
|
169
|
+
// Tinfoil 1.1.12 resets its document to "pending" after an exhausted
|
|
170
|
+
// AttestationError retry, so retain the SDK's explicit mismatch signal.
|
|
171
|
+
const mismatch = verificationDocument?.steps?.compareMeasurements?.status === 'failed'
|
|
172
|
+
|| isMeasurementMismatchError(verificationError);
|
|
173
|
+
const verified = hasCompleteVerificationEvidence(verificationDocument)
|
|
174
|
+
&& !mismatch
|
|
175
|
+
&& verifiedAtMs !== null;
|
|
176
|
+
const verificationAgeMs = verifiedAtMs === null ? null : nowMs - verifiedAtMs;
|
|
177
|
+
const clockRolledBack = verificationAgeMs !== null && verificationAgeMs < 0;
|
|
178
|
+
const expiresAtMs = verifiedAtMs === null || verificationMaxAgeMs === 0
|
|
179
|
+
? null
|
|
180
|
+
: verifiedAtMs + verificationMaxAgeMs;
|
|
181
|
+
const isStale = verified && (
|
|
182
|
+
clockRolledBack
|
|
183
|
+
|| (expiresAtMs !== null && nowMs >= expiresAtMs)
|
|
184
|
+
);
|
|
185
|
+
const freshnessSeconds = expiresAtMs === null
|
|
186
|
+
? null
|
|
187
|
+
: clockRolledBack
|
|
188
|
+
? 0
|
|
189
|
+
: Math.max(0, Math.floor((expiresAtMs - nowMs) / 1000));
|
|
190
|
+
const verificationStatus = mismatch
|
|
191
|
+
? 'measurement_mismatch'
|
|
192
|
+
: verified && verifiedAtMs !== null
|
|
193
|
+
? isStale
|
|
194
|
+
? 'stale'
|
|
195
|
+
: 'verified'
|
|
196
|
+
: verificationError || verificationDocument
|
|
197
|
+
? 'verification_failed'
|
|
198
|
+
: 'not_checked';
|
|
95
199
|
return {
|
|
96
|
-
verified
|
|
200
|
+
verified,
|
|
201
|
+
verificationStatus,
|
|
202
|
+
verifiedAt: verifiedAtMs === null ? null : new Date(verifiedAtMs).toISOString(),
|
|
203
|
+
expiresAt: expiresAtMs === null
|
|
204
|
+
? null
|
|
205
|
+
: new Date(expiresAtMs).toISOString(),
|
|
206
|
+
freshnessSeconds,
|
|
97
207
|
verificationDocument,
|
|
98
208
|
error: verificationError ? String(verificationError.message || verificationError) : null,
|
|
99
209
|
};
|
package/lib/server.js
CHANGED
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
createSecureState,
|
|
24
24
|
fetchWithSecureClientRecovery,
|
|
25
25
|
} from './secureClientLifecycle.js';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
buildPrivateModeStatusContract,
|
|
28
|
+
buildPublicAttestationSummary,
|
|
29
|
+
} from './statusContract.js';
|
|
27
30
|
|
|
28
31
|
const MODELS = JSON.parse(
|
|
29
32
|
readFileSync(new URL('../models/private-tee.json', import.meta.url), 'utf8'),
|
|
@@ -82,6 +85,7 @@ function openAIModelList() {
|
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
export function privateModeStatus(apiBase, secureState, localBase, originPolicy) {
|
|
88
|
+
const verificationState = secureState.getVerificationState();
|
|
85
89
|
return {
|
|
86
90
|
ok: true,
|
|
87
91
|
mode: 'private_tee',
|
|
@@ -99,10 +103,10 @@ export function privateModeStatus(apiBase, secureState, localBase, originPolicy)
|
|
|
99
103
|
streaming_billing: 'precharged_reserve_with_verified_usage_refund',
|
|
100
104
|
api_local_proxy_required: true,
|
|
101
105
|
browser_frontend_local_proxy_required: false,
|
|
102
|
-
...buildPrivateModeStatusContract(),
|
|
106
|
+
...buildPrivateModeStatusContract(verificationState),
|
|
103
107
|
browser_origins_allowed: originPolicy.allowedOrigins,
|
|
104
108
|
models: openAIModelList().data,
|
|
105
|
-
attestation:
|
|
109
|
+
attestation: buildPublicAttestationSummary(verificationState),
|
|
106
110
|
};
|
|
107
111
|
}
|
|
108
112
|
|
|
@@ -183,9 +187,8 @@ async function runPreflight({ apiBase, apiKey, model, req, requestBodyBytes }) {
|
|
|
183
187
|
try {
|
|
184
188
|
const data = await response.json();
|
|
185
189
|
const cacheScope = typeof data?.cacheScope === 'string' ? data.cacheScope.trim() : '';
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return { ok: true, cacheScope, preflightTicket };
|
|
190
|
+
if (/^[a-f0-9]{64}$/i.test(cacheScope)) {
|
|
191
|
+
return { ok: true, cacheScope };
|
|
189
192
|
}
|
|
190
193
|
} catch {}
|
|
191
194
|
|
|
@@ -313,7 +316,6 @@ async function handleChatCompletion({ apiBase, apiKey, secureState, req, res, co
|
|
|
313
316
|
'x-nanogpt-private-model': model.id,
|
|
314
317
|
'x-nanogpt-private-stream': privateStreamRequested ? 'true' : 'false',
|
|
315
318
|
'x-nanogpt-private-cache-scope': buildPrivateModeCacheScopeProof(preflight.cacheScope),
|
|
316
|
-
'x-nanogpt-private-preflight-ticket': preflight.preflightTicket,
|
|
317
319
|
'x-query-source': 'api',
|
|
318
320
|
...copyLocalHeaders(req),
|
|
319
321
|
},
|
package/lib/statusContract.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
1
3
|
export const PRIVATE_MODE_FRONTEND_SUPPORTED_FEATURES = Object.freeze([
|
|
2
4
|
'text_chat',
|
|
3
5
|
'streaming',
|
|
@@ -43,12 +45,88 @@ export const PRIVATE_MODE_PLAINTEXT_VISIBLE_TO = Object.freeze([
|
|
|
43
45
|
'verified_tee_target',
|
|
44
46
|
]);
|
|
45
47
|
|
|
46
|
-
export
|
|
48
|
+
export const PRIVATE_MODE_KNOWN_LIMITATIONS = Object.freeze([
|
|
49
|
+
'NanoGPT still observes account, routing, timing, size, status, and usage metadata.',
|
|
50
|
+
'Provider-reported usage metrics are trusted for billing and are not end-to-end signed.',
|
|
51
|
+
'A verification receipt does not prove usage correctness, prompt non-retention, or response content authenticity on its own.',
|
|
52
|
+
'Verification receipts are browser-generated and unsigned; the hashed request identifier is for opaque correlation, not proof of server issuance.',
|
|
53
|
+
'NanoGPT does not implement attestation-gated KMS release of upstream credentials; the inference enclave is provider-operated.',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const PRIVATE_MODE_VERIFICATION_STATUSES = new Set([
|
|
57
|
+
'verified',
|
|
58
|
+
'unavailable',
|
|
59
|
+
'stale',
|
|
60
|
+
'measurement_mismatch',
|
|
61
|
+
'verification_failed',
|
|
62
|
+
'unsupported',
|
|
63
|
+
'not_checked',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
function stringIdentifier(value) {
|
|
67
|
+
return typeof value === 'string' && value.trim() ? value.trim().slice(0, 512) : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function publicKeyFingerprint(value) {
|
|
71
|
+
return typeof value === 'string' && value
|
|
72
|
+
? createHash('sha256').update(value).digest('hex')
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildPublicAttestationSummary(verificationState = {}) {
|
|
77
|
+
return {
|
|
78
|
+
verified: verificationState.verified === true,
|
|
79
|
+
verificationStatus: verificationState.verificationStatus,
|
|
80
|
+
verifiedAt: verificationState.verifiedAt,
|
|
81
|
+
expiresAt: verificationState.expiresAt,
|
|
82
|
+
freshnessSeconds: verificationState.freshnessSeconds,
|
|
83
|
+
error: verificationState.error ? 'Private Mode attestation failed.' : null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildPrivateModeStatusContract(verificationState = {}) {
|
|
88
|
+
const verificationStatus = PRIVATE_MODE_VERIFICATION_STATUSES.has(verificationState.verificationStatus)
|
|
89
|
+
? verificationState.verificationStatus
|
|
90
|
+
: 'not_checked';
|
|
91
|
+
const document = verificationState.verificationDocument || {};
|
|
92
|
+
const keyFingerprint = publicKeyFingerprint(document.hpkePublicKey);
|
|
47
93
|
return {
|
|
48
94
|
frontend_supported_features: [...PRIVATE_MODE_FRONTEND_SUPPORTED_FEATURES],
|
|
49
95
|
frontend_disabled_features: [...PRIVATE_MODE_FRONTEND_DISABLED_FEATURES],
|
|
50
96
|
nanogpt_visible: [...PRIVATE_MODE_NANOGPT_VISIBLE],
|
|
51
97
|
encrypted_from_nanogpt: [...PRIVATE_MODE_ENCRYPTED_FROM_NANOGPT],
|
|
52
98
|
plaintext_visible_to: [...PRIVATE_MODE_PLAINTEXT_VISIBLE_TO],
|
|
99
|
+
assurance: {
|
|
100
|
+
contract_version: 1,
|
|
101
|
+
assurance_level: 'private_mode',
|
|
102
|
+
transport: 'ehbp',
|
|
103
|
+
verification_status: verificationStatus,
|
|
104
|
+
eligible: verificationStatus === 'verified',
|
|
105
|
+
verified_by: verificationState.verifiedAt || verificationStatus !== 'not_checked'
|
|
106
|
+
? ['local_proxy:tinfoil-js']
|
|
107
|
+
: [],
|
|
108
|
+
...(verificationState.verifiedAt ? { verified_at: verificationState.verifiedAt } : {}),
|
|
109
|
+
...(verificationState.expiresAt ? { expires_at: verificationState.expiresAt } : {}),
|
|
110
|
+
...(Number.isFinite(verificationState.freshnessSeconds)
|
|
111
|
+
? { freshness_seconds: Math.max(0, Math.floor(verificationState.freshnessSeconds)) }
|
|
112
|
+
: {}),
|
|
113
|
+
...(stringIdentifier(document.codeFingerprint)
|
|
114
|
+
? { expected_measurement: stringIdentifier(document.codeFingerprint) }
|
|
115
|
+
: {}),
|
|
116
|
+
...(stringIdentifier(document.enclaveFingerprint)
|
|
117
|
+
? { observed_measurement: stringIdentifier(document.enclaveFingerprint) }
|
|
118
|
+
: {}),
|
|
119
|
+
...(stringIdentifier(document.releaseDigest)
|
|
120
|
+
? { release_digest: stringIdentifier(document.releaseDigest) }
|
|
121
|
+
: {}),
|
|
122
|
+
...(keyFingerprint ? { public_key_fingerprint: keyFingerprint } : {}),
|
|
123
|
+
receipt_available: true,
|
|
124
|
+
verifier: { name: 'tinfoil-js', version: '1.1.12' },
|
|
125
|
+
documentation_url: 'https://nano-gpt.com/private-mode-verification',
|
|
126
|
+
known_limitations: [...PRIVATE_MODE_KNOWN_LIMITATIONS],
|
|
127
|
+
...(verificationStatus !== 'verified' && verificationStatus !== 'not_checked'
|
|
128
|
+
? { error_code: verificationStatus }
|
|
129
|
+
: {}),
|
|
130
|
+
},
|
|
53
131
|
};
|
|
54
132
|
}
|
package/lib/verifyReceipt.js
CHANGED
|
@@ -2,6 +2,9 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { fetchAttestationBundle, Verifier } from 'tinfoil';
|
|
4
4
|
|
|
5
|
+
const PRIVATE_MODE_ATTESTATION_MAX_AGE_MS = 5 * 60 * 1000;
|
|
6
|
+
const PRIVATE_MODE_RECEIPT_CLOCK_SKEW_MS = 60 * 1000;
|
|
7
|
+
|
|
5
8
|
function isRecord(value) {
|
|
6
9
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
7
10
|
}
|
|
@@ -84,7 +87,7 @@ function assertReceiptFlag(failures, label, value) {
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
export async function verifyPrivateModeReceipt(receipt) {
|
|
87
|
-
if (!isRecord(receipt) || receipt.schemaVersion !== 1) {
|
|
90
|
+
if (!isRecord(receipt) || (receipt.schemaVersion !== 1 && receipt.schemaVersion !== 2)) {
|
|
88
91
|
throw new Error('Unsupported or invalid Private Mode receipt.');
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -113,6 +116,29 @@ export async function verifyPrivateModeReceipt(receipt) {
|
|
|
113
116
|
if (receipt.status !== 'verified') {
|
|
114
117
|
failures.push(`Receipt status is ${receipt.status || 'missing'}; expected verified.`);
|
|
115
118
|
}
|
|
119
|
+
if (receipt.schemaVersion === 2) {
|
|
120
|
+
if (receipt.assuranceLevel !== 'private_mode') {
|
|
121
|
+
failures.push('Receipt assurance level is not private_mode.');
|
|
122
|
+
}
|
|
123
|
+
const verifiedAt = Date.parse(receipt.verifiedAt);
|
|
124
|
+
const expiresAt = Date.parse(receipt.expiresAt);
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
if (
|
|
127
|
+
!Number.isFinite(verifiedAt)
|
|
128
|
+
|| !Number.isFinite(expiresAt)
|
|
129
|
+
|| expiresAt <= verifiedAt
|
|
130
|
+
|| expiresAt - verifiedAt > PRIVATE_MODE_ATTESTATION_MAX_AGE_MS
|
|
131
|
+
|| verifiedAt > now + PRIVATE_MODE_RECEIPT_CLOCK_SKEW_MS
|
|
132
|
+
) {
|
|
133
|
+
failures.push('Receipt freshness timestamps are invalid.');
|
|
134
|
+
} else if (expiresAt <= now) {
|
|
135
|
+
warnings.push('Receipt attestation freshness window has expired; fresh evidence was verified again now.');
|
|
136
|
+
}
|
|
137
|
+
const requestIdSha256 = receipt.response?.requestIdSha256;
|
|
138
|
+
if (requestIdSha256 !== undefined && !/^[a-f0-9]{64}$/i.test(requestIdSha256)) {
|
|
139
|
+
failures.push('Request identifier binding is not a SHA-256 digest.');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
116
142
|
|
|
117
143
|
const encryption = isRecord(receipt.encryption) ? receipt.encryption : {};
|
|
118
144
|
if (encryption.transport !== 'EHBP') {
|
|
@@ -126,6 +152,16 @@ export async function verifyPrivateModeReceipt(receipt) {
|
|
|
126
152
|
assertRequiredEqual(failures, 'Runtime measurement', doc?.enclaveFingerprint, receipt.verifier?.enclaveFingerprint);
|
|
127
153
|
assertRequiredEqual(failures, 'Release digest', doc?.releaseDigest, receipt.verifier?.releaseDigest);
|
|
128
154
|
assertRequiredEqual(failures, 'HPKE public key', doc?.hpkePublicKey, receipt.verifier?.hpkePublicKey);
|
|
155
|
+
if (receipt.schemaVersion === 2) {
|
|
156
|
+
assertRequiredEqual(
|
|
157
|
+
failures,
|
|
158
|
+
'HPKE public-key fingerprint',
|
|
159
|
+
isNonEmptyString(doc?.hpkePublicKey)
|
|
160
|
+
? createHash('sha256').update(doc.hpkePublicKey).digest('hex')
|
|
161
|
+
: undefined,
|
|
162
|
+
receipt.verifier?.hpkePublicKeySha256,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
129
165
|
assertOptionalEqual(warnings, 'Attestation bundle SHA-256', attestationBundleSha256, receipt.verifier?.attestationBundleSha256);
|
|
130
166
|
|
|
131
167
|
return {
|
|
@@ -137,6 +173,7 @@ export async function verifyPrivateModeReceipt(receipt) {
|
|
|
137
173
|
enclaveFingerprint: doc?.enclaveFingerprint,
|
|
138
174
|
attestationBundleSha256,
|
|
139
175
|
verificationDocumentSha256,
|
|
176
|
+
schemaVersion: receipt.schemaVersion,
|
|
140
177
|
};
|
|
141
178
|
}
|
|
142
179
|
|
|
@@ -155,6 +192,7 @@ export async function verifyPrivateModeReceiptCli(positionals, io = process) {
|
|
|
155
192
|
}
|
|
156
193
|
|
|
157
194
|
io.stdout.write('Private Mode receipt verified.\n');
|
|
195
|
+
io.stdout.write(`Receipt schema: v${result.schemaVersion}\n`);
|
|
158
196
|
io.stdout.write(`Enclave: ${result.enclaveURL}\n`);
|
|
159
197
|
io.stdout.write(`Expected measurement: ${result.codeFingerprint}\n`);
|
|
160
198
|
io.stdout.write(`Runtime measurement: ${result.enclaveFingerprint}\n`);
|
package/models/private-tee.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"name": "DeepSeek V4 Flash Private",
|
|
5
5
|
"upstreamModel": "deepseek-v4-flash",
|
|
6
6
|
"billingModel": "private/deepseek-v4-flash",
|
|
7
|
+
"providerPricingModel": "TEE/deepseek-v4-flash",
|
|
7
8
|
"teeTargetModel": "deepseek-v4-flash",
|
|
8
9
|
"thinkingMode": "deepseek-v4",
|
|
9
10
|
"maxOutputTokens": 1048576,
|
|
@@ -16,8 +17,10 @@
|
|
|
16
17
|
"name": "Kimi K3 Private",
|
|
17
18
|
"upstreamModel": "kimi-k3",
|
|
18
19
|
"billingModel": "TEE/kimi-k3",
|
|
20
|
+
"providerPricingModel": "TEE/kimi-k3",
|
|
19
21
|
"teeTargetModel": "kimi-k3",
|
|
20
|
-
"
|
|
22
|
+
"maxInputTokens": 256000,
|
|
23
|
+
"maxOutputTokens": 256000,
|
|
21
24
|
"created": 1786147200,
|
|
22
25
|
"ownedBy": "nanogpt-private-mode",
|
|
23
26
|
"aliases": ["TEE/kimi-k3"]
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
"name": "GPT OSS 120B Private",
|
|
28
31
|
"upstreamModel": "gpt-oss-120b",
|
|
29
32
|
"billingModel": "TEE/gpt-oss-120b",
|
|
33
|
+
"providerPricingModel": "openai/gpt-oss-120b",
|
|
30
34
|
"teeTargetModel": "gpt-oss-120b",
|
|
31
35
|
"created": 1764547200,
|
|
32
36
|
"ownedBy": "nanogpt-private-mode",
|
|
@@ -37,6 +41,7 @@
|
|
|
37
41
|
"name": "Llama 3.3 70B Private",
|
|
38
42
|
"upstreamModel": "llama3-3-70b",
|
|
39
43
|
"billingModel": "TEE/llama3-3-70b",
|
|
44
|
+
"providerPricingModel": "meta-llama/llama-3.3-70b-instruct",
|
|
40
45
|
"teeTargetModel": "llama3-3-70b",
|
|
41
46
|
"created": 1764547200,
|
|
42
47
|
"ownedBy": "nanogpt-private-mode",
|
|
@@ -47,6 +52,7 @@
|
|
|
47
52
|
"name": "GLM 5.2 Private",
|
|
48
53
|
"upstreamModel": "glm-5-2",
|
|
49
54
|
"billingModel": "TEE/glm-5-2",
|
|
55
|
+
"providerPricingModel": "TEE/glm-5.2",
|
|
50
56
|
"teeTargetModel": "glm-5-2",
|
|
51
57
|
"thinkingMode": "glm-5.2",
|
|
52
58
|
"created": 1781827200,
|
|
@@ -58,6 +64,7 @@
|
|
|
58
64
|
"name": "GLM 5.2 Thinking Private",
|
|
59
65
|
"upstreamModel": "glm-5-2",
|
|
60
66
|
"billingModel": "TEE/glm-5-2:thinking",
|
|
67
|
+
"providerPricingModel": "TEE/glm-5.2:thinking",
|
|
61
68
|
"teeTargetModel": "glm-5-2",
|
|
62
69
|
"thinkingMode": "glm-5.2",
|
|
63
70
|
"created": 1781827200,
|
|
@@ -69,6 +76,7 @@
|
|
|
69
76
|
"name": "Gemma 4 31B Private",
|
|
70
77
|
"upstreamModel": "gemma4-31b",
|
|
71
78
|
"billingModel": "TEE/gemma4-31b",
|
|
79
|
+
"providerPricingModel": "TEE/gemma4-31b",
|
|
72
80
|
"teeTargetModel": "gemma4-31b",
|
|
73
81
|
"thinkingMode": "gemma",
|
|
74
82
|
"created": 1764547200,
|
|
@@ -80,6 +88,7 @@
|
|
|
80
88
|
"name": "Gemma 4 31B Thinking Private",
|
|
81
89
|
"upstreamModel": "gemma4-31b",
|
|
82
90
|
"billingModel": "TEE/gemma4-31b:thinking",
|
|
91
|
+
"providerPricingModel": "TEE/gemma4-31b:thinking",
|
|
83
92
|
"teeTargetModel": "gemma4-31b",
|
|
84
93
|
"thinkingMode": "gemma",
|
|
85
94
|
"created": 1764547200,
|