@vizamodo/aws-sts-core 0.3.2 → 0.3.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/dist/sts/issue.js +100 -62
- package/package.json +1 -1
package/dist/sts/issue.js
CHANGED
|
@@ -8,7 +8,7 @@ const ALGORITHM = "AWS4-X509-ECDSA-SHA256";
|
|
|
8
8
|
const SERVICE = "rolesanywhere";
|
|
9
9
|
const PATH = "/sessions";
|
|
10
10
|
const PROFILE_TTL = {
|
|
11
|
-
Runtime:
|
|
11
|
+
Runtime: 30 * 60,
|
|
12
12
|
ConsoleReadOnly: 12 * 60 * 60,
|
|
13
13
|
ConsoleViewOnlyProd: 12 * 60 * 60,
|
|
14
14
|
BillingReadOnly: 2 * 60 * 60,
|
|
@@ -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;
|
|
@@ -105,11 +91,15 @@ export async function issueAwsCredentials(input) {
|
|
|
105
91
|
const der = base64ToBytes(normalizedCert);
|
|
106
92
|
let offset = 0;
|
|
107
93
|
function readLen() {
|
|
94
|
+
if (offset >= der.length)
|
|
95
|
+
throw new Error("DER overflow");
|
|
108
96
|
const b = der[offset++];
|
|
109
97
|
if ((b & 0x80) === 0)
|
|
110
98
|
return b;
|
|
111
99
|
const n = b & 0x7f;
|
|
112
100
|
let len = 0;
|
|
101
|
+
if (offset + n > der.length)
|
|
102
|
+
throw new Error("DER overflow");
|
|
113
103
|
for (let i = 0; i < n; i++) {
|
|
114
104
|
len = (len << 8) | der[offset++];
|
|
115
105
|
}
|
|
@@ -129,15 +119,18 @@ export async function issueAwsCredentials(input) {
|
|
|
129
119
|
if (der[offset++] !== 0x02)
|
|
130
120
|
throw new Error("bad serial tag");
|
|
131
121
|
const serialLen = readLen();
|
|
122
|
+
if (offset + serialLen > der.length)
|
|
123
|
+
throw new Error("DER overflow");
|
|
132
124
|
let serial = der.slice(offset, offset + serialLen);
|
|
133
125
|
if (serial.length > 1 && serial[0] === 0x00) {
|
|
134
126
|
serial = serial.slice(1);
|
|
135
127
|
}
|
|
136
|
-
|
|
128
|
+
// Build BigInt directly from bytes (avoids hex string allocation)
|
|
129
|
+
let serialBig = 0n;
|
|
137
130
|
for (let i = 0; i < serial.length; i++) {
|
|
138
|
-
|
|
131
|
+
serialBig = (serialBig << 8n) | BigInt(serial[i]);
|
|
139
132
|
}
|
|
140
|
-
certSerialDec =
|
|
133
|
+
certSerialDec = serialBig.toString();
|
|
141
134
|
cachedCertSerialDec = certSerialDec;
|
|
142
135
|
cachedCertSerialSource = normalizedCert;
|
|
143
136
|
}
|
|
@@ -146,6 +139,37 @@ export async function issueAwsCredentials(input) {
|
|
|
146
139
|
throw new InternalError("invalid_cert_der");
|
|
147
140
|
}
|
|
148
141
|
}
|
|
142
|
+
// ---- STS credential cache lookup (using certificate serial) ----
|
|
143
|
+
cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
|
|
144
|
+
const cachedEntry = stsCredentialCache.get(cacheKey);
|
|
145
|
+
// Only reuse cached credentials if they still have >10p remaining
|
|
146
|
+
const MIN_REMAINING_MS = 5 * 60 * 1000;
|
|
147
|
+
if (cachedEntry) {
|
|
148
|
+
// If refresh already in-flight → await the same promise
|
|
149
|
+
if (cachedEntry.expiresAt === 0) {
|
|
150
|
+
return cachedEntry.promise;
|
|
151
|
+
}
|
|
152
|
+
// If credentials still valid with safe remaining window → reuse
|
|
153
|
+
if (cachedEntry.expiresAt > Date.now() + MIN_REMAINING_MS) {
|
|
154
|
+
return cachedEntry.promise;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const { signingKey } = await getSigningMaterial({
|
|
158
|
+
certBase64: normalizedCert,
|
|
159
|
+
privateKeyPkcs8Base64,
|
|
160
|
+
});
|
|
161
|
+
// 2. Setup constants
|
|
162
|
+
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
163
|
+
const path = PATH;
|
|
164
|
+
const iso = new Date().toISOString(); // e.g. 2026-03-07T12:00:00.000Z
|
|
165
|
+
const amzDate = `${iso.slice(0, 4)}${iso.slice(5, 7)}${iso.slice(8, 10)}T${iso.slice(11, 13)}${iso.slice(14, 16)}${iso.slice(17, 19)}Z`;
|
|
166
|
+
const dateStamp = iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10);
|
|
167
|
+
const service = SERVICE;
|
|
168
|
+
// 3. Chuẩn bị Body & Cert
|
|
169
|
+
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
170
|
+
const payloadHash = await getPayloadHash(body);
|
|
171
|
+
// --- REFACTOR: Làm sạch chứng chỉ chặt chẽ ---
|
|
172
|
+
// const normalizedCert = normalizeCert(certBase64);
|
|
149
173
|
// 4. Tính toán Signature
|
|
150
174
|
const baseHeaders = {
|
|
151
175
|
"content-type": "application/json",
|
|
@@ -178,60 +202,74 @@ export async function issueAwsCredentials(input) {
|
|
|
178
202
|
"X-Amz-X509": normalizedCert,
|
|
179
203
|
"Authorization": `AWS4-X509-ECDSA-SHA256 Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`
|
|
180
204
|
});
|
|
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
|
|
205
|
+
const refreshPromise = (async () => {
|
|
206
|
+
try {
|
|
207
|
+
const res = await fetch(`https://${host}${path}`, {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: finalHeaders,
|
|
210
|
+
body,
|
|
196
211
|
});
|
|
197
|
-
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
console.warn("[aws-rejected]", {
|
|
214
|
+
status: res.status,
|
|
215
|
+
region,
|
|
216
|
+
profile
|
|
217
|
+
});
|
|
218
|
+
throw new InternalError("aws_rejected");
|
|
219
|
+
}
|
|
220
|
+
const json = await res.json();
|
|
221
|
+
const creds = json?.credentialSet?.[0]?.credentials;
|
|
222
|
+
if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
|
|
223
|
+
console.warn("[issueAwsCredentials] malformed AWS credential response");
|
|
224
|
+
throw new InternalError("aws_malformed_credentials");
|
|
225
|
+
}
|
|
226
|
+
const result = {
|
|
227
|
+
accessKeyId: creds.accessKeyId,
|
|
228
|
+
secretAccessKey: creds.secretAccessKey,
|
|
229
|
+
sessionToken: creds.sessionToken,
|
|
230
|
+
expiration: creds.expiration,
|
|
231
|
+
};
|
|
232
|
+
// subtract small clock‑skew safety window (5s)
|
|
233
|
+
const exp = new Date(creds.expiration).getTime() - 5000;
|
|
234
|
+
// update cache expiration after success
|
|
235
|
+
const entry = stsCredentialCache.get(cacheKey);
|
|
236
|
+
if (entry) {
|
|
237
|
+
entry.expiresAt = exp;
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
198
240
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
241
|
+
catch (e) {
|
|
242
|
+
// remove broken cache entry so next call can retry
|
|
243
|
+
stsCredentialCache.delete(cacheKey);
|
|
244
|
+
if (e instanceof InternalError)
|
|
245
|
+
throw e;
|
|
246
|
+
throw new InternalError("aws_unreachable");
|
|
204
247
|
}
|
|
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
|
-
}
|
|
248
|
+
})();
|
|
249
|
+
// store promise immediately to deduplicate concurrent refresh
|
|
250
|
+
stsCredentialCache.set(cacheKey, {
|
|
251
|
+
promise: refreshPromise,
|
|
252
|
+
expiresAt: 0,
|
|
253
|
+
});
|
|
254
|
+
return refreshPromise;
|
|
217
255
|
}
|
|
218
256
|
// ---- helpers ----
|
|
219
257
|
function normalizeCert(raw) {
|
|
220
258
|
if (raw.includes("BEGIN CERTIFICATE")) {
|
|
221
259
|
throw new InternalError("pem_not_allowed");
|
|
222
260
|
}
|
|
223
|
-
|
|
261
|
+
// remove whitespace and newlines to ensure stable cache keys
|
|
262
|
+
return raw.replace(/\s+/g, "");
|
|
224
263
|
}
|
|
225
264
|
async function sha256Hex(input) {
|
|
226
|
-
|
|
227
|
-
const hash = await crypto.subtle.digest("SHA-256",
|
|
265
|
+
// Tận dụng textEncoder có sẵn
|
|
266
|
+
const hash = await crypto.subtle.digest("SHA-256", textEncoder.encode(input));
|
|
228
267
|
const bytes = new Uint8Array(hash);
|
|
229
|
-
//
|
|
230
|
-
let
|
|
231
|
-
|
|
232
|
-
out += HEX_TABLE[bytes[i]];
|
|
268
|
+
const hexParts = new Array(32); // SHA-256 luôn là 32 bytes
|
|
269
|
+
for (let i = 0; i < 32; i++) {
|
|
270
|
+
hexParts[i] = HEX_TABLE[bytes[i]];
|
|
233
271
|
}
|
|
234
|
-
return
|
|
272
|
+
return hexParts.join("");
|
|
235
273
|
}
|
|
236
274
|
async function getCanonicalRequestHash(canonicalRequest) {
|
|
237
275
|
return sha256Hex(canonicalRequest);
|
|
@@ -241,7 +279,7 @@ async function getPayloadHash(body) {
|
|
|
241
279
|
// so caching adds complexity without measurable benefit.
|
|
242
280
|
return sha256Hex(body);
|
|
243
281
|
}
|
|
244
|
-
// Faster base64 decode → Uint8Array (
|
|
282
|
+
// Faster base64 decode → Uint8Array (V8 optimized path using built‑in vectorized conversion)
|
|
245
283
|
function base64ToBytes(base64) {
|
|
246
284
|
const binary = atob(base64);
|
|
247
285
|
const len = binary.length;
|