@appliedblockchain/silentdatarollup-core 1.0.9 → 1.0.10
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/dist/chunk-RWLHE5DT.mjs +3312 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2544 -2
- package/dist/index.mjs +5 -3
- package/dist/tests.js +181 -0
- package/dist/tests.mjs +1 -1
- package/package.json +3 -1
- package/dist/chunk-53A5RGL2.mjs +0 -771
|
@@ -0,0 +1,3312 @@
|
|
|
1
|
+
// src/Base.ts
|
|
2
|
+
import debug2 from "debug";
|
|
3
|
+
import {
|
|
4
|
+
getBytes,
|
|
5
|
+
keccak256 as keccak2563,
|
|
6
|
+
toUtf8Bytes as toUtf8Bytes3,
|
|
7
|
+
TypedDataEncoder
|
|
8
|
+
} from "ethers";
|
|
9
|
+
|
|
10
|
+
// src/constants.ts
|
|
11
|
+
import { keccak256, toUtf8Bytes } from "ethers";
|
|
12
|
+
var SIGN_RPC_METHODS = [
|
|
13
|
+
"sd_getTransactionsByAddress",
|
|
14
|
+
"eth_estimateGas",
|
|
15
|
+
"eth_getProof",
|
|
16
|
+
"eth_getTransactionByHash",
|
|
17
|
+
"eth_getTransactionReceipt",
|
|
18
|
+
"eth_getUserOperationReceipt",
|
|
19
|
+
"eth_getUserOperationByHash"
|
|
20
|
+
];
|
|
21
|
+
var eip721Domain = {
|
|
22
|
+
name: "Silent Data [Rollup]",
|
|
23
|
+
version: "1"
|
|
24
|
+
};
|
|
25
|
+
var delegateEIP721Types = {
|
|
26
|
+
Ticket: [
|
|
27
|
+
{ name: "expires", type: "string" },
|
|
28
|
+
{ name: "ephemeralAddress", type: "string" }
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
var DEBUG_NAMESPACE = "silentdata:core";
|
|
32
|
+
var DEBUG_NAMESPACE_SILENTDATA_INTERCEPTOR = "silentdata:interceptor";
|
|
33
|
+
var HEADER_SIGNATURE = "x-signature";
|
|
34
|
+
var HEADER_TIMESTAMP = "x-timestamp";
|
|
35
|
+
var HEADER_SIGNATURE_TYPE = "x-signature-type";
|
|
36
|
+
var HEADER_EIP712_SIGNATURE = "x-eip712-signature";
|
|
37
|
+
var HEADER_DELEGATE = "x-delegate";
|
|
38
|
+
var HEADER_DELEGATE_SIGNATURE = "x-delegate-signature";
|
|
39
|
+
var HEADER_EIP712_DELEGATE_SIGNATURE = "x-eip712-delegate-signature";
|
|
40
|
+
var HEADER_SIGNER_SWC = "x-signer-swc";
|
|
41
|
+
var HEADER_FROM_BLOCK = "x-from-block";
|
|
42
|
+
var ENTRYPOINT_ADDRESS = "0x34F5Bda45f2Ce00B646BD6B19D0F9817b5D8D398";
|
|
43
|
+
var DEFAULT_USER_OPERATION_RECEIPT_LOOKUP_RANGE = 1024;
|
|
44
|
+
var USER_OPERATION_EVENT_SIGNATURE = "UserOperationEvent(bytes32,address,address,uint256,bool,uint256,uint256)";
|
|
45
|
+
var USER_OPERATION_EVENT_HASH = keccak256(
|
|
46
|
+
toUtf8Bytes(USER_OPERATION_EVENT_SIGNATURE)
|
|
47
|
+
);
|
|
48
|
+
var DEFAULT_DELEGATE_EXPIRES = 10 * 60 * 60;
|
|
49
|
+
var DELEGATE_EXPIRATION_THRESHOLD_BUFFER = 5;
|
|
50
|
+
var WHITELISTED_METHODS = [
|
|
51
|
+
"sd_getTransactionsByAddress",
|
|
52
|
+
"sd_getVersion",
|
|
53
|
+
"eth_blockNumber",
|
|
54
|
+
"eth_call",
|
|
55
|
+
"eth_chainId",
|
|
56
|
+
"eth_estimateGas",
|
|
57
|
+
"eth_feeHistory",
|
|
58
|
+
"eth_gasPrice",
|
|
59
|
+
"eth_getBalance",
|
|
60
|
+
"eth_getBlockByHash",
|
|
61
|
+
"eth_getBlockByNumber",
|
|
62
|
+
"eth_getCode",
|
|
63
|
+
"eth_getFilterChanges",
|
|
64
|
+
"eth_getFilterLogs",
|
|
65
|
+
"eth_getHeaderByHash",
|
|
66
|
+
"eth_getLogs",
|
|
67
|
+
"eth_getProof",
|
|
68
|
+
"eth_getTransactionByHash",
|
|
69
|
+
"eth_getTransactionCount",
|
|
70
|
+
"eth_getTransactionReceipt",
|
|
71
|
+
"eth_maxPriorityFeePerGas",
|
|
72
|
+
"eth_newBlockFilter",
|
|
73
|
+
"eth_sendRawTransaction",
|
|
74
|
+
"eth_syncing",
|
|
75
|
+
"eth_newFilter",
|
|
76
|
+
"eth_newPendingTransactionFilter",
|
|
77
|
+
"net_listening",
|
|
78
|
+
"net_version",
|
|
79
|
+
"net_peerCount",
|
|
80
|
+
"web3_clientVersion",
|
|
81
|
+
"web3_sha3"
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// src/privateEvents.ts
|
|
85
|
+
import { keccak256 as keccak2562, toUtf8Bytes as toUtf8Bytes2 } from "ethers";
|
|
86
|
+
var PRIVATE_EVENT_SIGNATURE = "PrivateEvent(address[],bytes32,bytes)";
|
|
87
|
+
var PRIVATE_EVENT_SIGNATURE_HASH = keccak2562(
|
|
88
|
+
toUtf8Bytes2(PRIVATE_EVENT_SIGNATURE)
|
|
89
|
+
);
|
|
90
|
+
function calculateEventTypeHash(eventSignature) {
|
|
91
|
+
return keccak2562(toUtf8Bytes2(eventSignature));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/types.ts
|
|
95
|
+
var ChainId = /* @__PURE__ */ ((ChainId2) => {
|
|
96
|
+
ChainId2[ChainId2["MAINNET"] = 380929] = "MAINNET";
|
|
97
|
+
ChainId2[ChainId2["TESTNET"] = 381185] = "TESTNET";
|
|
98
|
+
return ChainId2;
|
|
99
|
+
})(ChainId || {});
|
|
100
|
+
var NetworkName = /* @__PURE__ */ ((NetworkName2) => {
|
|
101
|
+
NetworkName2["MAINNET"] = "sdr";
|
|
102
|
+
NetworkName2["TESTNET"] = "sdr-testnet";
|
|
103
|
+
return NetworkName2;
|
|
104
|
+
})(NetworkName || {});
|
|
105
|
+
var SignatureType = /* @__PURE__ */ ((SignatureType2) => {
|
|
106
|
+
SignatureType2["Raw"] = "RAW";
|
|
107
|
+
SignatureType2["EIP191"] = "EIP191";
|
|
108
|
+
SignatureType2["EIP712"] = "EIP712";
|
|
109
|
+
return SignatureType2;
|
|
110
|
+
})(SignatureType || {});
|
|
111
|
+
|
|
112
|
+
// src/utils.ts
|
|
113
|
+
import debug from "debug";
|
|
114
|
+
import { Wallet } from "ethers";
|
|
115
|
+
var log = debug(DEBUG_NAMESPACE);
|
|
116
|
+
function getAuthEIP721Types(payload) {
|
|
117
|
+
return {
|
|
118
|
+
Call: [
|
|
119
|
+
{
|
|
120
|
+
name: "request",
|
|
121
|
+
type: Array.isArray(payload) ? "JsonRPCRequest[]" : "JsonRPCRequest"
|
|
122
|
+
},
|
|
123
|
+
{ name: "timestamp", type: "string" }
|
|
124
|
+
],
|
|
125
|
+
JsonRPCRequest: [
|
|
126
|
+
{ name: "jsonrpc", type: "string" },
|
|
127
|
+
{ name: "method", type: "string" },
|
|
128
|
+
{ name: "params", type: "string" },
|
|
129
|
+
{ name: "id", type: "uint256" }
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function signAuthHeaderTypedData(signer, payload, timestamp, chainId) {
|
|
134
|
+
log("Preparing payload for signTypedData");
|
|
135
|
+
const preparePayload = (p) => ({
|
|
136
|
+
...p,
|
|
137
|
+
params: JSON.stringify(p.params)
|
|
138
|
+
});
|
|
139
|
+
const preparedPayload = Array.isArray(payload) ? payload.map(preparePayload) : preparePayload(payload);
|
|
140
|
+
const message = {
|
|
141
|
+
request: preparedPayload,
|
|
142
|
+
timestamp
|
|
143
|
+
};
|
|
144
|
+
const types = getAuthEIP721Types(payload);
|
|
145
|
+
const domain = { ...eip721Domain, chainId };
|
|
146
|
+
log("Signing typed data", JSON.stringify({ domain, types, message }, null, 2));
|
|
147
|
+
const signature = await signer.signTypedData(domain, types, message);
|
|
148
|
+
log("Signature generated:", signature);
|
|
149
|
+
return signature;
|
|
150
|
+
}
|
|
151
|
+
async function signAuthHeaderRawMessage(signer, payload, timestamp, chainId) {
|
|
152
|
+
log("Preparing raw message for signing");
|
|
153
|
+
const serialRequest = JSON.stringify(payload);
|
|
154
|
+
const xMessage = chainId + serialRequest + timestamp;
|
|
155
|
+
log("Raw message:", xMessage);
|
|
156
|
+
const signature = await signer.signMessage(xMessage);
|
|
157
|
+
log("Raw signature generated:", signature);
|
|
158
|
+
return signature;
|
|
159
|
+
}
|
|
160
|
+
async function getAuthHeaders(signer, payload, chainId, signatureType) {
|
|
161
|
+
const xTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
162
|
+
const headers = {
|
|
163
|
+
[HEADER_TIMESTAMP]: xTimestamp
|
|
164
|
+
};
|
|
165
|
+
switch (signatureType) {
|
|
166
|
+
case "RAW" /* Raw */:
|
|
167
|
+
log("Generating raw signature");
|
|
168
|
+
headers[HEADER_SIGNATURE] = await signAuthHeaderRawMessage(
|
|
169
|
+
signer,
|
|
170
|
+
payload,
|
|
171
|
+
xTimestamp,
|
|
172
|
+
chainId
|
|
173
|
+
);
|
|
174
|
+
break;
|
|
175
|
+
case "EIP712" /* EIP712 */:
|
|
176
|
+
log("Generating EIP712 signature");
|
|
177
|
+
headers[HEADER_EIP712_SIGNATURE] = await signAuthHeaderTypedData(
|
|
178
|
+
signer,
|
|
179
|
+
payload,
|
|
180
|
+
xTimestamp,
|
|
181
|
+
chainId
|
|
182
|
+
);
|
|
183
|
+
break;
|
|
184
|
+
default:
|
|
185
|
+
throw new Error(`Unsupported signature type: ${signatureType}`);
|
|
186
|
+
}
|
|
187
|
+
return headers;
|
|
188
|
+
}
|
|
189
|
+
function isSignableContractCall(payload, contracts) {
|
|
190
|
+
if (!contracts || contracts.length === 0) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
log("Checking if contract call is signable");
|
|
194
|
+
if (payload.method !== "eth_call") {
|
|
195
|
+
log("Payload method is not eth_call, returning false");
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const params = payload.params;
|
|
199
|
+
if (!params || params.length === 0 || typeof params[0] !== "object") {
|
|
200
|
+
log("Invalid params, returning false");
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const callTarget = params[0].to;
|
|
204
|
+
if (!callTarget) {
|
|
205
|
+
log("Missing call target, returning false");
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
log(`Call target: ${callTarget}`);
|
|
209
|
+
const contractIndex = contracts.findIndex(
|
|
210
|
+
(c) => c.contract.target.toString().toLowerCase() === callTarget.toLowerCase()
|
|
211
|
+
);
|
|
212
|
+
if (contractIndex < 0) {
|
|
213
|
+
log("Contract not found, returning false");
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
const callData = params[0].data;
|
|
217
|
+
if (!callData) {
|
|
218
|
+
log("Missing call data, returning false");
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const methodSignature = callData.slice(2, 10);
|
|
222
|
+
if (!methodSignature) {
|
|
223
|
+
log("Missing method signature, returning false");
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
log(`Method signature: ${methodSignature}`);
|
|
227
|
+
const { contract, contractMethodsToSign } = contracts[contractIndex];
|
|
228
|
+
const isSignable = contractMethodsToSign.some((methodName) => {
|
|
229
|
+
const fragment = contract.interface.getFunction(methodName);
|
|
230
|
+
return !!fragment && methodSignature.startsWith(fragment.selector.slice(2));
|
|
231
|
+
});
|
|
232
|
+
log("Is signable contract call:", isSignable);
|
|
233
|
+
return isSignable;
|
|
234
|
+
}
|
|
235
|
+
var defaultGetDelegate = async (provider) => {
|
|
236
|
+
return Wallet.createRandom();
|
|
237
|
+
};
|
|
238
|
+
var prepareTypedDataPayload = (p) => ({
|
|
239
|
+
...p,
|
|
240
|
+
params: JSON.stringify(p.params)
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// src/Base.ts
|
|
244
|
+
var log2 = debug2(DEBUG_NAMESPACE);
|
|
245
|
+
var SilentDataRollupBase = class {
|
|
246
|
+
constructor(config) {
|
|
247
|
+
this.currentDelegateSigner = null;
|
|
248
|
+
this.delegateSignerExpires = 0;
|
|
249
|
+
this.cachedDelegateHeaders = null;
|
|
250
|
+
this.cachedHeadersExpiry = 0;
|
|
251
|
+
this.delegateHeadersPromise = null;
|
|
252
|
+
this.contracts = [];
|
|
253
|
+
this._cachedNetwork = null;
|
|
254
|
+
this.config = {
|
|
255
|
+
...config,
|
|
256
|
+
authSignatureType: config.authSignatureType ?? "EIP191" /* EIP191 */
|
|
257
|
+
};
|
|
258
|
+
this.delegateConfig = this.resolveDelegateConfig(config);
|
|
259
|
+
log2(
|
|
260
|
+
"SilentDataRollupBase initialized with config:",
|
|
261
|
+
JSON.stringify(config, null, 2)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
resolveDelegateConfig(config) {
|
|
265
|
+
if (config.delegate === true) {
|
|
266
|
+
return {
|
|
267
|
+
getDelegate: defaultGetDelegate,
|
|
268
|
+
expires: DEFAULT_DELEGATE_EXPIRES
|
|
269
|
+
};
|
|
270
|
+
} else if (typeof config.delegate === "object") {
|
|
271
|
+
return {
|
|
272
|
+
getDelegate: config.delegate.getDelegate ?? defaultGetDelegate,
|
|
273
|
+
expires: config.delegate.expires ?? DEFAULT_DELEGATE_EXPIRES
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get cached network with simple caching
|
|
280
|
+
* @param provider - The provider to get the network from
|
|
281
|
+
* @returns Promise<Network> - The cached or freshly fetched network
|
|
282
|
+
*/
|
|
283
|
+
async getCachedNetwork(provider) {
|
|
284
|
+
if (!this._cachedNetwork) {
|
|
285
|
+
this._cachedNetwork = await provider.getNetwork();
|
|
286
|
+
log2("Network cached:", this._cachedNetwork);
|
|
287
|
+
}
|
|
288
|
+
return this._cachedNetwork;
|
|
289
|
+
}
|
|
290
|
+
async getDelegateSigner(provider) {
|
|
291
|
+
if (!this.delegateConfig) {
|
|
292
|
+
log2("getDelegateSigner: No delegate config, returning null");
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
296
|
+
log2("getDelegateSigner: Current time:", now);
|
|
297
|
+
const isDelegateSignerValid = this.currentDelegateSigner && this.delegateSignerExpires - DELEGATE_EXPIRATION_THRESHOLD_BUFFER > now;
|
|
298
|
+
if (isDelegateSignerValid) {
|
|
299
|
+
log2(
|
|
300
|
+
"getDelegateSigner: Returning existing delegate signer, expires in:",
|
|
301
|
+
this.delegateSignerExpires - now,
|
|
302
|
+
"seconds"
|
|
303
|
+
);
|
|
304
|
+
return this.currentDelegateSigner;
|
|
305
|
+
} else {
|
|
306
|
+
log2("getDelegateSigner: Getting new delegate signer");
|
|
307
|
+
try {
|
|
308
|
+
const newSigner = await this.delegateConfig.getDelegate(provider);
|
|
309
|
+
this.currentDelegateSigner = newSigner;
|
|
310
|
+
this.delegateSignerExpires = now + this.delegateConfig.expires;
|
|
311
|
+
log2(
|
|
312
|
+
"getDelegateSigner: New delegate signer set, expires in:",
|
|
313
|
+
this.delegateConfig.expires,
|
|
314
|
+
"seconds"
|
|
315
|
+
);
|
|
316
|
+
return newSigner;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
log2("getDelegateSigner: Error getting delegate signer:", error);
|
|
319
|
+
throw new Error("Failed to get delegate signer");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async getDelegateSignerMessage(provider) {
|
|
324
|
+
if (!this.delegateConfig) {
|
|
325
|
+
log2("No delegate config, returning null");
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const delegateSigner = await this.getDelegateSigner(provider);
|
|
329
|
+
if (!delegateSigner) {
|
|
330
|
+
log2("Failed to get delegate signer, returning null");
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
expires: new Date(this.delegateSignerExpires * 1e3).toISOString(),
|
|
335
|
+
ephemeralAddress: await delegateSigner.getAddress()
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Signs a raw delegate header message.
|
|
340
|
+
* This method can be overridden by extending classes to customize the signing process.
|
|
341
|
+
* The signer implementation decides whether to add EIP-191 prefix or not.
|
|
342
|
+
* @param provider - The provider used for signing
|
|
343
|
+
* @param message - The delegate signer message to be signed
|
|
344
|
+
* @param isSWC - Whether signing for smart wallet contract (EIP-1271)
|
|
345
|
+
* @returns A promise that resolves to the signature string
|
|
346
|
+
*/
|
|
347
|
+
async signDelegateHeader(provider, message, isSWC) {
|
|
348
|
+
log2("signDelegateHeader: Signing delegate header", message);
|
|
349
|
+
let bytesToSign;
|
|
350
|
+
if (isSWC) {
|
|
351
|
+
const messageHash = keccak2563(toUtf8Bytes3(message));
|
|
352
|
+
bytesToSign = getBytes(messageHash);
|
|
353
|
+
log2("Signing hash bytes for SWC", messageHash);
|
|
354
|
+
} else {
|
|
355
|
+
bytesToSign = toUtf8Bytes3(message);
|
|
356
|
+
log2("Signing message bytes for EOA", message);
|
|
357
|
+
}
|
|
358
|
+
const signature = await provider.signer.signMessage(bytesToSign);
|
|
359
|
+
log2("signDelegateHeader: Signature generated:", signature);
|
|
360
|
+
return signature;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Signs a typed delegate header message.
|
|
364
|
+
* This method can be overridden by extending classes to customize the signing process.
|
|
365
|
+
* @param provider - The provider used for signing
|
|
366
|
+
* @param chainId - The chain ID
|
|
367
|
+
* @param message - The delegate signer message to be signed
|
|
368
|
+
* @param isSWC - Whether signing for smart wallet contract (EIP-1271)
|
|
369
|
+
* @returns A promise that resolves to the signature string
|
|
370
|
+
*/
|
|
371
|
+
async signTypedDelegateHeader(provider, chainId, message, isSWC) {
|
|
372
|
+
log2("signTypedDelegateHeader: Signing typed delegate header");
|
|
373
|
+
log2(
|
|
374
|
+
"signTypedDelegateHeader: Typed message:",
|
|
375
|
+
JSON.stringify(message, null, 2)
|
|
376
|
+
);
|
|
377
|
+
const domain = { ...eip721Domain, chainId };
|
|
378
|
+
if (isSWC) {
|
|
379
|
+
const messageHash = TypedDataEncoder.hash(
|
|
380
|
+
domain,
|
|
381
|
+
delegateEIP721Types,
|
|
382
|
+
message
|
|
383
|
+
);
|
|
384
|
+
const hashBytes = getBytes(messageHash);
|
|
385
|
+
const signature2 = await provider.signer.signMessage(hashBytes);
|
|
386
|
+
log2("signTypedDelegateHeader: Typed SWC signature generated:", signature2);
|
|
387
|
+
return signature2;
|
|
388
|
+
}
|
|
389
|
+
const signature = await provider.signer.signTypedData(
|
|
390
|
+
domain,
|
|
391
|
+
delegateEIP721Types,
|
|
392
|
+
message
|
|
393
|
+
);
|
|
394
|
+
log2("signTypedDelegateHeader: Signature generated:", signature);
|
|
395
|
+
return signature;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* IMPORTANT: Return the cached promise (currentPromise), not the resolved value.
|
|
399
|
+
* This ensures multiple concurrent callers share the same in-flight request,
|
|
400
|
+
* preventing redundant API calls.
|
|
401
|
+
*/
|
|
402
|
+
async getDelegateHeaders(provider) {
|
|
403
|
+
log2("Getting delegate headers");
|
|
404
|
+
if (!this.delegateHeadersPromise) {
|
|
405
|
+
this.delegateHeadersPromise = this.generateDelegateHeaders(provider);
|
|
406
|
+
}
|
|
407
|
+
const currentPromise = this.delegateHeadersPromise;
|
|
408
|
+
try {
|
|
409
|
+
const delegateHeaders = await currentPromise;
|
|
410
|
+
log2("Delegate headers:", JSON.stringify(delegateHeaders, null, 2));
|
|
411
|
+
return currentPromise;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
log2("Error getting delegate headers:", error);
|
|
414
|
+
throw new Error("Failed to get delegate headers");
|
|
415
|
+
} finally {
|
|
416
|
+
if (this.delegateHeadersPromise === currentPromise) {
|
|
417
|
+
this.delegateHeadersPromise = null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async generateDelegateHeaders(provider) {
|
|
422
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
423
|
+
const signatureType = this.config.authSignatureType;
|
|
424
|
+
const isCachedHeadersValid = this.cachedDelegateHeaders && this.cachedHeadersExpiry - DELEGATE_EXPIRATION_THRESHOLD_BUFFER > now;
|
|
425
|
+
if (isCachedHeadersValid) {
|
|
426
|
+
log2("Returning cached delegate headers");
|
|
427
|
+
return this.cachedDelegateHeaders;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
const delegateSignerMessage = await this.getDelegateSignerMessage(provider);
|
|
431
|
+
if (!delegateSignerMessage) {
|
|
432
|
+
throw new Error("Failed to get delegate signer message");
|
|
433
|
+
}
|
|
434
|
+
const delegateSigner = await this.getDelegateSigner(provider);
|
|
435
|
+
if (!delegateSigner) {
|
|
436
|
+
throw new Error("Failed to get delegate signer");
|
|
437
|
+
}
|
|
438
|
+
const headers = {
|
|
439
|
+
[HEADER_DELEGATE]: JSON.stringify(delegateSignerMessage)
|
|
440
|
+
};
|
|
441
|
+
const chainId = (await this.getCachedNetwork(provider)).chainId.toString();
|
|
442
|
+
const isSWC = !!this.config.smartWalletAddress;
|
|
443
|
+
switch (signatureType) {
|
|
444
|
+
case "EIP191" /* EIP191 */:
|
|
445
|
+
case "RAW" /* Raw */: {
|
|
446
|
+
log2("Generating delegate signature");
|
|
447
|
+
const delegateMessageToSign = chainId + JSON.stringify(delegateSignerMessage);
|
|
448
|
+
headers[HEADER_DELEGATE_SIGNATURE] = await this.signDelegateHeader(
|
|
449
|
+
provider,
|
|
450
|
+
delegateMessageToSign,
|
|
451
|
+
isSWC
|
|
452
|
+
);
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case "EIP712" /* EIP712 */:
|
|
456
|
+
log2("Generating delegate EIP712 signature");
|
|
457
|
+
headers[HEADER_EIP712_DELEGATE_SIGNATURE] = await this.signTypedDelegateHeader(
|
|
458
|
+
provider,
|
|
459
|
+
chainId,
|
|
460
|
+
delegateSignerMessage,
|
|
461
|
+
isSWC
|
|
462
|
+
);
|
|
463
|
+
break;
|
|
464
|
+
default:
|
|
465
|
+
throw new Error(`Unsupported signature type: ${signatureType}`);
|
|
466
|
+
}
|
|
467
|
+
this.cachedDelegateHeaders = headers;
|
|
468
|
+
this.cachedHeadersExpiry = new Date(delegateSignerMessage.expires).getTime() / 1e3;
|
|
469
|
+
return this.cachedDelegateHeaders;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
log2("Error getting delegate headers:", error);
|
|
472
|
+
throw new Error("Failed to get delegate headers");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async getAuthHeaders(provider, payload) {
|
|
476
|
+
log2("Getting auth headers", JSON.stringify(payload, null, 2));
|
|
477
|
+
const xTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
478
|
+
const headers = {
|
|
479
|
+
[HEADER_TIMESTAMP]: xTimestamp
|
|
480
|
+
};
|
|
481
|
+
const chainId = (await this.getCachedNetwork(provider)).chainId.toString();
|
|
482
|
+
const signatureType = this.config.authSignatureType ?? "EIP191" /* EIP191 */;
|
|
483
|
+
const isSWC = !!this.config.smartWalletAddress;
|
|
484
|
+
let payloadToSign = payload;
|
|
485
|
+
const isGetUserOperationReceipt = !Array.isArray(payload) && payload.method === "eth_getUserOperationReceipt";
|
|
486
|
+
if (isGetUserOperationReceipt) {
|
|
487
|
+
log2(
|
|
488
|
+
"Detected eth_getUserOperationReceipt, building custom eth_getLogs payload for signing"
|
|
489
|
+
);
|
|
490
|
+
let fromBlock;
|
|
491
|
+
try {
|
|
492
|
+
fromBlock = await this.getFromBlockForUserOperationReceipt(provider);
|
|
493
|
+
headers[HEADER_FROM_BLOCK] = fromBlock.toString();
|
|
494
|
+
log2(`Added ${HEADER_FROM_BLOCK} header:`, fromBlock.toString());
|
|
495
|
+
} catch (error) {
|
|
496
|
+
log2(
|
|
497
|
+
"Error calculating fromBlock for eth_getUserOperationReceipt:",
|
|
498
|
+
error
|
|
499
|
+
);
|
|
500
|
+
throw new Error(
|
|
501
|
+
"Failed to calculate fromBlock for eth_getUserOperationReceipt"
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
payloadToSign = this.buildGetUserOperationReceiptSigningPayload(
|
|
505
|
+
payload,
|
|
506
|
+
fromBlock
|
|
507
|
+
);
|
|
508
|
+
log2(
|
|
509
|
+
"Using custom eth_getLogs payload for signing:",
|
|
510
|
+
JSON.stringify(payloadToSign, null, 2)
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
switch (signatureType) {
|
|
514
|
+
case "EIP191" /* EIP191 */:
|
|
515
|
+
case "RAW" /* Raw */:
|
|
516
|
+
log2("Generating auth header signature");
|
|
517
|
+
headers[HEADER_SIGNATURE] = await this.signAuthHeader(
|
|
518
|
+
provider,
|
|
519
|
+
payloadToSign,
|
|
520
|
+
xTimestamp,
|
|
521
|
+
chainId,
|
|
522
|
+
isSWC
|
|
523
|
+
);
|
|
524
|
+
break;
|
|
525
|
+
case "EIP712" /* EIP712 */:
|
|
526
|
+
log2("Generating auth header typed signature");
|
|
527
|
+
headers[HEADER_EIP712_SIGNATURE] = await this.signTypedAuthHeader(
|
|
528
|
+
provider,
|
|
529
|
+
payloadToSign,
|
|
530
|
+
xTimestamp,
|
|
531
|
+
chainId,
|
|
532
|
+
isSWC
|
|
533
|
+
);
|
|
534
|
+
break;
|
|
535
|
+
default:
|
|
536
|
+
throw new Error(`Unsupported signature type: ${signatureType}`);
|
|
537
|
+
}
|
|
538
|
+
log2("Auth headers:", JSON.stringify(headers, null, 2));
|
|
539
|
+
return headers;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Signs auth header.
|
|
543
|
+
*/
|
|
544
|
+
async signAuthHeader(provider, payload, timestamp, chainId, isSWC) {
|
|
545
|
+
const xMessage = this.prepareMessage(chainId, payload, timestamp);
|
|
546
|
+
const delegateSigner = await this.getDelegateSigner(this);
|
|
547
|
+
const signer = delegateSigner ?? provider.signer;
|
|
548
|
+
const usingDelegate = !!delegateSigner;
|
|
549
|
+
let bytesToSign;
|
|
550
|
+
if (isSWC && !usingDelegate) {
|
|
551
|
+
const messageHash = keccak2563(toUtf8Bytes3(xMessage));
|
|
552
|
+
bytesToSign = getBytes(messageHash);
|
|
553
|
+
log2("Signing hash bytes for SWC");
|
|
554
|
+
} else {
|
|
555
|
+
bytesToSign = toUtf8Bytes3(xMessage);
|
|
556
|
+
log2("Signing message bytes for EOA");
|
|
557
|
+
}
|
|
558
|
+
const signature = await signer.signMessage(bytesToSign);
|
|
559
|
+
log2("Message signed raw. Signature:", signature);
|
|
560
|
+
return signature;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Signs auth header using typed data signature.
|
|
564
|
+
*/
|
|
565
|
+
async signTypedAuthHeader(provider, payload, timestamp, chainId, isSWC) {
|
|
566
|
+
const message = this.prepareTypedData(payload, timestamp);
|
|
567
|
+
const types = getAuthEIP721Types(payload);
|
|
568
|
+
const delegateSigner = await this.getDelegateSigner(this);
|
|
569
|
+
const signer = delegateSigner ?? provider.signer;
|
|
570
|
+
const usingDelegate = !!delegateSigner;
|
|
571
|
+
const domain = { ...eip721Domain, chainId };
|
|
572
|
+
if (isSWC && !usingDelegate) {
|
|
573
|
+
const messageHash2 = TypedDataEncoder.hash(domain, types, message);
|
|
574
|
+
log2("EIP-712 hash (SWC without delegate):", messageHash2);
|
|
575
|
+
const hashBytes = getBytes(messageHash2);
|
|
576
|
+
const signature2 = await signer.signMessage(hashBytes);
|
|
577
|
+
log2("Message signed with EIP-712 for SWC. Signature:", signature2);
|
|
578
|
+
return signature2;
|
|
579
|
+
}
|
|
580
|
+
log2(
|
|
581
|
+
"Signing typed data",
|
|
582
|
+
JSON.stringify({ message, types, domain }, null, 2)
|
|
583
|
+
);
|
|
584
|
+
const messageHash = TypedDataEncoder.hash(domain, types, message);
|
|
585
|
+
log2("EIP-712 hash (EOA):", messageHash);
|
|
586
|
+
const signature = await this.signTypedData(signer, domain, types, message);
|
|
587
|
+
log2("Message signed with EIP-712. Signature:", signature);
|
|
588
|
+
return signature;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Signs a message using the provided signer.
|
|
592
|
+
* This method can be overridden to customize the signing process.
|
|
593
|
+
* @param signer - The signer to use
|
|
594
|
+
* @param message - The message to sign
|
|
595
|
+
* @returns A promise that resolves to the signature string
|
|
596
|
+
*/
|
|
597
|
+
async signMessage(signer, message) {
|
|
598
|
+
return signer.signMessage(message);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Signs typed data using the provided signer.
|
|
602
|
+
* This method can be overridden to customize the signing process.
|
|
603
|
+
* @param signer - The signer to use
|
|
604
|
+
* @param domain - The EIP-712 domain
|
|
605
|
+
* @param types - The EIP-712 types
|
|
606
|
+
* @param message - The message to sign
|
|
607
|
+
* @returns A promise that resolves to the signature string
|
|
608
|
+
*/
|
|
609
|
+
async signTypedData(signer, domain, types, message) {
|
|
610
|
+
return signer.signTypedData(domain, types, message);
|
|
611
|
+
}
|
|
612
|
+
setContract(contract, contractMethodsToSign) {
|
|
613
|
+
log2("Setting contract and methods to sign: ", contractMethodsToSign);
|
|
614
|
+
this.contracts.push({
|
|
615
|
+
contract,
|
|
616
|
+
contractMethodsToSign
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Calculates the fromBlock value for eth_getUserOperationReceipt requests.
|
|
621
|
+
* Gets the current block number and subtracts the configured userOperationReceiptLookupRange.
|
|
622
|
+
*
|
|
623
|
+
* IMPORTANT: The bundler strictly validates this value and will reject the request if:
|
|
624
|
+
* - The header is missing
|
|
625
|
+
* - The value is < 0
|
|
626
|
+
* - The value is > current block number
|
|
627
|
+
* - The value is too far back (< currentBlock - userOperationReceiptLookupRange)
|
|
628
|
+
*
|
|
629
|
+
* This method ensures the returned value is always within the valid range:
|
|
630
|
+
* max(0, currentBlock - userOperationReceiptLookupRange) <= fromBlock <= currentBlock
|
|
631
|
+
*
|
|
632
|
+
* @param provider - The provider to use for fetching the current block number
|
|
633
|
+
* @returns A promise that resolves to the fromBlock value as a bigint
|
|
634
|
+
* @throws Error if unable to fetch the current block number
|
|
635
|
+
*/
|
|
636
|
+
async getFromBlockForUserOperationReceipt(provider) {
|
|
637
|
+
const lookupRange = BigInt(
|
|
638
|
+
this.config.userOperationReceiptLookupRange ?? DEFAULT_USER_OPERATION_RECEIPT_LOOKUP_RANGE
|
|
639
|
+
);
|
|
640
|
+
log2("User operation receipt lookup range:", lookupRange.toString());
|
|
641
|
+
let currentBlockNumber;
|
|
642
|
+
if (typeof provider.getBlockNumber === "function") {
|
|
643
|
+
currentBlockNumber = BigInt(await provider.getBlockNumber());
|
|
644
|
+
} else if (typeof provider.request === "function") {
|
|
645
|
+
const blockNumberHex = await provider.request({
|
|
646
|
+
method: "eth_blockNumber",
|
|
647
|
+
params: []
|
|
648
|
+
});
|
|
649
|
+
currentBlockNumber = BigInt(blockNumberHex);
|
|
650
|
+
} else {
|
|
651
|
+
throw new Error(
|
|
652
|
+
"Provider does not support getBlockNumber or request method"
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
log2("Current block number:", currentBlockNumber.toString());
|
|
656
|
+
const fromBlock = currentBlockNumber > lookupRange ? currentBlockNumber - lookupRange : 0n;
|
|
657
|
+
log2("Calculated fromBlock:", fromBlock.toString());
|
|
658
|
+
return fromBlock;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Builds a custom eth_getLogs payload for signing when the original request is eth_getUserOperationReceipt.
|
|
662
|
+
* This method can be overridden to customize the payload construction.
|
|
663
|
+
*
|
|
664
|
+
* IMPORTANT: The bundler must reconstruct this exact payload to verify the signature.
|
|
665
|
+
* The bundler should use the same `id` from the original eth_getUserOperationReceipt request
|
|
666
|
+
* when constructing the eth_getLogs request to send to the RPC node.
|
|
667
|
+
*
|
|
668
|
+
* @param payload - The original eth_getUserOperationReceipt payload
|
|
669
|
+
* @param fromBlock - The fromBlock value to use in the eth_getLogs filter
|
|
670
|
+
* @returns A JsonRpcPayload with method 'eth_getLogs' to be used for signing
|
|
671
|
+
*/
|
|
672
|
+
buildGetUserOperationReceiptSigningPayload(payload, fromBlock) {
|
|
673
|
+
return {
|
|
674
|
+
jsonrpc: payload.jsonrpc,
|
|
675
|
+
method: "eth_getLogs",
|
|
676
|
+
params: [
|
|
677
|
+
{
|
|
678
|
+
address: ENTRYPOINT_ADDRESS,
|
|
679
|
+
fromBlock: `0x${fromBlock.toString(16)}`,
|
|
680
|
+
topics: [
|
|
681
|
+
PRIVATE_EVENT_SIGNATURE_HASH,
|
|
682
|
+
USER_OPERATION_EVENT_HASH
|
|
683
|
+
// eventType
|
|
684
|
+
]
|
|
685
|
+
}
|
|
686
|
+
],
|
|
687
|
+
id: payload.id ?? 1
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Prepares the message to be signed for the x-signature header.
|
|
692
|
+
*/
|
|
693
|
+
prepareMessage(chainId, payload, timestamp) {
|
|
694
|
+
log2("Preparing raw message for signing", {
|
|
695
|
+
payload: JSON.stringify(payload, null, 2),
|
|
696
|
+
timestamp
|
|
697
|
+
});
|
|
698
|
+
const serialRequest = JSON.stringify(payload);
|
|
699
|
+
const xMessage = chainId + serialRequest + timestamp;
|
|
700
|
+
log2("Raw message to be signed:", xMessage);
|
|
701
|
+
return xMessage;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Prepares the message to be signed for the x-eip712-signature header.
|
|
705
|
+
*/
|
|
706
|
+
prepareTypedData(payload, timestamp) {
|
|
707
|
+
log2("Preparing payload for signTypedData");
|
|
708
|
+
const preparedPayload = Array.isArray(payload) ? payload.map(prepareTypedDataPayload) : prepareTypedDataPayload(payload);
|
|
709
|
+
const message = {
|
|
710
|
+
request: preparedPayload,
|
|
711
|
+
timestamp
|
|
712
|
+
};
|
|
713
|
+
log2("Prepared payload for signTypedData", JSON.stringify(message, null, 2));
|
|
714
|
+
return message;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// src/contract.ts
|
|
719
|
+
import { Contract as Contract2, Interface } from "ethers";
|
|
720
|
+
var SilentDataRollupContract = class extends Contract2 {
|
|
721
|
+
constructor(config) {
|
|
722
|
+
const { address, abi, runner, contractMethodsToSign } = config;
|
|
723
|
+
const contractInterface = new Interface(abi);
|
|
724
|
+
contractMethodsToSign.forEach((method) => {
|
|
725
|
+
if (!contractInterface.hasFunction(method)) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`Method to sign '${method}' not found in the contract ABI`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
super(address, abi, runner);
|
|
732
|
+
const baseProvider = runner.baseProvider || runner.provider?.baseProvider;
|
|
733
|
+
if (typeof baseProvider?.setContract === "function") {
|
|
734
|
+
baseProvider.setContract(this, contractMethodsToSign);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// src/audit/challenge.ts
|
|
740
|
+
import { ethers as ethers2 } from "ethers";
|
|
741
|
+
|
|
742
|
+
// src/audit/quote/validateTdxQuote.ts
|
|
743
|
+
import * as x5095 from "@peculiar/x509";
|
|
744
|
+
|
|
745
|
+
// src/audit/quote/quoteStructs.ts
|
|
746
|
+
var intelQuoteV4Version = 4;
|
|
747
|
+
var QuoteVersion = 4;
|
|
748
|
+
var AttestationKeyType = 2;
|
|
749
|
+
var rtmrsCount = 4;
|
|
750
|
+
var RtmrSize = 48;
|
|
751
|
+
var qeSvnSize = 2;
|
|
752
|
+
var pceSvnSize = 2;
|
|
753
|
+
var TeeTcbSvnSize = 16;
|
|
754
|
+
var QeVendorIDSize = 16;
|
|
755
|
+
var userDataSize = 20;
|
|
756
|
+
var MrSeamSize = 48;
|
|
757
|
+
var mrSignerSeamSize = 48;
|
|
758
|
+
var TeeTDX = 129;
|
|
759
|
+
var seamAttributesSize = 8;
|
|
760
|
+
var TdAttributesSize = 8;
|
|
761
|
+
var XfamSize = 8;
|
|
762
|
+
var MrTdSize = 48;
|
|
763
|
+
var MrConfigIDSize = 48;
|
|
764
|
+
var MrOwnerSize = 48;
|
|
765
|
+
var MrOwnerConfigSize = 48;
|
|
766
|
+
var cpuSvnSize = 16;
|
|
767
|
+
var attributesSize = 16;
|
|
768
|
+
var ReportDataSize = 64;
|
|
769
|
+
var headerVersionStart = 0;
|
|
770
|
+
var headerVersionEnd = 2;
|
|
771
|
+
var headerAttestationKeyTypeStart = headerVersionEnd;
|
|
772
|
+
var headerAttestationKeyTypeEnd = 4;
|
|
773
|
+
var headerTeeTypeStart = headerAttestationKeyTypeEnd;
|
|
774
|
+
var headerTeeTypeEnd = 8;
|
|
775
|
+
var headerPceSvnStart = headerTeeTypeEnd;
|
|
776
|
+
var headerPceSvnEnd = 10;
|
|
777
|
+
var headerQeSvnStart = headerPceSvnEnd;
|
|
778
|
+
var headerQeSvnEnd = 12;
|
|
779
|
+
var headerQeVendorIDStart = headerQeSvnEnd;
|
|
780
|
+
var headerQeVendorIDEnd = 28;
|
|
781
|
+
var headerUserDataStart = headerQeVendorIDEnd;
|
|
782
|
+
var tdTeeTcbSvnStart = 0;
|
|
783
|
+
var tdTeeTcbSvnEnd = 16;
|
|
784
|
+
var tdMrSeamStart = tdTeeTcbSvnEnd;
|
|
785
|
+
var tdMrSeamEnd = 64;
|
|
786
|
+
var tdMrSignerSeamStart = tdMrSeamEnd;
|
|
787
|
+
var tdMrSignerSeamEnd = 112;
|
|
788
|
+
var tdSeamAttributesStart = tdMrSignerSeamEnd;
|
|
789
|
+
var tdSeamAttributesEnd = 120;
|
|
790
|
+
var tdAttributesStart = tdSeamAttributesEnd;
|
|
791
|
+
var tdAttributesEnd = 128;
|
|
792
|
+
var tdXfamStart = tdAttributesEnd;
|
|
793
|
+
var tdXfamEnd = 136;
|
|
794
|
+
var tdMrTdStart = tdXfamEnd;
|
|
795
|
+
var tdMrTdEnd = 184;
|
|
796
|
+
var tdMrConfigIDStart = tdMrTdEnd;
|
|
797
|
+
var tdMrConfigIDEnd = 232;
|
|
798
|
+
var tdMrOwnerStart = tdMrConfigIDEnd;
|
|
799
|
+
var tdMrOwnerEnd = 280;
|
|
800
|
+
var tdMrOwnerConfigStart = tdMrOwnerEnd;
|
|
801
|
+
var tdMrOwnerConfigEnd = 328;
|
|
802
|
+
var tdRtmrsStart = tdMrOwnerConfigEnd;
|
|
803
|
+
var tdRtmrsEnd = 520;
|
|
804
|
+
var tdReportDataStart = tdRtmrsEnd;
|
|
805
|
+
var headerSize = 48;
|
|
806
|
+
var tdQuoteBodySize = 584;
|
|
807
|
+
var qeReportSize = 384;
|
|
808
|
+
var qeCPUSvnStart = 0;
|
|
809
|
+
var qeCPUSvnEnd = 16;
|
|
810
|
+
var qeMiscSelectStart = qeCPUSvnEnd;
|
|
811
|
+
var qeMiscSelectEnd = 20;
|
|
812
|
+
var qeReserved1Start = qeMiscSelectEnd;
|
|
813
|
+
var qeReserved1End = 48;
|
|
814
|
+
var qeAttributesStart = qeReserved1End;
|
|
815
|
+
var qeAttributesEnd = 64;
|
|
816
|
+
var qeMrEnclaveStart = qeAttributesEnd;
|
|
817
|
+
var qeMrEnclaveEnd = 96;
|
|
818
|
+
var qeReserved2Start = qeMrEnclaveEnd;
|
|
819
|
+
var qeReserved2End = 128;
|
|
820
|
+
var qeMrSignerStart = qeReserved2End;
|
|
821
|
+
var qeMrSignerEnd = 160;
|
|
822
|
+
var qeReserved3Start = qeMrSignerEnd;
|
|
823
|
+
var qeReserved3End = 256;
|
|
824
|
+
var qeIsvProdIDStart = qeReserved3End;
|
|
825
|
+
var qeIsvProdIDEnd = 258;
|
|
826
|
+
var qeIsvSvnStart = qeIsvProdIDEnd;
|
|
827
|
+
var qeIsvSvnEnd = 260;
|
|
828
|
+
var qeReserved4Start = qeIsvSvnEnd;
|
|
829
|
+
var qeReserved4End = 320;
|
|
830
|
+
var qeReportDataStart = qeReserved4End;
|
|
831
|
+
var qeReportDataEnd = 384;
|
|
832
|
+
var reserved1Size = 28;
|
|
833
|
+
var mrEnclaveSize = 32;
|
|
834
|
+
var reserved2Size = 32;
|
|
835
|
+
var mrSignerSize = 32;
|
|
836
|
+
var reserved3Size = 96;
|
|
837
|
+
var reserved4Size = 60;
|
|
838
|
+
var ErrQuoteV4AuthDataNil = new Error("QuoteV4 authData is nil");
|
|
839
|
+
var ErrQuoteV4Nil = new Error("QuoteV4 is nil");
|
|
840
|
+
var ErrHeaderNil = new Error("header is nil");
|
|
841
|
+
var ErrAttestationKeyType = new Error(
|
|
842
|
+
"attestation key type not supported"
|
|
843
|
+
);
|
|
844
|
+
var ErrTeeType = new Error("TEE type is not TDX");
|
|
845
|
+
var ErrTDQuoteBodyNil = new Error("TD quote body is nil");
|
|
846
|
+
var ErrQeReportNil = new Error("QE Report is nil");
|
|
847
|
+
function checkQuoteV4(q) {
|
|
848
|
+
if (!q) {
|
|
849
|
+
throw ErrQuoteV4Nil;
|
|
850
|
+
}
|
|
851
|
+
checkHeaderSizes(q.header);
|
|
852
|
+
checkTdQuoteBodySizes(q.tdQuoteBody);
|
|
853
|
+
if (q.signedData.signature.length !== 64)
|
|
854
|
+
throw new Error("signature wrong size");
|
|
855
|
+
if (q.signedData.ecdsaAttestationKey.length !== 64)
|
|
856
|
+
throw new Error("attestationKey wrong size");
|
|
857
|
+
}
|
|
858
|
+
function checkHeaderSizes(h) {
|
|
859
|
+
if (h.qeSvn.length !== qeSvnSize) throw new Error("qeSvn wrong size");
|
|
860
|
+
if (h.pceSvn.length !== pceSvnSize) throw new Error("pceSvn wrong size");
|
|
861
|
+
if (h.qeVendorId.length !== QeVendorIDSize)
|
|
862
|
+
throw new Error("qeVendorId wrong size");
|
|
863
|
+
if (h.userData.length !== userDataSize) throw new Error("userData wrong size");
|
|
864
|
+
}
|
|
865
|
+
function checkTdQuoteBodySizes(b) {
|
|
866
|
+
if (b.mrSeam.length !== MrSeamSize) throw new Error("mrSeam wrong size");
|
|
867
|
+
if (b.tdAttributes.length !== TdAttributesSize)
|
|
868
|
+
throw new Error("tdAttributes wrong size");
|
|
869
|
+
if (b.xfam.length !== XfamSize) throw new Error("xfam wrong size");
|
|
870
|
+
if (b.mrTd.length !== MrTdSize) throw new Error("mrTd wrong size");
|
|
871
|
+
if (b.mrConfigId.length !== MrConfigIDSize)
|
|
872
|
+
throw new Error("mrConfigId wrong size");
|
|
873
|
+
if (b.mrOwner.length !== MrOwnerSize) throw new Error("mrOwner wrong size");
|
|
874
|
+
if (b.mrOwnerConfig.length !== MrOwnerConfigSize)
|
|
875
|
+
throw new Error("mrOwnerConfig wrong size");
|
|
876
|
+
if (b.reportData.length !== ReportDataSize)
|
|
877
|
+
throw new Error("reportData wrong size");
|
|
878
|
+
if (b.rtmrs.length !== rtmrsCount) throw new Error("rtmrs count != 4");
|
|
879
|
+
for (const r of b.rtmrs)
|
|
880
|
+
if (r.length !== RtmrSize) throw new Error("rtmr wrong size");
|
|
881
|
+
}
|
|
882
|
+
function HeaderToAbiBytes(header) {
|
|
883
|
+
if (!header) {
|
|
884
|
+
throw ErrHeaderNil;
|
|
885
|
+
}
|
|
886
|
+
checkHeader(header);
|
|
887
|
+
const data = new Uint8Array(headerSize);
|
|
888
|
+
const view = new DataView(data.buffer);
|
|
889
|
+
view.setUint16(headerVersionStart, header.version, true);
|
|
890
|
+
view.setUint16(headerAttestationKeyTypeStart, header.attestationKeyType, true);
|
|
891
|
+
view.setUint32(headerTeeTypeStart, header.teeType, true);
|
|
892
|
+
data.set(header.pceSvn, headerPceSvnStart);
|
|
893
|
+
data.set(header.qeSvn, headerQeSvnStart);
|
|
894
|
+
data.set(header.qeVendorId, headerQeVendorIDStart);
|
|
895
|
+
data.set(header.userData, headerUserDataStart);
|
|
896
|
+
return data;
|
|
897
|
+
}
|
|
898
|
+
function checkHeader(header) {
|
|
899
|
+
if (!header) {
|
|
900
|
+
throw ErrHeaderNil;
|
|
901
|
+
}
|
|
902
|
+
if (header.version >= 1 << 16) {
|
|
903
|
+
throw new Error(
|
|
904
|
+
`version field size must fit in 2 bytes, got ${header.version}`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
if (header.version !== QuoteVersion) {
|
|
908
|
+
throw new Error(`version ${header.version} not supported`);
|
|
909
|
+
}
|
|
910
|
+
if (header.attestationKeyType >= 1 << 16) {
|
|
911
|
+
throw new Error(
|
|
912
|
+
`attestation key type field size must fit in 2 bytes, got ${header.attestationKeyType}`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
if (header.attestationKeyType !== AttestationKeyType) {
|
|
916
|
+
throw ErrAttestationKeyType;
|
|
917
|
+
}
|
|
918
|
+
if (header.teeType !== TeeTDX) {
|
|
919
|
+
throw ErrTeeType;
|
|
920
|
+
}
|
|
921
|
+
if (header.qeSvn.length !== qeSvnSize) {
|
|
922
|
+
throw new Error(
|
|
923
|
+
`qeSvn size is ${header.qeSvn.length} bytes. Expected ${qeSvnSize} bytes`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
if (header.pceSvn.length !== pceSvnSize) {
|
|
927
|
+
throw new Error(
|
|
928
|
+
`pceSvn size is ${header.pceSvn.length} bytes. Expected ${pceSvnSize} bytes`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
if (header.qeVendorId.length !== QeVendorIDSize) {
|
|
932
|
+
throw new Error(
|
|
933
|
+
`qeVendorId size is ${header.qeVendorId.length} bytes. Expected ${QeVendorIDSize} bytes`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
if (header.userData.length !== userDataSize) {
|
|
937
|
+
throw new Error(
|
|
938
|
+
`user data size is ${header.userData.length} bytes. Expected ${userDataSize} bytes`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function TdQuoteBodyToAbiBytes(tdQuoteBody) {
|
|
943
|
+
if (!tdQuoteBody) {
|
|
944
|
+
throw ErrTDQuoteBodyNil;
|
|
945
|
+
}
|
|
946
|
+
checkTDQuoteBody(tdQuoteBody);
|
|
947
|
+
const data = new Uint8Array(tdQuoteBodySize);
|
|
948
|
+
data.set(tdQuoteBody.teeTcbSvn, tdTeeTcbSvnStart);
|
|
949
|
+
data.set(tdQuoteBody.mrSeam, tdMrSeamStart);
|
|
950
|
+
data.set(tdQuoteBody.mrSignerSeam, tdMrSignerSeamStart);
|
|
951
|
+
data.set(tdQuoteBody.seamAttributes, tdSeamAttributesStart);
|
|
952
|
+
data.set(tdQuoteBody.tdAttributes, tdAttributesStart);
|
|
953
|
+
data.set(tdQuoteBody.xfam, tdXfamStart);
|
|
954
|
+
data.set(tdQuoteBody.mrTd, tdMrTdStart);
|
|
955
|
+
data.set(tdQuoteBody.mrConfigId, tdMrConfigIDStart);
|
|
956
|
+
data.set(tdQuoteBody.mrOwner, tdMrOwnerStart);
|
|
957
|
+
data.set(tdQuoteBody.mrOwnerConfig, tdMrOwnerConfigStart);
|
|
958
|
+
let offset = tdRtmrsStart;
|
|
959
|
+
for (let i = 0; i < rtmrsCount; i++) {
|
|
960
|
+
const rtmr = tdQuoteBody.rtmrs[i];
|
|
961
|
+
if (!rtmr || rtmr.length !== RtmrSize) {
|
|
962
|
+
throw new Error(`RTMR[${i}] is missing or invalid`);
|
|
963
|
+
}
|
|
964
|
+
data.set(rtmr, offset);
|
|
965
|
+
offset += RtmrSize;
|
|
966
|
+
}
|
|
967
|
+
data.set(tdQuoteBody.reportData, tdReportDataStart);
|
|
968
|
+
return data;
|
|
969
|
+
}
|
|
970
|
+
function enclaveReportToAbiBytes(report) {
|
|
971
|
+
if (!report) {
|
|
972
|
+
throw ErrQeReportNil;
|
|
973
|
+
}
|
|
974
|
+
checkQeReport(report);
|
|
975
|
+
const data = new Uint8Array(qeReportSize);
|
|
976
|
+
data.set(report.cpuSvn, qeCPUSvnStart);
|
|
977
|
+
new DataView(data.buffer).setUint32(
|
|
978
|
+
qeMiscSelectStart,
|
|
979
|
+
report.miscSelect,
|
|
980
|
+
true
|
|
981
|
+
);
|
|
982
|
+
data.set(report.reserved1, qeReserved1Start);
|
|
983
|
+
data.set(report.attributes, qeAttributesStart);
|
|
984
|
+
data.set(report.mrEnclave, qeMrEnclaveStart);
|
|
985
|
+
data.set(report.reserved2, qeReserved2Start);
|
|
986
|
+
data.set(report.mrSigner, qeMrSignerStart);
|
|
987
|
+
data.set(report.reserved3, qeReserved3Start);
|
|
988
|
+
new DataView(data.buffer).setUint16(qeIsvProdIDStart, report.isvProdId, true);
|
|
989
|
+
new DataView(data.buffer).setUint16(qeIsvSvnStart, report.isvSvn, true);
|
|
990
|
+
data.set(report.reserved4, qeReserved4Start);
|
|
991
|
+
data.set(report.reportData, qeReportDataStart);
|
|
992
|
+
return data;
|
|
993
|
+
}
|
|
994
|
+
function checkQeReport(report) {
|
|
995
|
+
if (!report) {
|
|
996
|
+
throw ErrQeReportNil;
|
|
997
|
+
}
|
|
998
|
+
if (report.cpuSvn.length !== cpuSvnSize) {
|
|
999
|
+
throw new Error(
|
|
1000
|
+
`cpuSvn size is ${report.cpuSvn.length} bytes. Expected ${cpuSvnSize} bytes`
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
if (report.reserved1.length !== reserved1Size) {
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
`reserved1 size is ${report.reserved1.length} bytes. Expected ${reserved1Size} bytes`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
if (report.attributes.length !== attributesSize) {
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`attributes size is ${report.attributes.length} bytes. Expected ${attributesSize} bytes`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
if (report.mrEnclave.length !== mrEnclaveSize) {
|
|
1014
|
+
throw new Error(
|
|
1015
|
+
`mrEnclave size is ${report.mrEnclave.length} bytes. Expected ${mrEnclaveSize} bytes`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
if (report.reserved2.length !== reserved2Size) {
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
`reserved2 size is ${report.reserved2.length} bytes. Expected ${reserved2Size} bytes`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
if (report.mrSigner.length !== mrSignerSize) {
|
|
1024
|
+
throw new Error(
|
|
1025
|
+
`mrSigner size is ${report.mrSigner.length} bytes. Expected ${mrSignerSize} bytes`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
if (report.reserved3.length !== reserved3Size) {
|
|
1029
|
+
throw new Error(
|
|
1030
|
+
`reserved3 size is ${report.reserved3.length} bytes. Expected ${reserved3Size} bytes`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
if (report.isvProdId >= 1 << 16) {
|
|
1034
|
+
throw new Error(`isvProdId must fit in 2 bytes, got ${report.isvProdId}`);
|
|
1035
|
+
}
|
|
1036
|
+
if (report.isvSvn >= 1 << 16) {
|
|
1037
|
+
throw new Error(`isvSvn must fit in 2 bytes, got ${report.isvSvn}`);
|
|
1038
|
+
}
|
|
1039
|
+
if (report.reserved4.length !== reserved4Size) {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
`reserved4 size is ${report.reserved4.length} bytes. Expected ${reserved4Size} bytes`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
if (report.reportData.length !== ReportDataSize) {
|
|
1045
|
+
throw new Error(
|
|
1046
|
+
`reportData size is ${report.reportData.length} bytes. Expected ${ReportDataSize} bytes`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function checkTDQuoteBody(tdQuoteBody) {
|
|
1051
|
+
if (!tdQuoteBody) {
|
|
1052
|
+
throw ErrTDQuoteBodyNil;
|
|
1053
|
+
}
|
|
1054
|
+
if (tdQuoteBody.teeTcbSvn.length !== TeeTcbSvnSize) {
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
`teeTcbSvn size is ${tdQuoteBody.teeTcbSvn.length} bytes. Expected ${TeeTcbSvnSize} bytes`
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
if (tdQuoteBody.mrSeam.length !== MrSeamSize) {
|
|
1060
|
+
throw new Error(
|
|
1061
|
+
`mrSeam size is ${tdQuoteBody.mrSeam.length} bytes. Expected ${MrSeamSize} bytes`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
if (tdQuoteBody.mrSignerSeam.length !== mrSignerSeamSize) {
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
`mrSignerSeam size is ${tdQuoteBody.mrSignerSeam.length} bytes. Expected ${mrSignerSeamSize} bytes`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
if (tdQuoteBody.seamAttributes.length !== seamAttributesSize) {
|
|
1070
|
+
throw new Error(
|
|
1071
|
+
`seamAttributes size is ${tdQuoteBody.seamAttributes.length} bytes. Expected ${seamAttributesSize} bytes`
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
if (tdQuoteBody.tdAttributes.length !== TdAttributesSize) {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`tdAttributes size is ${tdQuoteBody.tdAttributes.length} bytes. Expected ${TdAttributesSize} bytes`
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
if (tdQuoteBody.xfam.length !== XfamSize) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`xfam size is ${tdQuoteBody.xfam.length} bytes. Expected ${XfamSize} bytes`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
if (tdQuoteBody.mrTd.length !== MrTdSize) {
|
|
1085
|
+
throw new Error(
|
|
1086
|
+
`mrTd size is ${tdQuoteBody.mrTd.length} bytes. Expected ${MrTdSize} bytes`
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
if (tdQuoteBody.mrConfigId.length !== MrConfigIDSize) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
`mrConfigId size is ${tdQuoteBody.mrConfigId.length} bytes. Expected ${MrConfigIDSize} bytes`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
if (tdQuoteBody.mrOwner.length !== MrOwnerSize) {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`mrOwner size is ${tdQuoteBody.mrOwner.length} bytes. Expected ${MrOwnerSize} bytes`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
if (tdQuoteBody.mrOwnerConfig.length !== MrOwnerConfigSize) {
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`mrOwnerConfig size is ${tdQuoteBody.mrOwnerConfig.length} bytes. Expected ${MrOwnerConfigSize} bytes`
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
if (tdQuoteBody.rtmrs.length !== rtmrsCount) {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
`rtmrs count is ${tdQuoteBody.rtmrs.length}. Expected ${rtmrsCount}`
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
for (let i = 0; i < rtmrsCount; i++) {
|
|
1110
|
+
if (tdQuoteBody.rtmrs[i].length !== RtmrSize) {
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
`rtmr${i} size is ${tdQuoteBody.rtmrs[i].length} bytes. Expected ${RtmrSize} bytes`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function enclaveReportToProto(b) {
|
|
1118
|
+
const data = b.slice();
|
|
1119
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1120
|
+
const enclaveReport = {
|
|
1121
|
+
cpuSvn: data.slice(qeCPUSvnStart, qeCPUSvnEnd),
|
|
1122
|
+
miscSelect: view.getUint32(qeMiscSelectStart, true),
|
|
1123
|
+
reserved1: data.slice(qeReserved1Start, qeReserved1End),
|
|
1124
|
+
attributes: data.slice(qeAttributesStart, qeAttributesEnd),
|
|
1125
|
+
mrEnclave: data.slice(qeMrEnclaveStart, qeMrEnclaveEnd),
|
|
1126
|
+
reserved2: data.slice(qeReserved2Start, qeReserved2End),
|
|
1127
|
+
mrSigner: data.slice(qeMrSignerStart, qeMrSignerEnd),
|
|
1128
|
+
reserved3: data.slice(qeReserved3Start, qeReserved3End),
|
|
1129
|
+
isvProdId: view.getUint16(qeIsvProdIDStart, true),
|
|
1130
|
+
isvSvn: view.getUint16(qeIsvSvnStart, true),
|
|
1131
|
+
reserved4: data.slice(qeReserved4Start, qeReserved4End),
|
|
1132
|
+
reportData: data.slice(qeReportDataStart, qeReportDataEnd)
|
|
1133
|
+
};
|
|
1134
|
+
checkQeReport(enclaveReport);
|
|
1135
|
+
return enclaveReport;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/audit/quote/base64-utils.ts
|
|
1139
|
+
import * as asn1js from "asn1js";
|
|
1140
|
+
import * as x509 from "@peculiar/x509";
|
|
1141
|
+
function encodeBase64(data) {
|
|
1142
|
+
let binary = "";
|
|
1143
|
+
for (let i = 0; i < data.byteLength; i++) {
|
|
1144
|
+
binary += String.fromCharCode(data[i]);
|
|
1145
|
+
}
|
|
1146
|
+
return globalThis.btoa(binary);
|
|
1147
|
+
}
|
|
1148
|
+
function leU16(buf, off = 0) {
|
|
1149
|
+
return buf[off] | buf[off + 1] << 8;
|
|
1150
|
+
}
|
|
1151
|
+
function leU32(buf, off = 0) {
|
|
1152
|
+
return (buf[off] | buf[off + 1] << 8 | buf[off + 2] << 16 | buf[off + 3] << 24) >>> 0;
|
|
1153
|
+
}
|
|
1154
|
+
function toBigIntLE(u) {
|
|
1155
|
+
let r = 0n;
|
|
1156
|
+
for (let i = u.length - 1; i >= 0; --i) r = r << 8n | BigInt(u[i]);
|
|
1157
|
+
return r;
|
|
1158
|
+
}
|
|
1159
|
+
function decodeU16(node, name) {
|
|
1160
|
+
if (!(node instanceof asn1js.Integer))
|
|
1161
|
+
throw new Error(`${name} must be INTEGER`);
|
|
1162
|
+
const v = node.valueBlock.valueDec;
|
|
1163
|
+
if (v < 0 || v > 65535) throw new Error(`${name} out of range`);
|
|
1164
|
+
return v;
|
|
1165
|
+
}
|
|
1166
|
+
function decodeU8(node, name) {
|
|
1167
|
+
if (!(node instanceof asn1js.Integer))
|
|
1168
|
+
throw new Error(`${name} must be INTEGER`);
|
|
1169
|
+
const v = node.valueBlock.valueDec;
|
|
1170
|
+
if (v < 0 || v > 255) throw new Error(`${name} out of range`);
|
|
1171
|
+
return v;
|
|
1172
|
+
}
|
|
1173
|
+
async function verifyEcdsaSignature(pubKey, msg, sig) {
|
|
1174
|
+
const jwk = {
|
|
1175
|
+
kty: "EC",
|
|
1176
|
+
crv: "P-256",
|
|
1177
|
+
x: b64url(pubKey.slice(0, 32)),
|
|
1178
|
+
y: b64url(pubKey.slice(32)),
|
|
1179
|
+
ext: true
|
|
1180
|
+
};
|
|
1181
|
+
const key = await x509.cryptoProvider.get().subtle.importKey(
|
|
1182
|
+
"jwk",
|
|
1183
|
+
jwk,
|
|
1184
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1185
|
+
false,
|
|
1186
|
+
["verify"]
|
|
1187
|
+
);
|
|
1188
|
+
return x509.cryptoProvider.get().subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, sig, msg);
|
|
1189
|
+
}
|
|
1190
|
+
function b64url(data) {
|
|
1191
|
+
return encodeBase64(data).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/audit/quote/validate.ts
|
|
1195
|
+
var xfamFixed1 = 0x00000003n;
|
|
1196
|
+
var xfamFixed0 = 0x0006dbe7n;
|
|
1197
|
+
var tdAttributesFixed1 = 0x0n;
|
|
1198
|
+
var tdxAttributesSeptVeDisSupport = 1n << 28n;
|
|
1199
|
+
var tdxAttributesPksSupport = 1n << 30n;
|
|
1200
|
+
var tdxAttributesPerfmonSupport = 1n << 63n;
|
|
1201
|
+
var tdAttributesFixed0 = 0x1n | tdxAttributesSeptVeDisSupport | tdxAttributesPksSupport | tdxAttributesPerfmonSupport;
|
|
1202
|
+
var must = {
|
|
1203
|
+
equalBytes(a, b, field) {
|
|
1204
|
+
if (a.length !== b.length) throw new Error(`${field} length mismatch`);
|
|
1205
|
+
for (let i = 0; i < a.length; ++i)
|
|
1206
|
+
if (a[i] !== b[i]) {
|
|
1207
|
+
throw new Error(`${field} mismatch - byte ${i}`);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
function validateTdxQuoteV4(q, opts) {
|
|
1212
|
+
checkQuoteV4(q);
|
|
1213
|
+
exactByteMatch(q, opts);
|
|
1214
|
+
minVersionCheck(q, opts);
|
|
1215
|
+
validateXfam(q.tdQuoteBody.xfam, xfamFixed1, xfamFixed0);
|
|
1216
|
+
validateTdAttributes(
|
|
1217
|
+
q.tdQuoteBody.tdAttributes,
|
|
1218
|
+
tdAttributesFixed1,
|
|
1219
|
+
tdAttributesFixed0
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
function exactByteMatch(q, opts) {
|
|
1223
|
+
const b = q.tdQuoteBody;
|
|
1224
|
+
const h = q.header;
|
|
1225
|
+
const bo = opts.tdQuoteBodyOptions;
|
|
1226
|
+
const ho = opts.headerOptions;
|
|
1227
|
+
const cmp = (name, given, expected, size) => {
|
|
1228
|
+
if (!expected) return;
|
|
1229
|
+
if (size && expected.length !== size)
|
|
1230
|
+
throw new Error(`${name} option must be ${size} bytes`);
|
|
1231
|
+
must.equalBytes(given, expected, name);
|
|
1232
|
+
};
|
|
1233
|
+
cmp("MR_SEAM", b.mrSeam, bo.mrSeam, MrSeamSize);
|
|
1234
|
+
cmp("TD_ATTRIBUTES", b.tdAttributes, bo.tdAttributes, TdAttributesSize);
|
|
1235
|
+
cmp("XFAM", b.xfam, bo.xfam, XfamSize);
|
|
1236
|
+
cmp("MR_TD", b.mrTd, bo.mrTd, MrTdSize);
|
|
1237
|
+
cmp("MR_CONFIG_ID", b.mrConfigId, bo.mrConfigID, MrConfigIDSize);
|
|
1238
|
+
cmp("MR_OWNER", b.mrOwner, bo.mrOwner, MrOwnerSize);
|
|
1239
|
+
cmp(
|
|
1240
|
+
"MR_OWNER_CONFIG",
|
|
1241
|
+
b.mrOwnerConfig,
|
|
1242
|
+
bo.mrOwnerConfig,
|
|
1243
|
+
MrOwnerConfigSize
|
|
1244
|
+
);
|
|
1245
|
+
if (bo.rtmrs) {
|
|
1246
|
+
if (bo.rtmrs.length !== rtmrsCount)
|
|
1247
|
+
throw new Error("RTMR option len != 4");
|
|
1248
|
+
bo.rtmrs.forEach(
|
|
1249
|
+
(exp, i) => cmp(`RTMR[${i}]`, b.rtmrs[i], exp, RtmrSize)
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
cmp("REPORT_DATA", b.reportData, bo.reportData, ReportDataSize);
|
|
1253
|
+
cmp("QE_VENDOR_ID", h.qeVendorId, ho.qeVendorID, QeVendorIDSize);
|
|
1254
|
+
}
|
|
1255
|
+
function isSvnHigherOrEqual(quoteSvn, optionSvn) {
|
|
1256
|
+
if (!optionSvn) {
|
|
1257
|
+
return true;
|
|
1258
|
+
}
|
|
1259
|
+
for (let i = 0; i < quoteSvn.length; i++) {
|
|
1260
|
+
if (quoteSvn[i] < optionSvn[i]) {
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return true;
|
|
1265
|
+
}
|
|
1266
|
+
function minVersionCheck(quote, opts) {
|
|
1267
|
+
if (!isSvnHigherOrEqual(
|
|
1268
|
+
quote.tdQuoteBody.teeTcbSvn,
|
|
1269
|
+
opts.tdQuoteBodyOptions?.minimumTeeTcbSvn
|
|
1270
|
+
)) {
|
|
1271
|
+
throw new Error(
|
|
1272
|
+
`TEE TCB security-version number ${toBigIntLE(
|
|
1273
|
+
quote.tdQuoteBody.teeTcbSvn
|
|
1274
|
+
)} is less than the required minimum ${toBigIntLE(
|
|
1275
|
+
opts.tdQuoteBodyOptions.minimumTeeTcbSvn
|
|
1276
|
+
)}`
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
const qeSvn = leU16(quote.header.qeSvn);
|
|
1280
|
+
const pceSvn = leU16(quote.header.pceSvn);
|
|
1281
|
+
if (qeSvn < opts.headerOptions.minimumQeSvn) {
|
|
1282
|
+
throw new Error(
|
|
1283
|
+
`QE security-version number ${qeSvn} is less than the required minimum ${opts.headerOptions.minimumQeSvn}`
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
if (pceSvn < opts.headerOptions.minimumPceSvn) {
|
|
1287
|
+
throw new Error(
|
|
1288
|
+
`PCE security-version number ${pceSvn} is less than the required minimum ${opts.headerOptions.minimumPceSvn}`
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
function validateXfam(value, fixed1, fixed0) {
|
|
1294
|
+
if (value.length === 0) return;
|
|
1295
|
+
if (value.length !== XfamSize) {
|
|
1296
|
+
throw new Error("xfam size is invalid");
|
|
1297
|
+
}
|
|
1298
|
+
const xfam = new DataView(
|
|
1299
|
+
value.buffer,
|
|
1300
|
+
value.byteOffset,
|
|
1301
|
+
value.byteLength
|
|
1302
|
+
).getBigUint64(0, true);
|
|
1303
|
+
if ((xfam & fixed1) !== fixed1) {
|
|
1304
|
+
throw new Error(
|
|
1305
|
+
`unauthorized xfam 0x${xfam.toString(16)} as xfamFixed1 0x${fixed1.toString(16)} bits are unset`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
if ((xfam & ~fixed0) !== BigInt(0)) {
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
`unauthorized xfam 0x${xfam.toString(16)} as xfamFixed0 0x${fixed0.toString(16)} bits are set`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
function validateTdAttributes(value, fixed1, fixed0) {
|
|
1315
|
+
if (value.length === 0) return;
|
|
1316
|
+
if (value.length !== TdAttributesSize) {
|
|
1317
|
+
throw new Error("tdAttributes size is invalid");
|
|
1318
|
+
}
|
|
1319
|
+
const tdAttributes = new DataView(
|
|
1320
|
+
value.buffer,
|
|
1321
|
+
value.byteOffset,
|
|
1322
|
+
value.byteLength
|
|
1323
|
+
).getBigUint64(0, true);
|
|
1324
|
+
if ((tdAttributes & fixed1) !== fixed1) {
|
|
1325
|
+
throw new Error(
|
|
1326
|
+
`unauthorized tdAttributes 0x${tdAttributes.toString(16)} as tdAttributesFixed1 0x${fixed1.toString(16)} bits are unset`
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
if ((tdAttributes & ~fixed0) !== 0n) {
|
|
1330
|
+
throw new Error(
|
|
1331
|
+
`unauthorized tdAttributes 0x${tdAttributes.toString(16)} as tdAttributesFixed0 0x${fixed0.toString(16)} bits are set`
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/audit/quote/verify.ts
|
|
1337
|
+
import * as x5094 from "@peculiar/x509";
|
|
1338
|
+
|
|
1339
|
+
// src/audit/quote/pck.ts
|
|
1340
|
+
import * as asn1js3 from "asn1js";
|
|
1341
|
+
|
|
1342
|
+
// src/audit/quote/tcb.ts
|
|
1343
|
+
import * as asn1js2 from "asn1js";
|
|
1344
|
+
import * as x5092 from "@peculiar/x509";
|
|
1345
|
+
var tcbComponentSize = 16;
|
|
1346
|
+
var tcbSigningPhrase = "Intel SGX TCB Signing";
|
|
1347
|
+
var tcbInfoID = "TDX";
|
|
1348
|
+
var qeIdentityID = "TD_QE";
|
|
1349
|
+
var qeIdentityVersion = 2;
|
|
1350
|
+
var tcbInfoTdxModuleIDPrefix = "TDX_";
|
|
1351
|
+
var rootCertPhrase = "Intel SGX Root CA";
|
|
1352
|
+
var ErrRootCertNil = new Error("root certificate is empty");
|
|
1353
|
+
var ErrCollateralNil = new Error(
|
|
1354
|
+
"collateral received is an empty structure"
|
|
1355
|
+
);
|
|
1356
|
+
var ErrTcbInfoTcbLevelsMissing = new Error(
|
|
1357
|
+
"tcbInfo contains empty TcbLevels"
|
|
1358
|
+
);
|
|
1359
|
+
var ErrQeIdentityTcbLevelsMissing = new Error(
|
|
1360
|
+
"QeIdentity contains empty TcbLevels"
|
|
1361
|
+
);
|
|
1362
|
+
var ErrCrlEmpty = new Error("CRL is empty");
|
|
1363
|
+
var ErrTrustedCertEmpty = new Error("trusted certificate is empty");
|
|
1364
|
+
var ErrTcbStatus = new Error(
|
|
1365
|
+
"unable to find latest status of TCB, it is now OutOfDate"
|
|
1366
|
+
);
|
|
1367
|
+
var tcbInfoVersion = 3;
|
|
1368
|
+
var TcbComponentStatusUpToDate = "UpToDate";
|
|
1369
|
+
var tcbExtensionSize = 18;
|
|
1370
|
+
function sgxTcbComponentOid(component) {
|
|
1371
|
+
return [...sgxTcbComponentOidPrefix, component].join(".");
|
|
1372
|
+
}
|
|
1373
|
+
function extractTcbExtension(components, atcb) {
|
|
1374
|
+
const tcbComponents = new Uint8Array(tcbComponentSize);
|
|
1375
|
+
for (const ext of components) {
|
|
1376
|
+
if (!(ext instanceof asn1js2.Sequence))
|
|
1377
|
+
throw new Error("TCB component is not a SEQUENCE");
|
|
1378
|
+
if (ext.valueBlock.value.length !== 2)
|
|
1379
|
+
throw new Error("AttributeTypeAndValue must have 2 elements");
|
|
1380
|
+
const [oidBlock, rawVal] = ext.valueBlock.value;
|
|
1381
|
+
if (!(oidBlock instanceof asn1js2.ObjectIdentifier))
|
|
1382
|
+
throw new Error("Missing OID in TCB component");
|
|
1383
|
+
const value = unwrapSingleSet(rawVal);
|
|
1384
|
+
const oid = oidBlock.valueBlock.toString();
|
|
1385
|
+
for (let i = 0; i < tcbComponentSize; i++) {
|
|
1386
|
+
if (oid === sgxTcbComponentOid(i + 1)) {
|
|
1387
|
+
tcbComponents[i] = decodeU8(value, `sgxTcbComponent${i + 1}`);
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
if (oid === OidPCESvn) {
|
|
1392
|
+
atcb.pceSvn = decodeU16(value, "PCESvn");
|
|
1393
|
+
}
|
|
1394
|
+
if (oid === OidCPUSvn) {
|
|
1395
|
+
if (!(value instanceof asn1js2.OctetString))
|
|
1396
|
+
throw new Error("CPUSvn must be an OCTET STRING");
|
|
1397
|
+
const buf = new Uint8Array(value.valueBlock.valueHex);
|
|
1398
|
+
if (buf.length !== cpuSvnSize)
|
|
1399
|
+
throw new Error(
|
|
1400
|
+
`CPUSvn length is ${buf.length}, expected ${cpuSvnSize}`
|
|
1401
|
+
);
|
|
1402
|
+
atcb.cpuSvn = buf;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
atcb.cpuSvnComponents = tcbComponents;
|
|
1406
|
+
}
|
|
1407
|
+
async function verifyTCBinfo(options) {
|
|
1408
|
+
const collateral = options.collateral;
|
|
1409
|
+
if (!collateral) throw ErrCollateralNil;
|
|
1410
|
+
const tcbInfo = collateral.tdxTcbInfo.tcbInfo;
|
|
1411
|
+
const signature = collateral.tdxTcbInfo.signature;
|
|
1412
|
+
if (tcbInfo.id !== tcbInfoID) {
|
|
1413
|
+
throw new Error(
|
|
1414
|
+
`tcbInfo ID "${tcbInfo.id}" does not match with expected ID "${tcbInfoID}"`
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
if (tcbInfo.version !== tcbInfoVersion) {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
`tcbInfo version ${tcbInfo.version} does not match with expected version ${tcbInfoVersion}`
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
if (!Array.isArray(tcbInfo.tcbLevels) || tcbInfo.tcbLevels.length === 0) {
|
|
1423
|
+
throw ErrTcbInfoTcbLevelsMissing;
|
|
1424
|
+
}
|
|
1425
|
+
await verifyResponse(
|
|
1426
|
+
tcbSigningPhrase,
|
|
1427
|
+
collateral.tcbInfoIssuerRootCertificate,
|
|
1428
|
+
collateral.tcbInfoIssuerIntermediateCertificate,
|
|
1429
|
+
collateral.tcbInfoBody,
|
|
1430
|
+
signature,
|
|
1431
|
+
collateral.rootCaCrl,
|
|
1432
|
+
options
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
async function verifyQeIdentity(options) {
|
|
1436
|
+
const collateral = options.collateral;
|
|
1437
|
+
if (!collateral) throw ErrCollateralNil;
|
|
1438
|
+
const qeIdentity = collateral.qeIdentity.enclaveIdentity;
|
|
1439
|
+
const signature = collateral.qeIdentity.signature;
|
|
1440
|
+
if (qeIdentity.id !== qeIdentityID) {
|
|
1441
|
+
throw new Error(
|
|
1442
|
+
`QeIdentity ID "${qeIdentity.id}" does not match with expected ID "${qeIdentityID}"`
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
if (qeIdentity.version !== qeIdentityVersion) {
|
|
1446
|
+
throw new Error(
|
|
1447
|
+
`QeIdentity version ${qeIdentity.version} does not match with expected version ${qeIdentityVersion}`
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
if (!Array.isArray(qeIdentity.tcbLevels) || qeIdentity.tcbLevels.length === 0) {
|
|
1451
|
+
throw ErrQeIdentityTcbLevelsMissing;
|
|
1452
|
+
}
|
|
1453
|
+
await verifyResponse(
|
|
1454
|
+
tcbSigningPhrase,
|
|
1455
|
+
collateral.qeIdentityIssuerRootCertificate,
|
|
1456
|
+
collateral.qeIdentityIssuerIntermediateCertificate,
|
|
1457
|
+
collateral.enclaveIdentityBody,
|
|
1458
|
+
signature,
|
|
1459
|
+
collateral.rootCaCrl,
|
|
1460
|
+
options
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
async function verifyResponse(signingPhrase, rootCertificate, signingCertificate, rawBody, rawSignature, crl, options) {
|
|
1464
|
+
await validateCertificate(rootCertificate, rootCertificate, "rootCertPhrase");
|
|
1465
|
+
await validateCertificate(signingCertificate, rootCertificate, signingPhrase);
|
|
1466
|
+
const signature = Uint8Array.from(Buffer.from(rawSignature, "hex"));
|
|
1467
|
+
const publicKey = await x5092.cryptoProvider.get().subtle.importKey(
|
|
1468
|
+
"spki",
|
|
1469
|
+
signingCertificate.publicKey.rawData,
|
|
1470
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1471
|
+
false,
|
|
1472
|
+
["verify"]
|
|
1473
|
+
);
|
|
1474
|
+
const isValid = await x5092.cryptoProvider.get().subtle.verify(
|
|
1475
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
1476
|
+
publicKey,
|
|
1477
|
+
signature,
|
|
1478
|
+
rawBody
|
|
1479
|
+
);
|
|
1480
|
+
if (!isValid) {
|
|
1481
|
+
throw new Error(
|
|
1482
|
+
"Could not verify response body signature using the signing certificate"
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
if (options.checkRevocations) {
|
|
1486
|
+
if (options.getCollateral) {
|
|
1487
|
+
if (!crl) {
|
|
1488
|
+
throw new Error("Missing CRL for revocation check");
|
|
1489
|
+
}
|
|
1490
|
+
await validateCRL(crl, rootCertificate);
|
|
1491
|
+
for (const entry of crl.entries) {
|
|
1492
|
+
if (entry.serialNumber === signingCertificate.serialNumber) {
|
|
1493
|
+
throw new Error(
|
|
1494
|
+
`Signing certificate was revoked at ${entry.revocationDate}`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
} else {
|
|
1499
|
+
throw new Error("Revocation check is enabled, but GetCollateral is false");
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
async function validateCertificate(cert, parent, expectedPhrase) {
|
|
1504
|
+
if (!cert) throw new Error("certificate is nil");
|
|
1505
|
+
if (!parent) throw new Error("parent certificate is nil");
|
|
1506
|
+
const cnList = cert.subjectName.getField("CN");
|
|
1507
|
+
const subjectCN = cnList.length > 0 ? cnList[0] : null;
|
|
1508
|
+
if (subjectCN !== expectedPhrase) {
|
|
1509
|
+
throw new Error(
|
|
1510
|
+
`"${subjectCN}" is not expected in certificate's subject name. Expected "${expectedPhrase}"`
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
const issuerName = cert.issuerName.toString();
|
|
1514
|
+
const parentSubject = parent.subjectName.toString();
|
|
1515
|
+
if (issuerName !== parentSubject) {
|
|
1516
|
+
throw new Error(
|
|
1517
|
+
`Certificate issuer name ("${issuerName}") does not match parent certificate subject name ("${parentSubject}")`
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
const verified = await cert.verify({ publicKey: parent });
|
|
1521
|
+
if (!verified) {
|
|
1522
|
+
throw new Error(
|
|
1523
|
+
"Certificate signature verification using parent certificate failed"
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async function validateCRL(crl, trustedCertificate) {
|
|
1528
|
+
if (!crl) {
|
|
1529
|
+
throw ErrCrlEmpty;
|
|
1530
|
+
}
|
|
1531
|
+
if (!trustedCertificate) {
|
|
1532
|
+
throw ErrTrustedCertEmpty;
|
|
1533
|
+
}
|
|
1534
|
+
if (crl.issuer.toString() !== trustedCertificate.subject.toString()) {
|
|
1535
|
+
throw new Error(
|
|
1536
|
+
`CRL issuer's name "${crl.issuer.toString()}" does not match expected name "${trustedCertificate.subject.toString()}"`
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
try {
|
|
1540
|
+
await crl.verify(trustedCertificate);
|
|
1541
|
+
} catch (err) {
|
|
1542
|
+
throw new Error(
|
|
1543
|
+
`CRL signature verification failed using trusted certificate: ${err}`
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
async function verifyTdQuoteBody(tdQuoteBody, options) {
|
|
1548
|
+
const { tcbInfo, pckCertExtensions } = options;
|
|
1549
|
+
if (pckCertExtensions.fmspc !== tcbInfo.fmspc) {
|
|
1550
|
+
throw new Error(
|
|
1551
|
+
`FMSPC mismatch: PCK Certificate(${pckCertExtensions.fmspc}) vs Intel PCS(${tcbInfo.fmspc})`
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
if (pckCertExtensions.pceid !== tcbInfo.pceID) {
|
|
1555
|
+
throw new Error(
|
|
1556
|
+
`PCEID mismatch: PCK Certificate(${pckCertExtensions.pceid}) vs Intel PCS(${tcbInfo.pceID})`
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
const reportedMrSigner = tdQuoteBody.mrSignerSeam;
|
|
1560
|
+
const expectedMrSigner = tcbInfo.tdxModule.mrsigner.bytes;
|
|
1561
|
+
if (!equalBytes(reportedMrSigner, expectedMrSigner)) {
|
|
1562
|
+
throw new Error(
|
|
1563
|
+
`MRSIGNERSEAM mismatch: quote(${toHex(reportedMrSigner)}) vs PCS(${toHex(expectedMrSigner)})`
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
const mask = tcbInfo.tdxModule.attributesMask.bytes;
|
|
1567
|
+
const seamAttrs = tdQuoteBody.seamAttributes;
|
|
1568
|
+
if (mask.length !== seamAttrs.length) {
|
|
1569
|
+
throw new Error(
|
|
1570
|
+
`SeamAttributes length mismatch: quote(${seamAttrs.length}) vs PCS mask(${mask.length})`
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
const maskedAttrs = applyMask(mask, seamAttrs);
|
|
1574
|
+
const expectedAttrs = tcbInfo.tdxModule.attributes.bytes;
|
|
1575
|
+
if (!equalBytes(maskedAttrs, expectedAttrs)) {
|
|
1576
|
+
throw new Error(
|
|
1577
|
+
`AttributesMask mismatch: quote(${toHex(maskedAttrs)}) vs PCS(${toHex(expectedAttrs)})`
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
checkTcbInfoTcbStatus(tcbInfo, tdQuoteBody, pckCertExtensions);
|
|
1581
|
+
}
|
|
1582
|
+
async function checkTcbInfoTcbStatus(tcbInfo, tdQuoteBody, pckCertExtensions) {
|
|
1583
|
+
const tcbLevels = tcbInfo.tcbLevels;
|
|
1584
|
+
const matchingTcbLevel = getMatchingTcbLevel(
|
|
1585
|
+
tcbLevels,
|
|
1586
|
+
tdQuoteBody,
|
|
1587
|
+
pckCertExtensions.tcb.pceSvn,
|
|
1588
|
+
pckCertExtensions.tcb.cpuSvnComponents
|
|
1589
|
+
);
|
|
1590
|
+
if (!matchingTcbLevel) {
|
|
1591
|
+
throw new Error("Failed to find matching TCB Level");
|
|
1592
|
+
}
|
|
1593
|
+
if (tdQuoteBody.teeTcbSvn[1] > 0) {
|
|
1594
|
+
const matchingTdxModuleTcbLevel = await getMatchingTdxModuleTcbLevel(
|
|
1595
|
+
tcbInfo.tdxModuleIdentities,
|
|
1596
|
+
tdQuoteBody.teeTcbSvn
|
|
1597
|
+
);
|
|
1598
|
+
if (!matchingTdxModuleTcbLevel) {
|
|
1599
|
+
throw new Error("Failed to find matching TDX Module TCB Level");
|
|
1600
|
+
}
|
|
1601
|
+
if (matchingTdxModuleTcbLevel.tcbStatus !== TcbComponentStatusUpToDate) {
|
|
1602
|
+
throw new Error(
|
|
1603
|
+
`TDX Module TCB Status is not "${TcbComponentStatusUpToDate}", found "${matchingTdxModuleTcbLevel.tcbStatus}"`
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (matchingTcbLevel.tcbStatus !== TcbComponentStatusUpToDate) {
|
|
1609
|
+
throw new Error(
|
|
1610
|
+
`TCB Status is not "${TcbComponentStatusUpToDate}", found "${matchingTcbLevel.tcbStatus}"`
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
async function getMatchingTdxModuleTcbLevel(tcbInfoTdxModuleIdentities, teeTcbSvn) {
|
|
1615
|
+
const tdxModuleVersion = teeTcbSvn.slice(1, 2);
|
|
1616
|
+
const tdxModuleIdentityID = tcbInfoTdxModuleIDPrefix + toHex(tdxModuleVersion);
|
|
1617
|
+
const tdxModuleIsvSvn = teeTcbSvn[0];
|
|
1618
|
+
for (const tdxModuleIdentity of tcbInfoTdxModuleIdentities) {
|
|
1619
|
+
if (tdxModuleIdentityID === tdxModuleIdentity.id) {
|
|
1620
|
+
for (const tcbLevel of tdxModuleIdentity.tcbLevels) {
|
|
1621
|
+
if (tdxModuleIsvSvn >= tcbLevel.tcb.isvsvn) {
|
|
1622
|
+
return tcbLevel;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
throw new Error(
|
|
1626
|
+
`could not find a TDX Module Identity TCB Level matching the TDX Module's ISVSVN (${tdxModuleIsvSvn})`
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
throw new Error(
|
|
1631
|
+
`could not find a TDX Module Identity (${tdxModuleIdentityID}) matching the given TEE TDX version (${toHex(tdxModuleVersion)})`
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
function checkQeTcbStatus(tcbLevels, isvSvn) {
|
|
1635
|
+
for (const level of tcbLevels) {
|
|
1636
|
+
if (level.tcb.isvsvn <= isvSvn) {
|
|
1637
|
+
if (level.tcbStatus !== "UpToDate") {
|
|
1638
|
+
throw new Error(
|
|
1639
|
+
`TCB Status is not "UpToDate", found "${level.tcbStatus}"`
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
throw ErrTcbStatus;
|
|
1646
|
+
}
|
|
1647
|
+
function toHex(bytes) {
|
|
1648
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1649
|
+
}
|
|
1650
|
+
function applyMask(a, b) {
|
|
1651
|
+
if (a.length !== b.length) {
|
|
1652
|
+
throw new Error(
|
|
1653
|
+
`applyMask: input lengths differ (a: ${a.length}, b: ${b.length})`
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
const result = new Uint8Array(a.length);
|
|
1657
|
+
for (let i = 0; i < a.length; i++) {
|
|
1658
|
+
result[i] = a[i] & b[i];
|
|
1659
|
+
}
|
|
1660
|
+
return result;
|
|
1661
|
+
}
|
|
1662
|
+
function getMatchingTcbLevel(tcbLevels, tdReport, pckCertPceSvn, pckCertCPUSvnComponents) {
|
|
1663
|
+
for (const tcbLevel of tcbLevels) {
|
|
1664
|
+
if (isCPUSvnHigherOrEqual(
|
|
1665
|
+
pckCertCPUSvnComponents,
|
|
1666
|
+
tcbLevel.tcb.sgxTcbcomponents
|
|
1667
|
+
) && pckCertPceSvn >= tcbLevel.tcb.pcesvn && isTdxTcbSvnHigherOrEqual(
|
|
1668
|
+
tdReport.teeTcbSvn,
|
|
1669
|
+
tcbLevel.tcb.tdxTcbcomponents
|
|
1670
|
+
)) {
|
|
1671
|
+
return tcbLevel;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
throw new Error("no matching TCB level found");
|
|
1675
|
+
}
|
|
1676
|
+
function isCPUSvnHigherOrEqual(pckCertCPUSvnComponents, sgxTcbcomponents) {
|
|
1677
|
+
if (pckCertCPUSvnComponents.length !== sgxTcbcomponents.length) {
|
|
1678
|
+
return false;
|
|
1679
|
+
}
|
|
1680
|
+
for (let i = 0; i < pckCertCPUSvnComponents.length; i++) {
|
|
1681
|
+
if (pckCertCPUSvnComponents[i] < sgxTcbcomponents[i].svn) {
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
function isTdxTcbSvnHigherOrEqual(teeTcbSvn, tdxTcbcomponents) {
|
|
1688
|
+
if (teeTcbSvn.length !== tdxTcbcomponents.length) {
|
|
1689
|
+
return false;
|
|
1690
|
+
}
|
|
1691
|
+
let start = 0;
|
|
1692
|
+
if (teeTcbSvn[1] > 0) {
|
|
1693
|
+
start = 2;
|
|
1694
|
+
}
|
|
1695
|
+
for (let i = start; i < teeTcbSvn.length; i++) {
|
|
1696
|
+
if (teeTcbSvn[i] < tdxTcbcomponents[i].svn) {
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
return true;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// src/audit/quote/pck.ts
|
|
1704
|
+
var pckCertChainCertificationDataTypeStart = 0;
|
|
1705
|
+
var pckCertChainCertificationDataTypeEnd = 2;
|
|
1706
|
+
var pckCertChainSizeStart = pckCertChainCertificationDataTypeEnd;
|
|
1707
|
+
var pckCertChainSizeEnd = 6;
|
|
1708
|
+
var pckCertChainDataStart = pckCertChainSizeEnd;
|
|
1709
|
+
var pckReportCertificationDataType = 5;
|
|
1710
|
+
var pckCertExtensionSize = 6;
|
|
1711
|
+
var sgxExtensionMinSize = 4;
|
|
1712
|
+
var ppidSize = 16;
|
|
1713
|
+
var pceIDSize = 2;
|
|
1714
|
+
var fmspcSize = 6;
|
|
1715
|
+
var processorIssuerID = "processor";
|
|
1716
|
+
var platformIssuerID = "platform";
|
|
1717
|
+
var processorIssuer = "Intel SGX PCK Processor CA";
|
|
1718
|
+
var pckCertPhrase = "Intel SGX PCK Certificate";
|
|
1719
|
+
var platformIssuer = "Intel SGX PCK Platform CA";
|
|
1720
|
+
var intermediateCertPhrase = "Intel SGX PCK Platform CA";
|
|
1721
|
+
var OidPPID = "1.2.840.113741.1.13.1.1";
|
|
1722
|
+
var OidPCEID = "1.2.840.113741.1.13.1.3";
|
|
1723
|
+
var OidTCB = "1.2.840.113741.1.13.1.2";
|
|
1724
|
+
var OidFMSPC = "1.2.840.113741.1.13.1.4";
|
|
1725
|
+
var OidCPUSvn = "1.2.840.113741.1.13.1.2.18";
|
|
1726
|
+
var OidPCESvn = "1.2.840.113741.1.13.1.2.17";
|
|
1727
|
+
var OidSgxExtension = "1.2.840.113741.1.13.1";
|
|
1728
|
+
var sgxTcbComponentOidPrefix = "1.2.840.113741.1.13.1.2";
|
|
1729
|
+
var ErrPckCertChainNil = new Error("PCK certificate chain is nil");
|
|
1730
|
+
var ErrPCKCertChainInvalid = new Error(
|
|
1731
|
+
"incomplete PCK Certificate chain found, should contain 3 concatenated PEM-formatted 'CERTIFICATE'-type block (PCK Leaf Cert||Intermediate CA Cert||Root CA Cert)"
|
|
1732
|
+
);
|
|
1733
|
+
var ErrRootCaCertExpired = new Error(
|
|
1734
|
+
"root CA certificate in PCK certificate chain has expired"
|
|
1735
|
+
);
|
|
1736
|
+
var ErrIntermediateCertNil = new Error(
|
|
1737
|
+
"intermediate certificate is empty"
|
|
1738
|
+
);
|
|
1739
|
+
var ErrPCKCertNil = new Error("PCK certificate is empty");
|
|
1740
|
+
var ErrRevocationCheckFailed = new Error(
|
|
1741
|
+
"unable to check for certificate revocation as GetCollateral parameter in the options is set to false"
|
|
1742
|
+
);
|
|
1743
|
+
var ErrIntermediateCaCertExpired = new Error(
|
|
1744
|
+
"intermediate CA certificate in PCK certificate chain has expired"
|
|
1745
|
+
);
|
|
1746
|
+
var ErrPckLeafCertExpired = new Error(
|
|
1747
|
+
"PCK leaf certificate in PCK certificate chain has expired"
|
|
1748
|
+
);
|
|
1749
|
+
var ErrPckCertCANil = new Error(
|
|
1750
|
+
"could not find CA from PCK certificate"
|
|
1751
|
+
);
|
|
1752
|
+
var trustedRootCertificate;
|
|
1753
|
+
function unwrapSingleSet(n) {
|
|
1754
|
+
if (n instanceof asn1js3.Set) {
|
|
1755
|
+
if (n.valueBlock.value.length !== 1) {
|
|
1756
|
+
throw new Error("SET wrapper should contain exactly one element");
|
|
1757
|
+
}
|
|
1758
|
+
return n.valueBlock.value[0];
|
|
1759
|
+
}
|
|
1760
|
+
return n;
|
|
1761
|
+
}
|
|
1762
|
+
function pckCertificateChainToProto(b) {
|
|
1763
|
+
const data = b.slice();
|
|
1764
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1765
|
+
const certificateDataType = view.getUint16(
|
|
1766
|
+
pckCertChainCertificationDataTypeStart,
|
|
1767
|
+
true
|
|
1768
|
+
);
|
|
1769
|
+
const size = view.getUint32(pckCertChainSizeStart, true);
|
|
1770
|
+
const pckCertChain = data.slice(pckCertChainDataStart);
|
|
1771
|
+
const pckCertificateChain = {
|
|
1772
|
+
certificateDataType,
|
|
1773
|
+
size,
|
|
1774
|
+
pckCertChain
|
|
1775
|
+
};
|
|
1776
|
+
checkPCKCertificateChain(pckCertificateChain);
|
|
1777
|
+
return pckCertificateChain;
|
|
1778
|
+
}
|
|
1779
|
+
function checkPCKCertificateChain(chain) {
|
|
1780
|
+
if (!chain) {
|
|
1781
|
+
throw ErrPckCertChainNil;
|
|
1782
|
+
}
|
|
1783
|
+
const { certificateDataType, size, pckCertChain } = chain;
|
|
1784
|
+
if (certificateDataType >= 1 << 16) {
|
|
1785
|
+
throw new Error(
|
|
1786
|
+
`certification data type expected to be 2 bytes, got ${certificateDataType}`
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
if (certificateDataType !== pckReportCertificationDataType) {
|
|
1790
|
+
throw new Error(
|
|
1791
|
+
`PCK certificate chain data type invalid, got ${certificateDataType}, expected ${pckReportCertificationDataType}`
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
if (size !== pckCertChain.length) {
|
|
1795
|
+
throw new Error(
|
|
1796
|
+
`PCK certificate chain size is ${pckCertChain.length}. Expected size ${size}`
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
function findMatchingExtension(extns, oid) {
|
|
1801
|
+
const match = extns.find((ext) => ext.type === oid);
|
|
1802
|
+
if (!match) {
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
`Unable to find extension with OID ${oid} in PCK certificate`
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
return match;
|
|
1808
|
+
}
|
|
1809
|
+
async function pckCertificateExtensions(cert) {
|
|
1810
|
+
if (cert.extensions.length !== pckCertExtensionSize) {
|
|
1811
|
+
throw new Error(
|
|
1812
|
+
`PCK certificate extensions length found ${cert.extensions.length}. Expected ${pckCertExtensionSize}`
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
const sgxExt = findMatchingExtension(cert.extensions, OidSgxExtension);
|
|
1816
|
+
if (!sgxExt) {
|
|
1817
|
+
throw new Error(
|
|
1818
|
+
"Could not find SGX extension present in the PCK certificate"
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
const { result: inner, offset } = asn1js3.fromBER(sgxExt.value);
|
|
1822
|
+
if (offset === -1 || !(inner instanceof asn1js3.Sequence)) {
|
|
1823
|
+
throw new Error("Expected SGX extension to be a SEQUENCE");
|
|
1824
|
+
}
|
|
1825
|
+
const remaining = sgxExt.value.slice(offset);
|
|
1826
|
+
if (remaining.byteLength !== 0) {
|
|
1827
|
+
throw new Error("SGX extension has trailing bytes");
|
|
1828
|
+
}
|
|
1829
|
+
return extractSgxExtensions(inner.valueBlock.value);
|
|
1830
|
+
}
|
|
1831
|
+
function extractCaFromPckCert(pckCert) {
|
|
1832
|
+
const cnList = pckCert.issuerName.getField("CN");
|
|
1833
|
+
const pckIssuer = cnList.length > 0 ? cnList[0] : null;
|
|
1834
|
+
if (pckIssuer === platformIssuer) {
|
|
1835
|
+
return platformIssuerID;
|
|
1836
|
+
}
|
|
1837
|
+
if (pckIssuer === processorIssuer) {
|
|
1838
|
+
return processorIssuerID;
|
|
1839
|
+
}
|
|
1840
|
+
throw ErrPckCertCANil;
|
|
1841
|
+
}
|
|
1842
|
+
function extractSgxExtensions(extensions) {
|
|
1843
|
+
if (extensions.length < sgxExtensionMinSize) {
|
|
1844
|
+
throw new Error(
|
|
1845
|
+
`SGX Extension has length ${extensions.length}. It should have a minimum length of ${sgxExtensionMinSize}`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
let ppid;
|
|
1849
|
+
let tcb;
|
|
1850
|
+
let pceid;
|
|
1851
|
+
let fmspc;
|
|
1852
|
+
for (const ext of extensions) {
|
|
1853
|
+
if (!(ext instanceof asn1js3.Sequence)) {
|
|
1854
|
+
throw new Error("Expected each SGX extension to be a SEQUENCE");
|
|
1855
|
+
}
|
|
1856
|
+
const bytes = ext.toBER(false);
|
|
1857
|
+
const { result: parsed, offset } = asn1js3.fromBER(bytes);
|
|
1858
|
+
if (offset === -1 || !(parsed instanceof asn1js3.Sequence)) {
|
|
1859
|
+
throw new Error("Could not parse SGX extension entry");
|
|
1860
|
+
}
|
|
1861
|
+
const typeOidBlock = parsed.valueBlock.value[0];
|
|
1862
|
+
if (!(typeOidBlock instanceof asn1js3.ObjectIdentifier)) {
|
|
1863
|
+
throw new Error("Missing Object Identifier in SGX extension entry");
|
|
1864
|
+
}
|
|
1865
|
+
const oid = typeOidBlock.valueBlock.toString();
|
|
1866
|
+
if (oid === OidPPID) {
|
|
1867
|
+
ppid = extractAsn1OctetStringExtension(
|
|
1868
|
+
"PPID",
|
|
1869
|
+
ext,
|
|
1870
|
+
ppidSize
|
|
1871
|
+
);
|
|
1872
|
+
} else if (oid === OidTCB) {
|
|
1873
|
+
tcb = extractAsn1SequenceTcbExtension(ext);
|
|
1874
|
+
} else if (oid === OidPCEID) {
|
|
1875
|
+
pceid = extractAsn1OctetStringExtension(
|
|
1876
|
+
"PCEID",
|
|
1877
|
+
ext,
|
|
1878
|
+
pceIDSize
|
|
1879
|
+
);
|
|
1880
|
+
} else if (oid === OidFMSPC) {
|
|
1881
|
+
fmspc = extractAsn1OctetStringExtension(
|
|
1882
|
+
"FMSPC",
|
|
1883
|
+
ext,
|
|
1884
|
+
fmspcSize
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
if (!ppid || !tcb || !pceid || !fmspc) {
|
|
1889
|
+
throw new Error(
|
|
1890
|
+
"Missing one or more required SGX extensions in PCK certificate"
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
return { ppid, tcb, pceid, fmspc };
|
|
1894
|
+
}
|
|
1895
|
+
function extractAsn1OctetStringExtension(name, seq, expectedSize) {
|
|
1896
|
+
if (seq.valueBlock.value.length !== 2) {
|
|
1897
|
+
throw new Error(`${name} extension should have 2 elements`);
|
|
1898
|
+
}
|
|
1899
|
+
const valueNode = unwrapSingleSet(seq.valueBlock.value[1]);
|
|
1900
|
+
if (!(valueNode instanceof asn1js3.OctetString)) {
|
|
1901
|
+
throw new Error(`${name} extension expected OCTET STRING`);
|
|
1902
|
+
}
|
|
1903
|
+
const bytes = new Uint8Array(valueNode.valueBlock.valueHex);
|
|
1904
|
+
if (bytes.length !== expectedSize) {
|
|
1905
|
+
throw new Error(
|
|
1906
|
+
`${name} has length ${bytes.length}, expected ${expectedSize}`
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
return Buffer.from(bytes).toString("hex");
|
|
1910
|
+
}
|
|
1911
|
+
function extractAsn1SequenceTcbExtension(ext) {
|
|
1912
|
+
const raw = ext.toBER(false);
|
|
1913
|
+
const { result: outerSeq, offset } = asn1js3.fromBER(raw);
|
|
1914
|
+
if (offset === -1 || !(outerSeq instanceof asn1js3.Sequence)) {
|
|
1915
|
+
throw new Error("Could not parse TCB extension inside SGX extension");
|
|
1916
|
+
}
|
|
1917
|
+
const values = outerSeq.valueBlock.value;
|
|
1918
|
+
if (values.length !== 2) {
|
|
1919
|
+
throw new Error(
|
|
1920
|
+
`TCB extension sequence has ${values.length} elements, expected 2`
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
const inner = values[1];
|
|
1924
|
+
const innerRaw = inner.toBER(false);
|
|
1925
|
+
const { result: innerSeq, offset: innerOffset } = asn1js3.fromBER(innerRaw);
|
|
1926
|
+
if (innerOffset === -1 || !(innerSeq instanceof asn1js3.Sequence)) {
|
|
1927
|
+
throw new Error("Could not parse TCB components inside TCB extension");
|
|
1928
|
+
}
|
|
1929
|
+
const components = innerSeq.valueBlock.value;
|
|
1930
|
+
if (components.length !== tcbExtensionSize) {
|
|
1931
|
+
throw new Error(
|
|
1932
|
+
`TCB components length is ${components.length}, expected ${tcbExtensionSize}`
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
const tcbv = {};
|
|
1936
|
+
extractTcbExtension(components, tcbv);
|
|
1937
|
+
return tcbv;
|
|
1938
|
+
}
|
|
1939
|
+
async function verifyPCKCertificationChain(options) {
|
|
1940
|
+
const { chain, collateral } = options;
|
|
1941
|
+
if (!chain?.rootCert) throw ErrRootCertNil;
|
|
1942
|
+
if (!chain.intermediateCerts) throw ErrIntermediateCertNil;
|
|
1943
|
+
if (!chain.pckCertificate) throw ErrPCKCertNil;
|
|
1944
|
+
const rootCert = chain.rootCert;
|
|
1945
|
+
const intermediateCert = chain.intermediateCerts;
|
|
1946
|
+
const pckCert = chain.pckCertificate;
|
|
1947
|
+
await validateCertificate2(rootCert, rootCert, rootCertPhrase);
|
|
1948
|
+
await validateCertificate2(intermediateCert, rootCert, intermediateCertPhrase);
|
|
1949
|
+
await validateCertificate2(pckCert, intermediateCert, pckCertPhrase);
|
|
1950
|
+
const opts = x509Options(options.trustedRoots, intermediateCert, options.now);
|
|
1951
|
+
const ok = await pckCert.verify(opts);
|
|
1952
|
+
if (!ok) {
|
|
1953
|
+
throw new Error("error verifying PCK Certificate");
|
|
1954
|
+
}
|
|
1955
|
+
if (options.checkRevocations) {
|
|
1956
|
+
if (!options.getCollateral) throw ErrRevocationCheckFailed;
|
|
1957
|
+
await validateCRL(collateral?.rootCaCrl, rootCert);
|
|
1958
|
+
await validateCRL(collateral?.pckCrl, intermediateCert);
|
|
1959
|
+
if (collateral?.pckCrl?.issuer !== pckCert.issuer) {
|
|
1960
|
+
throw new Error(
|
|
1961
|
+
`issuer "${collateral.pckCrl.issuer}" of PCK CRL does not match PCK Certificate issuer "${pckCert.issuer}"`
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
for (const entry of collateral.rootCaCrl?.entries ?? []) {
|
|
1965
|
+
if (intermediateCert.serialNumber === entry.serialNumber) {
|
|
1966
|
+
throw new Error(
|
|
1967
|
+
`Intermediate certificate was revoked at ${entry.revocationDate}`
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
for (const entry of collateral.pckCrl?.entries ?? []) {
|
|
1972
|
+
if (pckCert.serialNumber === entry.serialNumber) {
|
|
1973
|
+
throw new Error(
|
|
1974
|
+
`PCK Leaf certificate was revoked at ${entry.revocationDate}`
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
await checkCertificateExpiration(chain, options);
|
|
1980
|
+
}
|
|
1981
|
+
async function checkCertificateExpiration(chain, options) {
|
|
1982
|
+
const currentTime = options.now;
|
|
1983
|
+
if (currentTime > chain.rootCert.notAfter) {
|
|
1984
|
+
throw ErrRootCaCertExpired;
|
|
1985
|
+
}
|
|
1986
|
+
if (currentTime > chain.intermediateCerts.notAfter) {
|
|
1987
|
+
throw ErrIntermediateCaCertExpired;
|
|
1988
|
+
}
|
|
1989
|
+
if (currentTime > chain.pckCertificate.notAfter) {
|
|
1990
|
+
throw ErrPckLeafCertExpired;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
async function validateCertificate2(cert, parent, expectedPhrase) {
|
|
1994
|
+
if (!cert) throw new Error("certificate is nil");
|
|
1995
|
+
if (!parent) throw new Error("parent certificate is nil");
|
|
1996
|
+
const cnList = cert.subjectName.getField("CN");
|
|
1997
|
+
const subjectCN = cnList.length > 0 ? cnList[0] : null;
|
|
1998
|
+
if (subjectCN !== expectedPhrase) {
|
|
1999
|
+
throw new Error(
|
|
2000
|
+
`"${subjectCN}" is not expected in certificate's subject name. Expected "${expectedPhrase}"`
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
const issuerName = cert.issuerName.toString();
|
|
2004
|
+
const parentSubject = parent.subjectName.toString();
|
|
2005
|
+
if (issuerName !== parentSubject) {
|
|
2006
|
+
throw new Error(
|
|
2007
|
+
`Certificate issuer name ("${issuerName}") does not match parent certificate subject name ("${parentSubject}")`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
const verified = await cert.verify({ publicKey: parent });
|
|
2011
|
+
if (!verified) {
|
|
2012
|
+
throw new Error(
|
|
2013
|
+
"Certificate signature verification using parent certificate failed"
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
function x509Options(trustedRoots, intermediateCert, now) {
|
|
2018
|
+
let publicKeySource;
|
|
2019
|
+
if (intermediateCert) {
|
|
2020
|
+
publicKeySource = intermediateCert;
|
|
2021
|
+
} else if (trustedRoots && trustedRoots.length > 0) {
|
|
2022
|
+
publicKeySource = trustedRoots[0];
|
|
2023
|
+
} else {
|
|
2024
|
+
publicKeySource = trustedRootCertificate;
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
date: now,
|
|
2028
|
+
publicKey: publicKeySource
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// src/audit/quote/collateral.ts
|
|
2033
|
+
import * as x5093 from "@peculiar/x509";
|
|
2034
|
+
import * as asn1js4 from "asn1js";
|
|
2035
|
+
var ErrMissingTcbInfoBody = new Error(
|
|
2036
|
+
"missing tcbInfo body in the collaterals obtained"
|
|
2037
|
+
);
|
|
2038
|
+
var ErrMissingEnclaveIdentityBody = new Error(
|
|
2039
|
+
"missing enclaveIdentity body in the collaterals obtained"
|
|
2040
|
+
);
|
|
2041
|
+
var ErrTcbInfoNil = new Error("tcbInfo is empty in collaterals");
|
|
2042
|
+
var ErrQeIdentityNil = new Error("QeIdentity is empty in collaterals");
|
|
2043
|
+
var ErrMissingTcbInfoSigningCert = new Error(
|
|
2044
|
+
"missing signing certificate in the issuer chain of tcbInfo"
|
|
2045
|
+
);
|
|
2046
|
+
var ErrMissingTcbInfoRootCert = new Error(
|
|
2047
|
+
"missing root certificate in the issuer chain of tcbInfo"
|
|
2048
|
+
);
|
|
2049
|
+
var ErrMissingQeIdentitySigningCert = new Error(
|
|
2050
|
+
"missing signing certificate in the issuer chain of QeIdentity"
|
|
2051
|
+
);
|
|
2052
|
+
var ErrMissingQeIdentityRootCert = new Error(
|
|
2053
|
+
"missing root certificate in the issuer chain of QeIdentity"
|
|
2054
|
+
);
|
|
2055
|
+
var ErrMissingPckCrl = new Error(
|
|
2056
|
+
"missing PCK CRL in the collaterals obtained"
|
|
2057
|
+
);
|
|
2058
|
+
var ErrMissingRootCaCrl = new Error(
|
|
2059
|
+
"missing ROOT CA CRL in the collaterals obtained"
|
|
2060
|
+
);
|
|
2061
|
+
var ErrMissingPCKCrlSigningCert = new Error(
|
|
2062
|
+
"missing signing certificate in the issuer chain of PCK CRL"
|
|
2063
|
+
);
|
|
2064
|
+
var ErrMissingPCKCrlRootCert = new Error(
|
|
2065
|
+
"missing root certificate in the issuer chain of PCK CRL"
|
|
2066
|
+
);
|
|
2067
|
+
var ErrTcbInfoExpired = new Error("tcbInfo has expired");
|
|
2068
|
+
var ErrQeIdentityExpired = new Error("QeIdentity has expired");
|
|
2069
|
+
var ErrTcbInfoSigningCertExpired = new Error(
|
|
2070
|
+
"tcbInfo signing certificate has expired"
|
|
2071
|
+
);
|
|
2072
|
+
var ErrTcbInfoRootCertExpired = new Error(
|
|
2073
|
+
"tcbInfo root certificate has expired"
|
|
2074
|
+
);
|
|
2075
|
+
var ErrQeIdentityRootCertExpired = new Error(
|
|
2076
|
+
"QeIdentity root certificate has expired"
|
|
2077
|
+
);
|
|
2078
|
+
var ErrQeIdentitySigningCertExpired = new Error(
|
|
2079
|
+
"QeIdentity signing certificate has expired"
|
|
2080
|
+
);
|
|
2081
|
+
var ErrRootCaCrlExpired = new Error("root CA CRL has expired");
|
|
2082
|
+
var ErrPCKCrlExpired = new Error("PCK CRL has expired");
|
|
2083
|
+
var ErrPCKCrlSigningCertExpired = new Error(
|
|
2084
|
+
"PCK CRL signing certificate has expired"
|
|
2085
|
+
);
|
|
2086
|
+
var ErrPCKCrlRootCertExpired = new Error(
|
|
2087
|
+
"PCK CRL root certificate has expired"
|
|
2088
|
+
);
|
|
2089
|
+
var ErrEmptyRootCRLUrl = new Error(
|
|
2090
|
+
"empty url found in QeIdentity issuer's chain which is required to receive ROOT CA CRL"
|
|
2091
|
+
);
|
|
2092
|
+
var sgxPckCrlIssuerChainPhrase = "Sgx-Pck-Crl-Issuer-Chain";
|
|
2093
|
+
var sgxQeIdentityIssuerChainPhrase = "Sgx-Enclave-Identity-Issuer-Chain";
|
|
2094
|
+
var enclaveIdentityPhrase = "enclaveIdentity";
|
|
2095
|
+
var tcbInfoIssuerChainPhrase = "Tcb-Info-Issuer-Chain";
|
|
2096
|
+
var tcbInfoPhrase = "tcbInfo";
|
|
2097
|
+
var OID_CRL_DISTRIBUTION_POINTS = "2.5.29.31";
|
|
2098
|
+
var pcsTdxBaseURL = "https://api.trustedservices.intel.com/tdx/certification/v4";
|
|
2099
|
+
var pcsSgxBaseURL = "https://api.trustedservices.intel.com/sgx/certification/v4";
|
|
2100
|
+
var RetryHTTPSGetter = class {
|
|
2101
|
+
constructor(underlying = new SimpleHTTPSGetter(), timeoutMs = 12e4, maxRetryDelayMs = 3e4) {
|
|
2102
|
+
this.underlying = underlying;
|
|
2103
|
+
this.timeoutMs = timeoutMs;
|
|
2104
|
+
this.maxRetryDelayMs = maxRetryDelayMs;
|
|
2105
|
+
}
|
|
2106
|
+
async get(url) {
|
|
2107
|
+
const deadline = Date.now() + this.timeoutMs;
|
|
2108
|
+
let delay = 2e3;
|
|
2109
|
+
for (; ; ) {
|
|
2110
|
+
try {
|
|
2111
|
+
return await this.underlying.get(url);
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
if (Date.now() + delay > deadline) {
|
|
2114
|
+
throw err;
|
|
2115
|
+
}
|
|
2116
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
2117
|
+
delay = Math.min(delay * 2, this.maxRetryDelayMs);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
var SimpleHTTPSGetter = class {
|
|
2123
|
+
async get(url) {
|
|
2124
|
+
const resp = await fetch(url);
|
|
2125
|
+
if (!resp.ok) {
|
|
2126
|
+
throw new Error(`failed to retrieve ${url}, status code ${resp.status}`);
|
|
2127
|
+
}
|
|
2128
|
+
const headers = {};
|
|
2129
|
+
resp.headers.forEach((value, key) => {
|
|
2130
|
+
if (!headers[key]) headers[key] = [];
|
|
2131
|
+
headers[key].push(value);
|
|
2132
|
+
});
|
|
2133
|
+
const body = new Uint8Array(await resp.arrayBuffer());
|
|
2134
|
+
return [headers, body];
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
var CRLUnavailableError = class extends Error {
|
|
2138
|
+
constructor(causes) {
|
|
2139
|
+
super("CRL is unavailable");
|
|
2140
|
+
this.name = "CRLUnavailableError";
|
|
2141
|
+
this.causes = causes;
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
var AttestationRecreationError = class extends Error {
|
|
2145
|
+
constructor(message) {
|
|
2146
|
+
super(message);
|
|
2147
|
+
this.name = "AttestationRecreationError";
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
function defaultHTTPSGetter() {
|
|
2151
|
+
return new RetryHTTPSGetter();
|
|
2152
|
+
}
|
|
2153
|
+
async function verifyCollateral(options) {
|
|
2154
|
+
const collateral = options.collateral;
|
|
2155
|
+
if (!collateral) throw ErrCollateralNil;
|
|
2156
|
+
if (!collateral.tcbInfoBody) throw ErrMissingTcbInfoBody;
|
|
2157
|
+
if (!collateral.enclaveIdentityBody) throw ErrMissingEnclaveIdentityBody;
|
|
2158
|
+
if (Object.keys(collateral.tdxTcbInfo || {}).length === 0) throw ErrTcbInfoNil;
|
|
2159
|
+
if (Object.keys(collateral.qeIdentity || {}).length === 0)
|
|
2160
|
+
throw ErrQeIdentityNil;
|
|
2161
|
+
if (!collateral.tcbInfoIssuerIntermediateCertificate)
|
|
2162
|
+
throw ErrMissingTcbInfoSigningCert;
|
|
2163
|
+
if (!collateral.tcbInfoIssuerRootCertificate) throw ErrMissingTcbInfoRootCert;
|
|
2164
|
+
if (!collateral.qeIdentityIssuerIntermediateCertificate)
|
|
2165
|
+
throw ErrMissingQeIdentitySigningCert;
|
|
2166
|
+
if (!collateral.qeIdentityIssuerRootCertificate)
|
|
2167
|
+
throw ErrMissingQeIdentityRootCert;
|
|
2168
|
+
if (options.checkRevocations) {
|
|
2169
|
+
if (!collateral.pckCrl) throw ErrMissingPckCrl;
|
|
2170
|
+
if (!collateral.rootCaCrl) throw ErrMissingRootCaCrl;
|
|
2171
|
+
if (!collateral.pckCrlIssuerIntermediateCertificate)
|
|
2172
|
+
throw ErrMissingPCKCrlSigningCert;
|
|
2173
|
+
if (!collateral.pckCrlIssuerRootCertificate) throw ErrMissingPCKCrlRootCert;
|
|
2174
|
+
}
|
|
2175
|
+
await checkCollateralExpiration(collateral, options);
|
|
2176
|
+
}
|
|
2177
|
+
async function checkCollateralExpiration(collateral, options) {
|
|
2178
|
+
const currentTime = options.now;
|
|
2179
|
+
const tcbInfo = collateral.tdxTcbInfo.tcbInfo;
|
|
2180
|
+
const qeIdentity = collateral.qeIdentity.enclaveIdentity;
|
|
2181
|
+
if (currentTime > tcbInfo.nextUpdate) {
|
|
2182
|
+
throw ErrTcbInfoExpired;
|
|
2183
|
+
}
|
|
2184
|
+
if (currentTime > qeIdentity.nextUpdate) {
|
|
2185
|
+
throw ErrQeIdentityExpired;
|
|
2186
|
+
}
|
|
2187
|
+
if (currentTime > collateral.tcbInfoIssuerIntermediateCertificate.notAfter) {
|
|
2188
|
+
throw ErrTcbInfoSigningCertExpired;
|
|
2189
|
+
}
|
|
2190
|
+
if (currentTime > collateral.tcbInfoIssuerRootCertificate.notAfter) {
|
|
2191
|
+
throw ErrTcbInfoRootCertExpired;
|
|
2192
|
+
}
|
|
2193
|
+
if (currentTime > collateral.qeIdentityIssuerRootCertificate.notAfter) {
|
|
2194
|
+
throw ErrQeIdentityRootCertExpired;
|
|
2195
|
+
}
|
|
2196
|
+
if (currentTime > collateral.qeIdentityIssuerIntermediateCertificate.notAfter) {
|
|
2197
|
+
throw ErrQeIdentitySigningCertExpired;
|
|
2198
|
+
}
|
|
2199
|
+
if (options.checkRevocations) {
|
|
2200
|
+
if (currentTime > collateral.rootCaCrl.nextUpdate) {
|
|
2201
|
+
throw ErrRootCaCrlExpired;
|
|
2202
|
+
}
|
|
2203
|
+
if (currentTime > collateral.pckCrl.nextUpdate) {
|
|
2204
|
+
throw ErrPCKCrlExpired;
|
|
2205
|
+
}
|
|
2206
|
+
if (currentTime > collateral.pckCrlIssuerIntermediateCertificate.notAfter) {
|
|
2207
|
+
throw ErrPCKCrlSigningCertExpired;
|
|
2208
|
+
}
|
|
2209
|
+
if (currentTime > collateral.pckCrlIssuerRootCertificate.notAfter) {
|
|
2210
|
+
throw ErrPCKCrlRootCertExpired;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
async function obtainCollateral(fmspc, ca, options) {
|
|
2215
|
+
const getter = options.getter ?? defaultHTTPSGetter();
|
|
2216
|
+
const collateral = {};
|
|
2217
|
+
await getTcbInfo(fmspc, getter, collateral);
|
|
2218
|
+
await getQeIdentity(getter, collateral);
|
|
2219
|
+
if (options.checkRevocations) {
|
|
2220
|
+
await getPckCrl(ca, getter, collateral);
|
|
2221
|
+
await getRootCrl(getter, collateral);
|
|
2222
|
+
}
|
|
2223
|
+
return collateral;
|
|
2224
|
+
}
|
|
2225
|
+
function pckCrlURL(ca) {
|
|
2226
|
+
return `${pcsSgxBaseURL}/pckcrl?ca=${encodeURIComponent(ca)}&encoding=der`;
|
|
2227
|
+
}
|
|
2228
|
+
async function getPckCrl(ca, getter, collateral) {
|
|
2229
|
+
const pckCrlURLStr = pckCrlURL(ca);
|
|
2230
|
+
let headers;
|
|
2231
|
+
let body;
|
|
2232
|
+
try {
|
|
2233
|
+
;
|
|
2234
|
+
[headers, body] = await getter.get(pckCrlURLStr);
|
|
2235
|
+
} catch (err) {
|
|
2236
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2237
|
+
throw new CRLUnavailableError([error, new Error("could not fetch PCK CRL")]);
|
|
2238
|
+
}
|
|
2239
|
+
let intermediateCert;
|
|
2240
|
+
let rootCert;
|
|
2241
|
+
try {
|
|
2242
|
+
;
|
|
2243
|
+
[intermediateCert, rootCert] = headerToIssuerChain(
|
|
2244
|
+
headers,
|
|
2245
|
+
sgxPckCrlIssuerChainPhrase
|
|
2246
|
+
);
|
|
2247
|
+
} catch (err) {
|
|
2248
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2249
|
+
throw error;
|
|
2250
|
+
}
|
|
2251
|
+
collateral.pckCrlIssuerIntermediateCertificate = intermediateCert;
|
|
2252
|
+
collateral.pckCrlIssuerRootCertificate = rootCert;
|
|
2253
|
+
try {
|
|
2254
|
+
collateral.pckCrl = bodyToCrl(body);
|
|
2255
|
+
} catch (err) {
|
|
2256
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2257
|
+
throw new CRLUnavailableError([error, new Error("could not fetch PCK CRL")]);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
function headerToIssuerChain(headers, phrase) {
|
|
2261
|
+
const values = headers[phrase];
|
|
2262
|
+
if (!values || values.length !== 1) {
|
|
2263
|
+
throw new Error(
|
|
2264
|
+
`issuer chain is expected to be of size 1, found ${values?.length ?? 0}`
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
const encodedChain = values[0];
|
|
2268
|
+
if (!encodedChain) {
|
|
2269
|
+
throw new Error(`issuer chain certificates missing in "${phrase}"`);
|
|
2270
|
+
}
|
|
2271
|
+
let certChain;
|
|
2272
|
+
try {
|
|
2273
|
+
certChain = decodeURIComponent(encodedChain);
|
|
2274
|
+
} catch (err) {
|
|
2275
|
+
throw new Error(`unable to decode issuer chain in "${phrase}": ${err}`);
|
|
2276
|
+
}
|
|
2277
|
+
const pemBlocks = x5093.PemConverter.decode(certChain);
|
|
2278
|
+
if (pemBlocks.length !== 2) {
|
|
2279
|
+
throw new Error(
|
|
2280
|
+
`expected 2 PEM blocks (intermediate + root), found ${pemBlocks.length}`
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
const intermediate = new x5093.X509Certificate(pemBlocks[0]);
|
|
2284
|
+
const root = new x5093.X509Certificate(pemBlocks[1]);
|
|
2285
|
+
return [intermediate, root];
|
|
2286
|
+
}
|
|
2287
|
+
async function getQeIdentity(getter, collateral) {
|
|
2288
|
+
const QeIdentityURL = qeIdentityURL();
|
|
2289
|
+
let headers;
|
|
2290
|
+
let body;
|
|
2291
|
+
try {
|
|
2292
|
+
;
|
|
2293
|
+
[headers, body] = await getter.get(QeIdentityURL);
|
|
2294
|
+
} catch (err) {
|
|
2295
|
+
throw new AttestationRecreationError(
|
|
2296
|
+
`could not receive QeIdentity response: ${err}`
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
let intermediateCert;
|
|
2300
|
+
let rootCert;
|
|
2301
|
+
try {
|
|
2302
|
+
;
|
|
2303
|
+
[intermediateCert, rootCert] = headerToIssuerChain(
|
|
2304
|
+
headers,
|
|
2305
|
+
sgxQeIdentityIssuerChainPhrase
|
|
2306
|
+
);
|
|
2307
|
+
} catch (err) {
|
|
2308
|
+
throw new AttestationRecreationError(err.message);
|
|
2309
|
+
}
|
|
2310
|
+
collateral.qeIdentityIssuerIntermediateCertificate = intermediateCert;
|
|
2311
|
+
collateral.qeIdentityIssuerRootCertificate = rootCert;
|
|
2312
|
+
try {
|
|
2313
|
+
collateral.qeIdentity = JSON.parse(
|
|
2314
|
+
new TextDecoder().decode(body)
|
|
2315
|
+
);
|
|
2316
|
+
} catch (err) {
|
|
2317
|
+
throw new AttestationRecreationError(
|
|
2318
|
+
`unable to unmarshal QeIdentity response: ${err}`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
try {
|
|
2322
|
+
collateral.enclaveIdentityBody = bodyToRawMessage(
|
|
2323
|
+
enclaveIdentityPhrase,
|
|
2324
|
+
body
|
|
2325
|
+
);
|
|
2326
|
+
} catch (err) {
|
|
2327
|
+
throw new AttestationRecreationError(err.message);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
function bodyToRawMessage(name, body) {
|
|
2331
|
+
if (body.length === 0) {
|
|
2332
|
+
throw new Error(`"${name}" is empty`);
|
|
2333
|
+
}
|
|
2334
|
+
const decoded = new TextDecoder().decode(body);
|
|
2335
|
+
let rawBody;
|
|
2336
|
+
try {
|
|
2337
|
+
rawBody = JSON.parse(decoded);
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
throw new Error(`could not convert "${name}" body to raw message: ${err}`);
|
|
2340
|
+
}
|
|
2341
|
+
const val = rawBody[name];
|
|
2342
|
+
if (val === void 0) {
|
|
2343
|
+
throw new Error(`"${name}" field is missing in the response received`);
|
|
2344
|
+
}
|
|
2345
|
+
const json = JSON.stringify(val);
|
|
2346
|
+
return new TextEncoder().encode(json);
|
|
2347
|
+
}
|
|
2348
|
+
function qeIdentityURL() {
|
|
2349
|
+
return `${pcsTdxBaseURL}/qe/identity`;
|
|
2350
|
+
}
|
|
2351
|
+
function TcbInfoURL(fmspc) {
|
|
2352
|
+
return `${pcsTdxBaseURL}/tcb?fmspc=${fmspc}`;
|
|
2353
|
+
}
|
|
2354
|
+
async function getTcbInfo(fmspc, getter, collateral) {
|
|
2355
|
+
const tcbInfoURL = TcbInfoURL(fmspc);
|
|
2356
|
+
let header;
|
|
2357
|
+
let body;
|
|
2358
|
+
try {
|
|
2359
|
+
;
|
|
2360
|
+
[header, body] = await getter.get(tcbInfoURL);
|
|
2361
|
+
} catch (err) {
|
|
2362
|
+
throw new AttestationRecreationError(
|
|
2363
|
+
`could not receive tcbInfo response: ${err}`
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
let intermediateCert, rootCert;
|
|
2367
|
+
try {
|
|
2368
|
+
;
|
|
2369
|
+
[intermediateCert, rootCert] = headerToIssuerChain(
|
|
2370
|
+
header,
|
|
2371
|
+
tcbInfoIssuerChainPhrase
|
|
2372
|
+
);
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
throw new AttestationRecreationError(err.message);
|
|
2375
|
+
}
|
|
2376
|
+
collateral.tcbInfoIssuerIntermediateCertificate = intermediateCert;
|
|
2377
|
+
collateral.tcbInfoIssuerRootCertificate = rootCert;
|
|
2378
|
+
try {
|
|
2379
|
+
collateral.tdxTcbInfo = JSON.parse(
|
|
2380
|
+
new TextDecoder().decode(body)
|
|
2381
|
+
);
|
|
2382
|
+
} catch (err) {
|
|
2383
|
+
throw new AttestationRecreationError(
|
|
2384
|
+
`unable to unmarshal tcbInfo response: ${err}`
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
try {
|
|
2388
|
+
collateral.tcbInfoBody = bodyToRawMessage(tcbInfoPhrase, body);
|
|
2389
|
+
} catch (err) {
|
|
2390
|
+
throw new AttestationRecreationError(err.message);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
function getCrlDistributionPoints(cert) {
|
|
2394
|
+
const ext = cert.extensions.find(
|
|
2395
|
+
(e) => e.type === OID_CRL_DISTRIBUTION_POINTS
|
|
2396
|
+
);
|
|
2397
|
+
if (!ext) return [];
|
|
2398
|
+
const outer = asn1js4.fromBER(ext.value);
|
|
2399
|
+
if (outer.offset === -1 || !(outer.result instanceof asn1js4.OctetString))
|
|
2400
|
+
return [];
|
|
2401
|
+
const inner = asn1js4.fromBER(outer.result.valueBlock.valueHex);
|
|
2402
|
+
if (inner.offset === -1 || !(inner.result instanceof asn1js4.Sequence))
|
|
2403
|
+
return [];
|
|
2404
|
+
const seq = inner.result;
|
|
2405
|
+
if (seq.valueBlock.value.length === 0) return [];
|
|
2406
|
+
const dp = seq.valueBlock.value[0];
|
|
2407
|
+
if (!(dp instanceof asn1js4.Constructed) || dp.valueBlock.value.length === 0)
|
|
2408
|
+
return [];
|
|
2409
|
+
const dpn = dp.valueBlock.value[0];
|
|
2410
|
+
if (!(dpn instanceof asn1js4.Constructed)) return [];
|
|
2411
|
+
const names = new GeneralNames({ schema: dpn });
|
|
2412
|
+
return names.names.filter((n) => n.type === 6).map((n) => n.value);
|
|
2413
|
+
}
|
|
2414
|
+
var GeneralName = class {
|
|
2415
|
+
constructor(type, value) {
|
|
2416
|
+
this.type = type;
|
|
2417
|
+
this.value = value;
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
var GeneralNames = class {
|
|
2421
|
+
constructor(params) {
|
|
2422
|
+
this.names = [];
|
|
2423
|
+
const items = params.schema.valueBlock.value;
|
|
2424
|
+
for (const el of items) {
|
|
2425
|
+
if (el.idBlock.tagClass === 3) {
|
|
2426
|
+
const tag = el.idBlock.tagNumber;
|
|
2427
|
+
if (tag === 6 && el instanceof asn1js4.Primitive) {
|
|
2428
|
+
const uri = new asn1js4.IA5String({ valueHex: el.valueBlock.valueHex });
|
|
2429
|
+
this.names.push(new GeneralName(tag, uri.valueBlock.value));
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
async function getRootCrl(getter, collateral) {
|
|
2436
|
+
const rootCrlURLs = getCrlDistributionPoints(
|
|
2437
|
+
collateral.qeIdentityIssuerRootCertificate
|
|
2438
|
+
);
|
|
2439
|
+
if (rootCrlURLs.length === 0) {
|
|
2440
|
+
throw ErrEmptyRootCRLUrl;
|
|
2441
|
+
}
|
|
2442
|
+
const errors = [];
|
|
2443
|
+
for (const url of rootCrlURLs) {
|
|
2444
|
+
try {
|
|
2445
|
+
const [, body] = await getter.get(url);
|
|
2446
|
+
collateral.rootCaCrl = bodyToCrl(body);
|
|
2447
|
+
return;
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
errors.push(err instanceof Error ? err : new Error(String(err)));
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
throw new CRLUnavailableError([
|
|
2453
|
+
...errors,
|
|
2454
|
+
new Error("could not fetch root CRL")
|
|
2455
|
+
]);
|
|
2456
|
+
}
|
|
2457
|
+
function bodyToCrl(body) {
|
|
2458
|
+
try {
|
|
2459
|
+
return new x5093.X509Crl(body);
|
|
2460
|
+
} catch (err) {
|
|
2461
|
+
throw new Error(`unable to parse DER bytes of CRL: ${err}`);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// src/audit/quote/verify.ts
|
|
2466
|
+
var ErrOptionsNil = new Error("options parameter is empty");
|
|
2467
|
+
var ErrRootCaCertExpired2 = new Error(
|
|
2468
|
+
"root CA certificate in PCK certificate chain has expired"
|
|
2469
|
+
);
|
|
2470
|
+
var ErrHashVerificationFail = new Error(
|
|
2471
|
+
"unable to verify message digest using quote's signature and ecdsa attestation key"
|
|
2472
|
+
);
|
|
2473
|
+
var ErrSHA256VerificationFail = new Error(
|
|
2474
|
+
"QE Report Data does not match with value of SHA 256 calculated over the concatenation of ECDSA Attestation Key and QE Authenticated Data"
|
|
2475
|
+
);
|
|
2476
|
+
async function extractChainFromQuoteV4(quote) {
|
|
2477
|
+
const certChainBytes = quote.signedData?.certificationData?.qeReportCertificationData?.pckCertificateChainData?.pckCertChain;
|
|
2478
|
+
if (!certChainBytes) throw ErrPckCertChainNil;
|
|
2479
|
+
const pemString = new TextDecoder().decode(certChainBytes);
|
|
2480
|
+
const pemBlocks = x5094.PemConverter.decode(pemString);
|
|
2481
|
+
if (pemBlocks.length < 3) {
|
|
2482
|
+
throw ErrPCKCertChainInvalid;
|
|
2483
|
+
}
|
|
2484
|
+
const pckCert = new x5094.X509Certificate(pemBlocks[0]);
|
|
2485
|
+
const intermediateCert = new x5094.X509Certificate(pemBlocks[1]);
|
|
2486
|
+
const rootCert = new x5094.X509Certificate(pemBlocks[2]);
|
|
2487
|
+
return {
|
|
2488
|
+
pckCertificate: pckCert,
|
|
2489
|
+
rootCert,
|
|
2490
|
+
intermediateCerts: intermediateCert
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
async function localVerifyTdxQuote(aquote, opts) {
|
|
2494
|
+
if (!opts) throw ErrOptionsNil;
|
|
2495
|
+
checkQuoteV4(aquote);
|
|
2496
|
+
const chain = await extractChainFromQuoteV4(aquote);
|
|
2497
|
+
const exts = await pckCertificateExtensions(chain.pckCertificate);
|
|
2498
|
+
let acollateral;
|
|
2499
|
+
if (opts.getCollateral) {
|
|
2500
|
+
const ca = extractCaFromPckCert(chain.pckCertificate);
|
|
2501
|
+
acollateral = await obtainCollateral(exts.fmspc, ca, opts);
|
|
2502
|
+
}
|
|
2503
|
+
opts.collateral = acollateral;
|
|
2504
|
+
opts.pckCertExtensions = exts;
|
|
2505
|
+
opts.chain = chain;
|
|
2506
|
+
if (!opts.now) opts.now = /* @__PURE__ */ new Date();
|
|
2507
|
+
await verifyEvidenceV4(aquote, opts);
|
|
2508
|
+
return exts;
|
|
2509
|
+
}
|
|
2510
|
+
function equalBytes(a, b) {
|
|
2511
|
+
if (a.length !== b.length) return false;
|
|
2512
|
+
for (let i = 0; i < a.length; i++) {
|
|
2513
|
+
if (a[i] !== b[i]) return false;
|
|
2514
|
+
}
|
|
2515
|
+
return true;
|
|
2516
|
+
}
|
|
2517
|
+
function getHeaderAndTdQuoteBodyInAbiBytes(aquote) {
|
|
2518
|
+
const header = HeaderToAbiBytes(aquote.header);
|
|
2519
|
+
if (!header) {
|
|
2520
|
+
throw new Error("could not convert header to ABI bytes");
|
|
2521
|
+
}
|
|
2522
|
+
const tdQuoteBody = TdQuoteBodyToAbiBytes(aquote.tdQuoteBody);
|
|
2523
|
+
if (!tdQuoteBody) {
|
|
2524
|
+
throw new Error("could not convert TD Quote Body to ABI bytes");
|
|
2525
|
+
}
|
|
2526
|
+
const combined = new Uint8Array(header.length + tdQuoteBody.length);
|
|
2527
|
+
combined.set(header, 0);
|
|
2528
|
+
combined.set(tdQuoteBody, header.length);
|
|
2529
|
+
return combined;
|
|
2530
|
+
}
|
|
2531
|
+
async function verifyEvidenceV4(aquote, options) {
|
|
2532
|
+
if (aquote.header.teeType !== TeeTDX) {
|
|
2533
|
+
throw new Error("Invalid TEE type in quote (expected TDX)");
|
|
2534
|
+
}
|
|
2535
|
+
await verifyPCKCertificationChain(options);
|
|
2536
|
+
if (options.getCollateral) {
|
|
2537
|
+
await verifyCollateral(options);
|
|
2538
|
+
await verifyTCBinfo(options);
|
|
2539
|
+
await verifyQeIdentity(options);
|
|
2540
|
+
}
|
|
2541
|
+
await verifyQuote(aquote, options);
|
|
2542
|
+
}
|
|
2543
|
+
async function verifyQuote(aquote, options) {
|
|
2544
|
+
const chain = options.chain;
|
|
2545
|
+
const collateral = options.collateral;
|
|
2546
|
+
const pckCertExtensions = options.pckCertExtensions;
|
|
2547
|
+
const attestKey = aquote.signedData?.ecdsaAttestationKey;
|
|
2548
|
+
if (!attestKey) {
|
|
2549
|
+
throw new Error("Missing attestation key in quote");
|
|
2550
|
+
}
|
|
2551
|
+
const signature = aquote.signedData?.signature;
|
|
2552
|
+
if (!signature) {
|
|
2553
|
+
throw new Error("Missing signature in quote");
|
|
2554
|
+
}
|
|
2555
|
+
const message = getHeaderAndTdQuoteBodyInAbiBytes(aquote);
|
|
2556
|
+
const isValid = await verifyEcdsaSignature(attestKey, message, signature);
|
|
2557
|
+
if (!isValid) {
|
|
2558
|
+
throw ErrHashVerificationFail;
|
|
2559
|
+
}
|
|
2560
|
+
const qeReportCertificationData = aquote.signedData?.certificationData?.qeReportCertificationData;
|
|
2561
|
+
if (!qeReportCertificationData)
|
|
2562
|
+
throw new Error("Missing QE report certification data");
|
|
2563
|
+
await tdxProtoQeReportSignature(
|
|
2564
|
+
qeReportCertificationData,
|
|
2565
|
+
chain.pckCertificate
|
|
2566
|
+
);
|
|
2567
|
+
await verifyHash256(aquote);
|
|
2568
|
+
if (collateral) {
|
|
2569
|
+
await verifyTdQuoteBody(aquote.tdQuoteBody, {
|
|
2570
|
+
tcbInfo: collateral.tdxTcbInfo.tcbInfo,
|
|
2571
|
+
pckCertExtensions
|
|
2572
|
+
});
|
|
2573
|
+
await verifyQeReport(qeReportCertificationData.qeReport, {
|
|
2574
|
+
qeIdentity: collateral.qeIdentity.enclaveIdentity
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
async function verifyQeReport(qeReport, options) {
|
|
2579
|
+
const { qeIdentity } = options;
|
|
2580
|
+
const miscSelectMaskBytes = qeIdentity.miscselectMask.bytes;
|
|
2581
|
+
const miscSelectBytes = qeIdentity.miscselect.bytes;
|
|
2582
|
+
if (miscSelectMaskBytes.length !== 4) {
|
|
2583
|
+
throw new Error(
|
|
2584
|
+
`MISCSELECTMask field size (${miscSelectMaskBytes.length}) is not 4`
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
if (miscSelectBytes.length !== 4) {
|
|
2588
|
+
throw new Error(
|
|
2589
|
+
`MISCSELECT field size (${miscSelectBytes.length}) is not 4`
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
const viewMask = new DataView(
|
|
2593
|
+
miscSelectMaskBytes.buffer,
|
|
2594
|
+
miscSelectMaskBytes.byteOffset,
|
|
2595
|
+
4
|
|
2596
|
+
);
|
|
2597
|
+
const viewSelect = new DataView(
|
|
2598
|
+
miscSelectBytes.buffer,
|
|
2599
|
+
miscSelectBytes.byteOffset,
|
|
2600
|
+
4
|
|
2601
|
+
);
|
|
2602
|
+
const miscSelectMask = qeReport.miscSelect & viewMask.getUint32(0, true);
|
|
2603
|
+
const miscSelect = viewSelect.getUint32(0, true);
|
|
2604
|
+
if (miscSelectMask !== miscSelect) {
|
|
2605
|
+
throw new Error(
|
|
2606
|
+
`MISCSELECT mismatch: expected ${miscSelect}, got masked ${miscSelectMask}`
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
const attrsMask = qeIdentity.attributesMask.bytes;
|
|
2610
|
+
const reportAttrs = qeReport.attributes;
|
|
2611
|
+
if (attrsMask.length !== reportAttrs.length) {
|
|
2612
|
+
throw new Error(
|
|
2613
|
+
`AttributesMask size (${attrsMask.length}) does not match report attributes size (${reportAttrs.length})`
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
const maskedAttrs = applyMask(attrsMask, reportAttrs);
|
|
2617
|
+
const expectedAttrs = qeIdentity.attributes.bytes;
|
|
2618
|
+
if (!equalBytes(maskedAttrs, expectedAttrs)) {
|
|
2619
|
+
throw new Error(
|
|
2620
|
+
`Attributes mismatch: masked=${toHex(maskedAttrs)} expected=${toHex(expectedAttrs)}`
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
const mrSignerReport = qeReport.mrSigner;
|
|
2624
|
+
const mrSignerExpected = qeIdentity.mrsigner.bytes;
|
|
2625
|
+
if (!equalBytes(mrSignerReport, mrSignerExpected)) {
|
|
2626
|
+
throw new Error(
|
|
2627
|
+
`MRSIGNER mismatch: report=${toHex(mrSignerReport)} expected=${toHex(mrSignerExpected)}`
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
if (qeReport.isvProdId !== qeIdentity.isvProdID) {
|
|
2631
|
+
throw new Error(
|
|
2632
|
+
`ISV PRODID mismatch: report=${qeReport.isvProdId} expected=${qeIdentity.isvProdID}`
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
checkQeTcbStatus(qeIdentity.tcbLevels, qeReport.isvSvn);
|
|
2636
|
+
}
|
|
2637
|
+
async function verifyHash256(aquote) {
|
|
2638
|
+
const qeReportCertData = aquote.signedData?.certificationData?.qeReportCertificationData;
|
|
2639
|
+
if (!qeReportCertData) throw new Error("missing QE Report certification data");
|
|
2640
|
+
const qeReportData = qeReportCertData.qeReport?.reportData;
|
|
2641
|
+
const qeAuthData = qeReportCertData.qeAuthData?.data;
|
|
2642
|
+
const attestKey = aquote.signedData?.ecdsaAttestationKey;
|
|
2643
|
+
if (!qeReportData || !qeAuthData || !attestKey) {
|
|
2644
|
+
throw new Error("missing fields for QE report SHA-256 verification");
|
|
2645
|
+
}
|
|
2646
|
+
const concat = new Uint8Array(attestKey.length + qeAuthData.length);
|
|
2647
|
+
concat.set(attestKey, 0);
|
|
2648
|
+
concat.set(qeAuthData, attestKey.length);
|
|
2649
|
+
const hashBuffer = await x5094.cryptoProvider.get().subtle.digest("SHA-256", concat);
|
|
2650
|
+
let hashedMessage = new Uint8Array(hashBuffer);
|
|
2651
|
+
if (hashedMessage.length < qeReportData.length) {
|
|
2652
|
+
const padded = new Uint8Array(qeReportData.length);
|
|
2653
|
+
padded.set(hashedMessage, 0);
|
|
2654
|
+
hashedMessage = padded;
|
|
2655
|
+
}
|
|
2656
|
+
if (!equalBytes(hashedMessage, qeReportData)) {
|
|
2657
|
+
throw ErrSHA256VerificationFail;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
async function tdxProtoQeReportSignature(qeReportCertificationData, pckCertX509) {
|
|
2661
|
+
const rawReport = enclaveReportToAbiBytes(
|
|
2662
|
+
qeReportCertificationData.qeReport
|
|
2663
|
+
);
|
|
2664
|
+
if (!rawReport) {
|
|
2665
|
+
throw new Error("could not parse QE report");
|
|
2666
|
+
}
|
|
2667
|
+
await tdxQeReportSignature(
|
|
2668
|
+
rawReport,
|
|
2669
|
+
qeReportCertificationData.qeReportSignature,
|
|
2670
|
+
pckCertX509
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2673
|
+
async function tdxQeReportSignature(qeReport, signature, pckCert) {
|
|
2674
|
+
const cryptoKey = await x5094.cryptoProvider.get().subtle.importKey(
|
|
2675
|
+
"spki",
|
|
2676
|
+
pckCert.publicKey.rawData,
|
|
2677
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
2678
|
+
false,
|
|
2679
|
+
["verify"]
|
|
2680
|
+
);
|
|
2681
|
+
const isValid = await x5094.cryptoProvider.get().subtle.verify(
|
|
2682
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
2683
|
+
cryptoKey,
|
|
2684
|
+
signature,
|
|
2685
|
+
qeReport
|
|
2686
|
+
);
|
|
2687
|
+
if (!isValid) {
|
|
2688
|
+
throw new Error(
|
|
2689
|
+
"QE report's signature verification using PCK Leaf Certificate failed"
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// src/audit/quote/validateTdxQuote.ts
|
|
2695
|
+
var CA_ROOT_PEM = `
|
|
2696
|
+
-----BEGIN CERTIFICATE-----
|
|
2697
|
+
MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw
|
|
2698
|
+
aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv
|
|
2699
|
+
cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ
|
|
2700
|
+
BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG
|
|
2701
|
+
A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0
|
|
2702
|
+
aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT
|
|
2703
|
+
AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7
|
|
2704
|
+
1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB
|
|
2705
|
+
uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ
|
|
2706
|
+
MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50
|
|
2707
|
+
ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV
|
|
2708
|
+
Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI
|
|
2709
|
+
KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg
|
|
2710
|
+
AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=
|
|
2711
|
+
-----END CERTIFICATE-----
|
|
2712
|
+
`;
|
|
2713
|
+
var ErrCertificationDataNil = new Error("certification data is nil");
|
|
2714
|
+
var ErrQeReportCertificationDataNil = new Error(
|
|
2715
|
+
"QE Report certification data is nil"
|
|
2716
|
+
);
|
|
2717
|
+
var ErrQeAuthDataNil = new Error("QE AuthData is nil");
|
|
2718
|
+
var ErrCertNil = new Error("certificate is nil");
|
|
2719
|
+
var ErrParentCertNil = new Error("parent certificate is nil");
|
|
2720
|
+
var ErrCertPubKeyType = new Error(
|
|
2721
|
+
"certificate public key is not of type ecdsa public key"
|
|
2722
|
+
);
|
|
2723
|
+
var ErrRootCaCertExpired3 = new Error(
|
|
2724
|
+
"root CA certificate in PCK certificate chain has expired"
|
|
2725
|
+
);
|
|
2726
|
+
var ErrPublicKeySize = new Error("public key is of unexpected size");
|
|
2727
|
+
var ErrPckExtInvalid = new Error(
|
|
2728
|
+
"unexpected leftover bytes for PCK certificate's extension"
|
|
2729
|
+
);
|
|
2730
|
+
var ErrSgxExtInvalid = new Error(
|
|
2731
|
+
"unexpected leftover bytes when parsing SGX extensions"
|
|
2732
|
+
);
|
|
2733
|
+
var ErrTcbExtInvalid = new Error(
|
|
2734
|
+
"unexpected leftover bytes for TCB extension inside SGX extension field"
|
|
2735
|
+
);
|
|
2736
|
+
var ErrTcbCompInvalid = new Error(
|
|
2737
|
+
"unexpected leftover bytes for TCB components in TCB Extension inside SGX extension field"
|
|
2738
|
+
);
|
|
2739
|
+
var signedDataSignatureStart = 0;
|
|
2740
|
+
var signedDataSignatureEnd = 64;
|
|
2741
|
+
var signedDataAttestationKeyStart = signedDataSignatureEnd;
|
|
2742
|
+
var signedDataAttestationKeyEnd = 128;
|
|
2743
|
+
var signedDataCertificationDataStart = signedDataAttestationKeyEnd;
|
|
2744
|
+
var certificateDataTypeStart = 0;
|
|
2745
|
+
var certificateDataTypeEnd = 2;
|
|
2746
|
+
var certificateSizeStart = certificateDataTypeEnd;
|
|
2747
|
+
var certificateSizeEnd = 6;
|
|
2748
|
+
var certificateDataStart = certificateSizeEnd;
|
|
2749
|
+
var qeReportCertificationDataType = 6;
|
|
2750
|
+
var signatureSize = 64;
|
|
2751
|
+
var enclaveReportStart = 0;
|
|
2752
|
+
var enclaveReportEnd = 384;
|
|
2753
|
+
var qeReportCertificationDataSignatureStart = enclaveReportEnd;
|
|
2754
|
+
var qeReportCertificationDataSignatureEnd = 448;
|
|
2755
|
+
var qeReportCertificationDataAuthDataStart = qeReportCertificationDataSignatureEnd;
|
|
2756
|
+
var authDataParsedDataSizeStart = 0;
|
|
2757
|
+
var authDataParsedDataSizeEnd = 2;
|
|
2758
|
+
var attestationKeySize = 64;
|
|
2759
|
+
async function getTrustedRoots(rot) {
|
|
2760
|
+
const inlines = rot.cabundles ?? [];
|
|
2761
|
+
if (inlines.length === 0) return null;
|
|
2762
|
+
const pool = [];
|
|
2763
|
+
for (const pemString of inlines) {
|
|
2764
|
+
const cert = new x5095.X509Certificate(pemString);
|
|
2765
|
+
pool.push(cert);
|
|
2766
|
+
}
|
|
2767
|
+
return pool;
|
|
2768
|
+
}
|
|
2769
|
+
async function rootOfTrustToVerifyOptions(rot) {
|
|
2770
|
+
return {
|
|
2771
|
+
checkRevocations: !!rot.checkCrl,
|
|
2772
|
+
getCollateral: !!rot.getCollateral,
|
|
2773
|
+
trustedRoots: await getTrustedRoots(rot) ?? void 0
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
function headerToProto(buf) {
|
|
2777
|
+
if (buf.length !== 48) throw new Error("header must be 48 bytes");
|
|
2778
|
+
return {
|
|
2779
|
+
version: leU16(buf, 0),
|
|
2780
|
+
attestationKeyType: leU16(buf, 2),
|
|
2781
|
+
teeType: leU32(buf, 4),
|
|
2782
|
+
pceSvn: buf.slice(8, 10),
|
|
2783
|
+
qeSvn: buf.slice(10, 12),
|
|
2784
|
+
qeVendorId: buf.slice(12, 28),
|
|
2785
|
+
userData: buf.slice(28, 48)
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
function tdQuoteBodyToProto(buf) {
|
|
2789
|
+
if (buf.length !== 584) throw new Error("TDQuoteBody must be 0x248 bytes");
|
|
2790
|
+
const body = {
|
|
2791
|
+
teeTcbSvn: buf.slice(0, 16),
|
|
2792
|
+
mrSeam: buf.slice(16, 64),
|
|
2793
|
+
mrSignerSeam: buf.slice(64, 112),
|
|
2794
|
+
seamAttributes: buf.slice(112, 120),
|
|
2795
|
+
tdAttributes: buf.slice(120, 128),
|
|
2796
|
+
xfam: buf.slice(128, 136),
|
|
2797
|
+
mrTd: buf.slice(136, 184),
|
|
2798
|
+
mrConfigId: buf.slice(184, 232),
|
|
2799
|
+
mrOwner: buf.slice(232, 280),
|
|
2800
|
+
mrOwnerConfig: buf.slice(280, 328),
|
|
2801
|
+
rtmrs: [
|
|
2802
|
+
buf.slice(328, 376),
|
|
2803
|
+
buf.slice(376, 424),
|
|
2804
|
+
buf.slice(424, 472),
|
|
2805
|
+
buf.slice(472, 520)
|
|
2806
|
+
],
|
|
2807
|
+
reportData: buf.slice(520, 584)
|
|
2808
|
+
};
|
|
2809
|
+
return body;
|
|
2810
|
+
}
|
|
2811
|
+
function signedDataToProto(b) {
|
|
2812
|
+
const data = b.slice();
|
|
2813
|
+
const signedData = {
|
|
2814
|
+
signature: data.slice(signedDataSignatureStart, signedDataSignatureEnd),
|
|
2815
|
+
ecdsaAttestationKey: data.slice(
|
|
2816
|
+
signedDataAttestationKeyStart,
|
|
2817
|
+
signedDataAttestationKeyEnd
|
|
2818
|
+
),
|
|
2819
|
+
certificationData: certificationDataToProto(
|
|
2820
|
+
data.slice(signedDataCertificationDataStart)
|
|
2821
|
+
)
|
|
2822
|
+
};
|
|
2823
|
+
checkEcdsa256BitQuoteV4AuthData(signedData);
|
|
2824
|
+
return signedData;
|
|
2825
|
+
}
|
|
2826
|
+
function checkEcdsa256BitQuoteV4AuthData(signedData) {
|
|
2827
|
+
if (!signedData) {
|
|
2828
|
+
throw ErrQuoteV4AuthDataNil;
|
|
2829
|
+
}
|
|
2830
|
+
if (signedData.signature.length !== signatureSize) {
|
|
2831
|
+
throw new Error(
|
|
2832
|
+
`signature size is ${signedData.signature.length} bytes. Expected ${signatureSize} bytes`
|
|
2833
|
+
);
|
|
2834
|
+
}
|
|
2835
|
+
if (signedData.ecdsaAttestationKey.length !== attestationKeySize) {
|
|
2836
|
+
throw new Error(
|
|
2837
|
+
`ecdsa attestation key size is ${signedData.ecdsaAttestationKey.length} bytes. Expected ${attestationKeySize} bytes`
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
checkCertificationData(signedData.certificationData);
|
|
2841
|
+
}
|
|
2842
|
+
function checkCertificationData(certification) {
|
|
2843
|
+
if (!certification) {
|
|
2844
|
+
throw ErrCertificationDataNil;
|
|
2845
|
+
}
|
|
2846
|
+
const { certificateDataType } = certification;
|
|
2847
|
+
if (certificateDataType >= 1 << 16) {
|
|
2848
|
+
throw new Error(
|
|
2849
|
+
`certification data type field size must fit in 2 bytes, got ${certificateDataType}`
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
if (certificateDataType !== qeReportCertificationDataType) {
|
|
2853
|
+
throw new Error(
|
|
2854
|
+
`certification data type invalid, got ${certificateDataType}, expected ${qeReportCertificationDataType}`
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
checkQeReportCertificationData(certification.qeReportCertificationData);
|
|
2858
|
+
}
|
|
2859
|
+
function checkQeReportCertificationData(qeReport) {
|
|
2860
|
+
if (!qeReport) {
|
|
2861
|
+
throw ErrQeReportCertificationDataNil;
|
|
2862
|
+
}
|
|
2863
|
+
checkQeReport(qeReport.qeReport);
|
|
2864
|
+
if (qeReport.qeReportSignature.length !== signatureSize) {
|
|
2865
|
+
throw new Error(
|
|
2866
|
+
`signature size is ${qeReport.qeReportSignature.length} bytes. Expected ${signatureSize} bytes`
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2869
|
+
checkQeAuthData(qeReport.qeAuthData);
|
|
2870
|
+
checkPCKCertificateChain(qeReport.pckCertificateChainData);
|
|
2871
|
+
}
|
|
2872
|
+
function checkQeAuthData(authData) {
|
|
2873
|
+
if (!authData) {
|
|
2874
|
+
throw ErrQeAuthDataNil;
|
|
2875
|
+
}
|
|
2876
|
+
const { parsedDataSize, data } = authData;
|
|
2877
|
+
if (parsedDataSize >= 1 << 16) {
|
|
2878
|
+
throw new Error(
|
|
2879
|
+
`parsed data size field must fit in 2 bytes, got ${parsedDataSize}`
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
if (parsedDataSize !== data.length) {
|
|
2883
|
+
throw new Error(
|
|
2884
|
+
`parsed data size is ${data.length} bytes. Expected ${parsedDataSize} bytes`
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
function certificationDataToProto(b) {
|
|
2889
|
+
const data = b.slice();
|
|
2890
|
+
const certificateDataType = new DataView(
|
|
2891
|
+
data.buffer,
|
|
2892
|
+
data.byteOffset,
|
|
2893
|
+
data.byteLength
|
|
2894
|
+
).getUint16(certificateDataTypeStart, true);
|
|
2895
|
+
const size = new DataView(
|
|
2896
|
+
data.buffer,
|
|
2897
|
+
data.byteOffset,
|
|
2898
|
+
data.byteLength
|
|
2899
|
+
).getUint32(certificateSizeStart, true);
|
|
2900
|
+
const rawCertificateData = data.slice(certificateDataStart);
|
|
2901
|
+
if (rawCertificateData.length !== size) {
|
|
2902
|
+
throw new Error(
|
|
2903
|
+
`size of certificate data is 0x${rawCertificateData.length.toString(16)}. Expected size 0x${size.toString(16)}`
|
|
2904
|
+
);
|
|
2905
|
+
}
|
|
2906
|
+
const qeReportCertificationData = qeReportCertificationDataToProto(rawCertificateData);
|
|
2907
|
+
const certification = {
|
|
2908
|
+
certificateDataType,
|
|
2909
|
+
size,
|
|
2910
|
+
qeReportCertificationData
|
|
2911
|
+
};
|
|
2912
|
+
checkCertificationData(certification);
|
|
2913
|
+
return certification;
|
|
2914
|
+
}
|
|
2915
|
+
function qeReportCertificationDataToProto(b) {
|
|
2916
|
+
const data = b.slice();
|
|
2917
|
+
const qeReportCertificationData = {};
|
|
2918
|
+
const enclaveReport = enclaveReportToProto(
|
|
2919
|
+
data.slice(enclaveReportStart, enclaveReportEnd)
|
|
2920
|
+
);
|
|
2921
|
+
qeReportCertificationData.qeReport = enclaveReport;
|
|
2922
|
+
qeReportCertificationData.qeReportSignature = data.slice(
|
|
2923
|
+
qeReportCertificationDataSignatureStart,
|
|
2924
|
+
qeReportCertificationDataSignatureEnd
|
|
2925
|
+
);
|
|
2926
|
+
const { authData, authDataSize } = qeAuthDataToProto(
|
|
2927
|
+
data.slice(qeReportCertificationDataAuthDataStart)
|
|
2928
|
+
);
|
|
2929
|
+
qeReportCertificationData.qeAuthData = authData;
|
|
2930
|
+
const pckCertStart = qeReportCertificationDataAuthDataStart + authDataSize;
|
|
2931
|
+
const pckCertChain = pckCertificateChainToProto(data.slice(pckCertStart));
|
|
2932
|
+
qeReportCertificationData.pckCertificateChainData = pckCertChain;
|
|
2933
|
+
checkQeReportCertificationData(qeReportCertificationData);
|
|
2934
|
+
return qeReportCertificationData;
|
|
2935
|
+
}
|
|
2936
|
+
function qeAuthDataToProto(b) {
|
|
2937
|
+
const data = b.slice();
|
|
2938
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
2939
|
+
const parsedDataSize = view.getUint16(authDataParsedDataSizeStart, true);
|
|
2940
|
+
const authDataStart = authDataParsedDataSizeEnd;
|
|
2941
|
+
const authDataEnd = authDataStart + parsedDataSize;
|
|
2942
|
+
const authData = {
|
|
2943
|
+
parsedDataSize,
|
|
2944
|
+
data: data.slice(authDataStart, authDataEnd)
|
|
2945
|
+
};
|
|
2946
|
+
checkQeAuthData(authData);
|
|
2947
|
+
return {
|
|
2948
|
+
authData,
|
|
2949
|
+
authDataSize: authDataEnd
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
function quoteToProtoV4(buf) {
|
|
2953
|
+
if (buf.length < 1020) {
|
|
2954
|
+
throw new Error("quote too small - min 0x3fc");
|
|
2955
|
+
}
|
|
2956
|
+
const header = headerToProto(buf.slice(0, 48));
|
|
2957
|
+
const body = tdQuoteBodyToProto(buf.slice(48, 632));
|
|
2958
|
+
const signedDataSize = leU32(buf, 632);
|
|
2959
|
+
const signedDataStart = 636;
|
|
2960
|
+
if (buf.length < signedDataStart + signedDataSize)
|
|
2961
|
+
throw new Error("signedDataSize exceeds buffer");
|
|
2962
|
+
const signedData = signedDataToProto(
|
|
2963
|
+
buf.slice(signedDataStart, signedDataStart + signedDataSize)
|
|
2964
|
+
);
|
|
2965
|
+
const extra = buf.slice(signedDataStart + signedDataSize);
|
|
2966
|
+
return {
|
|
2967
|
+
header,
|
|
2968
|
+
tdQuoteBody: body,
|
|
2969
|
+
signedDataSize,
|
|
2970
|
+
signedData,
|
|
2971
|
+
extraBytes: extra.length ? extra : void 0
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
function determineQuoteFormat(buf) {
|
|
2975
|
+
if (buf.length < 2) throw new Error("buffer too small to detect format");
|
|
2976
|
+
return leU16(buf);
|
|
2977
|
+
}
|
|
2978
|
+
function localValidateTdxQuote(quote, opts) {
|
|
2979
|
+
if (!opts) {
|
|
2980
|
+
throw ErrOptionsNil;
|
|
2981
|
+
}
|
|
2982
|
+
if (!quote || quote.tdQuoteBody === void 0) {
|
|
2983
|
+
throw new Error("unsupported quote type");
|
|
2984
|
+
}
|
|
2985
|
+
validateTdxQuoteV4(quote, opts);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// src/audit/registry.ts
|
|
2989
|
+
import { ethers } from "ethers";
|
|
2990
|
+
var REGISTRY_ABI = [
|
|
2991
|
+
"function isMeasurementRegistered(bytes32 measurement) view returns (bool)"
|
|
2992
|
+
];
|
|
2993
|
+
async function isMeasurementRegistered(provider, contractAddress, measurement) {
|
|
2994
|
+
const contract = new ethers.Contract(contractAddress, REGISTRY_ABI, provider);
|
|
2995
|
+
const measurementHex = ethers.hexlify(measurement);
|
|
2996
|
+
return contract.isMeasurementRegistered(measurementHex);
|
|
2997
|
+
}
|
|
2998
|
+
async function verifyQuoteRegistered(hashAggregates, ethereumEndpoint, contractAddress) {
|
|
2999
|
+
const provider = new ethers.JsonRpcProvider(ethereumEndpoint);
|
|
3000
|
+
for (const variant of hashAggregates) {
|
|
3001
|
+
try {
|
|
3002
|
+
if (await isMeasurementRegistered(provider, contractAddress, variant)) {
|
|
3003
|
+
return true;
|
|
3004
|
+
}
|
|
3005
|
+
} catch (err) {
|
|
3006
|
+
throw new Error(
|
|
3007
|
+
`Registry lookup failed for measurement ${ethers.hexlify(variant)}: ${err.message}`
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
return false;
|
|
3012
|
+
}
|
|
3013
|
+
function toU8Arr(str) {
|
|
3014
|
+
return Uint8Array.from(Buffer.from(str, "hex"));
|
|
3015
|
+
}
|
|
3016
|
+
function aggregateAllMeasurementsPCS(m) {
|
|
3017
|
+
const body = m.body;
|
|
3018
|
+
const exts = m.pckExt;
|
|
3019
|
+
return concatUint8Arrays([
|
|
3020
|
+
body.mrConfigId,
|
|
3021
|
+
body.mrOwner,
|
|
3022
|
+
body.mrOwnerConfig,
|
|
3023
|
+
body.mrSeam,
|
|
3024
|
+
body.mrSignerSeam,
|
|
3025
|
+
body.mrTd,
|
|
3026
|
+
//measurements.ReportData, # differs between quotes
|
|
3027
|
+
body.seamAttributes,
|
|
3028
|
+
body.tdAttributes,
|
|
3029
|
+
//measurements.TeeTcbSvn, # differs between machines and needs to be greater than some value
|
|
3030
|
+
body.xfam,
|
|
3031
|
+
body.rtmrs[0],
|
|
3032
|
+
body.rtmrs[1],
|
|
3033
|
+
body.rtmrs[2],
|
|
3034
|
+
body.rtmrs[3],
|
|
3035
|
+
toU8Arr(exts.fmspc),
|
|
3036
|
+
// Family-Model-Stepping-Platform-Custom SKU
|
|
3037
|
+
toU8Arr(exts.ppid)
|
|
3038
|
+
// specific machine
|
|
3039
|
+
]);
|
|
3040
|
+
}
|
|
3041
|
+
function aggregateCodeMeasurementsAllPCS(m) {
|
|
3042
|
+
const body = m.body;
|
|
3043
|
+
const exts = m.pckExt;
|
|
3044
|
+
return concatUint8Arrays([
|
|
3045
|
+
body.mrConfigId,
|
|
3046
|
+
body.mrOwner,
|
|
3047
|
+
body.mrOwnerConfig,
|
|
3048
|
+
body.mrSeam,
|
|
3049
|
+
body.mrSignerSeam,
|
|
3050
|
+
body.mrTd,
|
|
3051
|
+
body.rtmrs[0],
|
|
3052
|
+
body.rtmrs[1],
|
|
3053
|
+
body.rtmrs[2],
|
|
3054
|
+
body.rtmrs[3],
|
|
3055
|
+
toU8Arr(exts.fmspc)
|
|
3056
|
+
// Family-Model-Stepping-Platform-Custom SKU
|
|
3057
|
+
]);
|
|
3058
|
+
}
|
|
3059
|
+
function aggregateCodeMeasurementsTD(m) {
|
|
3060
|
+
const body = m.body;
|
|
3061
|
+
return concatUint8Arrays([
|
|
3062
|
+
body.mrTd,
|
|
3063
|
+
body.rtmrs[0],
|
|
3064
|
+
body.rtmrs[1],
|
|
3065
|
+
body.rtmrs[2],
|
|
3066
|
+
body.rtmrs[3]
|
|
3067
|
+
]);
|
|
3068
|
+
}
|
|
3069
|
+
function aggregateCodeMeasurementsAll(m) {
|
|
3070
|
+
const body = m.body;
|
|
3071
|
+
return concatUint8Arrays([
|
|
3072
|
+
body.mrConfigId,
|
|
3073
|
+
body.mrOwner,
|
|
3074
|
+
body.mrOwnerConfig,
|
|
3075
|
+
body.mrSeam,
|
|
3076
|
+
body.mrSignerSeam,
|
|
3077
|
+
body.mrTd,
|
|
3078
|
+
...body.rtmrs
|
|
3079
|
+
]);
|
|
3080
|
+
}
|
|
3081
|
+
function aggregateAllMeasurements(m) {
|
|
3082
|
+
const body = m.body;
|
|
3083
|
+
return concatUint8Arrays([
|
|
3084
|
+
body.mrConfigId,
|
|
3085
|
+
body.mrOwner,
|
|
3086
|
+
body.mrOwnerConfig,
|
|
3087
|
+
body.mrSeam,
|
|
3088
|
+
body.mrSignerSeam,
|
|
3089
|
+
body.mrTd,
|
|
3090
|
+
body.seamAttributes,
|
|
3091
|
+
body.tdAttributes,
|
|
3092
|
+
body.teeTcbSvn,
|
|
3093
|
+
body.xfam,
|
|
3094
|
+
...body.rtmrs
|
|
3095
|
+
]);
|
|
3096
|
+
}
|
|
3097
|
+
function concatUint8Arrays(arrays) {
|
|
3098
|
+
const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
3099
|
+
const result = new Uint8Array(totalLength);
|
|
3100
|
+
let offset = 0;
|
|
3101
|
+
for (const arr of arrays) {
|
|
3102
|
+
result.set(arr, offset);
|
|
3103
|
+
offset += arr.length;
|
|
3104
|
+
}
|
|
3105
|
+
return result;
|
|
3106
|
+
}
|
|
3107
|
+
async function hashMeasurements(input) {
|
|
3108
|
+
const first = await sha256(input);
|
|
3109
|
+
const second = await sha256(first);
|
|
3110
|
+
return second;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
// src/audit/quoteToHash.ts
|
|
3114
|
+
var CHALLENGE_DOMAIN_SEPARATOR = "CUSTOM-RPC ATTEST:";
|
|
3115
|
+
async function parseVerifyRawQuote(raw, rootPem) {
|
|
3116
|
+
const quote = await quoteToProto(raw);
|
|
3117
|
+
if (!quote) {
|
|
3118
|
+
throw new Error("Failed to parse quote");
|
|
3119
|
+
}
|
|
3120
|
+
const rot = {
|
|
3121
|
+
cabundles: [rootPem]
|
|
3122
|
+
};
|
|
3123
|
+
const verifyOptions = await rootOfTrustToVerifyOptions(rot);
|
|
3124
|
+
const exts = await localVerifyTdxQuote(quote, verifyOptions);
|
|
3125
|
+
const val_opts = {
|
|
3126
|
+
// defaults are reasonable
|
|
3127
|
+
headerOptions: {},
|
|
3128
|
+
tdQuoteBodyOptions: {}
|
|
3129
|
+
};
|
|
3130
|
+
localValidateTdxQuote(quote, val_opts);
|
|
3131
|
+
return { body: quote.tdQuoteBody, pckExt: exts };
|
|
3132
|
+
}
|
|
3133
|
+
async function quoteToProto(b) {
|
|
3134
|
+
const quoteFormat = determineQuoteFormat(b);
|
|
3135
|
+
if (!quoteFormat) {
|
|
3136
|
+
throw new Error("Failed to determine quote format");
|
|
3137
|
+
}
|
|
3138
|
+
switch (quoteFormat) {
|
|
3139
|
+
case intelQuoteV4Version:
|
|
3140
|
+
return quoteToProtoV4(b);
|
|
3141
|
+
default:
|
|
3142
|
+
throw new Error("Quote format not supported");
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
async function quoteToHash(rawQuote, rootPem) {
|
|
3146
|
+
const tdQuoteBody = await parseVerifyRawQuote(rawQuote, rootPem);
|
|
3147
|
+
if (!tdQuoteBody) {
|
|
3148
|
+
throw new Error("error parsing and verifying TDX quote");
|
|
3149
|
+
}
|
|
3150
|
+
const reportData = tdQuoteBody.body.reportData;
|
|
3151
|
+
const domainSeparator = reportData.slice(0, CHALLENGE_DOMAIN_SEPARATOR.length);
|
|
3152
|
+
const expectedDomain = new TextEncoder().encode(CHALLENGE_DOMAIN_SEPARATOR);
|
|
3153
|
+
if (!equalBytes(domainSeparator, expectedDomain)) {
|
|
3154
|
+
throw new Error("quote constructed for alternate purpose");
|
|
3155
|
+
}
|
|
3156
|
+
return {
|
|
3157
|
+
challenge: reportData.slice(CHALLENGE_DOMAIN_SEPARATOR.length),
|
|
3158
|
+
hashVariants: [
|
|
3159
|
+
await hashMeasurements(
|
|
3160
|
+
aggregateCodeMeasurementsTD(tdQuoteBody)
|
|
3161
|
+
),
|
|
3162
|
+
await hashMeasurements(
|
|
3163
|
+
aggregateCodeMeasurementsAll(tdQuoteBody)
|
|
3164
|
+
),
|
|
3165
|
+
await hashMeasurements(
|
|
3166
|
+
aggregateAllMeasurements(tdQuoteBody)
|
|
3167
|
+
),
|
|
3168
|
+
await hashMeasurements(
|
|
3169
|
+
aggregateAllMeasurementsPCS(tdQuoteBody)
|
|
3170
|
+
),
|
|
3171
|
+
await hashMeasurements(
|
|
3172
|
+
aggregateCodeMeasurementsAllPCS(tdQuoteBody)
|
|
3173
|
+
)
|
|
3174
|
+
]
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
async function sha256(input) {
|
|
3178
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", input);
|
|
3179
|
+
return new Uint8Array(hashBuffer);
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// src/audit/challenge.ts
|
|
3183
|
+
function compressUncompressedSecp256k1(pubUncompressed) {
|
|
3184
|
+
if (pubUncompressed.length !== 65 || pubUncompressed[0] !== 4) {
|
|
3185
|
+
throw new Error(
|
|
3186
|
+
`Unexpected pubkey format/length: ${pubUncompressed.length}`
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
const x = pubUncompressed.slice(1, 33);
|
|
3190
|
+
const y = pubUncompressed.slice(33, 65);
|
|
3191
|
+
const prefix = (y[31] & 1) === 0 ? 2 : 3;
|
|
3192
|
+
return Buffer.concat([Buffer.from([prefix]), Buffer.from(x)]);
|
|
3193
|
+
}
|
|
3194
|
+
var DomainSeparatorSign = "CUSTOM-RPC SIGN:";
|
|
3195
|
+
function stringToBytes(str) {
|
|
3196
|
+
let rawQuote;
|
|
3197
|
+
try {
|
|
3198
|
+
rawQuote = ethers2.getBytes("0x" + str);
|
|
3199
|
+
} catch (e) {
|
|
3200
|
+
throw new Error(`Invalid hex: ${e?.message ?? e}`);
|
|
3201
|
+
}
|
|
3202
|
+
return rawQuote;
|
|
3203
|
+
}
|
|
3204
|
+
async function validateTdxAttestation(attestBody, exporterKey, l1RpcUrl, registryContractAddress) {
|
|
3205
|
+
const text = attestBody.toString("utf8").trim();
|
|
3206
|
+
let json;
|
|
3207
|
+
try {
|
|
3208
|
+
json = JSON.parse(text);
|
|
3209
|
+
} catch (e) {
|
|
3210
|
+
throw new Error(`Invalid TDX attestation body JSON: ${e?.message ?? e}`);
|
|
3211
|
+
}
|
|
3212
|
+
const rawQuote = stringToBytes(json.quote);
|
|
3213
|
+
const { challenge, hashVariants } = await quoteToHash(rawQuote, CA_ROOT_PEM);
|
|
3214
|
+
const registered = await verifyQuoteRegistered(
|
|
3215
|
+
hashVariants,
|
|
3216
|
+
l1RpcUrl,
|
|
3217
|
+
registryContractAddress
|
|
3218
|
+
);
|
|
3219
|
+
if (!registered) {
|
|
3220
|
+
throw new Error("TDX measurement not registered in verification registry");
|
|
3221
|
+
}
|
|
3222
|
+
const challengeBuffer = Buffer.from(challenge);
|
|
3223
|
+
if (challengeBuffer.length < 32) {
|
|
3224
|
+
throw new Error(`Report data too short: ${challengeBuffer.length}`);
|
|
3225
|
+
}
|
|
3226
|
+
const pubHashFromQuote = challengeBuffer.subarray(challengeBuffer.length - 32);
|
|
3227
|
+
const pubCompressed = await recoverSignaturePubkey(
|
|
3228
|
+
exporterKey,
|
|
3229
|
+
json.signature
|
|
3230
|
+
);
|
|
3231
|
+
const pubHash = await sha256(pubCompressed);
|
|
3232
|
+
return bytesEqual(pubHashFromQuote, pubHash);
|
|
3233
|
+
}
|
|
3234
|
+
async function recoverSignaturePubkey(message, signatureHex) {
|
|
3235
|
+
const digest = await sha256(
|
|
3236
|
+
Buffer.concat([
|
|
3237
|
+
Buffer.from(DomainSeparatorSign, "utf8"),
|
|
3238
|
+
Buffer.from(message)
|
|
3239
|
+
])
|
|
3240
|
+
);
|
|
3241
|
+
const sigBytes = stringToBytes(signatureHex);
|
|
3242
|
+
if (sigBytes.length !== 65) {
|
|
3243
|
+
throw new Error(
|
|
3244
|
+
`Invalid signature length: ${sigBytes.length} (expected 65)`
|
|
3245
|
+
);
|
|
3246
|
+
}
|
|
3247
|
+
const sig = Buffer.from(sigBytes);
|
|
3248
|
+
if (sig[64] === 0 || sig[64] === 1) {
|
|
3249
|
+
sig[64] += 27;
|
|
3250
|
+
}
|
|
3251
|
+
let pubUncompressed;
|
|
3252
|
+
try {
|
|
3253
|
+
const pubHex = ethers2.SigningKey.recoverPublicKey(
|
|
3254
|
+
ethers2.hexlify(digest),
|
|
3255
|
+
ethers2.hexlify(sig)
|
|
3256
|
+
);
|
|
3257
|
+
pubUncompressed = ethers2.getBytes(pubHex);
|
|
3258
|
+
} catch (e) {
|
|
3259
|
+
throw new Error(
|
|
3260
|
+
`Failed to recover public key from signature: ${e?.message ?? e}`
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
const pubCompressed = compressUncompressedSecp256k1(pubUncompressed);
|
|
3264
|
+
return pubCompressed;
|
|
3265
|
+
}
|
|
3266
|
+
function bytesEqual(a, b) {
|
|
3267
|
+
if (a.length !== b.length) return false;
|
|
3268
|
+
for (let i = 0; i < a.length; i++) {
|
|
3269
|
+
if (a[i] !== b[i]) {
|
|
3270
|
+
return false;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
return true;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
export {
|
|
3277
|
+
SIGN_RPC_METHODS,
|
|
3278
|
+
eip721Domain,
|
|
3279
|
+
delegateEIP721Types,
|
|
3280
|
+
DEBUG_NAMESPACE,
|
|
3281
|
+
DEBUG_NAMESPACE_SILENTDATA_INTERCEPTOR,
|
|
3282
|
+
HEADER_SIGNATURE,
|
|
3283
|
+
HEADER_TIMESTAMP,
|
|
3284
|
+
HEADER_SIGNATURE_TYPE,
|
|
3285
|
+
HEADER_EIP712_SIGNATURE,
|
|
3286
|
+
HEADER_DELEGATE,
|
|
3287
|
+
HEADER_DELEGATE_SIGNATURE,
|
|
3288
|
+
HEADER_EIP712_DELEGATE_SIGNATURE,
|
|
3289
|
+
HEADER_SIGNER_SWC,
|
|
3290
|
+
HEADER_FROM_BLOCK,
|
|
3291
|
+
ENTRYPOINT_ADDRESS,
|
|
3292
|
+
DEFAULT_USER_OPERATION_RECEIPT_LOOKUP_RANGE,
|
|
3293
|
+
USER_OPERATION_EVENT_SIGNATURE,
|
|
3294
|
+
USER_OPERATION_EVENT_HASH,
|
|
3295
|
+
DEFAULT_DELEGATE_EXPIRES,
|
|
3296
|
+
DELEGATE_EXPIRATION_THRESHOLD_BUFFER,
|
|
3297
|
+
WHITELISTED_METHODS,
|
|
3298
|
+
PRIVATE_EVENT_SIGNATURE,
|
|
3299
|
+
PRIVATE_EVENT_SIGNATURE_HASH,
|
|
3300
|
+
calculateEventTypeHash,
|
|
3301
|
+
ChainId,
|
|
3302
|
+
NetworkName,
|
|
3303
|
+
SignatureType,
|
|
3304
|
+
getAuthEIP721Types,
|
|
3305
|
+
getAuthHeaders,
|
|
3306
|
+
isSignableContractCall,
|
|
3307
|
+
defaultGetDelegate,
|
|
3308
|
+
prepareTypedDataPayload,
|
|
3309
|
+
SilentDataRollupBase,
|
|
3310
|
+
SilentDataRollupContract,
|
|
3311
|
+
validateTdxAttestation
|
|
3312
|
+
};
|