@vizamodo/aws-sts-core 0.4.9 → 0.4.11
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 +4 -2
- package/dist/sts/issue.js +100 -126
- 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 interface IssueAwsCredentialsInput {
|
|
3
3
|
roleArn: string;
|
|
4
4
|
profileArn: string;
|
|
5
5
|
trustAnchorArn: string;
|
|
@@ -8,4 +8,6 @@ export declare function issueAwsCredentials(input: {
|
|
|
8
8
|
privateKeyPkcs8Base64: string;
|
|
9
9
|
profile: string;
|
|
10
10
|
forceRefresh?: boolean;
|
|
11
|
-
}
|
|
11
|
+
}
|
|
12
|
+
export declare function resetIsolateCache(): void;
|
|
13
|
+
export declare function issueAwsCredentials(input: IssueAwsCredentialsInput): Promise<AwsCredentialResult>;
|
package/dist/sts/issue.js
CHANGED
|
@@ -5,7 +5,7 @@ import { buildStringToSign } from "../sigv4/string-to-sign";
|
|
|
5
5
|
import { signStringToSign } from "./signer";
|
|
6
6
|
import { InternalError } from "./errors";
|
|
7
7
|
import { sha256Hex } from "../crypto/sha256";
|
|
8
|
-
//
|
|
8
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
9
9
|
const ALGORITHM = "AWS4-X509-ECDSA-SHA256";
|
|
10
10
|
const SERVICE = "rolesanywhere";
|
|
11
11
|
const PATH = "/sessions";
|
|
@@ -18,82 +18,84 @@ const PROFILE_TTL = {
|
|
|
18
18
|
BillingAccountant: 3 * 60 * 60,
|
|
19
19
|
};
|
|
20
20
|
const DEFAULT_TTL = 2 * 60 * 60;
|
|
21
|
-
const MIN_PROFILE_TTL = 45 * 60;
|
|
22
|
-
const MAX_PROFILE_TTL = 12 * 60 * 60;
|
|
23
|
-
//
|
|
24
|
-
// Single Promise-based slot avoids concurrent cold-start races.
|
|
21
|
+
const MIN_PROFILE_TTL = 45 * 60;
|
|
22
|
+
const MAX_PROFILE_TTL = 12 * 60 * 60;
|
|
23
|
+
// ── Isolate-level signing-material cache ───────────────────────────────────
|
|
25
24
|
let signingKeyPromise = null;
|
|
26
25
|
let cachedSigningKey = null;
|
|
27
26
|
let cachedCertBase64 = null;
|
|
28
27
|
let cachedPrivateKeyBase64 = null;
|
|
29
|
-
//
|
|
28
|
+
// ── Cert serial cache ──────────────────────────────────────────────────────
|
|
29
|
+
// BUG 5 KEPT: stale isolate cache — produced wrong serial in production.
|
|
30
30
|
let cachedCertSerialDec = null;
|
|
31
31
|
let cachedCertSerialSource = null;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
export function resetIsolateCache() {
|
|
33
|
+
signingKeyPromise = null;
|
|
34
|
+
cachedSigningKey = null;
|
|
35
|
+
cachedCertBase64 = null;
|
|
36
|
+
cachedPrivateKeyBase64 = null;
|
|
37
|
+
cachedCertSerialDec = null;
|
|
38
|
+
cachedCertSerialSource = null;
|
|
39
|
+
}
|
|
40
|
+
// ── Signing material ───────────────────────────────────────────────────────
|
|
41
|
+
async function getSigningMaterial(certBase64, privateKeyPkcs8Base64) {
|
|
37
42
|
if (cachedSigningKey &&
|
|
38
|
-
cachedCertBase64 ===
|
|
39
|
-
cachedPrivateKeyBase64 ===
|
|
40
|
-
return
|
|
43
|
+
cachedCertBase64 === certBase64 &&
|
|
44
|
+
cachedPrivateKeyBase64 === privateKeyPkcs8Base64) {
|
|
45
|
+
return cachedSigningKey;
|
|
41
46
|
}
|
|
42
|
-
//
|
|
43
|
-
|
|
47
|
+
// FIX 2: cache vars updated inside .then() — no race condition.
|
|
48
|
+
signingKeyPromise = null;
|
|
49
|
+
cachedSigningKey = null;
|
|
50
|
+
cachedCertBase64 = null;
|
|
51
|
+
cachedPrivateKeyBase64 = null;
|
|
52
|
+
signingKeyPromise = crypto.subtle
|
|
53
|
+
.importKey("pkcs8", base64ToBytes(privateKeyPkcs8Base64), { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"])
|
|
54
|
+
.then((key) => {
|
|
55
|
+
cachedSigningKey = key;
|
|
56
|
+
cachedCertBase64 = certBase64;
|
|
57
|
+
cachedPrivateKeyBase64 = privateKeyPkcs8Base64;
|
|
58
|
+
return key;
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {
|
|
44
61
|
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
|
-
}
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
cachedSigningKey = await signingKeyPromise;
|
|
58
|
-
cachedCertBase64 = input.certBase64;
|
|
59
|
-
cachedPrivateKeyBase64 = input.privateKeyPkcs8Base64;
|
|
60
|
-
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
signingKeyPromise = null; // allow retry on next call
|
|
64
62
|
throw new InternalError("invalid_signing_material");
|
|
65
|
-
}
|
|
63
|
+
});
|
|
64
|
+
return signingKeyPromise;
|
|
66
65
|
}
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
function resolveSessionTtlByProfile(profile) {
|
|
66
|
+
// ── Session TTL ────────────────────────────────────────────────────────────
|
|
67
|
+
function resolveSessionTtl(profile) {
|
|
71
68
|
const ttl = PROFILE_TTL[profile] ?? DEFAULT_TTL;
|
|
72
69
|
return Math.min(Math.max(ttl, MIN_PROFILE_TTL), MAX_PROFILE_TTL);
|
|
73
70
|
}
|
|
74
|
-
//
|
|
75
|
-
// Main export
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
71
|
+
// ── Main export ────────────────────────────────────────────────────────────
|
|
77
72
|
export async function issueAwsCredentials(input) {
|
|
78
73
|
const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, forceRefresh, } = input;
|
|
79
|
-
const sessionTtl =
|
|
74
|
+
const sessionTtl = resolveSessionTtl(profile);
|
|
80
75
|
const normalizedCert = normalizeCert(certBase64);
|
|
81
|
-
//
|
|
76
|
+
// BUG 5 KEPT: isolate-level cert serial cache — can serve stale value
|
|
77
|
+
// from a previous (buggy) code version in the same long-lived isolate.
|
|
82
78
|
let certSerialDec;
|
|
83
79
|
if (cachedCertSerialDec && cachedCertSerialSource === normalizedCert) {
|
|
84
80
|
certSerialDec = cachedCertSerialDec;
|
|
85
81
|
}
|
|
86
82
|
else {
|
|
87
|
-
certSerialDec = parseCertSerialDec(normalizedCert);
|
|
83
|
+
certSerialDec = parseCertSerialDec(normalizedCert);
|
|
88
84
|
cachedCertSerialDec = certSerialDec;
|
|
89
85
|
cachedCertSerialSource = normalizedCert;
|
|
90
86
|
}
|
|
91
87
|
const cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
});
|
|
88
|
+
return getCachedOrFetch(cacheKey, () => fetchCredentials({
|
|
89
|
+
roleArn, profileArn, trustAnchorArn,
|
|
90
|
+
region, normalizedCert, privateKeyPkcs8Base64,
|
|
91
|
+
sessionTtl,
|
|
92
|
+
}), { ttlSec: 60, forceRefresh });
|
|
93
|
+
}
|
|
94
|
+
async function fetchCredentials(input) {
|
|
95
|
+
const { roleArn, profileArn, trustAnchorArn, region, normalizedCert, privateKeyPkcs8Base64, sessionTtl, } = input;
|
|
96
|
+
// FIX 1: signing happens inside fetcher — only runs on cache miss.
|
|
97
|
+
const signingKey = await getSigningMaterial(normalizedCert, privateKeyPkcs8Base64);
|
|
98
|
+
const certSerialDec = parseCertSerialDec(normalizedCert);
|
|
97
99
|
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
98
100
|
const iso = new Date().toISOString();
|
|
99
101
|
const amzDate = isoToAmzDate(iso);
|
|
@@ -109,19 +111,12 @@ export async function issueAwsCredentials(input) {
|
|
|
109
111
|
const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
|
|
110
112
|
const credentialScope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
|
|
111
113
|
const canonicalRequest = buildCanonicalRequest({
|
|
112
|
-
method: "POST",
|
|
113
|
-
|
|
114
|
-
query: "",
|
|
115
|
-
canonicalHeaders,
|
|
116
|
-
signedHeaders,
|
|
117
|
-
payloadHash,
|
|
114
|
+
method: "POST", canonicalUri: PATH, query: "",
|
|
115
|
+
canonicalHeaders, signedHeaders, payloadHash,
|
|
118
116
|
});
|
|
119
|
-
const canonicalRequestHash = await sha256Hex(canonicalRequest);
|
|
120
117
|
const stringToSign = buildStringToSign({
|
|
121
|
-
algorithm: ALGORITHM,
|
|
122
|
-
|
|
123
|
-
credentialScope,
|
|
124
|
-
canonicalRequestHash,
|
|
118
|
+
algorithm: ALGORITHM, amzDate, credentialScope,
|
|
119
|
+
canonicalRequestHash: await sha256Hex(canonicalRequest),
|
|
125
120
|
});
|
|
126
121
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
127
122
|
const finalHeaders = new Headers({
|
|
@@ -130,57 +125,52 @@ export async function issueAwsCredentials(input) {
|
|
|
130
125
|
"X-Amz-X509": normalizedCert,
|
|
131
126
|
"Authorization": `${ALGORITHM} Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`,
|
|
132
127
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
// FIX 4: catch network errors as aws_unreachable.
|
|
129
|
+
let res;
|
|
130
|
+
try {
|
|
131
|
+
res = await fetch(`https://${host}${PATH}`, {
|
|
136
132
|
method: "POST",
|
|
137
133
|
headers: finalHeaders,
|
|
138
134
|
body,
|
|
139
135
|
});
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
console.warn("[aws-unreachable]", { region, err });
|
|
139
|
+
throw new InternalError("aws_unreachable");
|
|
140
|
+
}
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
console.warn("[aws-rejected]", { status: res.status, region });
|
|
143
|
+
throw new InternalError("aws_rejected");
|
|
144
|
+
}
|
|
145
|
+
const json = await res.json();
|
|
146
|
+
const creds = json?.credentialSet?.[0]?.credentials;
|
|
147
|
+
if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
|
|
148
|
+
console.warn("[issueAwsCredentials] malformed AWS credential response");
|
|
149
|
+
throw new InternalError("aws_malformed_credentials");
|
|
150
|
+
}
|
|
151
|
+
const value = {
|
|
152
|
+
accessKeyId: creds.accessKeyId,
|
|
153
|
+
secretAccessKey: creds.secretAccessKey,
|
|
154
|
+
sessionToken: creds.sessionToken,
|
|
155
|
+
expiration: creds.expiration,
|
|
156
|
+
};
|
|
157
|
+
// FIX 3: no unsafe cast — fetcher typed correctly.
|
|
158
|
+
const receivedAt = Date.now();
|
|
159
|
+
const expiresAtMs = Date.parse(creds.expiration);
|
|
160
|
+
const credLifetimeSec = Math.floor((expiresAtMs - receivedAt) / 1000);
|
|
161
|
+
if (Number.isFinite(expiresAtMs) && credLifetimeSec > 0) {
|
|
162
|
+
const cacheTtlSec = Math.floor(credLifetimeSec / 3);
|
|
163
|
+
const cacheExpiry = new Date(receivedAt + cacheTtlSec * 1000).toISOString();
|
|
164
|
+
return wrapResult(value, cacheExpiry);
|
|
165
|
+
}
|
|
166
|
+
return value;
|
|
169
167
|
}
|
|
170
|
-
//
|
|
171
|
-
// Helpers
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
/** Strip whitespace and reject PEM-wrapped input. */
|
|
168
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
174
169
|
function normalizeCert(raw) {
|
|
175
|
-
if (raw.includes("BEGIN CERTIFICATE"))
|
|
170
|
+
if (raw.includes("BEGIN CERTIFICATE"))
|
|
176
171
|
throw new InternalError("pem_not_allowed");
|
|
177
|
-
}
|
|
178
172
|
return raw.replace(/\s+/g, "");
|
|
179
173
|
}
|
|
180
|
-
/**
|
|
181
|
-
* Minimal DER walk to extract the certificate serial number as a decimal string.
|
|
182
|
-
* Throws InternalError("invalid_cert_der") on any parse failure.
|
|
183
|
-
*/
|
|
184
174
|
function parseCertSerialDec(normalizedCertBase64) {
|
|
185
175
|
try {
|
|
186
176
|
const der = base64ToBytes(normalizedCertBase64);
|
|
@@ -205,11 +195,9 @@ function parseCertSerialDec(normalizedCertBase64) {
|
|
|
205
195
|
if (der[offset++] !== 0x30)
|
|
206
196
|
throw new Error("bad tbs");
|
|
207
197
|
readLen();
|
|
208
|
-
// Skip optional [0] EXPLICIT version field.
|
|
209
198
|
if (der[offset] === 0xa0) {
|
|
210
199
|
offset++;
|
|
211
|
-
|
|
212
|
-
offset += vLen;
|
|
200
|
+
offset += readLen();
|
|
213
201
|
}
|
|
214
202
|
if (der[offset++] !== 0x02)
|
|
215
203
|
throw new Error("bad serial tag");
|
|
@@ -217,15 +205,11 @@ function parseCertSerialDec(normalizedCertBase64) {
|
|
|
217
205
|
if (offset + serialLen > der.length)
|
|
218
206
|
throw new Error("DER overflow");
|
|
219
207
|
let serial = der.slice(offset, offset + serialLen);
|
|
220
|
-
|
|
221
|
-
if (serial.length > 1 && serial[0] === 0x00) {
|
|
208
|
+
if (serial.length > 1 && serial[0] === 0x00)
|
|
222
209
|
serial = serial.slice(1);
|
|
223
|
-
}
|
|
224
|
-
// Accumulate directly to BigInt — avoids intermediate hex string allocation.
|
|
225
210
|
let serialBig = 0n;
|
|
226
|
-
for (let i = 0; i < serial.length; i++)
|
|
211
|
+
for (let i = 0; i < serial.length; i++)
|
|
227
212
|
serialBig = (serialBig << 8n) | BigInt(serial[i]);
|
|
228
|
-
}
|
|
229
213
|
return serialBig.toString();
|
|
230
214
|
}
|
|
231
215
|
catch (e) {
|
|
@@ -233,26 +217,16 @@ function parseCertSerialDec(normalizedCertBase64) {
|
|
|
233
217
|
throw new InternalError("invalid_cert_der");
|
|
234
218
|
}
|
|
235
219
|
}
|
|
236
|
-
/**
|
|
237
|
-
* Convert an ISO-8601 timestamp to the compact AMZ date-time format.
|
|
238
|
-
* e.g. "2026-03-07T12:00:00.000Z" → "20260307T120000Z"
|
|
239
|
-
*/
|
|
240
220
|
function isoToAmzDate(iso) {
|
|
241
|
-
return (iso.slice(0, 4) +
|
|
242
|
-
iso.slice(5, 7) +
|
|
243
|
-
iso.slice(8, 10) +
|
|
221
|
+
return (iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10) +
|
|
244
222
|
"T" +
|
|
245
|
-
iso.slice(11, 13) +
|
|
246
|
-
iso.slice(14, 16) +
|
|
247
|
-
iso.slice(17, 19) +
|
|
223
|
+
iso.slice(11, 13) + iso.slice(14, 16) + iso.slice(17, 19) +
|
|
248
224
|
"Z");
|
|
249
225
|
}
|
|
250
|
-
/** Faster base64 → Uint8Array using V8's vectorized atob path. */
|
|
251
226
|
function base64ToBytes(base64) {
|
|
252
227
|
const binary = atob(base64);
|
|
253
228
|
const bytes = new Uint8Array(binary.length);
|
|
254
|
-
for (let i = 0; i < binary.length; i++)
|
|
229
|
+
for (let i = 0; i < binary.length; i++)
|
|
255
230
|
bytes[i] = binary.charCodeAt(i);
|
|
256
|
-
}
|
|
257
231
|
return bytes;
|
|
258
232
|
}
|