@layr-labs/ecloud-sdk 0.4.0-dev.1 → 0.4.0-dev.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/VERSION +2 -0
  2. package/dist/attest.cjs +185 -0
  3. package/dist/attest.cjs.map +1 -0
  4. package/dist/attest.d.cts +28 -0
  5. package/dist/attest.d.ts +28 -0
  6. package/dist/attest.js +147 -0
  7. package/dist/attest.js.map +1 -0
  8. package/dist/billing.cjs +1380 -4
  9. package/dist/billing.cjs.map +1 -1
  10. package/dist/billing.d.cts +25 -3
  11. package/dist/billing.d.ts +25 -3
  12. package/dist/billing.js +1380 -4
  13. package/dist/billing.js.map +1 -1
  14. package/dist/browser.cjs +3 -2
  15. package/dist/browser.cjs.map +1 -1
  16. package/dist/browser.d.cts +4 -4
  17. package/dist/browser.d.ts +4 -4
  18. package/dist/browser.js +3 -2
  19. package/dist/browser.js.map +1 -1
  20. package/dist/{compute-DccJLbtV.d.cts → compute-BRDk7QM4.d.cts} +1 -1
  21. package/dist/{compute-DlilmZYC.d.ts → compute-CC55YQ_a.d.ts} +1 -1
  22. package/dist/compute.cjs +8 -3
  23. package/dist/compute.cjs.map +1 -1
  24. package/dist/compute.d.cts +2 -2
  25. package/dist/compute.d.ts +2 -2
  26. package/dist/compute.js +8 -3
  27. package/dist/compute.js.map +1 -1
  28. package/dist/{helpers-D_AbDeP4.d.ts → helpers-BcoV07Me.d.ts} +1 -1
  29. package/dist/{helpers-BNeMZYcY.d.cts → helpers-DdtPaQr9.d.cts} +1 -1
  30. package/dist/{index-DD7ZLbqD.d.cts → index-BEbhrwWl.d.cts} +1 -0
  31. package/dist/{index-DD7ZLbqD.d.ts → index-BEbhrwWl.d.ts} +1 -0
  32. package/dist/index.cjs +330 -95
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +7 -6
  35. package/dist/index.d.ts +7 -6
  36. package/dist/index.js +328 -95
  37. package/dist/index.js.map +1 -1
  38. package/package.json +16 -10
  39. package/tools/kms-client-linux-amd64 +0 -0
  40. package/tools/tls-keygen-linux-amd64 +0 -0
  41. package/LICENSE +0 -7
package/VERSION ADDED
@@ -0,0 +1,2 @@
1
+ version=0.4.0-dev.3
2
+ commit=28778e9448067097cb879bd612b696f99d4b031b
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/attest.ts
31
+ var attest_exports = {};
32
+ __export(attest_exports, {
33
+ AttestClient: () => AttestClient,
34
+ JwtProvider: () => JwtProvider
35
+ });
36
+ module.exports = __toCommonJS(attest_exports);
37
+
38
+ // src/client/modules/attest/attest-client.ts
39
+ var import_node_crypto = require("crypto");
40
+ var import_node_http = __toESM(require("http"), 1);
41
+ var import_jose = require("jose");
42
+ var DEFAULT_SOCKET_PATH = "/run/container_launcher/teeserver.sock";
43
+ var CHALLENGE_PREFIX = "COMPUTE_APP_JWT_REQUEST_RSA_KEY_V1";
44
+ var SIGNATURE_PREFIX = "COMPUTE_APP_KMS_SIGNATURE_V1";
45
+ var NULL_BYTE = Buffer.from([0]);
46
+ var AttestClient = class {
47
+ constructor(config) {
48
+ this.config = config;
49
+ }
50
+ async attest() {
51
+ const { publicKey, privateKey } = (0, import_node_crypto.generateKeyPairSync)("rsa", {
52
+ modulusLength: 4096,
53
+ publicKeyEncoding: { type: "spki", format: "pem" },
54
+ privateKeyEncoding: { type: "pkcs8", format: "pem" }
55
+ });
56
+ const challengeHash = (0, import_node_crypto.createHash)("sha256").update(CHALLENGE_PREFIX).update(NULL_BYTE).update(publicKey).digest();
57
+ const socketPath = this.config.socketPath ?? DEFAULT_SOCKET_PATH;
58
+ const attestationBytes = await this.getAttestation(socketPath, challengeHash);
59
+ const attestResponse = await this.postAttest(attestationBytes, publicKey);
60
+ this.verifySignature(JSON.stringify(attestResponse.data), attestResponse.signature);
61
+ const rsaPrivateKey = await crypto.subtle.importKey(
62
+ "pkcs8",
63
+ pemToBuffer(privateKey),
64
+ { name: "RSA-OAEP", hash: "SHA-256" },
65
+ false,
66
+ ["decrypt"]
67
+ );
68
+ const { plaintext } = await (0, import_jose.compactDecrypt)(
69
+ attestResponse.data.encryptedToken,
70
+ rsaPrivateKey
71
+ );
72
+ const decrypted = JSON.parse(new TextDecoder().decode(plaintext));
73
+ return decrypted.token;
74
+ }
75
+ verifySignature(dataJson, signature) {
76
+ const message = Buffer.concat([
77
+ Buffer.from(SIGNATURE_PREFIX),
78
+ NULL_BYTE,
79
+ Buffer.from(dataJson)
80
+ ]);
81
+ const valid = (0, import_node_crypto.verify)(
82
+ "sha256",
83
+ message,
84
+ this.config.kmsPublicKey,
85
+ Buffer.from(signature, "base64")
86
+ );
87
+ if (!valid) {
88
+ throw new Error("KMS response signature verification failed");
89
+ }
90
+ }
91
+ getAttestation(socketPath, challenge) {
92
+ return new Promise((resolve, reject) => {
93
+ const body = JSON.stringify({ challenge: challenge.toString("base64") });
94
+ const req = import_node_http.default.request(
95
+ {
96
+ socketPath,
97
+ path: "/v1/bound_evidence",
98
+ method: "POST",
99
+ headers: {
100
+ "Content-Type": "application/json",
101
+ "Content-Length": Buffer.byteLength(body)
102
+ }
103
+ },
104
+ (res) => {
105
+ const chunks = [];
106
+ res.on("data", (chunk) => chunks.push(chunk));
107
+ res.on("end", () => {
108
+ if (res.statusCode !== 200) {
109
+ reject(new Error(`TEE attestation failed (${res.statusCode}): ${Buffer.concat(chunks).toString()}`));
110
+ return;
111
+ }
112
+ resolve(Buffer.concat(chunks));
113
+ });
114
+ }
115
+ );
116
+ req.on("error", (err) => reject(new Error(`TEE attestation request failed: ${err.message}`)));
117
+ req.write(body);
118
+ req.end();
119
+ });
120
+ }
121
+ async postAttest(attestationBytes, rsaPublicKey) {
122
+ const url = `${this.config.kmsServerURL}/auth/attest`;
123
+ const body = JSON.stringify({
124
+ version: 3,
125
+ attestation: attestationBytes.toString("base64"),
126
+ rsaKey: rsaPublicKey,
127
+ audience: this.config.audience
128
+ });
129
+ const response = await fetch(url, {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/json" },
132
+ body
133
+ });
134
+ if (!response.ok) {
135
+ const text = await response.text();
136
+ throw new Error(`KMS attest failed (${response.status}): ${text}`);
137
+ }
138
+ return response.json();
139
+ }
140
+ };
141
+ function pemToBuffer(pem) {
142
+ const b64 = pem.replace(/-----[A-Z ]+-----/g, "").replace(/\s/g, "");
143
+ const buf = Buffer.from(b64, "base64");
144
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
145
+ }
146
+
147
+ // src/client/modules/attest/jwt-provider.ts
148
+ var JwtProvider = class {
149
+ constructor(attestClient, bufferSeconds = 30) {
150
+ this.attestClient = attestClient;
151
+ this.bufferSeconds = bufferSeconds;
152
+ }
153
+ async getToken() {
154
+ if (this.cachedToken && !this.isExpiringSoon()) {
155
+ return this.cachedToken;
156
+ }
157
+ if (this.pending) {
158
+ return this.pending;
159
+ }
160
+ this.pending = this.attestClient.attest().then((token) => {
161
+ this.cachedToken = token;
162
+ this.expiresAt = this.decodeJwtExp(token);
163
+ return token;
164
+ }).finally(() => {
165
+ this.pending = void 0;
166
+ });
167
+ return this.pending;
168
+ }
169
+ isExpiringSoon() {
170
+ if (!this.expiresAt) return true;
171
+ return Date.now() / 1e3 >= this.expiresAt - this.bufferSeconds;
172
+ }
173
+ decodeJwtExp(jwt) {
174
+ const payload = jwt.split(".")[1];
175
+ if (!payload) return void 0;
176
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString());
177
+ return decoded.exp;
178
+ }
179
+ };
180
+ // Annotate the CommonJS export names for ESM import in node:
181
+ 0 && (module.exports = {
182
+ AttestClient,
183
+ JwtProvider
184
+ });
185
+ //# sourceMappingURL=attest.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/attest.ts","../src/client/modules/attest/attest-client.ts","../src/client/modules/attest/jwt-provider.ts"],"sourcesContent":["/**\n * Attestation module entry point\n *\n * Import from \"@layr-labs/ecloud-sdk/attest\" for direct access to attest module\n */\n\nexport * from \"./client/modules/attest\";\n","import { generateKeyPairSync, createHash, verify } from 'node:crypto';\nimport http from 'node:http';\nimport { compactDecrypt } from 'jose';\n\nexport interface AttestClientConfig {\n kmsServerURL: string;\n kmsPublicKey: string;\n audience: string;\n socketPath?: string;\n}\n\nconst DEFAULT_SOCKET_PATH = '/run/container_launcher/teeserver.sock';\nconst CHALLENGE_PREFIX = 'COMPUTE_APP_JWT_REQUEST_RSA_KEY_V1';\nconst SIGNATURE_PREFIX = 'COMPUTE_APP_KMS_SIGNATURE_V1';\nconst NULL_BYTE = Buffer.from([0x00]);\n\nexport class AttestClient {\n private config: AttestClientConfig;\n\n constructor(config: AttestClientConfig) {\n this.config = config;\n }\n\n async attest(): Promise<string> {\n const { publicKey, privateKey } = generateKeyPairSync('rsa', {\n modulusLength: 4096,\n publicKeyEncoding: { type: 'spki', format: 'pem' } as const,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' } as const,\n });\n\n const challengeHash = createHash('sha256')\n .update(CHALLENGE_PREFIX)\n .update(NULL_BYTE)\n .update(publicKey)\n .digest();\n\n const socketPath = this.config.socketPath ?? DEFAULT_SOCKET_PATH;\n const attestationBytes = await this.getAttestation(socketPath, challengeHash);\n const attestResponse = await this.postAttest(attestationBytes, publicKey);\n\n this.verifySignature(JSON.stringify(attestResponse.data), attestResponse.signature);\n\n const rsaPrivateKey = await crypto.subtle.importKey(\n 'pkcs8',\n pemToBuffer(privateKey),\n { name: 'RSA-OAEP', hash: 'SHA-256' },\n false,\n ['decrypt'],\n );\n\n const { plaintext } = await compactDecrypt(\n attestResponse.data.encryptedToken,\n rsaPrivateKey,\n );\n\n const decrypted = JSON.parse(new TextDecoder().decode(plaintext));\n return decrypted.token;\n }\n\n private verifySignature(\n dataJson: string,\n signature: string,\n ): void {\n const message = Buffer.concat([\n Buffer.from(SIGNATURE_PREFIX),\n NULL_BYTE,\n Buffer.from(dataJson),\n ]);\n\n const valid = verify(\n 'sha256',\n message,\n this.config.kmsPublicKey,\n Buffer.from(signature, 'base64'),\n );\n\n if (!valid) {\n throw new Error('KMS response signature verification failed');\n }\n }\n\n private getAttestation(socketPath: string, challenge: Buffer): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ challenge: challenge.toString('base64') });\n\n const req = http.request(\n {\n socketPath,\n path: '/v1/bound_evidence',\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(body),\n },\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('end', () => {\n if (res.statusCode !== 200) {\n reject(new Error(`TEE attestation failed (${res.statusCode}): ${Buffer.concat(chunks).toString()}`));\n return;\n }\n resolve(Buffer.concat(chunks));\n });\n },\n );\n\n req.on('error', (err) => reject(new Error(`TEE attestation request failed: ${err.message}`)));\n req.write(body);\n req.end();\n });\n }\n\n private async postAttest(\n attestationBytes: Buffer,\n rsaPublicKey: string,\n ): Promise<{ data: { encryptedToken: string }; signature: string }> {\n const url = `${this.config.kmsServerURL}/auth/attest`;\n const body = JSON.stringify({\n version: 3,\n attestation: attestationBytes.toString('base64'),\n rsaKey: rsaPublicKey,\n audience: this.config.audience,\n });\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`KMS attest failed (${response.status}): ${text}`);\n }\n\n return response.json() as Promise<{ data: { encryptedToken: string }; signature: string }>;\n }\n}\n\nfunction pemToBuffer(pem: string): ArrayBuffer {\n const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\\s/g, '');\n const buf = Buffer.from(b64, 'base64');\n\n // Copy to a fresh ArrayBuffer to avoid offset issues with Buffer's shared pool\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n}\n","import type { AttestClient } from './attest-client';\n\nexport class JwtProvider {\n private attestClient: AttestClient;\n private bufferSeconds: number;\n private cachedToken?: string;\n private expiresAt?: number;\n private pending?: Promise<string>;\n\n constructor(attestClient: AttestClient, bufferSeconds: number = 30) {\n this.attestClient = attestClient;\n this.bufferSeconds = bufferSeconds;\n }\n\n async getToken(): Promise<string> {\n if (this.cachedToken && !this.isExpiringSoon()) {\n return this.cachedToken;\n }\n\n if (this.pending) {\n return this.pending;\n }\n\n this.pending = this.attestClient\n .attest()\n .then((token) => {\n this.cachedToken = token;\n this.expiresAt = this.decodeJwtExp(token);\n return token;\n })\n .finally(() => {\n this.pending = undefined;\n });\n\n return this.pending;\n }\n\n private isExpiringSoon(): boolean {\n if (!this.expiresAt) return true;\n return Date.now() / 1000 >= this.expiresAt - this.bufferSeconds;\n }\n\n private decodeJwtExp(jwt: string): number | undefined {\n const payload = jwt.split('.')[1];\n if (!payload) return undefined;\n\n const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString());\n return decoded.exp;\n }\n}\n"],"mappings":"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","names":["http"]}
@@ -0,0 +1,28 @@
1
+ interface AttestClientConfig {
2
+ kmsServerURL: string;
3
+ kmsPublicKey: string;
4
+ audience: string;
5
+ socketPath?: string;
6
+ }
7
+ declare class AttestClient {
8
+ private config;
9
+ constructor(config: AttestClientConfig);
10
+ attest(): Promise<string>;
11
+ private verifySignature;
12
+ private getAttestation;
13
+ private postAttest;
14
+ }
15
+
16
+ declare class JwtProvider {
17
+ private attestClient;
18
+ private bufferSeconds;
19
+ private cachedToken?;
20
+ private expiresAt?;
21
+ private pending?;
22
+ constructor(attestClient: AttestClient, bufferSeconds?: number);
23
+ getToken(): Promise<string>;
24
+ private isExpiringSoon;
25
+ private decodeJwtExp;
26
+ }
27
+
28
+ export { AttestClient, type AttestClientConfig, JwtProvider };
@@ -0,0 +1,28 @@
1
+ interface AttestClientConfig {
2
+ kmsServerURL: string;
3
+ kmsPublicKey: string;
4
+ audience: string;
5
+ socketPath?: string;
6
+ }
7
+ declare class AttestClient {
8
+ private config;
9
+ constructor(config: AttestClientConfig);
10
+ attest(): Promise<string>;
11
+ private verifySignature;
12
+ private getAttestation;
13
+ private postAttest;
14
+ }
15
+
16
+ declare class JwtProvider {
17
+ private attestClient;
18
+ private bufferSeconds;
19
+ private cachedToken?;
20
+ private expiresAt?;
21
+ private pending?;
22
+ constructor(attestClient: AttestClient, bufferSeconds?: number);
23
+ getToken(): Promise<string>;
24
+ private isExpiringSoon;
25
+ private decodeJwtExp;
26
+ }
27
+
28
+ export { AttestClient, type AttestClientConfig, JwtProvider };
package/dist/attest.js ADDED
@@ -0,0 +1,147 @@
1
+ // src/client/modules/attest/attest-client.ts
2
+ import { generateKeyPairSync, createHash, verify } from "crypto";
3
+ import http from "http";
4
+ import { compactDecrypt } from "jose";
5
+ var DEFAULT_SOCKET_PATH = "/run/container_launcher/teeserver.sock";
6
+ var CHALLENGE_PREFIX = "COMPUTE_APP_JWT_REQUEST_RSA_KEY_V1";
7
+ var SIGNATURE_PREFIX = "COMPUTE_APP_KMS_SIGNATURE_V1";
8
+ var NULL_BYTE = Buffer.from([0]);
9
+ var AttestClient = class {
10
+ constructor(config) {
11
+ this.config = config;
12
+ }
13
+ async attest() {
14
+ const { publicKey, privateKey } = generateKeyPairSync("rsa", {
15
+ modulusLength: 4096,
16
+ publicKeyEncoding: { type: "spki", format: "pem" },
17
+ privateKeyEncoding: { type: "pkcs8", format: "pem" }
18
+ });
19
+ const challengeHash = createHash("sha256").update(CHALLENGE_PREFIX).update(NULL_BYTE).update(publicKey).digest();
20
+ const socketPath = this.config.socketPath ?? DEFAULT_SOCKET_PATH;
21
+ const attestationBytes = await this.getAttestation(socketPath, challengeHash);
22
+ const attestResponse = await this.postAttest(attestationBytes, publicKey);
23
+ this.verifySignature(JSON.stringify(attestResponse.data), attestResponse.signature);
24
+ const rsaPrivateKey = await crypto.subtle.importKey(
25
+ "pkcs8",
26
+ pemToBuffer(privateKey),
27
+ { name: "RSA-OAEP", hash: "SHA-256" },
28
+ false,
29
+ ["decrypt"]
30
+ );
31
+ const { plaintext } = await compactDecrypt(
32
+ attestResponse.data.encryptedToken,
33
+ rsaPrivateKey
34
+ );
35
+ const decrypted = JSON.parse(new TextDecoder().decode(plaintext));
36
+ return decrypted.token;
37
+ }
38
+ verifySignature(dataJson, signature) {
39
+ const message = Buffer.concat([
40
+ Buffer.from(SIGNATURE_PREFIX),
41
+ NULL_BYTE,
42
+ Buffer.from(dataJson)
43
+ ]);
44
+ const valid = verify(
45
+ "sha256",
46
+ message,
47
+ this.config.kmsPublicKey,
48
+ Buffer.from(signature, "base64")
49
+ );
50
+ if (!valid) {
51
+ throw new Error("KMS response signature verification failed");
52
+ }
53
+ }
54
+ getAttestation(socketPath, challenge) {
55
+ return new Promise((resolve, reject) => {
56
+ const body = JSON.stringify({ challenge: challenge.toString("base64") });
57
+ const req = http.request(
58
+ {
59
+ socketPath,
60
+ path: "/v1/bound_evidence",
61
+ method: "POST",
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ "Content-Length": Buffer.byteLength(body)
65
+ }
66
+ },
67
+ (res) => {
68
+ const chunks = [];
69
+ res.on("data", (chunk) => chunks.push(chunk));
70
+ res.on("end", () => {
71
+ if (res.statusCode !== 200) {
72
+ reject(new Error(`TEE attestation failed (${res.statusCode}): ${Buffer.concat(chunks).toString()}`));
73
+ return;
74
+ }
75
+ resolve(Buffer.concat(chunks));
76
+ });
77
+ }
78
+ );
79
+ req.on("error", (err) => reject(new Error(`TEE attestation request failed: ${err.message}`)));
80
+ req.write(body);
81
+ req.end();
82
+ });
83
+ }
84
+ async postAttest(attestationBytes, rsaPublicKey) {
85
+ const url = `${this.config.kmsServerURL}/auth/attest`;
86
+ const body = JSON.stringify({
87
+ version: 3,
88
+ attestation: attestationBytes.toString("base64"),
89
+ rsaKey: rsaPublicKey,
90
+ audience: this.config.audience
91
+ });
92
+ const response = await fetch(url, {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body
96
+ });
97
+ if (!response.ok) {
98
+ const text = await response.text();
99
+ throw new Error(`KMS attest failed (${response.status}): ${text}`);
100
+ }
101
+ return response.json();
102
+ }
103
+ };
104
+ function pemToBuffer(pem) {
105
+ const b64 = pem.replace(/-----[A-Z ]+-----/g, "").replace(/\s/g, "");
106
+ const buf = Buffer.from(b64, "base64");
107
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
108
+ }
109
+
110
+ // src/client/modules/attest/jwt-provider.ts
111
+ var JwtProvider = class {
112
+ constructor(attestClient, bufferSeconds = 30) {
113
+ this.attestClient = attestClient;
114
+ this.bufferSeconds = bufferSeconds;
115
+ }
116
+ async getToken() {
117
+ if (this.cachedToken && !this.isExpiringSoon()) {
118
+ return this.cachedToken;
119
+ }
120
+ if (this.pending) {
121
+ return this.pending;
122
+ }
123
+ this.pending = this.attestClient.attest().then((token) => {
124
+ this.cachedToken = token;
125
+ this.expiresAt = this.decodeJwtExp(token);
126
+ return token;
127
+ }).finally(() => {
128
+ this.pending = void 0;
129
+ });
130
+ return this.pending;
131
+ }
132
+ isExpiringSoon() {
133
+ if (!this.expiresAt) return true;
134
+ return Date.now() / 1e3 >= this.expiresAt - this.bufferSeconds;
135
+ }
136
+ decodeJwtExp(jwt) {
137
+ const payload = jwt.split(".")[1];
138
+ if (!payload) return void 0;
139
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString());
140
+ return decoded.exp;
141
+ }
142
+ };
143
+ export {
144
+ AttestClient,
145
+ JwtProvider
146
+ };
147
+ //# sourceMappingURL=attest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/modules/attest/attest-client.ts","../src/client/modules/attest/jwt-provider.ts"],"sourcesContent":["import { generateKeyPairSync, createHash, verify } from 'node:crypto';\nimport http from 'node:http';\nimport { compactDecrypt } from 'jose';\n\nexport interface AttestClientConfig {\n kmsServerURL: string;\n kmsPublicKey: string;\n audience: string;\n socketPath?: string;\n}\n\nconst DEFAULT_SOCKET_PATH = '/run/container_launcher/teeserver.sock';\nconst CHALLENGE_PREFIX = 'COMPUTE_APP_JWT_REQUEST_RSA_KEY_V1';\nconst SIGNATURE_PREFIX = 'COMPUTE_APP_KMS_SIGNATURE_V1';\nconst NULL_BYTE = Buffer.from([0x00]);\n\nexport class AttestClient {\n private config: AttestClientConfig;\n\n constructor(config: AttestClientConfig) {\n this.config = config;\n }\n\n async attest(): Promise<string> {\n const { publicKey, privateKey } = generateKeyPairSync('rsa', {\n modulusLength: 4096,\n publicKeyEncoding: { type: 'spki', format: 'pem' } as const,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' } as const,\n });\n\n const challengeHash = createHash('sha256')\n .update(CHALLENGE_PREFIX)\n .update(NULL_BYTE)\n .update(publicKey)\n .digest();\n\n const socketPath = this.config.socketPath ?? DEFAULT_SOCKET_PATH;\n const attestationBytes = await this.getAttestation(socketPath, challengeHash);\n const attestResponse = await this.postAttest(attestationBytes, publicKey);\n\n this.verifySignature(JSON.stringify(attestResponse.data), attestResponse.signature);\n\n const rsaPrivateKey = await crypto.subtle.importKey(\n 'pkcs8',\n pemToBuffer(privateKey),\n { name: 'RSA-OAEP', hash: 'SHA-256' },\n false,\n ['decrypt'],\n );\n\n const { plaintext } = await compactDecrypt(\n attestResponse.data.encryptedToken,\n rsaPrivateKey,\n );\n\n const decrypted = JSON.parse(new TextDecoder().decode(plaintext));\n return decrypted.token;\n }\n\n private verifySignature(\n dataJson: string,\n signature: string,\n ): void {\n const message = Buffer.concat([\n Buffer.from(SIGNATURE_PREFIX),\n NULL_BYTE,\n Buffer.from(dataJson),\n ]);\n\n const valid = verify(\n 'sha256',\n message,\n this.config.kmsPublicKey,\n Buffer.from(signature, 'base64'),\n );\n\n if (!valid) {\n throw new Error('KMS response signature verification failed');\n }\n }\n\n private getAttestation(socketPath: string, challenge: Buffer): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ challenge: challenge.toString('base64') });\n\n const req = http.request(\n {\n socketPath,\n path: '/v1/bound_evidence',\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Content-Length': Buffer.byteLength(body),\n },\n },\n (res) => {\n const chunks: Buffer[] = [];\n res.on('data', (chunk: Buffer) => chunks.push(chunk));\n res.on('end', () => {\n if (res.statusCode !== 200) {\n reject(new Error(`TEE attestation failed (${res.statusCode}): ${Buffer.concat(chunks).toString()}`));\n return;\n }\n resolve(Buffer.concat(chunks));\n });\n },\n );\n\n req.on('error', (err) => reject(new Error(`TEE attestation request failed: ${err.message}`)));\n req.write(body);\n req.end();\n });\n }\n\n private async postAttest(\n attestationBytes: Buffer,\n rsaPublicKey: string,\n ): Promise<{ data: { encryptedToken: string }; signature: string }> {\n const url = `${this.config.kmsServerURL}/auth/attest`;\n const body = JSON.stringify({\n version: 3,\n attestation: attestationBytes.toString('base64'),\n rsaKey: rsaPublicKey,\n audience: this.config.audience,\n });\n\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body,\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`KMS attest failed (${response.status}): ${text}`);\n }\n\n return response.json() as Promise<{ data: { encryptedToken: string }; signature: string }>;\n }\n}\n\nfunction pemToBuffer(pem: string): ArrayBuffer {\n const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\\s/g, '');\n const buf = Buffer.from(b64, 'base64');\n\n // Copy to a fresh ArrayBuffer to avoid offset issues with Buffer's shared pool\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);\n}\n","import type { AttestClient } from './attest-client';\n\nexport class JwtProvider {\n private attestClient: AttestClient;\n private bufferSeconds: number;\n private cachedToken?: string;\n private expiresAt?: number;\n private pending?: Promise<string>;\n\n constructor(attestClient: AttestClient, bufferSeconds: number = 30) {\n this.attestClient = attestClient;\n this.bufferSeconds = bufferSeconds;\n }\n\n async getToken(): Promise<string> {\n if (this.cachedToken && !this.isExpiringSoon()) {\n return this.cachedToken;\n }\n\n if (this.pending) {\n return this.pending;\n }\n\n this.pending = this.attestClient\n .attest()\n .then((token) => {\n this.cachedToken = token;\n this.expiresAt = this.decodeJwtExp(token);\n return token;\n })\n .finally(() => {\n this.pending = undefined;\n });\n\n return this.pending;\n }\n\n private isExpiringSoon(): boolean {\n if (!this.expiresAt) return true;\n return Date.now() / 1000 >= this.expiresAt - this.bufferSeconds;\n }\n\n private decodeJwtExp(jwt: string): number | undefined {\n const payload = jwt.split('.')[1];\n if (!payload) return undefined;\n\n const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString());\n return decoded.exp;\n }\n}\n"],"mappings":"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","names":[]}