@vizamodo/aws-sts-core 0.3.2 → 0.3.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.
- package/dist/sts/issue.js +92 -59
- package/package.json +1 -1
package/dist/sts/issue.js
CHANGED
|
@@ -25,6 +25,7 @@ let cachedPrivateKeyBase64 = null;
|
|
|
25
25
|
// ---- cached certificate serial (DER parse is expensive, cert rarely changes) ----
|
|
26
26
|
let cachedCertSerialDec = null;
|
|
27
27
|
let cachedCertSerialSource = null;
|
|
28
|
+
const stsCredentialCache = new Map();
|
|
28
29
|
// ---- shared encoder ----
|
|
29
30
|
const textEncoder = new TextEncoder();
|
|
30
31
|
// Precomputed hex table for fast byte→hex conversion (no per-byte toString alloc)
|
|
@@ -78,22 +79,7 @@ export async function issueAwsCredentials(input) {
|
|
|
78
79
|
// Normalize certificate once so every subsystem (cache, DER parse, headers)
|
|
79
80
|
// uses the exact same canonical string
|
|
80
81
|
const normalizedCert = normalizeCert(certBase64);
|
|
81
|
-
|
|
82
|
-
certBase64: normalizedCert,
|
|
83
|
-
privateKeyPkcs8Base64,
|
|
84
|
-
});
|
|
85
|
-
// 2. Setup constants
|
|
86
|
-
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
87
|
-
const path = PATH;
|
|
88
|
-
const now = new Date();
|
|
89
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
90
|
-
const dateStamp = amzDate.slice(0, 8);
|
|
91
|
-
const service = SERVICE;
|
|
92
|
-
// 3. Chuẩn bị Body & Cert
|
|
93
|
-
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
94
|
-
const payloadHash = await getPayloadHash(body);
|
|
95
|
-
// --- REFACTOR: Làm sạch chứng chỉ chặt chẽ ---
|
|
96
|
-
// const normalizedCert = normalizeCert(certBase64);
|
|
82
|
+
let cacheKey = "";
|
|
97
83
|
// --- REFACTOR: Parser ASN.1 an toàn để lấy Serial Number (DECIMAL) ---
|
|
98
84
|
// --- MINIMAL DER WALK: extract serial number ---
|
|
99
85
|
let certSerialDec;
|
|
@@ -133,11 +119,12 @@ export async function issueAwsCredentials(input) {
|
|
|
133
119
|
if (serial.length > 1 && serial[0] === 0x00) {
|
|
134
120
|
serial = serial.slice(1);
|
|
135
121
|
}
|
|
136
|
-
|
|
122
|
+
// Build BigInt directly from bytes (avoids hex string allocation)
|
|
123
|
+
let serialBig = 0n;
|
|
137
124
|
for (let i = 0; i < serial.length; i++) {
|
|
138
|
-
|
|
125
|
+
serialBig = (serialBig << 8n) | BigInt(serial[i]);
|
|
139
126
|
}
|
|
140
|
-
certSerialDec =
|
|
127
|
+
certSerialDec = serialBig.toString();
|
|
141
128
|
cachedCertSerialDec = certSerialDec;
|
|
142
129
|
cachedCertSerialSource = normalizedCert;
|
|
143
130
|
}
|
|
@@ -146,6 +133,37 @@ export async function issueAwsCredentials(input) {
|
|
|
146
133
|
throw new InternalError("invalid_cert_der");
|
|
147
134
|
}
|
|
148
135
|
}
|
|
136
|
+
// ---- STS credential cache lookup (using certificate serial) ----
|
|
137
|
+
cacheKey = `${region}|${roleArn}|${profileArn}|${certSerialDec}`;
|
|
138
|
+
const cachedEntry = stsCredentialCache.get(cacheKey);
|
|
139
|
+
// Only reuse cached credentials if they still have >1h1m remaining
|
|
140
|
+
const MIN_REMAINING_MS = 61 * 60 * 1000;
|
|
141
|
+
if (cachedEntry) {
|
|
142
|
+
// If credentials already resolved and still valid → reuse
|
|
143
|
+
if (cachedEntry.expiresAt > Date.now() + MIN_REMAINING_MS) {
|
|
144
|
+
return cachedEntry.promise;
|
|
145
|
+
}
|
|
146
|
+
// If a refresh is already in-flight → await same promise
|
|
147
|
+
if (cachedEntry.expiresAt === 0) {
|
|
148
|
+
return cachedEntry.promise;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const { signingKey } = await getSigningMaterial({
|
|
152
|
+
certBase64: normalizedCert,
|
|
153
|
+
privateKeyPkcs8Base64,
|
|
154
|
+
});
|
|
155
|
+
// 2. Setup constants
|
|
156
|
+
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
157
|
+
const path = PATH;
|
|
158
|
+
const now = new Date();
|
|
159
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
160
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
161
|
+
const service = SERVICE;
|
|
162
|
+
// 3. Chuẩn bị Body & Cert
|
|
163
|
+
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
164
|
+
const payloadHash = await getPayloadHash(body);
|
|
165
|
+
// --- REFACTOR: Làm sạch chứng chỉ chặt chẽ ---
|
|
166
|
+
// const normalizedCert = normalizeCert(certBase64);
|
|
149
167
|
// 4. Tính toán Signature
|
|
150
168
|
const baseHeaders = {
|
|
151
169
|
"content-type": "application/json",
|
|
@@ -178,42 +196,58 @@ export async function issueAwsCredentials(input) {
|
|
|
178
196
|
"X-Amz-X509": normalizedCert,
|
|
179
197
|
"Authorization": `AWS4-X509-ECDSA-SHA256 Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`
|
|
180
198
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (!res.ok) {
|
|
188
|
-
const errorBody = await res.text();
|
|
189
|
-
// Debug logs
|
|
190
|
-
console.error("[aws-rejected] Request details", {
|
|
191
|
-
status: res.status,
|
|
192
|
-
response: errorBody,
|
|
193
|
-
region,
|
|
194
|
-
profile,
|
|
195
|
-
amzDate
|
|
199
|
+
const refreshPromise = (async () => {
|
|
200
|
+
try {
|
|
201
|
+
const res = await fetch(`https://${host}${path}`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: finalHeaders,
|
|
204
|
+
body,
|
|
196
205
|
});
|
|
197
|
-
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
const errorBody = await res.text();
|
|
208
|
+
console.error("[aws-rejected] Request details", {
|
|
209
|
+
status: res.status,
|
|
210
|
+
response: errorBody,
|
|
211
|
+
region,
|
|
212
|
+
profile,
|
|
213
|
+
amzDate
|
|
214
|
+
});
|
|
215
|
+
throw new InternalError("aws_rejected");
|
|
216
|
+
}
|
|
217
|
+
const json = await res.json();
|
|
218
|
+
const creds = json?.credentialSet?.[0]?.credentials;
|
|
219
|
+
if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
|
|
220
|
+
console.error("[issueAwsCredentials] malformed AWS credential response", { json });
|
|
221
|
+
throw new InternalError("aws_malformed_credentials");
|
|
222
|
+
}
|
|
223
|
+
const result = {
|
|
224
|
+
accessKeyId: creds.accessKeyId,
|
|
225
|
+
secretAccessKey: creds.secretAccessKey,
|
|
226
|
+
sessionToken: creds.sessionToken,
|
|
227
|
+
expiration: creds.expiration,
|
|
228
|
+
};
|
|
229
|
+
const exp = new Date(creds.expiration).getTime();
|
|
230
|
+
// update cache expiration after success
|
|
231
|
+
const entry = stsCredentialCache.get(cacheKey);
|
|
232
|
+
if (entry) {
|
|
233
|
+
entry.expiresAt = exp;
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
198
236
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
237
|
+
catch (e) {
|
|
238
|
+
// remove broken cache entry so next call can retry
|
|
239
|
+
stsCredentialCache.delete(cacheKey);
|
|
240
|
+
if (e instanceof InternalError)
|
|
241
|
+
throw e;
|
|
242
|
+
throw new InternalError("aws_unreachable");
|
|
204
243
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
catch (e) {
|
|
213
|
-
if (e instanceof InternalError)
|
|
214
|
-
throw e;
|
|
215
|
-
throw new InternalError("aws_unreachable");
|
|
216
|
-
}
|
|
244
|
+
})();
|
|
245
|
+
// store promise immediately to deduplicate concurrent refresh
|
|
246
|
+
stsCredentialCache.set(cacheKey, {
|
|
247
|
+
promise: refreshPromise,
|
|
248
|
+
expiresAt: 0,
|
|
249
|
+
});
|
|
250
|
+
return refreshPromise;
|
|
217
251
|
}
|
|
218
252
|
// ---- helpers ----
|
|
219
253
|
function normalizeCert(raw) {
|
|
@@ -223,15 +257,14 @@ function normalizeCert(raw) {
|
|
|
223
257
|
return raw.trim();
|
|
224
258
|
}
|
|
225
259
|
async function sha256Hex(input) {
|
|
226
|
-
|
|
227
|
-
const hash = await crypto.subtle.digest("SHA-256",
|
|
260
|
+
// Tận dụng textEncoder có sẵn
|
|
261
|
+
const hash = await crypto.subtle.digest("SHA-256", textEncoder.encode(input));
|
|
228
262
|
const bytes = new Uint8Array(hash);
|
|
229
|
-
//
|
|
230
|
-
let
|
|
231
|
-
|
|
232
|
-
out += HEX_TABLE[bytes[i]];
|
|
263
|
+
const hexParts = new Array(32); // SHA-256 luôn là 32 bytes
|
|
264
|
+
for (let i = 0; i < 32; i++) {
|
|
265
|
+
hexParts[i] = HEX_TABLE[bytes[i]];
|
|
233
266
|
}
|
|
234
|
-
return
|
|
267
|
+
return hexParts.join("");
|
|
235
268
|
}
|
|
236
269
|
async function getCanonicalRequestHash(canonicalRequest) {
|
|
237
270
|
return sha256Hex(canonicalRequest);
|