@invisible-labs/sdk 0.6.0-devnet.4 → 0.6.0-devnet.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -13
- package/dist/.invisible-sdk-build-target.json +2 -2
- package/dist/{chunk-UADJCUPK.js → chunk-3MDHAS4D.js} +6 -5
- package/dist/chunk-3MDHAS4D.js.map +1 -0
- package/dist/{chunk-E4DCEDAG.js → chunk-3TGQ4CZ3.js} +178 -2
- package/dist/chunk-3TGQ4CZ3.js.map +1 -0
- package/dist/chunk-DCTYBUY3.js +258 -0
- package/dist/chunk-DCTYBUY3.js.map +1 -0
- package/dist/{chunk-NHEHCUS5.js → chunk-EBWTAXNT.js} +3 -3
- package/dist/{chunk-NHEHCUS5.js.map → chunk-EBWTAXNT.js.map} +1 -1
- package/dist/{chunk-V5HXJRBW.js → chunk-HJFMDMPY.js} +3 -3
- package/dist/chunk-HJFMDMPY.js.map +1 -0
- package/dist/{chunk-GHBQF3A7.js → chunk-QRN46R3F.js} +5 -2
- package/dist/chunk-QRN46R3F.js.map +1 -0
- package/dist/chunk-Y7AK6RBV.js +4189 -0
- package/dist/chunk-Y7AK6RBV.js.map +1 -0
- package/dist/{coordinator-ZNLGAYXD.js → coordinator-PEWHNZ7I.js} +3 -3
- package/dist/{coordinator-ZNLGAYXD.js.map → coordinator-PEWHNZ7I.js.map} +1 -1
- package/dist/events.js +3 -3
- package/dist/index.d.ts +14 -4
- package/dist/index.js +74 -11
- package/dist/index.js.map +1 -1
- package/dist/lp.d.ts +33 -9
- package/dist/lp.js +305 -45
- package/dist/lp.js.map +1 -1
- package/dist/presets.d.ts +6 -2
- package/dist/presets.js +8 -8
- package/dist/presets.js.map +1 -1
- package/dist/stats.js +1 -1
- package/dist/storage.js +2 -243
- package/dist/storage.js.map +1 -1
- package/dist/{types.generated-DJBTjLpP.d.ts → types.generated-YhFu9ZBR.d.ts} +29 -4
- package/dist/user.d.ts +31 -4
- package/dist/user.js +409 -57
- package/dist/user.js.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-5NNEO7IK.js +0 -1150
- package/dist/chunk-5NNEO7IK.js.map +0 -1
- package/dist/chunk-BRDLLGCZ.js +0 -59
- package/dist/chunk-BRDLLGCZ.js.map +0 -1
- package/dist/chunk-DIA6U2CV.js +0 -1927
- package/dist/chunk-DIA6U2CV.js.map +0 -1
- package/dist/chunk-E4DCEDAG.js.map +0 -1
- package/dist/chunk-GHBQF3A7.js.map +0 -1
- package/dist/chunk-UADJCUPK.js.map +0 -1
- package/dist/chunk-V5HXJRBW.js.map +0 -1
package/dist/chunk-DIA6U2CV.js
DELETED
|
@@ -1,1927 +0,0 @@
|
|
|
1
|
-
import { AttestationError } from './chunk-GHBQF3A7.js';
|
|
2
|
-
import * as x509 from '@peculiar/x509';
|
|
3
|
-
|
|
4
|
-
var DEFAULT_AZURE_JWKS_TIMEOUT_MS = 3e4;
|
|
5
|
-
var TPM_GENERATED_VALUE = 4283712327;
|
|
6
|
-
var TPM_ST_ATTEST_QUOTE = 32792;
|
|
7
|
-
function assertAttestResponse(value) {
|
|
8
|
-
if (typeof value !== "object" || value === null) {
|
|
9
|
-
throw err("QUOTE_DECODE", "attestation response is not a JSON object");
|
|
10
|
-
}
|
|
11
|
-
const v = value;
|
|
12
|
-
const fields = ["quote", "nonce", "pubkey", "binary_hash", "mode"];
|
|
13
|
-
for (const field of fields) {
|
|
14
|
-
if (typeof v[field] !== "string") {
|
|
15
|
-
throw err(
|
|
16
|
-
"QUOTE_DECODE",
|
|
17
|
-
`attestation response missing or non-string field: "${field}" (got ${typeof v[field]})`
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
if (v["mode"] !== "dev" && v["mode"] !== "prod") {
|
|
22
|
-
throw err("QUOTE_DECODE", `attestation response has unknown mode: "${v["mode"]}"`);
|
|
23
|
-
}
|
|
24
|
-
if (v["runtime_profile"] !== void 0 && v["runtime_profile"] !== "prod-hardened" && v["runtime_profile"] !== "prod-devnet-simplified") {
|
|
25
|
-
throw err(
|
|
26
|
-
"QUOTE_DECODE",
|
|
27
|
-
`attestation response has unknown runtime profile: "${v["runtime_profile"]}"`
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
if (v["local_attestation"] !== void 0 && typeof v["local_attestation"] !== "boolean") {
|
|
31
|
-
throw err(
|
|
32
|
-
"QUOTE_DECODE",
|
|
33
|
-
`attestation response has non-boolean local_attestation: ${typeof v["local_attestation"]}`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
var INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256 = "44a0196b2b99f889b8e149e95b807a350e7424964399e885a7cbb8ccfab674d3";
|
|
38
|
-
var REASON_TO_CODE = {
|
|
39
|
-
// Structural / chain / quote-signature failures -> the cert-chain bucket.
|
|
40
|
-
QUOTE_DECODE: "CERT_CHAIN_INVALID",
|
|
41
|
-
QUOTE_STRUCTURE: "CERT_CHAIN_INVALID",
|
|
42
|
-
UNSUPPORTED_PLATFORM: "CERT_CHAIN_INVALID",
|
|
43
|
-
PCK_CHAIN_MISSING: "CERT_CHAIN_INVALID",
|
|
44
|
-
PCK_CHAIN_PARSE: "CERT_CHAIN_INVALID",
|
|
45
|
-
ROOT_FINGERPRINT_MISMATCH: "CERT_CHAIN_INVALID",
|
|
46
|
-
PCK_CHAIN_SIGNATURE: "CERT_CHAIN_INVALID",
|
|
47
|
-
DCAP_COLLATERAL_SIGNATURE: "CERT_CHAIN_INVALID",
|
|
48
|
-
DCAP_COLLATERAL_MISSING: "TCB_REJECTED",
|
|
49
|
-
DCAP_COLLATERAL_PARSE: "TCB_REJECTED",
|
|
50
|
-
DCAP_COLLATERAL_REVOKED: "TCB_REJECTED",
|
|
51
|
-
TCB_STATUS_REJECTED: "TCB_REJECTED",
|
|
52
|
-
QE_IDENTITY_REJECTED: "TCB_REJECTED",
|
|
53
|
-
QUOTE_SIGNATURE: "CERT_CHAIN_INVALID",
|
|
54
|
-
QE_REPORT_BINDING: "CERT_CHAIN_INVALID",
|
|
55
|
-
QE_REPORT_SIGNATURE: "CERT_CHAIN_INVALID",
|
|
56
|
-
// Freshness.
|
|
57
|
-
NONCE_MISMATCH: "FRESHNESS_NONCE_MISMATCH",
|
|
58
|
-
NONCE_FIELD_MISMATCH: "FRESHNESS_NONCE_MISMATCH",
|
|
59
|
-
TPM_NONCE_MISMATCH: "AZURE_TPM_QUALIFYING_DATA_MISMATCH",
|
|
60
|
-
// Noise channel binding (pubkey / report-data binding / bound JSON fields).
|
|
61
|
-
CHANNEL_BINDING_MISMATCH: "NOISE_BINDING_MISMATCH",
|
|
62
|
-
PUBKEY_FIELD_MISMATCH: "NOISE_BINDING_MISMATCH",
|
|
63
|
-
BINARY_HASH_FORMAT: "NOISE_BINDING_MISMATCH",
|
|
64
|
-
BINARY_HASH_MISMATCH: "NOISE_BINDING_MISMATCH",
|
|
65
|
-
// Debug bit.
|
|
66
|
-
DEBUG_BIT_SET: "DEBUG_BIT_SET",
|
|
67
|
-
MAA_DEBUG_BIT_SET: "DEBUG_BIT_SET",
|
|
68
|
-
// Mode / measurement pinning.
|
|
69
|
-
MODE_MISMATCH: "MRTD_MISMATCH",
|
|
70
|
-
MRTD_MISMATCH: "MRTD_MISMATCH",
|
|
71
|
-
RUNTIME_PROFILE_MISMATCH: "RUNTIME_PROFILE_MISMATCH",
|
|
72
|
-
// Local fallback disabled -> not attested.
|
|
73
|
-
LOCAL_ATTESTATION_DISABLED: "NOT_ATTESTED",
|
|
74
|
-
// Azure MAA token issuer / JWKS / policy / signature / compliance / binding.
|
|
75
|
-
MAA_TOKEN_MISSING: "AZURE_MAA_JWKS_INVALID",
|
|
76
|
-
MAA_TOKEN_PARSE: "AZURE_MAA_JWKS_INVALID",
|
|
77
|
-
MAA_TOKEN_EXPIRED: "AZURE_MAA_JWKS_INVALID",
|
|
78
|
-
MAA_TOKEN_SIGNATURE: "AZURE_MAA_JWKS_INVALID",
|
|
79
|
-
MAA_TRUST_POLICY_MISSING: "AZURE_MAA_ISSUER_MISMATCH",
|
|
80
|
-
MAA_ISSUER_MISMATCH: "AZURE_MAA_ISSUER_MISMATCH",
|
|
81
|
-
MAA_JWKS_MISMATCH: "AZURE_MAA_JWKS_INVALID",
|
|
82
|
-
MAA_POLICY_HASH_MISMATCH: "AZURE_MAA_POLICY_HASH_MISMATCH",
|
|
83
|
-
MAA_COMPLIANCE_MISMATCH: "AZURE_MAA_NOT_COMPLIANT",
|
|
84
|
-
MAA_REPORTDATA_MISMATCH: "AZURE_MAA_DCAP_MISMATCH",
|
|
85
|
-
// Azure HCL key blob hash binding.
|
|
86
|
-
HCL_KEYS_MISSING: "AZURE_HCL_KEYS_MISMATCH",
|
|
87
|
-
HCL_KEYS_HASH_MISMATCH: "AZURE_HCL_KEYS_MISMATCH",
|
|
88
|
-
HCL_KEYS_PARSE: "AZURE_AK_PUB_INVALID",
|
|
89
|
-
// Azure TPM AK quote.
|
|
90
|
-
TPM_QUOTE_MISSING: "AZURE_TPM_AK_QUOTE_INVALID",
|
|
91
|
-
TPM_QUOTE_SIGNATURE: "AZURE_TPM_AK_QUOTE_INVALID"
|
|
92
|
-
};
|
|
93
|
-
function err(reason, message) {
|
|
94
|
-
return new AttestationError(REASON_TO_CODE[reason], `[${reason}] ${message}`);
|
|
95
|
-
}
|
|
96
|
-
var MEASUREMENT_LEN = 48;
|
|
97
|
-
var HEADER_SIZE = 48;
|
|
98
|
-
var HEADER_VERSION_OFF = 0;
|
|
99
|
-
var HEADER_ATT_KEY_OFF = 2;
|
|
100
|
-
var HEADER_TEE_TYPE_OFF = 4;
|
|
101
|
-
var TDX_TEE_TYPE = 129;
|
|
102
|
-
var ECDSA_P256_KEY_TYPE = 2;
|
|
103
|
-
var BODY_OFFSET = 48;
|
|
104
|
-
var BODY_SIZE = 584;
|
|
105
|
-
var BODY_TEE_TCB_SVN_OFF = 0;
|
|
106
|
-
var BODY_MRSIGNERSEAM_OFF = 64;
|
|
107
|
-
var BODY_SEAM_ATTRIBUTES_OFF = 112;
|
|
108
|
-
var BODY_TD_ATTR_OFF = 120;
|
|
109
|
-
var BODY_MRTD_OFF = 136;
|
|
110
|
-
var BODY_RTMR0_OFF = 328;
|
|
111
|
-
var BODY_RTMR1_OFF = 376;
|
|
112
|
-
var BODY_RTMR2_OFF = 424;
|
|
113
|
-
var BODY_RTMR3_OFF = 472;
|
|
114
|
-
var BODY_REPORT_DATA_OFF = 520;
|
|
115
|
-
var BODY_REPORT_DATA_LEN = 64;
|
|
116
|
-
var SIG_DATA_LEN_OFFSET = BODY_OFFSET + BODY_SIZE;
|
|
117
|
-
var SIG_QUOTE_SIG_OFF = 0;
|
|
118
|
-
var SIG_ATT_KEY_OFF = 64;
|
|
119
|
-
var SIG_QE_CERT_DATA_OFF = 128;
|
|
120
|
-
var SIG_QE_CERT_DATA_HEADER_SIZE = 6;
|
|
121
|
-
var SIG_QE_REPORT_SIZE = 384;
|
|
122
|
-
var SIG_QE_REPORT_SIG_SIZE = 64;
|
|
123
|
-
var SGX_REPORT_MISCSELECT_OFF = 16;
|
|
124
|
-
var SGX_REPORT_ATTRIBUTES_OFF = 48;
|
|
125
|
-
var SGX_REPORT_MRSIGNER_OFF = 128;
|
|
126
|
-
var SGX_REPORT_ISV_PROD_ID_OFF = 256;
|
|
127
|
-
var SGX_REPORT_ISV_SVN_OFF = 258;
|
|
128
|
-
var SGX_REPORT_DATA_OFF = 320;
|
|
129
|
-
var QE_CERT_TYPE_PCK_CHAIN = 5;
|
|
130
|
-
var QE_CERT_TYPE_QE_REPORT_CERT_DATA = 6;
|
|
131
|
-
var INTEL_PCK_SGX_EXTENSIONS_OID = "1.2.840.113741.1.13.1";
|
|
132
|
-
var INTEL_PCK_EXTENSION_TCB_OID = "1.2.840.113741.1.13.1.2";
|
|
133
|
-
var INTEL_PCK_EXTENSION_PCE_ID_OID = "1.2.840.113741.1.13.1.3";
|
|
134
|
-
var INTEL_PCK_EXTENSION_FMSPC_OID = "1.2.840.113741.1.13.1.4";
|
|
135
|
-
var INTEL_PCK_EXTENSION_PCESVN_OID = "1.2.840.113741.1.13.1.2.17";
|
|
136
|
-
var INTEL_PCK_EXTENSION_CPUSVN_OID = "1.2.840.113741.1.13.1.2.18";
|
|
137
|
-
var INTEL_PCK_EXTENSION_SGX_TCB_COMPONENT_OID_PREFIX = "1.2.840.113741.1.13.1.2.";
|
|
138
|
-
var INTEL_TCB_SIGNING_SUBJECT = "CN=Intel SGX TCB Signing, O=Intel Corporation, L=Santa Clara, ST=CA, C=US";
|
|
139
|
-
var TEST_TCB_SIGNING_SUBJECT = "CN=Test SGX and TDX TCB Signing";
|
|
140
|
-
var ACCEPTED_TCB_STATUSES = /* @__PURE__ */ new Set(["UpToDate"]);
|
|
141
|
-
var ACCEPTED_TDX_MODULE_STATUSES = /* @__PURE__ */ new Set(["UpToDate"]);
|
|
142
|
-
var ACCEPTED_QE_IDENTITY_STATUSES = /* @__PURE__ */ new Set(["UpToDate"]);
|
|
143
|
-
async function verifyAttestation(params) {
|
|
144
|
-
const { response, expectedNonce, expectedNoiseStaticPub, policy, timeoutMs } = params;
|
|
145
|
-
if (policy.expectedRuntimeProfile !== void 0 && response.runtime_profile !== policy.expectedRuntimeProfile) {
|
|
146
|
-
throw err(
|
|
147
|
-
"RUNTIME_PROFILE_MISMATCH",
|
|
148
|
-
`runtime profile ${String(response.runtime_profile)} does not match the approved release profile ${policy.expectedRuntimeProfile}`
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
if (response.local_attestation === true) {
|
|
152
|
-
return verifyLocalAttestation({
|
|
153
|
-
response,
|
|
154
|
-
expectedNonce,
|
|
155
|
-
expectedNoiseStaticPub,
|
|
156
|
-
policy
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
const raw = decodeQuoteB64(response.quote);
|
|
160
|
-
ensureStructure(raw);
|
|
161
|
-
if (response.platform === "azure-tdx-dcap") {
|
|
162
|
-
return verifyAzureTdxAttestation({
|
|
163
|
-
raw,
|
|
164
|
-
response,
|
|
165
|
-
expectedNonce,
|
|
166
|
-
expectedNoiseStaticPub,
|
|
167
|
-
policy,
|
|
168
|
-
timeoutMs
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
if (response.platform && response.platform !== "tdx-dcap") {
|
|
172
|
-
throw err(
|
|
173
|
-
"UNSUPPORTED_PLATFORM",
|
|
174
|
-
`attestation not supported for platform "${response.platform}"`
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
return verifyGcpTdxAttestation({
|
|
178
|
-
raw,
|
|
179
|
-
response,
|
|
180
|
-
expectedNonce,
|
|
181
|
-
expectedNoiseStaticPub,
|
|
182
|
-
policy
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
async function verifyGcpTdxAttestation(params) {
|
|
186
|
-
const { raw, response, expectedNonce, expectedNoiseStaticPub, policy } = params;
|
|
187
|
-
const body = raw.subarray(BODY_OFFSET, BODY_OFFSET + BODY_SIZE);
|
|
188
|
-
const reportData = body.subarray(
|
|
189
|
-
BODY_REPORT_DATA_OFF,
|
|
190
|
-
BODY_REPORT_DATA_OFF + BODY_REPORT_DATA_LEN
|
|
191
|
-
);
|
|
192
|
-
if (!constantTimeEq(reportData.subarray(0, 32), expectedNonce)) {
|
|
193
|
-
throw err(
|
|
194
|
-
"NONCE_MISMATCH",
|
|
195
|
-
"REPORT_DATA[0:32] does not match verifier nonce - possible replay"
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
if (response.nonce.toLowerCase() !== bytesToHex(expectedNonce)) {
|
|
199
|
-
throw err("NONCE_FIELD_MISMATCH", `JSON nonce ${response.nonce} does not match verifier nonce`);
|
|
200
|
-
}
|
|
201
|
-
if (!constantTimeEq(reportData.subarray(32, 64), expectedNoiseStaticPub)) {
|
|
202
|
-
throw err(
|
|
203
|
-
"CHANNEL_BINDING_MISMATCH",
|
|
204
|
-
"REPORT_DATA[32:64] does not match Noise static pubkey from handshake"
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
const expectedPubHex = bytesToHex(expectedNoiseStaticPub);
|
|
208
|
-
if (response.pubkey.toLowerCase() !== expectedPubHex) {
|
|
209
|
-
throw err(
|
|
210
|
-
"PUBKEY_FIELD_MISMATCH",
|
|
211
|
-
"attestation pubkey field does not match Noise static pubkey from handshake"
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
if (!/^[0-9a-f]{64}$/i.test(response.binary_hash)) {
|
|
215
|
-
throw err("BINARY_HASH_FORMAT", "binary_hash field is not a 32-byte hex digest");
|
|
216
|
-
}
|
|
217
|
-
if (policy.expectedBinaryHash !== void 0 && response.binary_hash.toLowerCase() !== policy.expectedBinaryHash.toLowerCase()) {
|
|
218
|
-
throw err(
|
|
219
|
-
"BINARY_HASH_MISMATCH",
|
|
220
|
-
`binary hash ${response.binary_hash.toLowerCase()} does not match the approved release pin`
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
await verifyDcapQuoteAndCollateral({ raw, body, response, policy });
|
|
224
|
-
assertDebugBitClear(body);
|
|
225
|
-
const measurements = parseMeasurements(body);
|
|
226
|
-
if (policy.requiredMode !== void 0 && response.mode !== policy.requiredMode) {
|
|
227
|
-
throw err(
|
|
228
|
-
"MODE_MISMATCH",
|
|
229
|
-
`attestation mode ${response.mode} does not match required ${policy.requiredMode}`
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
if (response.mode === "prod") {
|
|
233
|
-
if ((policy.expectedMrtd === void 0 || policy.expectedMrtd === null) && policy.allowUnpinnedProdMrtdForRelease !== true) {
|
|
234
|
-
throw err(
|
|
235
|
-
"MRTD_MISMATCH",
|
|
236
|
-
"production attestation requires a pinned MRTD but none was configured"
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
if (policy.expectedMrtd !== void 0 && policy.expectedMrtd !== null && measurements.mrtd !== policy.expectedMrtd.toLowerCase()) {
|
|
240
|
-
throw err(
|
|
241
|
-
"MRTD_MISMATCH",
|
|
242
|
-
`MRTD ${measurements.mrtd} does not match pinned ${policy.expectedMrtd.toLowerCase()}`
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return {
|
|
247
|
-
mode: response.mode,
|
|
248
|
-
binaryHash: response.binary_hash,
|
|
249
|
-
runtimeProfile: response.runtime_profile,
|
|
250
|
-
mrtd: measurements.mrtd,
|
|
251
|
-
rtmr0: measurements.rtmr0,
|
|
252
|
-
rtmr1: measurements.rtmr1,
|
|
253
|
-
rtmr2: measurements.rtmr2,
|
|
254
|
-
rtmr3: measurements.rtmr3,
|
|
255
|
-
pubkeyHex: response.pubkey.toLowerCase(),
|
|
256
|
-
localAttestation: false
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
async function verifyDcapQuoteAndCollateral(params) {
|
|
260
|
-
const { raw, body, response, policy } = params;
|
|
261
|
-
const sigData = readSigData(raw);
|
|
262
|
-
const qeCertData = parseQeCertificationData(sigData);
|
|
263
|
-
const pckChain = extractPckChain(qeCertData.qeCertificationData);
|
|
264
|
-
const expectedRootFp = policy.intelRootFingerprint?.toLowerCase() ?? INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256;
|
|
265
|
-
const rootFp = await sha256Hex(new Uint8Array(pckChain[pckChain.length - 1].rawData));
|
|
266
|
-
if (rootFp !== expectedRootFp) {
|
|
267
|
-
throw err(
|
|
268
|
-
"ROOT_FINGERPRINT_MISMATCH",
|
|
269
|
-
`PCK root fingerprint ${rootFp} does not match pinned ${expectedRootFp}`
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
await verifyChainSignatures(pckChain);
|
|
273
|
-
const quoteSig = sigData.subarray(SIG_QUOTE_SIG_OFF, SIG_QUOTE_SIG_OFF + 64);
|
|
274
|
-
const attKey = sigData.subarray(SIG_ATT_KEY_OFF, SIG_ATT_KEY_OFF + 64);
|
|
275
|
-
const headerAndBody = raw.subarray(0, BODY_OFFSET + BODY_SIZE);
|
|
276
|
-
await verifyP256Signature(
|
|
277
|
-
rawXyToSpki(attKey),
|
|
278
|
-
quoteSig,
|
|
279
|
-
headerAndBody,
|
|
280
|
-
"QUOTE_SIGNATURE",
|
|
281
|
-
"quote header||body signature"
|
|
282
|
-
);
|
|
283
|
-
const { qeReport, qeReportSig, qeAuthData } = qeCertData;
|
|
284
|
-
const expectedBinding = await sha256(concat(attKey, qeAuthData));
|
|
285
|
-
const qeReportData = qeReport.subarray(SGX_REPORT_DATA_OFF, SGX_REPORT_DATA_OFF + 32);
|
|
286
|
-
if (!constantTimeEq(qeReportData, expectedBinding)) {
|
|
287
|
-
throw err(
|
|
288
|
-
"QE_REPORT_BINDING",
|
|
289
|
-
"QE report does not bind the in-quote attestation key (REPORT_DATA mismatch)"
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
const pckLeafSpki = await leafSpki(pckChain[0]);
|
|
293
|
-
await verifyP256Signature(
|
|
294
|
-
pckLeafSpki,
|
|
295
|
-
qeReportSig,
|
|
296
|
-
qeReport,
|
|
297
|
-
"QE_REPORT_SIGNATURE",
|
|
298
|
-
"QE report signature"
|
|
299
|
-
);
|
|
300
|
-
await verifyDcapCollateral({
|
|
301
|
-
collateral: response.dcap_collateral,
|
|
302
|
-
pckChain,
|
|
303
|
-
body,
|
|
304
|
-
qeReport,
|
|
305
|
-
mode: response.mode,
|
|
306
|
-
policy
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
async function verifyDcapCollateral(params) {
|
|
310
|
-
const { collateral, pckChain, body, qeReport, mode, policy } = params;
|
|
311
|
-
if (!collateral) {
|
|
312
|
-
if (policy.allowMissingDcapCollateral === true && mode === "dev" && policy.requiredMode === "dev") {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
throw err("DCAP_COLLATERAL_MISSING", "TDX quote is missing Intel DCAP collateral");
|
|
316
|
-
}
|
|
317
|
-
const now = /* @__PURE__ */ new Date();
|
|
318
|
-
const rootCert = pckChain[pckChain.length - 1];
|
|
319
|
-
const rootCrl = parseCrl(collateral.rootCaCrlPem, "root_ca_crl");
|
|
320
|
-
await verifyCrl(rootCrl, rootCert, "root_ca_crl", now);
|
|
321
|
-
const pckCrlIssuerChain = await verifyCollateralIssuerChain(
|
|
322
|
-
parsePemCertificateChain(collateral.pckCrlIssuerChainPem, "pck_crl_issuer_chain", 2),
|
|
323
|
-
policy,
|
|
324
|
-
"pck_crl_issuer_chain"
|
|
325
|
-
);
|
|
326
|
-
await assertNotRevokedByRoot(rootCrl, pckCrlIssuerChain[0], "PCK CRL issuer");
|
|
327
|
-
await assertSameCertificate(pckCrlIssuerChain[0], pckChain[1], "PCK CRL issuer");
|
|
328
|
-
const pckCrl = parseCrl(collateral.pckCrlPem, "pck_crl");
|
|
329
|
-
await verifyCrl(pckCrl, pckCrlIssuerChain[0], "pck_crl", now);
|
|
330
|
-
if (pckCrl.findRevoked(pckChain[0])) {
|
|
331
|
-
throw err("DCAP_COLLATERAL_REVOKED", "PCK leaf certificate is revoked by PCK CRL");
|
|
332
|
-
}
|
|
333
|
-
const tcbInfo = await verifySignedDcapJson({
|
|
334
|
-
signed: collateral.tcbInfo,
|
|
335
|
-
wrapperField: "tcbInfo",
|
|
336
|
-
policy,
|
|
337
|
-
rootCrl,
|
|
338
|
-
label: "tcb_info"
|
|
339
|
-
});
|
|
340
|
-
const qeIdentity = await verifySignedDcapJson({
|
|
341
|
-
signed: collateral.qeIdentity,
|
|
342
|
-
wrapperField: "enclaveIdentity",
|
|
343
|
-
policy,
|
|
344
|
-
rootCrl,
|
|
345
|
-
label: "qe_identity"
|
|
346
|
-
});
|
|
347
|
-
assertMatchingTcbEvaluationDataNumber(tcbInfo, qeIdentity);
|
|
348
|
-
verifyTcbInfo(tcbInfo, pckChain[0], body, now);
|
|
349
|
-
verifyQeIdentity(qeIdentity, qeReport, now);
|
|
350
|
-
}
|
|
351
|
-
function assertDebugBitClear(body) {
|
|
352
|
-
if ((body[BODY_TD_ATTR_OFF] & 1) !== 0) {
|
|
353
|
-
throw err("DEBUG_BIT_SET", "TD_ATTRIBUTES DEBUG bit set - refusing to attest a debuggable TD");
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
async function verifySignedDcapJson(params) {
|
|
357
|
-
const { signed, wrapperField, policy, rootCrl, label } = params;
|
|
358
|
-
if (!signed || typeof signed.body !== "string" || typeof signed.signature !== "string" || typeof signed.issuerChainPem !== "string") {
|
|
359
|
-
throw err("DCAP_COLLATERAL_MISSING", `${label} collateral is incomplete`);
|
|
360
|
-
}
|
|
361
|
-
let parsed;
|
|
362
|
-
try {
|
|
363
|
-
parsed = JSON.parse(signed.body);
|
|
364
|
-
} catch (e) {
|
|
365
|
-
throw err("DCAP_COLLATERAL_PARSE", `${label} body is not valid JSON: ${e}`);
|
|
366
|
-
}
|
|
367
|
-
const signedBody = exactSignedDcapJsonBody(signed.body, parsed, wrapperField, label);
|
|
368
|
-
const body = signedBody.parsed;
|
|
369
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
370
|
-
throw err("DCAP_COLLATERAL_PARSE", `${label} body is not a JSON object`);
|
|
371
|
-
}
|
|
372
|
-
const issuerChain = await verifyCollateralIssuerChain(
|
|
373
|
-
parsePemCertificateChain(signed.issuerChainPem, `${label}_issuer_chain`, 2),
|
|
374
|
-
policy,
|
|
375
|
-
`${label}_issuer_chain`
|
|
376
|
-
);
|
|
377
|
-
await assertNotRevokedByRoot(rootCrl, issuerChain[0], `${label} signing certificate`);
|
|
378
|
-
assertDcapJsonSignerIdentity(issuerChain[0], issuerChain[issuerChain.length - 1], policy, label);
|
|
379
|
-
if (!/^[0-9a-f]{128}$/i.test(signed.signature)) {
|
|
380
|
-
throw err("DCAP_COLLATERAL_PARSE", `${label} signature is not a 64-byte hex ECDSA signature`);
|
|
381
|
-
}
|
|
382
|
-
await verifyP256Signature(
|
|
383
|
-
await leafSpki(issuerChain[0]),
|
|
384
|
-
hexToBytes(signed.signature),
|
|
385
|
-
new TextEncoder().encode(signedBody.body),
|
|
386
|
-
"DCAP_COLLATERAL_SIGNATURE",
|
|
387
|
-
`${label} signature`
|
|
388
|
-
);
|
|
389
|
-
return body;
|
|
390
|
-
}
|
|
391
|
-
function exactSignedDcapJsonBody(raw, parsed, wrapperField, label) {
|
|
392
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
393
|
-
return { body: raw, parsed };
|
|
394
|
-
}
|
|
395
|
-
if (!(wrapperField in parsed)) {
|
|
396
|
-
return { body: raw, parsed };
|
|
397
|
-
}
|
|
398
|
-
const body = extractTopLevelJsonProperty(raw, wrapperField);
|
|
399
|
-
if (body === null) {
|
|
400
|
-
throw err(
|
|
401
|
-
"DCAP_COLLATERAL_PARSE",
|
|
402
|
-
`${label} PCS wrapper contains ${wrapperField} but exact signed body bytes could not be extracted`
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
try {
|
|
406
|
-
return { body, parsed: JSON.parse(body) };
|
|
407
|
-
} catch (e) {
|
|
408
|
-
throw err("DCAP_COLLATERAL_PARSE", `${label} exact signed body is not valid JSON: ${e}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
function extractTopLevelJsonProperty(raw, property) {
|
|
412
|
-
let offset = skipJsonWhitespace(raw, 0);
|
|
413
|
-
if (raw[offset] !== "{") return null;
|
|
414
|
-
offset++;
|
|
415
|
-
while (offset < raw.length) {
|
|
416
|
-
offset = skipJsonWhitespace(raw, offset);
|
|
417
|
-
if (raw[offset] === "}") return null;
|
|
418
|
-
if (raw[offset] === ",") {
|
|
419
|
-
offset++;
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
const key = readJsonString(raw, offset);
|
|
423
|
-
if (key === null) return null;
|
|
424
|
-
offset = skipJsonWhitespace(raw, key.end);
|
|
425
|
-
if (raw[offset] !== ":") return null;
|
|
426
|
-
offset = skipJsonWhitespace(raw, offset + 1);
|
|
427
|
-
const valueStart = offset;
|
|
428
|
-
const valueEnd = findJsonValueEnd(raw, valueStart);
|
|
429
|
-
if (valueEnd === null) return null;
|
|
430
|
-
if (key.value === property) return raw.slice(valueStart, valueEnd);
|
|
431
|
-
offset = valueEnd;
|
|
432
|
-
}
|
|
433
|
-
return null;
|
|
434
|
-
}
|
|
435
|
-
function skipJsonWhitespace(raw, offset) {
|
|
436
|
-
while (offset < raw.length && /[\t\n\r ]/u.test(raw[offset])) offset++;
|
|
437
|
-
return offset;
|
|
438
|
-
}
|
|
439
|
-
function readJsonString(raw, offset) {
|
|
440
|
-
if (raw[offset] !== '"') return null;
|
|
441
|
-
let cursor = offset + 1;
|
|
442
|
-
let escaped = false;
|
|
443
|
-
while (cursor < raw.length) {
|
|
444
|
-
const char = raw[cursor];
|
|
445
|
-
if (escaped) {
|
|
446
|
-
escaped = false;
|
|
447
|
-
} else if (char === "\\") {
|
|
448
|
-
escaped = true;
|
|
449
|
-
} else if (char === '"') {
|
|
450
|
-
const token = raw.slice(offset, cursor + 1);
|
|
451
|
-
try {
|
|
452
|
-
return { value: JSON.parse(token), end: cursor + 1 };
|
|
453
|
-
} catch {
|
|
454
|
-
return null;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
cursor++;
|
|
458
|
-
}
|
|
459
|
-
return null;
|
|
460
|
-
}
|
|
461
|
-
function findJsonValueEnd(raw, offset) {
|
|
462
|
-
if (offset >= raw.length) return null;
|
|
463
|
-
if (raw[offset] === '"') return readJsonString(raw, offset)?.end ?? null;
|
|
464
|
-
if (raw[offset] === "{" || raw[offset] === "[") {
|
|
465
|
-
const stack = [raw[offset]];
|
|
466
|
-
let cursor2 = offset + 1;
|
|
467
|
-
while (cursor2 < raw.length) {
|
|
468
|
-
const char = raw[cursor2];
|
|
469
|
-
if (char === '"') {
|
|
470
|
-
const stringToken = readJsonString(raw, cursor2);
|
|
471
|
-
if (stringToken === null) return null;
|
|
472
|
-
cursor2 = stringToken.end;
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
|
-
if (char === "{" || char === "[") {
|
|
476
|
-
stack.push(char);
|
|
477
|
-
} else if (char === "}" || char === "]") {
|
|
478
|
-
const open = stack.pop();
|
|
479
|
-
if (char === "}" && open !== "{" || char === "]" && open !== "[") return null;
|
|
480
|
-
if (stack.length === 0) return cursor2 + 1;
|
|
481
|
-
}
|
|
482
|
-
cursor2++;
|
|
483
|
-
}
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
|
-
let cursor = offset;
|
|
487
|
-
while (cursor < raw.length && !/[\t\n\r ,}\]]/u.test(raw[cursor])) cursor++;
|
|
488
|
-
return cursor > offset ? cursor : null;
|
|
489
|
-
}
|
|
490
|
-
function assertMatchingTcbEvaluationDataNumber(tcbInfo, qeIdentity) {
|
|
491
|
-
const tcbNumber = tcbInfo.tcbEvaluationDataNumber;
|
|
492
|
-
const qeNumber = qeIdentity.tcbEvaluationDataNumber;
|
|
493
|
-
if (!isTcbEvaluationDataNumber(tcbNumber) || !isTcbEvaluationDataNumber(qeNumber)) {
|
|
494
|
-
throw err(
|
|
495
|
-
"TCB_STATUS_REJECTED",
|
|
496
|
-
"TCB Info and QE Identity must include valid tcbEvaluationDataNumber values"
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
if (tcbNumber !== qeNumber) {
|
|
500
|
-
throw err(
|
|
501
|
-
"TCB_STATUS_REJECTED",
|
|
502
|
-
`TCB Info tcbEvaluationDataNumber ${tcbNumber} does not match QE Identity ${qeNumber}`
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function isTcbEvaluationDataNumber(value) {
|
|
507
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
508
|
-
}
|
|
509
|
-
async function verifyCollateralIssuerChain(certs, policy, label) {
|
|
510
|
-
const candidates = [certs, [...certs].reverse()];
|
|
511
|
-
let lastError;
|
|
512
|
-
for (const chain of candidates) {
|
|
513
|
-
try {
|
|
514
|
-
const rootFp = await sha256Hex(new Uint8Array(chain[chain.length - 1].rawData));
|
|
515
|
-
const expectedRootFp = policy.intelRootFingerprint?.toLowerCase() ?? INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256;
|
|
516
|
-
if (rootFp !== expectedRootFp) {
|
|
517
|
-
throw err(
|
|
518
|
-
"ROOT_FINGERPRINT_MISMATCH",
|
|
519
|
-
`${label} root fingerprint ${rootFp} does not match pinned ${expectedRootFp}`
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
await verifyChainSignatures(chain);
|
|
523
|
-
return chain;
|
|
524
|
-
} catch (e) {
|
|
525
|
-
lastError = e;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
if (lastError instanceof AttestationError) throw lastError;
|
|
529
|
-
throw err("DCAP_COLLATERAL_SIGNATURE", `${label} certificate chain failed verification`);
|
|
530
|
-
}
|
|
531
|
-
function assertDcapJsonSignerIdentity(leaf, root, policy, label) {
|
|
532
|
-
const expectedRootFp = policy.intelRootFingerprint?.toLowerCase() ?? INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256;
|
|
533
|
-
const allowsSyntheticRoot = expectedRootFp !== INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256;
|
|
534
|
-
const allowedSubjects = allowsSyntheticRoot ? [INTEL_TCB_SIGNING_SUBJECT, TEST_TCB_SIGNING_SUBJECT] : [INTEL_TCB_SIGNING_SUBJECT];
|
|
535
|
-
if (!allowedSubjects.includes(leaf.subject)) {
|
|
536
|
-
throw err(
|
|
537
|
-
"DCAP_COLLATERAL_SIGNATURE",
|
|
538
|
-
`${label} signing certificate subject ${leaf.subject} is not an Intel PCS TCB signing identity`
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
if (leaf.issuer !== root.subject) {
|
|
542
|
-
throw err(
|
|
543
|
-
"DCAP_COLLATERAL_SIGNATURE",
|
|
544
|
-
`${label} signing certificate issuer ${leaf.issuer} does not match root ${root.subject}`
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
const basicConstraints = leaf.getExtension(x509.BasicConstraintsExtension);
|
|
548
|
-
if (basicConstraints?.ca) {
|
|
549
|
-
throw err("DCAP_COLLATERAL_SIGNATURE", `${label} signing certificate must not be a CA`);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
function verifyTcbInfo(tcbInfo, pckLeaf, body, now) {
|
|
553
|
-
if (tcbInfo.id !== "TDX") {
|
|
554
|
-
throw err("TCB_STATUS_REJECTED", `TCB Info id ${String(tcbInfo.id)} is not TDX`);
|
|
555
|
-
}
|
|
556
|
-
if (tcbInfo.version !== 3 || tcbInfo.tcbType !== 0) {
|
|
557
|
-
throw err(
|
|
558
|
-
"TCB_STATUS_REJECTED",
|
|
559
|
-
`unsupported TCB Info version=${String(tcbInfo.version)} tcbType=${String(tcbInfo.tcbType)}`
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
assertFreshDcapJsonWindow(tcbInfo, "TCB Info", now);
|
|
563
|
-
const pckTcb = extractPckTcb(pckLeaf);
|
|
564
|
-
if (typeof tcbInfo.fmspc !== "string" || tcbInfo.fmspc.toLowerCase() !== pckTcb.fmspc) {
|
|
565
|
-
throw err(
|
|
566
|
-
"TCB_STATUS_REJECTED",
|
|
567
|
-
`TCB Info FMSPC ${String(tcbInfo.fmspc)} does not match PCK FMSPC ${pckTcb.fmspc}`
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
if (typeof tcbInfo.pceId !== "string" || tcbInfo.pceId.toLowerCase() !== pckTcb.pceId) {
|
|
571
|
-
throw err(
|
|
572
|
-
"TCB_STATUS_REJECTED",
|
|
573
|
-
`TCB Info PCE ID ${String(tcbInfo.pceId)} does not match PCK PCE ID ${pckTcb.pceId}`
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
if (!Array.isArray(tcbInfo.tcbLevels)) {
|
|
577
|
-
throw err("TCB_STATUS_REJECTED", "TCB Info has no tcbLevels array");
|
|
578
|
-
}
|
|
579
|
-
const teeTcbSvn = body.subarray(BODY_TEE_TCB_SVN_OFF, BODY_TEE_TCB_SVN_OFF + 16);
|
|
580
|
-
const matched = tcbInfo.tcbLevels.find((level) => tdxTcbLevelMatches(level, pckTcb, teeTcbSvn));
|
|
581
|
-
if (!matched || typeof matched !== "object") {
|
|
582
|
-
throw err("TCB_STATUS_REJECTED", "no TCB Info level matches PCK SGX TCB and quote TEE_TCB_SVN");
|
|
583
|
-
}
|
|
584
|
-
const status = matched["tcbStatus"];
|
|
585
|
-
if (typeof status !== "string" || !ACCEPTED_TCB_STATUSES.has(status)) {
|
|
586
|
-
throw err("TCB_STATUS_REJECTED", `TCB status ${String(status)} is not accepted`);
|
|
587
|
-
}
|
|
588
|
-
verifyTdxModuleIdentity(tcbInfo, body, teeTcbSvn);
|
|
589
|
-
}
|
|
590
|
-
function verifyQeIdentity(identity, qeReport, now) {
|
|
591
|
-
if (identity.id !== "TD_QE") {
|
|
592
|
-
throw err("QE_IDENTITY_REJECTED", `QE Identity id ${String(identity.id)} is not TD_QE`);
|
|
593
|
-
}
|
|
594
|
-
if (identity.version !== 2) {
|
|
595
|
-
throw err(
|
|
596
|
-
"QE_IDENTITY_REJECTED",
|
|
597
|
-
`unsupported QE Identity version ${String(identity.version)}`
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
assertFreshDcapJsonWindow(identity, "QE Identity", now);
|
|
601
|
-
assertMaskedHexMatches({
|
|
602
|
-
actual: qeReport.subarray(SGX_REPORT_MISCSELECT_OFF, SGX_REPORT_MISCSELECT_OFF + 4),
|
|
603
|
-
expectedHex: identity.miscselect,
|
|
604
|
-
maskHex: identity.miscselectMask,
|
|
605
|
-
label: "QE Identity MISCSELECT",
|
|
606
|
-
reason: "QE_IDENTITY_REJECTED"
|
|
607
|
-
});
|
|
608
|
-
assertMaskedHexMatches({
|
|
609
|
-
actual: qeReport.subarray(SGX_REPORT_ATTRIBUTES_OFF, SGX_REPORT_ATTRIBUTES_OFF + 16),
|
|
610
|
-
expectedHex: identity.attributes,
|
|
611
|
-
maskHex: identity.attributesMask,
|
|
612
|
-
label: "QE Identity ATTRIBUTES",
|
|
613
|
-
reason: "QE_IDENTITY_REJECTED"
|
|
614
|
-
});
|
|
615
|
-
const expectedMrSigner = bytesToHex(
|
|
616
|
-
qeReport.subarray(SGX_REPORT_MRSIGNER_OFF, SGX_REPORT_MRSIGNER_OFF + 32)
|
|
617
|
-
);
|
|
618
|
-
if (typeof identity.mrsigner !== "string" || identity.mrsigner.toLowerCase() !== expectedMrSigner) {
|
|
619
|
-
throw err(
|
|
620
|
-
"QE_IDENTITY_REJECTED",
|
|
621
|
-
`QE Identity MRSIGNER ${String(identity.mrsigner)} does not match QE report ${expectedMrSigner}`
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
const isvProdId = readU16LE(qeReport, SGX_REPORT_ISV_PROD_ID_OFF);
|
|
625
|
-
if (identity.isvprodid !== isvProdId) {
|
|
626
|
-
throw err(
|
|
627
|
-
"QE_IDENTITY_REJECTED",
|
|
628
|
-
`QE Identity ISVProdID ${String(identity.isvprodid)} does not match QE report ${isvProdId}`
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
if (!Array.isArray(identity.tcbLevels)) {
|
|
632
|
-
throw err("QE_IDENTITY_REJECTED", "QE Identity has no tcbLevels array");
|
|
633
|
-
}
|
|
634
|
-
const isvSvn = readU16LE(qeReport, SGX_REPORT_ISV_SVN_OFF);
|
|
635
|
-
const matched = identity.tcbLevels.find((level) => qeIdentityLevelMatches(level, isvSvn));
|
|
636
|
-
if (!matched || typeof matched !== "object") {
|
|
637
|
-
throw err("QE_IDENTITY_REJECTED", "no QE Identity level matches QE report ISVSVN");
|
|
638
|
-
}
|
|
639
|
-
const status = matched["tcbStatus"];
|
|
640
|
-
if (typeof status !== "string" || !ACCEPTED_QE_IDENTITY_STATUSES.has(status)) {
|
|
641
|
-
throw err("QE_IDENTITY_REJECTED", `QE Identity status ${String(status)} is not accepted`);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
function tdxTcbLevelMatches(level, pckTcb, teeTcbSvn) {
|
|
645
|
-
if (!level || typeof level !== "object") return false;
|
|
646
|
-
const tcb = level["tcb"];
|
|
647
|
-
if (!tcb || typeof tcb !== "object") return false;
|
|
648
|
-
const record = tcb;
|
|
649
|
-
const sgxComponents = record["sgxtcbcomponents"];
|
|
650
|
-
if (!tcbComponentsMatch(sgxComponents, pckTcb.sgxTcbSvns, 0)) return false;
|
|
651
|
-
const pceSvn = record["pcesvn"];
|
|
652
|
-
if (typeof pceSvn !== "number" || pckTcb.pceSvn < pceSvn) return false;
|
|
653
|
-
const tdxComponents = record["tdxtcbcomponents"];
|
|
654
|
-
const tdxStartIndex = teeTcbSvn[1] === 0 ? 0 : 2;
|
|
655
|
-
return tcbComponentsMatch(tdxComponents, Array.from(teeTcbSvn), tdxStartIndex);
|
|
656
|
-
}
|
|
657
|
-
function tcbComponentsMatch(components, actualSvns, startIndex) {
|
|
658
|
-
if (!Array.isArray(components) || components.length < 16) return false;
|
|
659
|
-
for (let index = startIndex; index < 16; index++) {
|
|
660
|
-
const component = components[index];
|
|
661
|
-
if (!component || typeof component !== "object") return false;
|
|
662
|
-
const svn = component["svn"];
|
|
663
|
-
if (typeof svn !== "number" || actualSvns[index] < svn) return false;
|
|
664
|
-
}
|
|
665
|
-
return true;
|
|
666
|
-
}
|
|
667
|
-
function verifyTdxModuleIdentity(tcbInfo, body, teeTcbSvn) {
|
|
668
|
-
const moduleVersion = teeTcbSvn[1];
|
|
669
|
-
const identity = getTdxModuleIdentity(tcbInfo, moduleVersion);
|
|
670
|
-
const expectedMrSigner = bytesToHex(
|
|
671
|
-
body.subarray(BODY_MRSIGNERSEAM_OFF, BODY_MRSIGNERSEAM_OFF + 48)
|
|
672
|
-
);
|
|
673
|
-
if (typeof identity["mrsigner"] !== "string" || identity["mrsigner"].toLowerCase() !== expectedMrSigner) {
|
|
674
|
-
throw err(
|
|
675
|
-
"TCB_STATUS_REJECTED",
|
|
676
|
-
`TDX Module Identity MRSIGNER ${String(identity["mrsigner"])} does not match quote ${expectedMrSigner}`
|
|
677
|
-
);
|
|
678
|
-
}
|
|
679
|
-
assertMaskedHexMatches({
|
|
680
|
-
actual: body.subarray(BODY_SEAM_ATTRIBUTES_OFF, BODY_SEAM_ATTRIBUTES_OFF + 8),
|
|
681
|
-
expectedHex: identity["attributes"],
|
|
682
|
-
maskHex: identity["attributesMask"],
|
|
683
|
-
label: "TDX Module Identity ATTRIBUTES",
|
|
684
|
-
reason: "TCB_STATUS_REJECTED"
|
|
685
|
-
});
|
|
686
|
-
if (moduleVersion < 1) return;
|
|
687
|
-
const tcbLevels = identity["tcbLevels"];
|
|
688
|
-
if (!Array.isArray(tcbLevels)) {
|
|
689
|
-
throw err("TCB_STATUS_REJECTED", "TDX Module Identity has no tcbLevels array");
|
|
690
|
-
}
|
|
691
|
-
const matched = tcbLevels.find((level) => tdxModuleTcbLevelMatches(level, teeTcbSvn[0]));
|
|
692
|
-
if (!matched || typeof matched !== "object") {
|
|
693
|
-
throw err("TCB_STATUS_REJECTED", "no TDX Module Identity level matches TEE_TCB_SVN ISVSVN");
|
|
694
|
-
}
|
|
695
|
-
const status = matched["tcbStatus"];
|
|
696
|
-
if (typeof status !== "string" || !ACCEPTED_TDX_MODULE_STATUSES.has(status)) {
|
|
697
|
-
throw err("TCB_STATUS_REJECTED", `TDX Module status ${String(status)} is not accepted`);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
function getTdxModuleIdentity(tcbInfo, moduleVersion) {
|
|
701
|
-
if (moduleVersion < 1) {
|
|
702
|
-
const module = tcbInfo.tdxModule;
|
|
703
|
-
if (!module || typeof module !== "object" || Array.isArray(module)) {
|
|
704
|
-
throw err("TCB_STATUS_REJECTED", "TCB Info has no tdxModule object");
|
|
705
|
-
}
|
|
706
|
-
return module;
|
|
707
|
-
}
|
|
708
|
-
const identities = tcbInfo.tdxModuleIdentities;
|
|
709
|
-
if (!Array.isArray(identities)) {
|
|
710
|
-
throw err("TCB_STATUS_REJECTED", "TCB Info has no tdxModuleIdentities array");
|
|
711
|
-
}
|
|
712
|
-
const moduleIdentity = identities.find((candidate) => {
|
|
713
|
-
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return false;
|
|
714
|
-
const id = candidate["id"];
|
|
715
|
-
return typeof id === "string" && tdxModuleIdMatches(id, moduleVersion);
|
|
716
|
-
});
|
|
717
|
-
if (!moduleIdentity || typeof moduleIdentity !== "object") {
|
|
718
|
-
throw err(
|
|
719
|
-
"TCB_STATUS_REJECTED",
|
|
720
|
-
`no TDX Module Identity matches TEE_TCB_SVN module version ${moduleVersion}`
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
return moduleIdentity;
|
|
724
|
-
}
|
|
725
|
-
function tdxModuleIdMatches(id, moduleVersion) {
|
|
726
|
-
const match = /^TDX_(\d+)$/.exec(id);
|
|
727
|
-
return match !== null && Number.parseInt(match[1], 10) === moduleVersion;
|
|
728
|
-
}
|
|
729
|
-
function tdxModuleTcbLevelMatches(level, isvSvn) {
|
|
730
|
-
if (!level || typeof level !== "object") return false;
|
|
731
|
-
const tcb = level["tcb"];
|
|
732
|
-
if (!tcb || typeof tcb !== "object") return false;
|
|
733
|
-
const record = tcb;
|
|
734
|
-
const required = typeof record["isvsvn"] === "number" ? record["isvsvn"] : typeof record["isvnsvn"] === "number" ? record["isvnsvn"] : void 0;
|
|
735
|
-
return typeof required === "number" && isvSvn >= required;
|
|
736
|
-
}
|
|
737
|
-
function qeIdentityLevelMatches(level, isvSvn) {
|
|
738
|
-
if (!level || typeof level !== "object") return false;
|
|
739
|
-
const tcb = level["tcb"];
|
|
740
|
-
if (!tcb || typeof tcb !== "object") return false;
|
|
741
|
-
const requiredIsvSvn = tcb["isvsvn"];
|
|
742
|
-
return typeof requiredIsvSvn === "number" && isvSvn >= requiredIsvSvn;
|
|
743
|
-
}
|
|
744
|
-
function assertMaskedHexMatches(params) {
|
|
745
|
-
const { actual, expectedHex, maskHex, label, reason } = params;
|
|
746
|
-
if (typeof expectedHex !== "string" || !isHexByteLength(expectedHex, actual.length)) {
|
|
747
|
-
throw err(reason, `${label} is missing or not ${actual.length} bytes of hex`);
|
|
748
|
-
}
|
|
749
|
-
if (typeof maskHex !== "string" || !isHexByteLength(maskHex, actual.length)) {
|
|
750
|
-
throw err(reason, `${label} mask is missing or not ${actual.length} bytes of hex`);
|
|
751
|
-
}
|
|
752
|
-
const expected = hexToBytes(expectedHex);
|
|
753
|
-
const mask = hexToBytes(maskHex);
|
|
754
|
-
for (let index = 0; index < actual.length; index++) {
|
|
755
|
-
if ((actual[index] & mask[index]) !== (expected[index] & mask[index])) {
|
|
756
|
-
throw err(reason, `${label} masked byte ${index} does not match signed collateral policy`);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
function isHexByteLength(value, byteLength) {
|
|
761
|
-
return value.length === byteLength * 2 && /^[0-9a-f]+$/i.test(value);
|
|
762
|
-
}
|
|
763
|
-
function assertFreshDcapJsonWindow(value, label, now) {
|
|
764
|
-
if (typeof value.issueDate !== "string" || typeof value.nextUpdate !== "string") {
|
|
765
|
-
throw err("DCAP_COLLATERAL_PARSE", `${label} is missing issueDate or nextUpdate`);
|
|
766
|
-
}
|
|
767
|
-
const issueDate = new Date(value.issueDate);
|
|
768
|
-
const nextUpdate = new Date(value.nextUpdate);
|
|
769
|
-
if (Number.isNaN(issueDate.getTime()) || Number.isNaN(nextUpdate.getTime())) {
|
|
770
|
-
throw err("DCAP_COLLATERAL_PARSE", `${label} has invalid issueDate or nextUpdate`);
|
|
771
|
-
}
|
|
772
|
-
if (now < issueDate) {
|
|
773
|
-
throw err("TCB_STATUS_REJECTED", `${label} is not yet valid`);
|
|
774
|
-
}
|
|
775
|
-
if (now > nextUpdate) {
|
|
776
|
-
throw err("TCB_STATUS_REJECTED", `${label} is stale past nextUpdate`);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
function parseCrl(pem, label) {
|
|
780
|
-
if (typeof pem !== "string" || pem.length === 0) {
|
|
781
|
-
throw err("DCAP_COLLATERAL_MISSING", `${label} is missing`);
|
|
782
|
-
}
|
|
783
|
-
try {
|
|
784
|
-
return new x509.X509Crl(pem);
|
|
785
|
-
} catch (e) {
|
|
786
|
-
throw err("DCAP_COLLATERAL_PARSE", `${label} is not a parseable X.509 CRL: ${e}`);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
async function verifyCrl(crl, issuer, label, now) {
|
|
790
|
-
if (crl.issuer !== issuer.subject) {
|
|
791
|
-
throw err(
|
|
792
|
-
"DCAP_COLLATERAL_SIGNATURE",
|
|
793
|
-
`${label} issuer ${crl.issuer} does not match expected ${issuer.subject}`
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
if (now < crl.thisUpdate) {
|
|
797
|
-
throw err("TCB_STATUS_REJECTED", `${label} is not yet valid`);
|
|
798
|
-
}
|
|
799
|
-
if (!crl.nextUpdate || now > crl.nextUpdate) {
|
|
800
|
-
throw err("TCB_STATUS_REJECTED", `${label} is stale past nextUpdate`);
|
|
801
|
-
}
|
|
802
|
-
let ok;
|
|
803
|
-
try {
|
|
804
|
-
ok = await crl.verify({ publicKey: issuer });
|
|
805
|
-
} catch (e) {
|
|
806
|
-
throw err("DCAP_COLLATERAL_SIGNATURE", `${label} signature verification threw: ${e}`);
|
|
807
|
-
}
|
|
808
|
-
if (!ok) {
|
|
809
|
-
throw err("DCAP_COLLATERAL_SIGNATURE", `${label} signature verification failed`);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
async function assertNotRevokedByRoot(rootCrl, cert, label) {
|
|
813
|
-
if (rootCrl.findRevoked(cert)) {
|
|
814
|
-
throw err("DCAP_COLLATERAL_REVOKED", `${label} is revoked by Intel root CRL`);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
async function assertSameCertificate(actual, expected, label) {
|
|
818
|
-
const actualFp = await sha256Hex(new Uint8Array(actual.rawData));
|
|
819
|
-
const expectedFp = await sha256Hex(new Uint8Array(expected.rawData));
|
|
820
|
-
if (actualFp !== expectedFp) {
|
|
821
|
-
throw err(
|
|
822
|
-
"DCAP_COLLATERAL_SIGNATURE",
|
|
823
|
-
`${label} fingerprint ${actualFp} does not match quote chain issuer ${expectedFp}`
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
function parsePemCertificateChain(pem, label, minCerts) {
|
|
828
|
-
if (typeof pem !== "string" || pem.length === 0) {
|
|
829
|
-
throw err("DCAP_COLLATERAL_MISSING", `${label} is missing`);
|
|
830
|
-
}
|
|
831
|
-
const matches = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) ?? [];
|
|
832
|
-
if (matches.length < minCerts) {
|
|
833
|
-
throw err(
|
|
834
|
-
"DCAP_COLLATERAL_PARSE",
|
|
835
|
-
`${label} has ${matches.length} cert(s), expected at least ${minCerts}`
|
|
836
|
-
);
|
|
837
|
-
}
|
|
838
|
-
return matches.map((certPem, index) => {
|
|
839
|
-
try {
|
|
840
|
-
return new x509.X509Certificate(certPem);
|
|
841
|
-
} catch (e) {
|
|
842
|
-
throw err("DCAP_COLLATERAL_PARSE", `${label} cert[${index}] is not parseable: ${e}`);
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
}
|
|
846
|
-
function extractPckTcb(cert) {
|
|
847
|
-
const sgxExtensions = cert.getExtension(INTEL_PCK_SGX_EXTENSIONS_OID);
|
|
848
|
-
if (sgxExtensions) {
|
|
849
|
-
return parsePckSgxExtensions(new Uint8Array(sgxExtensions.value));
|
|
850
|
-
}
|
|
851
|
-
return {
|
|
852
|
-
fmspc: extractPckExtensionHex(cert, INTEL_PCK_EXTENSION_FMSPC_OID, 6, "FMSPC"),
|
|
853
|
-
pceId: extractPckExtensionHex(cert, INTEL_PCK_EXTENSION_PCE_ID_OID, 2, "PCE ID"),
|
|
854
|
-
sgxTcbSvns: Array.from(
|
|
855
|
-
{ length: 16 },
|
|
856
|
-
(_, index) => extractPckIntegerExtension(
|
|
857
|
-
cert,
|
|
858
|
-
`${INTEL_PCK_EXTENSION_SGX_TCB_COMPONENT_OID_PREFIX}${index + 1}`,
|
|
859
|
-
`SGX TCB component ${index + 1}`
|
|
860
|
-
)
|
|
861
|
-
),
|
|
862
|
-
pceSvn: extractPckIntegerExtension(cert, INTEL_PCK_EXTENSION_PCESVN_OID, "PCESVN"),
|
|
863
|
-
cpuSvn: hexToBytes(extractPckExtensionHex(cert, INTEL_PCK_EXTENSION_CPUSVN_OID, 16, "CPUSVN"))
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
function parsePckSgxExtensions(bytes) {
|
|
867
|
-
const topLevel = unwrapDerOctetStringIfPresent(bytes);
|
|
868
|
-
const extensions = derChildren(expectDerTag(readDerTlv(topLevel, 0), 48, "SGX extensions"));
|
|
869
|
-
const parsed = {};
|
|
870
|
-
for (const extension of extensions) {
|
|
871
|
-
const contextTag = contextSpecificTagNumber(extension);
|
|
872
|
-
if (contextTag === 2) {
|
|
873
|
-
parsed.pceId = bytesToHex(readContextOctetString(extension, "PCE ID"));
|
|
874
|
-
continue;
|
|
875
|
-
}
|
|
876
|
-
if (contextTag === 3) {
|
|
877
|
-
parsed.fmspc = bytesToHex(readContextOctetString(extension, "FMSPC"));
|
|
878
|
-
continue;
|
|
879
|
-
}
|
|
880
|
-
if (contextTag === 1) {
|
|
881
|
-
const tcb = parsePckTcbExtension(extension);
|
|
882
|
-
parsed.sgxTcbSvns = tcb.sgxTcbSvns;
|
|
883
|
-
parsed.pceSvn = tcb.pceSvn;
|
|
884
|
-
parsed.cpuSvn = tcb.cpuSvn;
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
|
-
const children = derChildren(expectDerTag(extension, 48, "SGX extension item"));
|
|
888
|
-
if (children.length !== 2) {
|
|
889
|
-
throw err("TCB_STATUS_REJECTED", "PCK SGX extension item does not contain OID and value");
|
|
890
|
-
}
|
|
891
|
-
const oid = readDerOid(children[0]);
|
|
892
|
-
const value = children[1];
|
|
893
|
-
if (oid === INTEL_PCK_EXTENSION_PCE_ID_OID) {
|
|
894
|
-
parsed.pceId = bytesToHex(readPckChoiceOctetString(value, 2, "PCE ID"));
|
|
895
|
-
} else if (oid === INTEL_PCK_EXTENSION_FMSPC_OID) {
|
|
896
|
-
parsed.fmspc = bytesToHex(readPckChoiceOctetString(value, 3, "FMSPC"));
|
|
897
|
-
} else if (oid === INTEL_PCK_EXTENSION_TCB_OID) {
|
|
898
|
-
const tcb = parsePckTcbExtension(value);
|
|
899
|
-
parsed.sgxTcbSvns = tcb.sgxTcbSvns;
|
|
900
|
-
parsed.pceSvn = tcb.pceSvn;
|
|
901
|
-
parsed.cpuSvn = tcb.cpuSvn;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
if (parsed.fmspc === void 0 || parsed.pceId === void 0 || parsed.sgxTcbSvns === void 0 || parsed.pceSvn === void 0 || parsed.cpuSvn === void 0) {
|
|
905
|
-
throw err("TCB_STATUS_REJECTED", "PCK SGX extension is missing FMSPC, PCE ID, or TCB fields");
|
|
906
|
-
}
|
|
907
|
-
return parsed;
|
|
908
|
-
}
|
|
909
|
-
function parsePckTcbExtension(value) {
|
|
910
|
-
const tcbItems = pckTcbChildren(value);
|
|
911
|
-
const sgxTcbSvns = [];
|
|
912
|
-
let pceSvn;
|
|
913
|
-
let cpuSvn;
|
|
914
|
-
for (const item of tcbItems) {
|
|
915
|
-
const contextTag = contextSpecificTagNumber(item);
|
|
916
|
-
if (contextTag !== null) {
|
|
917
|
-
if (contextTag === 16) {
|
|
918
|
-
pceSvn = readContextInteger(item, "PCESVN");
|
|
919
|
-
} else if (contextTag === 17) {
|
|
920
|
-
cpuSvn = readContextOctetString(item, "CPUSVN");
|
|
921
|
-
} else if (contextTag >= 0 && contextTag <= 15) {
|
|
922
|
-
sgxTcbSvns[contextTag] = readContextInteger(item, `SGX TCB component ${contextTag + 1}`);
|
|
923
|
-
}
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
const children = derChildren(expectDerTag(item, 48, "PCK TCB item"));
|
|
927
|
-
if (children.length !== 2) {
|
|
928
|
-
throw err("TCB_STATUS_REJECTED", "PCK TCB item does not contain OID and value");
|
|
929
|
-
}
|
|
930
|
-
const oid = readDerOid(children[0]);
|
|
931
|
-
const valueTlv = children[1];
|
|
932
|
-
if (oid === INTEL_PCK_EXTENSION_PCESVN_OID) {
|
|
933
|
-
pceSvn = readPckChoiceInteger(valueTlv, 16, "PCESVN");
|
|
934
|
-
} else if (oid === INTEL_PCK_EXTENSION_CPUSVN_OID) {
|
|
935
|
-
cpuSvn = readPckChoiceOctetString(valueTlv, 17, "CPUSVN");
|
|
936
|
-
} else if (oid.startsWith(INTEL_PCK_EXTENSION_SGX_TCB_COMPONENT_OID_PREFIX)) {
|
|
937
|
-
const index = Number.parseInt(
|
|
938
|
-
oid.slice(INTEL_PCK_EXTENSION_SGX_TCB_COMPONENT_OID_PREFIX.length),
|
|
939
|
-
10
|
|
940
|
-
);
|
|
941
|
-
if (!Number.isInteger(index) || index < 1 || index > 16) {
|
|
942
|
-
throw err("TCB_STATUS_REJECTED", `PCK TCB component OID ${oid} is out of range`);
|
|
943
|
-
}
|
|
944
|
-
sgxTcbSvns[index - 1] = readPckChoiceInteger(
|
|
945
|
-
valueTlv,
|
|
946
|
-
index - 1,
|
|
947
|
-
`SGX TCB component ${index}`
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
if (sgxTcbSvns.length !== 16 || Array.from({ length: 16 }, (_, index) => sgxTcbSvns[index]).some((svn) => svn === void 0)) {
|
|
952
|
-
throw err("TCB_STATUS_REJECTED", "PCK TCB extension is missing SGX TCB components");
|
|
953
|
-
}
|
|
954
|
-
if (pceSvn === void 0 || cpuSvn === void 0) {
|
|
955
|
-
throw err("TCB_STATUS_REJECTED", "PCK TCB extension is missing PCESVN or CPUSVN");
|
|
956
|
-
}
|
|
957
|
-
if (cpuSvn.length !== 16) {
|
|
958
|
-
throw err("TCB_STATUS_REJECTED", `PCK CPUSVN has ${cpuSvn.length} bytes, expected 16`);
|
|
959
|
-
}
|
|
960
|
-
return { sgxTcbSvns, pceSvn, cpuSvn };
|
|
961
|
-
}
|
|
962
|
-
function extractPckExtensionHex(cert, oid, expectedBytes, label) {
|
|
963
|
-
const ext = cert.getExtension(oid);
|
|
964
|
-
if (!ext) {
|
|
965
|
-
throw err("TCB_STATUS_REJECTED", `PCK leaf is missing ${label} extension ${oid}`);
|
|
966
|
-
}
|
|
967
|
-
const decoded = unwrapDerOctetStringIfPresent(new Uint8Array(ext.value));
|
|
968
|
-
if (decoded.length !== expectedBytes) {
|
|
969
|
-
throw err(
|
|
970
|
-
"TCB_STATUS_REJECTED",
|
|
971
|
-
`PCK ${label} extension has ${decoded.length} bytes, expected ${expectedBytes}`
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
return bytesToHex(decoded);
|
|
975
|
-
}
|
|
976
|
-
function extractPckIntegerExtension(cert, oid, label) {
|
|
977
|
-
const ext = cert.getExtension(oid);
|
|
978
|
-
if (!ext) {
|
|
979
|
-
throw err("TCB_STATUS_REJECTED", `PCK leaf is missing ${label} extension ${oid}`);
|
|
980
|
-
}
|
|
981
|
-
return readDerInteger(readDerTlv(new Uint8Array(ext.value), 0), label);
|
|
982
|
-
}
|
|
983
|
-
function readDerTlv(source, start) {
|
|
984
|
-
if (start + 2 > source.length) {
|
|
985
|
-
throw err("TCB_STATUS_REJECTED", "truncated DER value");
|
|
986
|
-
}
|
|
987
|
-
const tag = source[start];
|
|
988
|
-
let offset = start + 1;
|
|
989
|
-
const firstLength = source[offset++];
|
|
990
|
-
let length;
|
|
991
|
-
if ((firstLength & 128) === 0) {
|
|
992
|
-
length = firstLength;
|
|
993
|
-
} else {
|
|
994
|
-
const octets = firstLength & 127;
|
|
995
|
-
if (octets === 0 || octets > 4 || offset + octets > source.length) {
|
|
996
|
-
throw err("TCB_STATUS_REJECTED", "unsupported DER length");
|
|
997
|
-
}
|
|
998
|
-
length = 0;
|
|
999
|
-
for (let i = 0; i < octets; i++) {
|
|
1000
|
-
length = length << 8 | source[offset++];
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
const valueStart = offset;
|
|
1004
|
-
const valueEnd = valueStart + length;
|
|
1005
|
-
if (valueEnd > source.length) {
|
|
1006
|
-
throw err("TCB_STATUS_REJECTED", "DER value length exceeds buffer");
|
|
1007
|
-
}
|
|
1008
|
-
return { tag, start, valueStart, valueEnd, end: valueEnd, source };
|
|
1009
|
-
}
|
|
1010
|
-
function expectDerTag(tlv, tag, label) {
|
|
1011
|
-
if (tlv.tag !== tag) {
|
|
1012
|
-
throw err("TCB_STATUS_REJECTED", `${label} has DER tag 0x${tlv.tag.toString(16)}`);
|
|
1013
|
-
}
|
|
1014
|
-
return tlv;
|
|
1015
|
-
}
|
|
1016
|
-
function derChildren(tlv) {
|
|
1017
|
-
const children = [];
|
|
1018
|
-
let offset = tlv.valueStart;
|
|
1019
|
-
while (offset < tlv.valueEnd) {
|
|
1020
|
-
const child = readDerTlv(tlv.source, offset);
|
|
1021
|
-
if (child.end > tlv.valueEnd) {
|
|
1022
|
-
throw err("TCB_STATUS_REJECTED", "DER child exceeds parent");
|
|
1023
|
-
}
|
|
1024
|
-
children.push(child);
|
|
1025
|
-
offset = child.end;
|
|
1026
|
-
}
|
|
1027
|
-
return children;
|
|
1028
|
-
}
|
|
1029
|
-
function readDerOid(tlv) {
|
|
1030
|
-
expectDerTag(tlv, 6, "OID");
|
|
1031
|
-
const bytes = tlv.source.subarray(tlv.valueStart, tlv.valueEnd);
|
|
1032
|
-
if (bytes.length === 0) {
|
|
1033
|
-
throw err("TCB_STATUS_REJECTED", "empty DER OID");
|
|
1034
|
-
}
|
|
1035
|
-
const parts = [Math.floor(bytes[0] / 40), bytes[0] % 40];
|
|
1036
|
-
let value = 0;
|
|
1037
|
-
for (const byte of bytes.subarray(1)) {
|
|
1038
|
-
value = value << 7 | byte & 127;
|
|
1039
|
-
if ((byte & 128) === 0) {
|
|
1040
|
-
parts.push(value);
|
|
1041
|
-
value = 0;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
if (value !== 0) {
|
|
1045
|
-
throw err("TCB_STATUS_REJECTED", "truncated DER OID");
|
|
1046
|
-
}
|
|
1047
|
-
return parts.join(".");
|
|
1048
|
-
}
|
|
1049
|
-
function readDerInteger(tlv, label) {
|
|
1050
|
-
expectDerTag(tlv, 2, label);
|
|
1051
|
-
const bytes = tlv.source.subarray(tlv.valueStart, tlv.valueEnd);
|
|
1052
|
-
if (bytes.length === 0 || (bytes[0] & 128) !== 0) {
|
|
1053
|
-
throw err("TCB_STATUS_REJECTED", `${label} is not a positive DER integer`);
|
|
1054
|
-
}
|
|
1055
|
-
let value = 0;
|
|
1056
|
-
for (const byte of bytes) value = value << 8 | byte;
|
|
1057
|
-
return value;
|
|
1058
|
-
}
|
|
1059
|
-
function readDerOctetString(tlv, label) {
|
|
1060
|
-
expectDerTag(tlv, 4, label);
|
|
1061
|
-
return tlv.source.subarray(tlv.valueStart, tlv.valueEnd);
|
|
1062
|
-
}
|
|
1063
|
-
function pckTcbChildren(value) {
|
|
1064
|
-
if (contextSpecificTagNumber(value) === 1) {
|
|
1065
|
-
if ((value.tag & 32) === 0) {
|
|
1066
|
-
throw err("TCB_STATUS_REJECTED", "PCK TCB field is not constructed");
|
|
1067
|
-
}
|
|
1068
|
-
const children = derChildren(value);
|
|
1069
|
-
if (children.length === 1 && children[0].tag === 48) {
|
|
1070
|
-
return derChildren(children[0]);
|
|
1071
|
-
}
|
|
1072
|
-
return children;
|
|
1073
|
-
}
|
|
1074
|
-
return derChildren(expectDerTag(value, 48, "PCK TCB extension"));
|
|
1075
|
-
}
|
|
1076
|
-
function readPckChoiceInteger(tlv, expectedTag, label) {
|
|
1077
|
-
const contextTag = contextSpecificTagNumber(tlv);
|
|
1078
|
-
if (contextTag !== null) {
|
|
1079
|
-
if (contextTag !== expectedTag) {
|
|
1080
|
-
throw err(
|
|
1081
|
-
"TCB_STATUS_REJECTED",
|
|
1082
|
-
`${label} uses context tag ${contextTag}, expected ${expectedTag}`
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1085
|
-
return readContextInteger(tlv, label);
|
|
1086
|
-
}
|
|
1087
|
-
return readDerInteger(tlv, label);
|
|
1088
|
-
}
|
|
1089
|
-
function readPckChoiceOctetString(tlv, expectedTag, label) {
|
|
1090
|
-
const contextTag = contextSpecificTagNumber(tlv);
|
|
1091
|
-
if (contextTag !== null) {
|
|
1092
|
-
if (contextTag !== expectedTag) {
|
|
1093
|
-
throw err(
|
|
1094
|
-
"TCB_STATUS_REJECTED",
|
|
1095
|
-
`${label} uses context tag ${contextTag}, expected ${expectedTag}`
|
|
1096
|
-
);
|
|
1097
|
-
}
|
|
1098
|
-
return readContextOctetString(tlv, label);
|
|
1099
|
-
}
|
|
1100
|
-
return readDerOctetString(tlv, label);
|
|
1101
|
-
}
|
|
1102
|
-
function contextSpecificTagNumber(tlv) {
|
|
1103
|
-
if ((tlv.tag & 192) !== 128) return null;
|
|
1104
|
-
const tagNumber = tlv.tag & 31;
|
|
1105
|
-
return tagNumber === 31 ? null : tagNumber;
|
|
1106
|
-
}
|
|
1107
|
-
function readContextInner(tlv) {
|
|
1108
|
-
if ((tlv.tag & 32) === 0) {
|
|
1109
|
-
throw err("TCB_STATUS_REJECTED", "PCK context-specific field is not constructed");
|
|
1110
|
-
}
|
|
1111
|
-
const children = derChildren(tlv);
|
|
1112
|
-
if (children.length !== 1) {
|
|
1113
|
-
throw err("TCB_STATUS_REJECTED", "PCK context-specific field has multiple values");
|
|
1114
|
-
}
|
|
1115
|
-
return children[0];
|
|
1116
|
-
}
|
|
1117
|
-
function readContextInteger(tlv, label) {
|
|
1118
|
-
const inner = (tlv.tag & 32) !== 0 ? readContextInner(tlv) : tlv;
|
|
1119
|
-
if ((tlv.tag & 32) !== 0) return readDerInteger(inner, label);
|
|
1120
|
-
return readDerIntegerValue(inner.source.subarray(inner.valueStart, inner.valueEnd), label);
|
|
1121
|
-
}
|
|
1122
|
-
function readContextOctetString(tlv, label) {
|
|
1123
|
-
const inner = (tlv.tag & 32) !== 0 ? readContextInner(tlv) : tlv;
|
|
1124
|
-
if ((tlv.tag & 32) !== 0) return readDerOctetString(inner, label);
|
|
1125
|
-
return inner.source.subarray(inner.valueStart, inner.valueEnd);
|
|
1126
|
-
}
|
|
1127
|
-
function readDerIntegerValue(bytes, label) {
|
|
1128
|
-
if (bytes.length === 0 || (bytes[0] & 128) !== 0) {
|
|
1129
|
-
throw err("TCB_STATUS_REJECTED", `${label} is not a positive DER integer`);
|
|
1130
|
-
}
|
|
1131
|
-
let value = 0;
|
|
1132
|
-
for (const byte of bytes) value = value << 8 | byte;
|
|
1133
|
-
return value;
|
|
1134
|
-
}
|
|
1135
|
-
function unwrapDerOctetStringIfPresent(bytes) {
|
|
1136
|
-
if (bytes.length >= 2 && bytes[0] === 4) {
|
|
1137
|
-
const tlv = readDerTlv(bytes, 0);
|
|
1138
|
-
if (tlv.end === bytes.length) return bytes.subarray(tlv.valueStart, tlv.valueEnd);
|
|
1139
|
-
}
|
|
1140
|
-
return bytes;
|
|
1141
|
-
}
|
|
1142
|
-
function hexToBytes(hex) {
|
|
1143
|
-
if (hex.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(hex)) {
|
|
1144
|
-
throw err("DCAP_COLLATERAL_PARSE", "invalid hex string");
|
|
1145
|
-
}
|
|
1146
|
-
const out = new Uint8Array(hex.length / 2);
|
|
1147
|
-
for (let i = 0; i < out.length; i++) {
|
|
1148
|
-
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1149
|
-
}
|
|
1150
|
-
return out;
|
|
1151
|
-
}
|
|
1152
|
-
var MAA_JKU_PATTERN = /^https:\/\/[a-z0-9-]+\.[a-z0-9]+\.attest\.azure\.net\/certs$/;
|
|
1153
|
-
function b64urlDecodeStr(s) {
|
|
1154
|
-
const padded = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4);
|
|
1155
|
-
return atob(padded);
|
|
1156
|
-
}
|
|
1157
|
-
function b64urlDecodeBytes(s) {
|
|
1158
|
-
const decoded = b64urlDecodeStr(s);
|
|
1159
|
-
const out = new Uint8Array(decoded.length);
|
|
1160
|
-
for (let i = 0; i < decoded.length; i++) out[i] = decoded.charCodeAt(i);
|
|
1161
|
-
return out;
|
|
1162
|
-
}
|
|
1163
|
-
async function verifyAzureMaaToken(token, policy, timeoutMs = DEFAULT_AZURE_JWKS_TIMEOUT_MS) {
|
|
1164
|
-
const parts = token.split(".");
|
|
1165
|
-
if (parts.length !== 3) {
|
|
1166
|
-
throw err("MAA_TOKEN_PARSE", `MAA token is not a valid JWT (${parts.length} parts)`);
|
|
1167
|
-
}
|
|
1168
|
-
const [headerB64, payloadB64, sigB64] = parts;
|
|
1169
|
-
let header;
|
|
1170
|
-
let payload;
|
|
1171
|
-
try {
|
|
1172
|
-
header = JSON.parse(b64urlDecodeStr(headerB64));
|
|
1173
|
-
payload = JSON.parse(b64urlDecodeStr(payloadB64));
|
|
1174
|
-
} catch (e) {
|
|
1175
|
-
throw err("MAA_TOKEN_PARSE", `failed to decode MAA JWT header/payload: ${e}`);
|
|
1176
|
-
}
|
|
1177
|
-
const exp = payload["exp"];
|
|
1178
|
-
if (typeof exp === "number" && Math.floor(Date.now() / 1e3) > exp) {
|
|
1179
|
-
throw err("MAA_TOKEN_EXPIRED", `MAA JWT expired at ${new Date(exp * 1e3).toISOString()}`);
|
|
1180
|
-
}
|
|
1181
|
-
const jku = header["jku"];
|
|
1182
|
-
const kid = header["kid"];
|
|
1183
|
-
if (!jku || !kid) {
|
|
1184
|
-
throw err("MAA_TOKEN_PARSE", "MAA JWT header missing jku or kid");
|
|
1185
|
-
}
|
|
1186
|
-
if (payload["iss"] !== policy.expectedIssuer) {
|
|
1187
|
-
throw err(
|
|
1188
|
-
"MAA_ISSUER_MISMATCH",
|
|
1189
|
-
`MAA JWT issuer ${String(payload["iss"])} does not match pinned ${policy.expectedIssuer}`
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
if (jku !== policy.expectedJwksUrl) {
|
|
1193
|
-
throw err(
|
|
1194
|
-
"MAA_JWKS_MISMATCH",
|
|
1195
|
-
`MAA JWT jku ${jku} does not match pinned ${policy.expectedJwksUrl}`
|
|
1196
|
-
);
|
|
1197
|
-
}
|
|
1198
|
-
if (!MAA_JKU_PATTERN.test(policy.expectedJwksUrl)) {
|
|
1199
|
-
throw err(
|
|
1200
|
-
"MAA_JWKS_MISMATCH",
|
|
1201
|
-
`pinned MAA JWKS URL does not match Azure attestation domain: ${policy.expectedJwksUrl}`
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
if (payload["x-ms-policy-hash"] !== policy.expectedPolicyHash) {
|
|
1205
|
-
throw err(
|
|
1206
|
-
"MAA_POLICY_HASH_MISMATCH",
|
|
1207
|
-
`MAA policy hash ${String(payload["x-ms-policy-hash"])} does not match pinned ${policy.expectedPolicyHash}`
|
|
1208
|
-
);
|
|
1209
|
-
}
|
|
1210
|
-
let jwks;
|
|
1211
|
-
try {
|
|
1212
|
-
jwks = await fetchMaaJwks(policy.expectedJwksUrl, timeoutMs);
|
|
1213
|
-
} catch (e) {
|
|
1214
|
-
throw err(
|
|
1215
|
-
"MAA_TOKEN_SIGNATURE",
|
|
1216
|
-
`failed to fetch MAA JWKS from ${policy.expectedJwksUrl}: ${e}`
|
|
1217
|
-
);
|
|
1218
|
-
}
|
|
1219
|
-
const jwk = jwks.keys.find((k) => k.kid === kid);
|
|
1220
|
-
if (!jwk) {
|
|
1221
|
-
throw err("MAA_TOKEN_SIGNATURE", `no key with kid=${kid} found in MAA JWKS`);
|
|
1222
|
-
}
|
|
1223
|
-
let sigPubKey;
|
|
1224
|
-
try {
|
|
1225
|
-
sigPubKey = await crypto.subtle.importKey(
|
|
1226
|
-
"jwk",
|
|
1227
|
-
{ kty: "RSA", n: jwk.n, e: jwk.e, alg: "RS256", ext: true },
|
|
1228
|
-
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
1229
|
-
false,
|
|
1230
|
-
["verify"]
|
|
1231
|
-
);
|
|
1232
|
-
} catch (e) {
|
|
1233
|
-
throw err("MAA_TOKEN_SIGNATURE", `failed to import MAA JWK: ${e}`);
|
|
1234
|
-
}
|
|
1235
|
-
const msg = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
1236
|
-
const sig = b64urlDecodeBytes(sigB64);
|
|
1237
|
-
const valid = await crypto.subtle.verify(
|
|
1238
|
-
{ name: "RSASSA-PKCS1-v1_5" },
|
|
1239
|
-
sigPubKey,
|
|
1240
|
-
sig,
|
|
1241
|
-
msg
|
|
1242
|
-
);
|
|
1243
|
-
if (!valid) {
|
|
1244
|
-
throw err("MAA_TOKEN_SIGNATURE", "MAA JWT RS256 signature verification failed");
|
|
1245
|
-
}
|
|
1246
|
-
if (payload["x-ms-compliance-status"] !== "azure-compliant-cvm") {
|
|
1247
|
-
throw err(
|
|
1248
|
-
"MAA_COMPLIANCE_MISMATCH",
|
|
1249
|
-
`MAA compliance status: "${payload["x-ms-compliance-status"]}" (expected "azure-compliant-cvm")`
|
|
1250
|
-
);
|
|
1251
|
-
}
|
|
1252
|
-
if (payload["tdx_td_attributes_debug"] !== false) {
|
|
1253
|
-
throw err("MAA_DEBUG_BIT_SET", "MAA token reports TDX debug bit set");
|
|
1254
|
-
}
|
|
1255
|
-
const reportData = payload["tdx_report_data"];
|
|
1256
|
-
if (typeof reportData !== "string" || !/^[0-9a-f]{128}$/i.test(reportData)) {
|
|
1257
|
-
throw err("MAA_REPORTDATA_MISMATCH", "MAA token missing valid 64-byte tdx_report_data claim");
|
|
1258
|
-
}
|
|
1259
|
-
return {
|
|
1260
|
-
reportData: reportData.toLowerCase(),
|
|
1261
|
-
mrtd: readMaaMeasurement(payload, "tdx_mrtd"),
|
|
1262
|
-
rtmr0: readMaaMeasurement(payload, "tdx_rtmr0"),
|
|
1263
|
-
rtmr1: readMaaMeasurement(payload, "tdx_rtmr1"),
|
|
1264
|
-
rtmr2: readMaaMeasurement(payload, "tdx_rtmr2"),
|
|
1265
|
-
rtmr3: readMaaMeasurement(payload, "tdx_rtmr3")
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
function readMaaMeasurement(payload, claim) {
|
|
1269
|
-
const value = payload[claim];
|
|
1270
|
-
if (typeof value !== "string" || !/^[0-9a-f]{96}$/i.test(value)) {
|
|
1271
|
-
throw err("MAA_REPORTDATA_MISMATCH", `MAA token missing valid ${claim} claim`);
|
|
1272
|
-
}
|
|
1273
|
-
return value.toLowerCase();
|
|
1274
|
-
}
|
|
1275
|
-
function assertMaaMeasurementsMatchDcap(maa, dcap) {
|
|
1276
|
-
const fields = ["mrtd", "rtmr0", "rtmr1", "rtmr2", "rtmr3"];
|
|
1277
|
-
for (const field of fields) {
|
|
1278
|
-
if (maa[field] !== dcap[field]) {
|
|
1279
|
-
throw err(
|
|
1280
|
-
"MAA_REPORTDATA_MISMATCH",
|
|
1281
|
-
`MAA ${field} ${maa[field]} does not match DCAP quote ${field.toUpperCase()} ${dcap[field]}`
|
|
1282
|
-
);
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
async function fetchMaaJwks(url, timeoutMs) {
|
|
1287
|
-
const boundedTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_AZURE_JWKS_TIMEOUT_MS;
|
|
1288
|
-
const controller = new AbortController();
|
|
1289
|
-
let timeoutId = null;
|
|
1290
|
-
const timeout = new Promise((_, reject) => {
|
|
1291
|
-
timeoutId = setTimeout(() => {
|
|
1292
|
-
controller.abort();
|
|
1293
|
-
reject(new Error(`timed out after ${boundedTimeoutMs}ms`));
|
|
1294
|
-
}, boundedTimeoutMs);
|
|
1295
|
-
});
|
|
1296
|
-
try {
|
|
1297
|
-
return await Promise.race([
|
|
1298
|
-
(async () => {
|
|
1299
|
-
const resp = await fetch(url, { signal: controller.signal });
|
|
1300
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
1301
|
-
return await resp.json();
|
|
1302
|
-
})(),
|
|
1303
|
-
timeout
|
|
1304
|
-
]);
|
|
1305
|
-
} finally {
|
|
1306
|
-
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
async function verifyTpmQuoteNonce(tpmQuoteB64, akPubKey, expectedNonce, expectedNoiseStaticPub, expectedBinaryHash) {
|
|
1310
|
-
let raw;
|
|
1311
|
-
try {
|
|
1312
|
-
raw = base64ToBytes(tpmQuoteB64);
|
|
1313
|
-
} catch (e) {
|
|
1314
|
-
throw err("TPM_QUOTE_SIGNATURE", `failed to decode tpm_quote: ${e}`);
|
|
1315
|
-
}
|
|
1316
|
-
if (raw.length < 8) {
|
|
1317
|
-
throw err("TPM_QUOTE_SIGNATURE", "tpm_quote too short");
|
|
1318
|
-
}
|
|
1319
|
-
const msgLen = new DataView(raw.buffer, raw.byteOffset).getUint32(
|
|
1320
|
-
0,
|
|
1321
|
-
true
|
|
1322
|
-
/* little-endian */
|
|
1323
|
-
);
|
|
1324
|
-
if (4 + msgLen > raw.length) {
|
|
1325
|
-
throw err("TPM_QUOTE_SIGNATURE", `tpm_quote msg_len=${msgLen} exceeds buffer`);
|
|
1326
|
-
}
|
|
1327
|
-
const tpmsAttest = raw.slice(4, 4 + msgLen);
|
|
1328
|
-
const sigBlob = raw.slice(4 + msgLen);
|
|
1329
|
-
if (sigBlob.length < 6) {
|
|
1330
|
-
throw err("TPM_QUOTE_SIGNATURE", "TPMT_SIGNATURE too short");
|
|
1331
|
-
}
|
|
1332
|
-
const sigSize = sigBlob[4] << 8 | sigBlob[5];
|
|
1333
|
-
const sigBytes = sigBlob.slice(6, 6 + sigSize);
|
|
1334
|
-
if (sigBytes.length !== sigSize) {
|
|
1335
|
-
throw err(
|
|
1336
|
-
"TPM_QUOTE_SIGNATURE",
|
|
1337
|
-
`TPMT_SIGNATURE truncated: need ${sigSize}, have ${sigBytes.length}`
|
|
1338
|
-
);
|
|
1339
|
-
}
|
|
1340
|
-
const valid = await crypto.subtle.verify(
|
|
1341
|
-
{ name: "RSASSA-PKCS1-v1_5" },
|
|
1342
|
-
akPubKey,
|
|
1343
|
-
sigBytes,
|
|
1344
|
-
tpmsAttest
|
|
1345
|
-
);
|
|
1346
|
-
if (!valid) {
|
|
1347
|
-
throw err("TPM_QUOTE_SIGNATURE", "TPM AK RSA signature verification failed");
|
|
1348
|
-
}
|
|
1349
|
-
const qdInput = new Uint8Array(
|
|
1350
|
-
expectedNonce.length + expectedNoiseStaticPub.length + expectedBinaryHash.length
|
|
1351
|
-
);
|
|
1352
|
-
qdInput.set(expectedNonce, 0);
|
|
1353
|
-
qdInput.set(expectedNoiseStaticPub, expectedNonce.length);
|
|
1354
|
-
qdInput.set(expectedBinaryHash, expectedNonce.length + expectedNoiseStaticPub.length);
|
|
1355
|
-
const expectedQd = new Uint8Array(await crypto.subtle.digest("SHA-256", qdInput));
|
|
1356
|
-
const extraData = readTpmsAttestExtraData(tpmsAttest);
|
|
1357
|
-
if (!constantTimeEq(extraData, expectedQd)) {
|
|
1358
|
-
throw err(
|
|
1359
|
-
"TPM_NONCE_MISMATCH",
|
|
1360
|
-
"TPMS_ATTEST extraData does not equal SHA-256(nonce || noise_pubkey || binary_hash)"
|
|
1361
|
-
);
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
function readTpmsAttestExtraData(tpmsAttest) {
|
|
1365
|
-
if (tpmsAttest.length < 10) {
|
|
1366
|
-
throw err("TPM_QUOTE_SIGNATURE", "TPMS_ATTEST too short");
|
|
1367
|
-
}
|
|
1368
|
-
const view = new DataView(tpmsAttest.buffer, tpmsAttest.byteOffset, tpmsAttest.byteLength);
|
|
1369
|
-
const magic = view.getUint32(0, false);
|
|
1370
|
-
if (magic !== TPM_GENERATED_VALUE) {
|
|
1371
|
-
throw err(
|
|
1372
|
-
"TPM_QUOTE_SIGNATURE",
|
|
1373
|
-
`TPMS_ATTEST magic ${magic.toString(16)} is not TPM_GENERATED_VALUE`
|
|
1374
|
-
);
|
|
1375
|
-
}
|
|
1376
|
-
const attestType = view.getUint16(4, false);
|
|
1377
|
-
if (attestType !== TPM_ST_ATTEST_QUOTE) {
|
|
1378
|
-
throw err("TPM_QUOTE_SIGNATURE", `TPMS_ATTEST type ${attestType.toString(16)} is not quote`);
|
|
1379
|
-
}
|
|
1380
|
-
let offset = 6;
|
|
1381
|
-
offset = readTpm2b(tpmsAttest, offset, "qualifiedSigner").nextOffset;
|
|
1382
|
-
return readTpm2b(tpmsAttest, offset, "extraData").data;
|
|
1383
|
-
}
|
|
1384
|
-
function readTpm2b(bytes, offset, label) {
|
|
1385
|
-
if (offset + 2 > bytes.length) {
|
|
1386
|
-
throw err("TPM_QUOTE_SIGNATURE", `TPMS_ATTEST truncated before ${label} size`);
|
|
1387
|
-
}
|
|
1388
|
-
const size = bytes[offset] << 8 | bytes[offset + 1];
|
|
1389
|
-
const start = offset + 2;
|
|
1390
|
-
const end = start + size;
|
|
1391
|
-
if (end > bytes.length) {
|
|
1392
|
-
throw err("TPM_QUOTE_SIGNATURE", `TPMS_ATTEST truncated in ${label}`);
|
|
1393
|
-
}
|
|
1394
|
-
return { data: bytes.slice(start, end), nextOffset: end };
|
|
1395
|
-
}
|
|
1396
|
-
function base64ToBytes(b64) {
|
|
1397
|
-
const normalized = b64.replace(/-/g, "+").replace(/_/g, "/");
|
|
1398
|
-
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
1399
|
-
const binaryStr = atob(padded);
|
|
1400
|
-
const out = new Uint8Array(binaryStr.length);
|
|
1401
|
-
for (let i = 0; i < binaryStr.length; i++) out[i] = binaryStr.charCodeAt(i);
|
|
1402
|
-
return out;
|
|
1403
|
-
}
|
|
1404
|
-
async function verifyAzureTdxAttestation(params) {
|
|
1405
|
-
const { raw, response, expectedNonce, expectedNoiseStaticPub, policy, timeoutMs } = params;
|
|
1406
|
-
const body = raw.subarray(BODY_OFFSET, BODY_OFFSET + BODY_SIZE);
|
|
1407
|
-
const dcapMeasurements = parseMeasurements(body);
|
|
1408
|
-
const reportData = body.subarray(
|
|
1409
|
-
BODY_REPORT_DATA_OFF,
|
|
1410
|
-
BODY_REPORT_DATA_OFF + BODY_REPORT_DATA_LEN
|
|
1411
|
-
);
|
|
1412
|
-
await verifyDcapQuoteAndCollateral({ raw, body, response, policy });
|
|
1413
|
-
assertDebugBitClear(body);
|
|
1414
|
-
if (!response.maa_token) {
|
|
1415
|
-
throw err("MAA_TOKEN_MISSING", "azure-tdx-dcap response missing maa_token");
|
|
1416
|
-
}
|
|
1417
|
-
if (!policy.azureMaa) {
|
|
1418
|
-
throw err(
|
|
1419
|
-
"MAA_TRUST_POLICY_MISSING",
|
|
1420
|
-
"azure-tdx-dcap attestation requires pinned MAA issuer, JWKS URL, and policy hash"
|
|
1421
|
-
);
|
|
1422
|
-
}
|
|
1423
|
-
const measurements = await verifyAzureMaaToken(response.maa_token, policy.azureMaa, timeoutMs);
|
|
1424
|
-
const reportDataHex = bytesToHex(reportData);
|
|
1425
|
-
if (measurements.reportData !== reportDataHex) {
|
|
1426
|
-
throw err(
|
|
1427
|
-
"MAA_REPORTDATA_MISMATCH",
|
|
1428
|
-
`MAA tdx_report_data ${measurements.reportData} does not match DCAP REPORTDATA ${reportDataHex}`
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
assertMaaMeasurementsMatchDcap(measurements, dcapMeasurements);
|
|
1432
|
-
if (!response.hcl_keys_json) {
|
|
1433
|
-
throw err("HCL_KEYS_MISSING", "azure-tdx-dcap response missing hcl_keys_json");
|
|
1434
|
-
}
|
|
1435
|
-
let hclKeysBytes;
|
|
1436
|
-
try {
|
|
1437
|
-
hclKeysBytes = base64ToBytes(response.hcl_keys_json);
|
|
1438
|
-
} catch (e) {
|
|
1439
|
-
throw err("HCL_KEYS_PARSE", `failed to decode hcl_keys_json base64: ${e}`);
|
|
1440
|
-
}
|
|
1441
|
-
const reportData0_32 = reportData.subarray(0, 32);
|
|
1442
|
-
const hclKeysHash = new Uint8Array(
|
|
1443
|
-
await crypto.subtle.digest("SHA-256", hclKeysBytes)
|
|
1444
|
-
);
|
|
1445
|
-
if (!constantTimeEq(hclKeysHash, reportData0_32)) {
|
|
1446
|
-
throw err(
|
|
1447
|
-
"HCL_KEYS_HASH_MISMATCH",
|
|
1448
|
-
"SHA-256(hcl_keys_json) does not match DCAP quote REPORTDATA[0:32]"
|
|
1449
|
-
);
|
|
1450
|
-
}
|
|
1451
|
-
let hclKeys;
|
|
1452
|
-
try {
|
|
1453
|
-
hclKeys = JSON.parse(new TextDecoder().decode(hclKeysBytes));
|
|
1454
|
-
} catch (e) {
|
|
1455
|
-
throw err("HCL_KEYS_PARSE", `failed to parse hcl_keys_json: ${e}`);
|
|
1456
|
-
}
|
|
1457
|
-
const keysArray = hclKeys["keys"];
|
|
1458
|
-
const akJwk = Array.isArray(keysArray) ? keysArray.find((k) => k["kid"] === "HCLAkPub") : void 0;
|
|
1459
|
-
if (!akJwk) {
|
|
1460
|
-
throw err("HCL_KEYS_PARSE", 'hcl_keys_json does not contain a key with kid="HCLAkPub"');
|
|
1461
|
-
}
|
|
1462
|
-
let akPubKey;
|
|
1463
|
-
try {
|
|
1464
|
-
akPubKey = await crypto.subtle.importKey(
|
|
1465
|
-
"jwk",
|
|
1466
|
-
{ kty: "RSA", n: akJwk["n"], e: akJwk["e"], ext: true },
|
|
1467
|
-
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
1468
|
-
false,
|
|
1469
|
-
["verify"]
|
|
1470
|
-
);
|
|
1471
|
-
} catch (e) {
|
|
1472
|
-
throw err("HCL_KEYS_PARSE", `failed to import HCLAkPub from hcl_keys_json: ${e}`);
|
|
1473
|
-
}
|
|
1474
|
-
if (!response.tpm_quote) {
|
|
1475
|
-
throw err("TPM_QUOTE_MISSING", "azure-tdx-dcap response missing tpm_quote");
|
|
1476
|
-
}
|
|
1477
|
-
if (!/^[0-9a-f]{64}$/i.test(response.binary_hash)) {
|
|
1478
|
-
throw err("BINARY_HASH_FORMAT", "binary_hash field is not a 32-byte hex digest");
|
|
1479
|
-
}
|
|
1480
|
-
if (response.mode === "prod" && policy.expectedBinaryHash === void 0) {
|
|
1481
|
-
throw err(
|
|
1482
|
-
"BINARY_HASH_MISMATCH",
|
|
1483
|
-
"production Azure attestation requires a pinned coordinator binary hash"
|
|
1484
|
-
);
|
|
1485
|
-
}
|
|
1486
|
-
if (policy.expectedBinaryHash !== void 0 && response.binary_hash.toLowerCase() !== policy.expectedBinaryHash.toLowerCase()) {
|
|
1487
|
-
throw err(
|
|
1488
|
-
"BINARY_HASH_MISMATCH",
|
|
1489
|
-
`binary hash ${response.binary_hash.toLowerCase()} does not match the approved release pin`
|
|
1490
|
-
);
|
|
1491
|
-
}
|
|
1492
|
-
await verifyTpmQuoteNonce(
|
|
1493
|
-
response.tpm_quote,
|
|
1494
|
-
akPubKey,
|
|
1495
|
-
expectedNonce,
|
|
1496
|
-
expectedNoiseStaticPub,
|
|
1497
|
-
hexToBytes(response.binary_hash)
|
|
1498
|
-
);
|
|
1499
|
-
if (response.nonce.toLowerCase() !== bytesToHex(expectedNonce)) {
|
|
1500
|
-
throw err("NONCE_FIELD_MISMATCH", `JSON nonce ${response.nonce} does not match verifier nonce`);
|
|
1501
|
-
}
|
|
1502
|
-
if (response.pubkey.toLowerCase() !== bytesToHex(expectedNoiseStaticPub)) {
|
|
1503
|
-
throw err(
|
|
1504
|
-
"PUBKEY_FIELD_MISMATCH",
|
|
1505
|
-
"attestation pubkey field does not match Noise static pubkey from handshake"
|
|
1506
|
-
);
|
|
1507
|
-
}
|
|
1508
|
-
if (!/^[0-9a-f]{64}$/i.test(response.binary_hash)) {
|
|
1509
|
-
throw err("BINARY_HASH_FORMAT", "binary_hash field is not a 32-byte hex digest");
|
|
1510
|
-
}
|
|
1511
|
-
if (policy.requiredMode !== void 0 && response.mode !== policy.requiredMode) {
|
|
1512
|
-
throw err(
|
|
1513
|
-
"MODE_MISMATCH",
|
|
1514
|
-
`attestation mode ${response.mode} does not match required ${policy.requiredMode}`
|
|
1515
|
-
);
|
|
1516
|
-
}
|
|
1517
|
-
if (response.mode === "prod") {
|
|
1518
|
-
if ((policy.expectedMrtd === void 0 || policy.expectedMrtd === null) && policy.allowUnpinnedProdMrtdForRelease !== true) {
|
|
1519
|
-
throw err(
|
|
1520
|
-
"MRTD_MISMATCH",
|
|
1521
|
-
"production attestation requires a pinned MRTD but none was configured"
|
|
1522
|
-
);
|
|
1523
|
-
}
|
|
1524
|
-
if (policy.expectedMrtd !== void 0 && policy.expectedMrtd !== null && dcapMeasurements.mrtd !== policy.expectedMrtd.toLowerCase()) {
|
|
1525
|
-
throw err(
|
|
1526
|
-
"MRTD_MISMATCH",
|
|
1527
|
-
`MRTD ${dcapMeasurements.mrtd} does not match pinned ${policy.expectedMrtd.toLowerCase()}`
|
|
1528
|
-
);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
return {
|
|
1532
|
-
mode: response.mode,
|
|
1533
|
-
binaryHash: response.binary_hash,
|
|
1534
|
-
runtimeProfile: response.runtime_profile,
|
|
1535
|
-
mrtd: dcapMeasurements.mrtd,
|
|
1536
|
-
rtmr0: dcapMeasurements.rtmr0,
|
|
1537
|
-
rtmr1: dcapMeasurements.rtmr1,
|
|
1538
|
-
rtmr2: dcapMeasurements.rtmr2,
|
|
1539
|
-
rtmr3: dcapMeasurements.rtmr3,
|
|
1540
|
-
pubkeyHex: response.pubkey.toLowerCase(),
|
|
1541
|
-
localAttestation: false
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
function verifyLocalAttestation(params) {
|
|
1545
|
-
const { response, expectedNonce, expectedNoiseStaticPub, policy } = params;
|
|
1546
|
-
if (policy.allowLocalAttestation !== true) {
|
|
1547
|
-
throw err(
|
|
1548
|
-
"LOCAL_ATTESTATION_DISABLED",
|
|
1549
|
-
"local attestation response received but local attestation is not enabled"
|
|
1550
|
-
);
|
|
1551
|
-
}
|
|
1552
|
-
if (policy.requiredMode !== void 0 && response.mode !== policy.requiredMode) {
|
|
1553
|
-
throw err(
|
|
1554
|
-
"MODE_MISMATCH",
|
|
1555
|
-
`attestation mode ${response.mode} does not match required ${policy.requiredMode}`
|
|
1556
|
-
);
|
|
1557
|
-
}
|
|
1558
|
-
if (response.mode !== "dev") {
|
|
1559
|
-
throw err(
|
|
1560
|
-
"LOCAL_ATTESTATION_DISABLED",
|
|
1561
|
-
"local attestation is only accepted for dev mode responses"
|
|
1562
|
-
);
|
|
1563
|
-
}
|
|
1564
|
-
if (response.nonce.toLowerCase() !== bytesToHex(expectedNonce)) {
|
|
1565
|
-
throw err("NONCE_FIELD_MISMATCH", `JSON nonce ${response.nonce} does not match verifier nonce`);
|
|
1566
|
-
}
|
|
1567
|
-
if (!/^[0-9a-f]{64}$/i.test(response.binary_hash)) {
|
|
1568
|
-
throw err("BINARY_HASH_FORMAT", "binary_hash field is not a 32-byte hex digest");
|
|
1569
|
-
}
|
|
1570
|
-
const expectedPubHex = bytesToHex(expectedNoiseStaticPub);
|
|
1571
|
-
if (response.pubkey.toLowerCase() !== expectedPubHex) {
|
|
1572
|
-
throw err(
|
|
1573
|
-
"PUBKEY_FIELD_MISMATCH",
|
|
1574
|
-
"local attestation pubkey field does not match Noise static pubkey from handshake"
|
|
1575
|
-
);
|
|
1576
|
-
}
|
|
1577
|
-
return {
|
|
1578
|
-
mode: response.mode,
|
|
1579
|
-
binaryHash: response.binary_hash.toLowerCase(),
|
|
1580
|
-
runtimeProfile: response.runtime_profile,
|
|
1581
|
-
mrtd: "local-dev-no-mrtd",
|
|
1582
|
-
rtmr0: "local-dev-no-rtmr0",
|
|
1583
|
-
rtmr1: "local-dev-no-rtmr1",
|
|
1584
|
-
rtmr2: "local-dev-no-rtmr2",
|
|
1585
|
-
rtmr3: "local-dev-no-rtmr3",
|
|
1586
|
-
pubkeyHex: response.pubkey.toLowerCase(),
|
|
1587
|
-
localAttestation: true
|
|
1588
|
-
};
|
|
1589
|
-
}
|
|
1590
|
-
function decodeQuoteB64(b64) {
|
|
1591
|
-
let bin;
|
|
1592
|
-
try {
|
|
1593
|
-
bin = atob(b64);
|
|
1594
|
-
} catch (e) {
|
|
1595
|
-
throw err("QUOTE_DECODE", `quote field is not valid base64: ${e}`);
|
|
1596
|
-
}
|
|
1597
|
-
const out = new Uint8Array(bin.length);
|
|
1598
|
-
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
1599
|
-
return out;
|
|
1600
|
-
}
|
|
1601
|
-
function ensureStructure(raw) {
|
|
1602
|
-
if (raw.length < HEADER_SIZE + BODY_SIZE + 4) {
|
|
1603
|
-
throw err(
|
|
1604
|
-
"QUOTE_STRUCTURE",
|
|
1605
|
-
`quote too short: ${raw.length} bytes (need >= ${HEADER_SIZE + BODY_SIZE + 4})`
|
|
1606
|
-
);
|
|
1607
|
-
}
|
|
1608
|
-
const version = readU16LE(raw, HEADER_VERSION_OFF);
|
|
1609
|
-
const attKey = readU16LE(raw, HEADER_ATT_KEY_OFF);
|
|
1610
|
-
const teeType = readU32LE(raw, HEADER_TEE_TYPE_OFF);
|
|
1611
|
-
if (version !== 4) {
|
|
1612
|
-
throw err("QUOTE_STRUCTURE", `unsupported quote version ${version}`);
|
|
1613
|
-
}
|
|
1614
|
-
if (teeType !== TDX_TEE_TYPE) {
|
|
1615
|
-
throw err("QUOTE_STRUCTURE", `not a TDX quote (TEE type 0x${teeType.toString(16)})`);
|
|
1616
|
-
}
|
|
1617
|
-
if (attKey !== ECDSA_P256_KEY_TYPE) {
|
|
1618
|
-
throw err(
|
|
1619
|
-
"QUOTE_STRUCTURE",
|
|
1620
|
-
`unsupported attestation key type ${attKey} (expected ECDSA-P256)`
|
|
1621
|
-
);
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
function readSigData(raw) {
|
|
1625
|
-
const sigDataLen = readU32LE(raw, SIG_DATA_LEN_OFFSET);
|
|
1626
|
-
const sigStart = SIG_DATA_LEN_OFFSET + 4;
|
|
1627
|
-
if (raw.length < sigStart + sigDataLen) {
|
|
1628
|
-
throw err("QUOTE_STRUCTURE", "quote truncated in signature data");
|
|
1629
|
-
}
|
|
1630
|
-
return raw.subarray(sigStart, sigStart + sigDataLen);
|
|
1631
|
-
}
|
|
1632
|
-
function parseMeasurements(body) {
|
|
1633
|
-
const m = (off) => bytesToHex(body.subarray(off, off + MEASUREMENT_LEN));
|
|
1634
|
-
return {
|
|
1635
|
-
mrtd: m(BODY_MRTD_OFF),
|
|
1636
|
-
rtmr0: m(BODY_RTMR0_OFF),
|
|
1637
|
-
rtmr1: m(BODY_RTMR1_OFF),
|
|
1638
|
-
rtmr2: m(BODY_RTMR2_OFF),
|
|
1639
|
-
rtmr3: m(BODY_RTMR3_OFF),
|
|
1640
|
-
tdAttr: bytesToHex(body.subarray(BODY_TD_ATTR_OFF, BODY_TD_ATTR_OFF + 8))
|
|
1641
|
-
};
|
|
1642
|
-
}
|
|
1643
|
-
var PEM_BEGIN = "-----BEGIN CERTIFICATE-----";
|
|
1644
|
-
var PEM_END = "-----END CERTIFICATE-----";
|
|
1645
|
-
var PEM_BEGIN_BYTES = new TextEncoder().encode(PEM_BEGIN);
|
|
1646
|
-
var PEM_END_BYTES = new TextEncoder().encode(PEM_END);
|
|
1647
|
-
function findBytes(hay, needle, from = 0) {
|
|
1648
|
-
if (needle.length === 0) return from;
|
|
1649
|
-
const last = hay.length - needle.length;
|
|
1650
|
-
outer: for (let i = from; i <= last; i++) {
|
|
1651
|
-
for (let j = 0; j < needle.length; j++) {
|
|
1652
|
-
if (hay[i + j] !== needle[j]) continue outer;
|
|
1653
|
-
}
|
|
1654
|
-
return i;
|
|
1655
|
-
}
|
|
1656
|
-
return -1;
|
|
1657
|
-
}
|
|
1658
|
-
function parseQeCertificationData(sigData) {
|
|
1659
|
-
if (sigData.length < SIG_QE_CERT_DATA_OFF + SIG_QE_CERT_DATA_HEADER_SIZE) {
|
|
1660
|
-
throw err("QUOTE_STRUCTURE", "signature data truncated before QE certification data header");
|
|
1661
|
-
}
|
|
1662
|
-
const outerCertType = readU16LE(sigData, SIG_QE_CERT_DATA_OFF);
|
|
1663
|
-
const outerCertSize = readU32LE(sigData, SIG_QE_CERT_DATA_OFF + 2);
|
|
1664
|
-
if (outerCertType !== QE_CERT_TYPE_QE_REPORT_CERT_DATA) {
|
|
1665
|
-
throw err(
|
|
1666
|
-
"QUOTE_STRUCTURE",
|
|
1667
|
-
`unexpected QE certification data type ${outerCertType} (expected ${QE_CERT_TYPE_QE_REPORT_CERT_DATA})`
|
|
1668
|
-
);
|
|
1669
|
-
}
|
|
1670
|
-
const outerDataStart = SIG_QE_CERT_DATA_OFF + SIG_QE_CERT_DATA_HEADER_SIZE;
|
|
1671
|
-
const outerDataEnd = outerDataStart + outerCertSize;
|
|
1672
|
-
if (sigData.length < outerDataEnd) {
|
|
1673
|
-
throw err("QUOTE_STRUCTURE", "signature data truncated in outer QE certification data");
|
|
1674
|
-
}
|
|
1675
|
-
const outerData = sigData.subarray(outerDataStart, outerDataEnd);
|
|
1676
|
-
const qeReportSigOff = SIG_QE_REPORT_SIZE;
|
|
1677
|
-
const qeAuthSizeOff = qeReportSigOff + SIG_QE_REPORT_SIG_SIZE;
|
|
1678
|
-
const qeAuthDataOff = qeAuthSizeOff + 2;
|
|
1679
|
-
if (outerData.length < qeAuthDataOff + SIG_QE_CERT_DATA_HEADER_SIZE) {
|
|
1680
|
-
throw err(
|
|
1681
|
-
"QUOTE_STRUCTURE",
|
|
1682
|
-
"signature data truncated before QE auth_data and nested certification data"
|
|
1683
|
-
);
|
|
1684
|
-
}
|
|
1685
|
-
const qeReport = outerData.subarray(0, SIG_QE_REPORT_SIZE);
|
|
1686
|
-
const qeReportSig = outerData.subarray(qeReportSigOff, qeReportSigOff + SIG_QE_REPORT_SIG_SIZE);
|
|
1687
|
-
const authSize = readU16LE(outerData, qeAuthSizeOff);
|
|
1688
|
-
const qeAuthDataEnd = qeAuthDataOff + authSize;
|
|
1689
|
-
if (outerData.length < qeAuthDataEnd + SIG_QE_CERT_DATA_HEADER_SIZE) {
|
|
1690
|
-
throw err("QUOTE_STRUCTURE", "signature data truncated in QE auth_data");
|
|
1691
|
-
}
|
|
1692
|
-
const qeAuthData = outerData.subarray(qeAuthDataOff, qeAuthDataEnd);
|
|
1693
|
-
const nestedCertType = readU16LE(outerData, qeAuthDataEnd);
|
|
1694
|
-
const nestedCertSize = readU32LE(outerData, qeAuthDataEnd + 2);
|
|
1695
|
-
if (nestedCertType !== QE_CERT_TYPE_PCK_CHAIN) {
|
|
1696
|
-
throw err(
|
|
1697
|
-
"PCK_CHAIN_MISSING",
|
|
1698
|
-
`cert data type ${nestedCertType} is not PCK chain (expected ${QE_CERT_TYPE_PCK_CHAIN})`
|
|
1699
|
-
);
|
|
1700
|
-
}
|
|
1701
|
-
const nestedDataEnd = qeAuthDataEnd + SIG_QE_CERT_DATA_HEADER_SIZE + nestedCertSize;
|
|
1702
|
-
if (outerData.length < nestedDataEnd) {
|
|
1703
|
-
throw err("QUOTE_STRUCTURE", "signature data truncated in nested QE certification data");
|
|
1704
|
-
}
|
|
1705
|
-
return {
|
|
1706
|
-
qeReport,
|
|
1707
|
-
qeReportSig,
|
|
1708
|
-
qeAuthData,
|
|
1709
|
-
qeCertificationData: outerData.subarray(qeAuthDataEnd, nestedDataEnd)
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
function extractPckChain(qeCertificationData) {
|
|
1713
|
-
if (qeCertificationData.length < SIG_QE_CERT_DATA_HEADER_SIZE) {
|
|
1714
|
-
throw err("PCK_CHAIN_PARSE", "nested QE certification data header truncated");
|
|
1715
|
-
}
|
|
1716
|
-
const certType = readU16LE(qeCertificationData, 0);
|
|
1717
|
-
const certSize = readU32LE(qeCertificationData, 2);
|
|
1718
|
-
if (certType !== QE_CERT_TYPE_PCK_CHAIN) {
|
|
1719
|
-
throw err(
|
|
1720
|
-
"PCK_CHAIN_MISSING",
|
|
1721
|
-
`cert data type ${certType} is not PCK chain (expected ${QE_CERT_TYPE_PCK_CHAIN})`
|
|
1722
|
-
);
|
|
1723
|
-
}
|
|
1724
|
-
if (qeCertificationData.length < SIG_QE_CERT_DATA_HEADER_SIZE + certSize) {
|
|
1725
|
-
throw err("PCK_CHAIN_PARSE", "nested QE certification data truncated");
|
|
1726
|
-
}
|
|
1727
|
-
const certBytes = qeCertificationData.subarray(
|
|
1728
|
-
SIG_QE_CERT_DATA_HEADER_SIZE,
|
|
1729
|
-
SIG_QE_CERT_DATA_HEADER_SIZE + certSize
|
|
1730
|
-
);
|
|
1731
|
-
const certs = [];
|
|
1732
|
-
let cursor = 0;
|
|
1733
|
-
while (cursor < certBytes.length) {
|
|
1734
|
-
const begin = findBytes(certBytes, PEM_BEGIN_BYTES, cursor);
|
|
1735
|
-
if (begin === -1) break;
|
|
1736
|
-
const endTag = findBytes(certBytes, PEM_END_BYTES, begin);
|
|
1737
|
-
if (endTag === -1) {
|
|
1738
|
-
throw err("PCK_CHAIN_PARSE", "unterminated PEM block");
|
|
1739
|
-
}
|
|
1740
|
-
const end = endTag + PEM_END_BYTES.length;
|
|
1741
|
-
const pem = new TextDecoder("utf-8").decode(certBytes.subarray(begin, end));
|
|
1742
|
-
try {
|
|
1743
|
-
certs.push(new x509.X509Certificate(pem));
|
|
1744
|
-
} catch (e) {
|
|
1745
|
-
throw err("PCK_CHAIN_PARSE", `failed to parse PEM cert: ${e}`);
|
|
1746
|
-
}
|
|
1747
|
-
cursor = end;
|
|
1748
|
-
}
|
|
1749
|
-
if (certs.length === 0) {
|
|
1750
|
-
throw err("PCK_CHAIN_MISSING", "no parseable certs in PCK chain");
|
|
1751
|
-
}
|
|
1752
|
-
if (certs.length < 3) {
|
|
1753
|
-
throw err(
|
|
1754
|
-
"PCK_CHAIN_MISSING",
|
|
1755
|
-
`PCK chain has only ${certs.length} cert(s); Intel DCAP requires leaf + CA + Root (min 3)`
|
|
1756
|
-
);
|
|
1757
|
-
}
|
|
1758
|
-
return certs;
|
|
1759
|
-
}
|
|
1760
|
-
async function verifyChainSignatures(chain) {
|
|
1761
|
-
const now = /* @__PURE__ */ new Date();
|
|
1762
|
-
for (let i = 0; i < chain.length - 1; i++) {
|
|
1763
|
-
const cert = chain[i];
|
|
1764
|
-
if (now < cert.notBefore) {
|
|
1765
|
-
throw err(
|
|
1766
|
-
"PCK_CHAIN_SIGNATURE",
|
|
1767
|
-
`cert[${i}] (${cert.subject}) is not yet valid (notBefore=${cert.notBefore.toISOString()})`
|
|
1768
|
-
);
|
|
1769
|
-
}
|
|
1770
|
-
if (now > cert.notAfter) {
|
|
1771
|
-
throw err(
|
|
1772
|
-
"PCK_CHAIN_SIGNATURE",
|
|
1773
|
-
`cert[${i}] (${cert.subject}) has expired (notAfter=${cert.notAfter.toISOString()})`
|
|
1774
|
-
);
|
|
1775
|
-
}
|
|
1776
|
-
let ok;
|
|
1777
|
-
try {
|
|
1778
|
-
ok = await chain[i].verify({ publicKey: chain[i + 1].publicKey });
|
|
1779
|
-
} catch (e) {
|
|
1780
|
-
throw err(
|
|
1781
|
-
"PCK_CHAIN_SIGNATURE",
|
|
1782
|
-
`cert[${i}] (${chain[i].subject}) signature verification threw: ${e}`
|
|
1783
|
-
);
|
|
1784
|
-
}
|
|
1785
|
-
if (!ok) {
|
|
1786
|
-
throw err(
|
|
1787
|
-
"PCK_CHAIN_SIGNATURE",
|
|
1788
|
-
`cert[${i}] (${chain[i].subject}) signature does not verify against issuer cert[${i + 1}]`
|
|
1789
|
-
);
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
const root = chain[chain.length - 1];
|
|
1793
|
-
if (now < root.notBefore) {
|
|
1794
|
-
throw err(
|
|
1795
|
-
"PCK_CHAIN_SIGNATURE",
|
|
1796
|
-
`root cert (${root.subject}) is not yet valid (notBefore=${root.notBefore.toISOString()})`
|
|
1797
|
-
);
|
|
1798
|
-
}
|
|
1799
|
-
if (now > root.notAfter) {
|
|
1800
|
-
throw err(
|
|
1801
|
-
"PCK_CHAIN_SIGNATURE",
|
|
1802
|
-
`root cert (${root.subject}) has expired (notAfter=${root.notAfter.toISOString()})`
|
|
1803
|
-
);
|
|
1804
|
-
}
|
|
1805
|
-
let rootOk;
|
|
1806
|
-
try {
|
|
1807
|
-
rootOk = await root.verify({ publicKey: root.publicKey });
|
|
1808
|
-
} catch (e) {
|
|
1809
|
-
throw err(
|
|
1810
|
-
"PCK_CHAIN_SIGNATURE",
|
|
1811
|
-
`root cert (${root.subject}) self-signature verification threw: ${e}`
|
|
1812
|
-
);
|
|
1813
|
-
}
|
|
1814
|
-
if (!rootOk) {
|
|
1815
|
-
throw err("PCK_CHAIN_SIGNATURE", `root cert (${root.subject}) is not self-signed`);
|
|
1816
|
-
}
|
|
1817
|
-
for (let i = 1; i < chain.length; i++) {
|
|
1818
|
-
const bc = chain[i].getExtension(x509.BasicConstraintsExtension);
|
|
1819
|
-
if (!bc || !bc.ca) {
|
|
1820
|
-
throw err(
|
|
1821
|
-
"PCK_CHAIN_SIGNATURE",
|
|
1822
|
-
`cert[${i}] (${chain[i].subject}) is not a CA cert (BasicConstraints.cA is not true)`
|
|
1823
|
-
);
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
async function leafSpki(leaf) {
|
|
1828
|
-
const key = await leaf.publicKey.export();
|
|
1829
|
-
const spki = await crypto.subtle.exportKey("spki", key);
|
|
1830
|
-
return new Uint8Array(spki);
|
|
1831
|
-
}
|
|
1832
|
-
function rawXyToSpki(xy) {
|
|
1833
|
-
if (xy.length !== 64) {
|
|
1834
|
-
throw err("QUOTE_STRUCTURE", `attestation pubkey must be 64 bytes (got ${xy.length})`);
|
|
1835
|
-
}
|
|
1836
|
-
const uncompressed = new Uint8Array(65);
|
|
1837
|
-
uncompressed[0] = 4;
|
|
1838
|
-
uncompressed.set(xy, 1);
|
|
1839
|
-
const prefix = new Uint8Array([
|
|
1840
|
-
48,
|
|
1841
|
-
89,
|
|
1842
|
-
48,
|
|
1843
|
-
19,
|
|
1844
|
-
6,
|
|
1845
|
-
7,
|
|
1846
|
-
42,
|
|
1847
|
-
134,
|
|
1848
|
-
72,
|
|
1849
|
-
206,
|
|
1850
|
-
61,
|
|
1851
|
-
2,
|
|
1852
|
-
1,
|
|
1853
|
-
6,
|
|
1854
|
-
8,
|
|
1855
|
-
42,
|
|
1856
|
-
134,
|
|
1857
|
-
72,
|
|
1858
|
-
206,
|
|
1859
|
-
61,
|
|
1860
|
-
3,
|
|
1861
|
-
1,
|
|
1862
|
-
7,
|
|
1863
|
-
3,
|
|
1864
|
-
66,
|
|
1865
|
-
0
|
|
1866
|
-
]);
|
|
1867
|
-
return concat(prefix, uncompressed);
|
|
1868
|
-
}
|
|
1869
|
-
async function verifyP256Signature(spki, rsSig, data, failureReason, what) {
|
|
1870
|
-
if (rsSig.length !== 64) {
|
|
1871
|
-
throw err(failureReason, `${what}: signature must be 64 bytes (r||s)`);
|
|
1872
|
-
}
|
|
1873
|
-
let key;
|
|
1874
|
-
try {
|
|
1875
|
-
key = await crypto.subtle.importKey(
|
|
1876
|
-
"spki",
|
|
1877
|
-
spki,
|
|
1878
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1879
|
-
false,
|
|
1880
|
-
["verify"]
|
|
1881
|
-
);
|
|
1882
|
-
} catch (e) {
|
|
1883
|
-
throw err(failureReason, `${what}: failed to import key: ${e}`);
|
|
1884
|
-
}
|
|
1885
|
-
const ok = await crypto.subtle.verify(
|
|
1886
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
1887
|
-
key,
|
|
1888
|
-
rsSig,
|
|
1889
|
-
data
|
|
1890
|
-
);
|
|
1891
|
-
if (!ok) {
|
|
1892
|
-
throw err(failureReason, `${what}: ECDSA verification failed`);
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
async function sha256(data) {
|
|
1896
|
-
return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
|
|
1897
|
-
}
|
|
1898
|
-
async function sha256Hex(data) {
|
|
1899
|
-
return bytesToHex(await sha256(data));
|
|
1900
|
-
}
|
|
1901
|
-
function readU16LE(buf, off) {
|
|
1902
|
-
return buf[off] | buf[off + 1] << 8;
|
|
1903
|
-
}
|
|
1904
|
-
function readU32LE(buf, off) {
|
|
1905
|
-
return (buf[off] | buf[off + 1] << 8 | buf[off + 2] << 16 | buf[off + 3] << 24) >>> 0;
|
|
1906
|
-
}
|
|
1907
|
-
function constantTimeEq(a, b) {
|
|
1908
|
-
if (a.length !== b.length) return false;
|
|
1909
|
-
let diff = 0;
|
|
1910
|
-
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
1911
|
-
return diff === 0;
|
|
1912
|
-
}
|
|
1913
|
-
function concat(a, b) {
|
|
1914
|
-
const out = new Uint8Array(a.length + b.length);
|
|
1915
|
-
out.set(a, 0);
|
|
1916
|
-
out.set(b, a.length);
|
|
1917
|
-
return out;
|
|
1918
|
-
}
|
|
1919
|
-
function bytesToHex(b) {
|
|
1920
|
-
let s = "";
|
|
1921
|
-
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
|
|
1922
|
-
return s;
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
export { INTEL_SGX_ROOT_CA_FINGERPRINT_SHA256, assertAttestResponse, verifyAttestation };
|
|
1926
|
-
//# sourceMappingURL=chunk-DIA6U2CV.js.map
|
|
1927
|
-
//# sourceMappingURL=chunk-DIA6U2CV.js.map
|