@vizamodo/aws-sts-core 0.4.7 → 0.4.9
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.d.ts +2 -11
- package/dist/sts/issue.js +110 -167
- package/package.json +1 -1
package/dist/sts/issue.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AwsCredentialResult } from "../types";
|
|
2
|
-
export
|
|
2
|
+
export declare function issueAwsCredentials(input: {
|
|
3
3
|
roleArn: string;
|
|
4
4
|
profileArn: string;
|
|
5
5
|
trustAnchorArn: string;
|
|
@@ -8,13 +8,4 @@ export interface IssueAwsCredentialsInput {
|
|
|
8
8
|
privateKeyPkcs8Base64: string;
|
|
9
9
|
profile: string;
|
|
10
10
|
forceRefresh?: boolean;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Reset isolate-level signing material and cert serial caches.
|
|
14
|
-
* ONLY for use in tests — forces re-import of signing key on next call.
|
|
15
|
-
*
|
|
16
|
-
* L1 cache is bypassed in tests via `forceRefresh: true` on each call —
|
|
17
|
-
* there is no need to expose a cache-clear API from the core library.
|
|
18
|
-
*/
|
|
19
|
-
export declare function resetIsolateCache(): void;
|
|
20
|
-
export declare function issueAwsCredentials(input: IssueAwsCredentialsInput): Promise<AwsCredentialResult>;
|
|
11
|
+
}): Promise<AwsCredentialResult>;
|
package/dist/sts/issue.js
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
+
import { getCachedOrFetch, wrapResult } from "@vizamodo/edge-cache-core";
|
|
1
2
|
import { canonicalizeHeaders } from "../sigv4/headers";
|
|
2
3
|
import { buildCanonicalRequest } from "../sigv4/canonical";
|
|
3
4
|
import { buildStringToSign } from "../sigv4/string-to-sign";
|
|
4
5
|
import { signStringToSign } from "./signer";
|
|
5
6
|
import { InternalError } from "./errors";
|
|
6
7
|
import { sha256Hex } from "../crypto/sha256";
|
|
7
|
-
|
|
8
|
-
// ── Constants ──────────────────────────────────────────────────────────────
|
|
8
|
+
// ---- constants ----
|
|
9
9
|
const ALGORITHM = "AWS4-X509-ECDSA-SHA256";
|
|
10
10
|
const SERVICE = "rolesanywhere";
|
|
11
11
|
const PATH = "/sessions";
|
|
12
|
-
/**
|
|
13
|
-
* Per-profile AWS session duration (seconds).
|
|
14
|
-
* Clamped to [MIN_SESSION_TTL, MAX_SESSION_TTL] at resolution time.
|
|
15
|
-
*/
|
|
16
12
|
const PROFILE_TTL = {
|
|
17
13
|
Runtime: 45 * 60,
|
|
18
14
|
ConsoleReadOnly: 12 * 60 * 60,
|
|
@@ -21,89 +17,83 @@ const PROFILE_TTL = {
|
|
|
21
17
|
BillingAdmin: 3 * 60 * 60,
|
|
22
18
|
BillingAccountant: 3 * 60 * 60,
|
|
23
19
|
};
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
//
|
|
28
|
-
// Single Promise slot
|
|
20
|
+
const DEFAULT_TTL = 2 * 60 * 60;
|
|
21
|
+
const MIN_PROFILE_TTL = 45 * 60; // 45 min — lower guard
|
|
22
|
+
const MAX_PROFILE_TTL = 12 * 60 * 60; // 12 h — upper guard
|
|
23
|
+
// ---- isolate-level signing material cache ----
|
|
24
|
+
// Single Promise-based slot avoids concurrent cold-start races.
|
|
29
25
|
let signingKeyPromise = null;
|
|
30
26
|
let cachedSigningKey = null;
|
|
31
27
|
let cachedCertBase64 = null;
|
|
32
28
|
let cachedPrivateKeyBase64 = null;
|
|
33
|
-
//
|
|
34
|
-
// DER walk is CPU-bound; the cert rarely rotates within an isolate lifetime.
|
|
29
|
+
// ---- certificate serial cache (DER walk is CPU-bound, cert rarely rotates) ----
|
|
35
30
|
let cachedCertSerialDec = null;
|
|
36
31
|
let cachedCertSerialSource = null;
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* L1 cache is bypassed in tests via `forceRefresh: true` on each call —
|
|
43
|
-
* there is no need to expose a cache-clear API from the core library.
|
|
44
|
-
*/
|
|
45
|
-
export function resetIsolateCache() {
|
|
46
|
-
signingKeyPromise = null;
|
|
47
|
-
cachedSigningKey = null;
|
|
48
|
-
cachedCertBase64 = null;
|
|
49
|
-
cachedPrivateKeyBase64 = null;
|
|
50
|
-
cachedCertSerialDec = null;
|
|
51
|
-
cachedCertSerialSource = null;
|
|
52
|
-
}
|
|
53
|
-
// ── Signing material ───────────────────────────────────────────────────────
|
|
54
|
-
async function getSigningMaterial(certBase64, privateKeyPkcs8Base64) {
|
|
55
|
-
// Fast path: same material already imported in this isolate.
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Signing material
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
async function getSigningMaterial(input) {
|
|
36
|
+
// Fast path: same material already imported.
|
|
56
37
|
if (cachedSigningKey &&
|
|
57
|
-
cachedCertBase64 === certBase64 &&
|
|
58
|
-
cachedPrivateKeyBase64 === privateKeyPkcs8Base64) {
|
|
59
|
-
return cachedSigningKey;
|
|
38
|
+
cachedCertBase64 === input.certBase64 &&
|
|
39
|
+
cachedPrivateKeyBase64 === input.privateKeyPkcs8Base64) {
|
|
40
|
+
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
41
|
+
}
|
|
42
|
+
// Material rotated — discard the stale resolved key so we re-import.
|
|
43
|
+
if (cachedSigningKey) {
|
|
44
|
+
signingKeyPromise = null;
|
|
45
|
+
cachedSigningKey = null;
|
|
46
|
+
}
|
|
47
|
+
if (!signingKeyPromise) {
|
|
48
|
+
try {
|
|
49
|
+
const keyBuffer = base64ToBytes(input.privateKeyPkcs8Base64);
|
|
50
|
+
signingKeyPromise = crypto.subtle.importKey("pkcs8", keyBuffer, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new InternalError("invalid_signing_material");
|
|
54
|
+
}
|
|
60
55
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
signingKeyPromise = crypto.subtle
|
|
69
|
-
.importKey("pkcs8", base64ToBytes(privateKeyPkcs8Base64), { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"])
|
|
70
|
-
.then((key) => {
|
|
71
|
-
// Update cache atomically after successful import.
|
|
72
|
-
// All concurrent callers awaiting this promise will see
|
|
73
|
-
// consistent state and hit the fast path on next call.
|
|
74
|
-
cachedSigningKey = key;
|
|
75
|
-
cachedCertBase64 = certBase64;
|
|
76
|
-
cachedPrivateKeyBase64 = privateKeyPkcs8Base64;
|
|
77
|
-
return key;
|
|
78
|
-
})
|
|
79
|
-
.catch(() => {
|
|
56
|
+
try {
|
|
57
|
+
cachedSigningKey = await signingKeyPromise;
|
|
58
|
+
cachedCertBase64 = input.certBase64;
|
|
59
|
+
cachedPrivateKeyBase64 = input.privateKeyPkcs8Base64;
|
|
60
|
+
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
80
63
|
signingKeyPromise = null; // allow retry on next call
|
|
81
64
|
throw new InternalError("invalid_signing_material");
|
|
82
|
-
}
|
|
83
|
-
return signingKeyPromise;
|
|
65
|
+
}
|
|
84
66
|
}
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Profile TTL resolution
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
function resolveSessionTtlByProfile(profile) {
|
|
71
|
+
const ttl = PROFILE_TTL[profile] ?? DEFAULT_TTL;
|
|
72
|
+
return Math.min(Math.max(ttl, MIN_PROFILE_TTL), MAX_PROFILE_TTL);
|
|
89
73
|
}
|
|
90
|
-
//
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Main export
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
91
77
|
export async function issueAwsCredentials(input) {
|
|
92
78
|
const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, forceRefresh, } = input;
|
|
93
|
-
const sessionTtl =
|
|
79
|
+
const sessionTtl = resolveSessionTtlByProfile(profile);
|
|
94
80
|
const normalizedCert = normalizeCert(certBase64);
|
|
95
|
-
//
|
|
96
|
-
|
|
81
|
+
// ---- DER serial extraction (with isolate-level cache) ----
|
|
82
|
+
let certSerialDec;
|
|
83
|
+
if (cachedCertSerialDec && cachedCertSerialSource === normalizedCert) {
|
|
84
|
+
certSerialDec = cachedCertSerialDec;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
certSerialDec = parseCertSerialDec(normalizedCert); // throws InternalError on bad DER
|
|
88
|
+
cachedCertSerialDec = certSerialDec;
|
|
89
|
+
cachedCertSerialSource = normalizedCert;
|
|
90
|
+
}
|
|
97
91
|
const cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
async function fetchCredentials(input) {
|
|
105
|
-
const { roleArn, profileArn, trustAnchorArn, region, normalizedCert, privateKeyPkcs8Base64, sessionTtl, } = input;
|
|
106
|
-
const signingKey = await getSigningMaterial(normalizedCert, privateKeyPkcs8Base64);
|
|
92
|
+
// ---- Build SigV4 request ----
|
|
93
|
+
const { signingKey } = await getSigningMaterial({
|
|
94
|
+
certBase64: normalizedCert,
|
|
95
|
+
privateKeyPkcs8Base64,
|
|
96
|
+
});
|
|
107
97
|
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
108
98
|
const iso = new Date().toISOString();
|
|
109
99
|
const amzDate = isoToAmzDate(iso);
|
|
@@ -134,105 +124,59 @@ async function fetchCredentials(input) {
|
|
|
134
124
|
canonicalRequestHash,
|
|
135
125
|
});
|
|
136
126
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
137
|
-
const certSerialDec = getCertSerialDec(normalizedCert);
|
|
138
|
-
// 🔍 Deep debug helper (compare with working version)
|
|
139
|
-
function debugAwsSigning() {
|
|
140
|
-
try {
|
|
141
|
-
console.debug("[aws-debug][input]", {
|
|
142
|
-
roleArn,
|
|
143
|
-
profileArn,
|
|
144
|
-
trustAnchorArn,
|
|
145
|
-
region,
|
|
146
|
-
sessionTtl,
|
|
147
|
-
});
|
|
148
|
-
console.debug("[aws-debug][cert]", {
|
|
149
|
-
length: normalizedCert.length,
|
|
150
|
-
preview: normalizedCert.slice(0, 30),
|
|
151
|
-
});
|
|
152
|
-
console.debug("[aws-debug][serial]", {
|
|
153
|
-
serialDec: certSerialDec,
|
|
154
|
-
serialHex: BigInt(certSerialDec).toString(16),
|
|
155
|
-
});
|
|
156
|
-
console.debug("[aws-debug][headers]", baseHeaders);
|
|
157
|
-
console.debug("[aws-debug][signedHeaders]", signedHeaders);
|
|
158
|
-
console.debug("[aws-debug][canonicalRequest]", canonicalRequest);
|
|
159
|
-
console.debug("[aws-debug][stringToSign]", stringToSign);
|
|
160
|
-
console.debug("[aws-debug][signatureHex]", signatureHex);
|
|
161
|
-
}
|
|
162
|
-
catch (e) {
|
|
163
|
-
console.warn("[aws-debug][error]", e);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
debugAwsSigning();
|
|
167
127
|
const finalHeaders = new Headers({
|
|
168
128
|
"Content-Type": "application/json",
|
|
169
129
|
"X-Amz-Date": amzDate,
|
|
170
130
|
"X-Amz-X509": normalizedCert,
|
|
171
131
|
"Authorization": `${ALGORITHM} Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`,
|
|
172
132
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
res = await fetch(`https://${host}${PATH}`, {
|
|
133
|
+
const issuedAt = Date.now(); // snapshot before the network round-trip
|
|
134
|
+
return getCachedOrFetch(cacheKey, async () => {
|
|
135
|
+
const res = await fetch(`https://${host}${PATH}`, {
|
|
176
136
|
method: "POST",
|
|
177
137
|
headers: finalHeaders,
|
|
178
138
|
body,
|
|
179
139
|
});
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const receivedAt = Date.now();
|
|
210
|
-
const credLifetimeSec = Math.floor((expiresAtMs - receivedAt) / 1000);
|
|
211
|
-
if (Number.isFinite(expiresAtMs) && credLifetimeSec > 0) {
|
|
212
|
-
const cacheTtlSec = Math.floor(credLifetimeSec / 3);
|
|
213
|
-
const cacheExpiry = new Date(receivedAt + cacheTtlSec * 1000).toISOString();
|
|
214
|
-
return wrapResult(value, cacheExpiry);
|
|
215
|
-
}
|
|
216
|
-
return value;
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
console.warn("[aws-rejected]", { status: res.status, region, profile });
|
|
142
|
+
throw new InternalError("aws_rejected");
|
|
143
|
+
}
|
|
144
|
+
const json = await res.json();
|
|
145
|
+
const creds = json?.credentialSet?.[0]?.credentials;
|
|
146
|
+
if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
|
|
147
|
+
console.warn("[issueAwsCredentials] malformed AWS credential response");
|
|
148
|
+
throw new InternalError("aws_malformed_credentials");
|
|
149
|
+
}
|
|
150
|
+
const value = {
|
|
151
|
+
accessKeyId: creds.accessKeyId,
|
|
152
|
+
secretAccessKey: creds.secretAccessKey,
|
|
153
|
+
sessionToken: creds.sessionToken,
|
|
154
|
+
expiration: creds.expiration,
|
|
155
|
+
};
|
|
156
|
+
// derive TTL = 1/3 lifetime
|
|
157
|
+
const expiresAtMs = Date.parse(creds.expiration);
|
|
158
|
+
const credLifetimeSec = Math.floor((expiresAtMs - issuedAt) / 1000);
|
|
159
|
+
if (Number.isFinite(expiresAtMs) && credLifetimeSec > 0) {
|
|
160
|
+
const edgeCacheTtlSec = Math.floor(credLifetimeSec / 3);
|
|
161
|
+
const edgeCacheExpiry = new Date(issuedAt + edgeCacheTtlSec * 1000).toISOString();
|
|
162
|
+
return wrapResult(value, edgeCacheExpiry);
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}, {
|
|
166
|
+
ttlSec: 60,
|
|
167
|
+
...(forceRefresh !== undefined ? { forceRefresh } : {})
|
|
168
|
+
});
|
|
217
169
|
}
|
|
218
|
-
//
|
|
219
|
-
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Helpers
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
/** Strip whitespace and reject PEM-wrapped input. */
|
|
220
174
|
function normalizeCert(raw) {
|
|
221
175
|
if (raw.includes("BEGIN CERTIFICATE")) {
|
|
222
176
|
throw new InternalError("pem_not_allowed");
|
|
223
177
|
}
|
|
224
178
|
return raw.replace(/\s+/g, "");
|
|
225
179
|
}
|
|
226
|
-
/** Return cert serial as decimal string, using isolate-level cache. */
|
|
227
|
-
function getCertSerialDec(normalizedCert) {
|
|
228
|
-
if (cachedCertSerialDec && cachedCertSerialSource === normalizedCert) {
|
|
229
|
-
return cachedCertSerialDec;
|
|
230
|
-
}
|
|
231
|
-
const serial = parseCertSerialDec(normalizedCert);
|
|
232
|
-
cachedCertSerialDec = serial;
|
|
233
|
-
cachedCertSerialSource = normalizedCert;
|
|
234
|
-
return serial;
|
|
235
|
-
}
|
|
236
180
|
/**
|
|
237
181
|
* Minimal DER walk to extract the certificate serial number as a decimal string.
|
|
238
182
|
* Throws InternalError("invalid_cert_der") on any parse failure.
|
|
@@ -255,30 +199,29 @@ function parseCertSerialDec(normalizedCertBase64) {
|
|
|
255
199
|
len = (len << 8) | der[offset++];
|
|
256
200
|
return len;
|
|
257
201
|
}
|
|
258
|
-
// Certificate ::= SEQUENCE
|
|
259
202
|
if (der[offset++] !== 0x30)
|
|
260
203
|
throw new Error("bad cert");
|
|
261
204
|
readLen();
|
|
262
|
-
// tbsCertificate ::= SEQUENCE
|
|
263
205
|
if (der[offset++] !== 0x30)
|
|
264
206
|
throw new Error("bad tbs");
|
|
265
207
|
readLen();
|
|
266
|
-
//
|
|
208
|
+
// Skip optional [0] EXPLICIT version field.
|
|
267
209
|
if (der[offset] === 0xa0) {
|
|
268
|
-
offset++;
|
|
269
|
-
|
|
210
|
+
offset++;
|
|
211
|
+
const vLen = readLen();
|
|
212
|
+
offset += vLen;
|
|
270
213
|
}
|
|
271
|
-
// 👉 SERIAL MUST be the next INTEGER after optional version
|
|
272
214
|
if (der[offset++] !== 0x02)
|
|
273
215
|
throw new Error("bad serial tag");
|
|
274
|
-
const
|
|
275
|
-
if (offset +
|
|
216
|
+
const serialLen = readLen();
|
|
217
|
+
if (offset + serialLen > der.length)
|
|
276
218
|
throw new Error("DER overflow");
|
|
277
|
-
let serial = der.slice(offset, offset +
|
|
278
|
-
// Strip
|
|
219
|
+
let serial = der.slice(offset, offset + serialLen);
|
|
220
|
+
// Strip ASN.1 sign-extension padding byte.
|
|
279
221
|
if (serial.length > 1 && serial[0] === 0x00) {
|
|
280
222
|
serial = serial.slice(1);
|
|
281
223
|
}
|
|
224
|
+
// Accumulate directly to BigInt — avoids intermediate hex string allocation.
|
|
282
225
|
let serialBig = 0n;
|
|
283
226
|
for (let i = 0; i < serial.length; i++) {
|
|
284
227
|
serialBig = (serialBig << 8n) | BigInt(serial[i]);
|
|
@@ -291,7 +234,7 @@ function parseCertSerialDec(normalizedCertBase64) {
|
|
|
291
234
|
}
|
|
292
235
|
}
|
|
293
236
|
/**
|
|
294
|
-
* Convert ISO-8601 to compact AMZ date-time format.
|
|
237
|
+
* Convert an ISO-8601 timestamp to the compact AMZ date-time format.
|
|
295
238
|
* e.g. "2026-03-07T12:00:00.000Z" → "20260307T120000Z"
|
|
296
239
|
*/
|
|
297
240
|
function isoToAmzDate(iso) {
|
|
@@ -304,7 +247,7 @@ function isoToAmzDate(iso) {
|
|
|
304
247
|
iso.slice(17, 19) +
|
|
305
248
|
"Z");
|
|
306
249
|
}
|
|
307
|
-
/** base64 → Uint8Array
|
|
250
|
+
/** Faster base64 → Uint8Array using V8's vectorized atob path. */
|
|
308
251
|
function base64ToBytes(base64) {
|
|
309
252
|
const binary = atob(base64);
|
|
310
253
|
const bytes = new Uint8Array(binary.length);
|