@vizamodo/aws-sts-core 0.4.22 → 0.4.24
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 +0 -1
- package/dist/sts/issue.js +123 -96
- package/package.json +1 -1
package/dist/sts/issue.d.ts
CHANGED
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,22 +18,20 @@ 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
|
-
//
|
|
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.
|
|
25
25
|
let signingKeyPromise = null;
|
|
26
26
|
let cachedSigningKey = null;
|
|
27
27
|
let cachedCertBase64 = null;
|
|
28
28
|
let cachedPrivateKeyBase64 = null;
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
// ── Signing material ───────────────────────────────────────────────────────
|
|
29
|
+
// ---- certificate serial cache (DER walk is CPU-bound, cert rarely rotates) ----
|
|
30
|
+
let cachedCertSerialDec = null;
|
|
31
|
+
let cachedCertSerialSource = null;
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Signing material
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
37
35
|
async function getSigningMaterial(input) {
|
|
38
36
|
// Fast path: same material already imported.
|
|
39
37
|
if (cachedSigningKey &&
|
|
@@ -41,42 +39,57 @@ async function getSigningMaterial(input) {
|
|
|
41
39
|
cachedPrivateKeyBase64 === input.privateKeyPkcs8Base64) {
|
|
42
40
|
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
43
41
|
}
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
cachedSigningKey = await signingKeyPromise;
|
|
54
58
|
cachedCertBase64 = input.certBase64;
|
|
55
59
|
cachedPrivateKeyBase64 = input.privateKeyPkcs8Base64;
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
signingKeyPromise = null;
|
|
60
|
+
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
signingKeyPromise = null; // allow retry on next call
|
|
60
64
|
throw new InternalError("invalid_signing_material");
|
|
61
|
-
}
|
|
62
|
-
const key = await signingKeyPromise;
|
|
63
|
-
return { signingKey: key, certBase64: input.certBase64 };
|
|
65
|
+
}
|
|
64
66
|
}
|
|
65
|
-
//
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Profile TTL resolution
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
66
70
|
function resolveSessionTtlByProfile(profile) {
|
|
67
71
|
const ttl = PROFILE_TTL[profile] ?? DEFAULT_TTL;
|
|
68
72
|
return Math.min(Math.max(ttl, MIN_PROFILE_TTL), MAX_PROFILE_TTL);
|
|
69
73
|
}
|
|
70
|
-
//
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Main export
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
71
77
|
export async function issueAwsCredentials(input) {
|
|
72
78
|
const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, forceRefresh, } = input;
|
|
73
79
|
const sessionTtl = resolveSessionTtlByProfile(profile);
|
|
74
80
|
const normalizedCert = normalizeCert(certBase64);
|
|
75
|
-
//
|
|
76
|
-
|
|
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
|
+
}
|
|
77
91
|
const cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
|
|
78
|
-
//
|
|
79
|
-
// request even when L1/L2 cache would have returned a hit.
|
|
92
|
+
// ---- Build SigV4 request ----
|
|
80
93
|
const { signingKey } = await getSigningMaterial({
|
|
81
94
|
certBase64: normalizedCert,
|
|
82
95
|
privateKeyPkcs8Base64,
|
|
@@ -96,12 +109,19 @@ export async function issueAwsCredentials(input) {
|
|
|
96
109
|
const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
|
|
97
110
|
const credentialScope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
|
|
98
111
|
const canonicalRequest = buildCanonicalRequest({
|
|
99
|
-
method: "POST",
|
|
100
|
-
|
|
112
|
+
method: "POST",
|
|
113
|
+
canonicalUri: PATH,
|
|
114
|
+
query: "",
|
|
115
|
+
canonicalHeaders,
|
|
116
|
+
signedHeaders,
|
|
117
|
+
payloadHash,
|
|
101
118
|
});
|
|
119
|
+
const canonicalRequestHash = await sha256Hex(canonicalRequest);
|
|
102
120
|
const stringToSign = buildStringToSign({
|
|
103
|
-
algorithm: ALGORITHM,
|
|
104
|
-
|
|
121
|
+
algorithm: ALGORITHM,
|
|
122
|
+
amzDate,
|
|
123
|
+
credentialScope,
|
|
124
|
+
canonicalRequestHash,
|
|
105
125
|
});
|
|
106
126
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
107
127
|
const finalHeaders = new Headers({
|
|
@@ -110,31 +130,15 @@ export async function issueAwsCredentials(input) {
|
|
|
110
130
|
"X-Amz-X509": normalizedCert,
|
|
111
131
|
"Authorization": `${ALGORITHM} Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`,
|
|
112
132
|
});
|
|
133
|
+
const issuedAt = Date.now(); // snapshot before the network round-trip
|
|
113
134
|
return getCachedOrFetch(cacheKey, async () => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
headers: finalHeaders,
|
|
120
|
-
body,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
console.warn("[aws-unreachable]", { region, err });
|
|
125
|
-
throw new InternalError("aws_unreachable");
|
|
126
|
-
}
|
|
135
|
+
const res = await fetch(`https://${host}${PATH}`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: finalHeaders,
|
|
138
|
+
body,
|
|
139
|
+
});
|
|
127
140
|
if (!res.ok) {
|
|
128
|
-
|
|
129
|
-
console.error("[aws-rejected]", {
|
|
130
|
-
status: res.status,
|
|
131
|
-
region,
|
|
132
|
-
profile,
|
|
133
|
-
errorBody,
|
|
134
|
-
certSerial: certSerialDec,
|
|
135
|
-
amzDate,
|
|
136
|
-
credentialScope,
|
|
137
|
-
});
|
|
141
|
+
console.warn("[aws-rejected]", { status: res.status, region, profile });
|
|
138
142
|
throw new InternalError("aws_rejected");
|
|
139
143
|
}
|
|
140
144
|
const json = await res.json();
|
|
@@ -149,66 +153,79 @@ export async function issueAwsCredentials(input) {
|
|
|
149
153
|
sessionToken: creds.sessionToken,
|
|
150
154
|
expiration: creds.expiration,
|
|
151
155
|
};
|
|
152
|
-
//
|
|
153
|
-
const receivedAt = Date.now();
|
|
156
|
+
// derive TTL = 1/3 lifetime
|
|
154
157
|
const expiresAtMs = Date.parse(creds.expiration);
|
|
155
|
-
const credLifetimeSec = Math.floor((expiresAtMs -
|
|
158
|
+
const credLifetimeSec = Math.floor((expiresAtMs - issuedAt) / 1000);
|
|
156
159
|
if (Number.isFinite(expiresAtMs) && credLifetimeSec > 0) {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
return wrapResult(value,
|
|
160
|
+
const edgeCacheTtlSec = Math.floor(credLifetimeSec / 3);
|
|
161
|
+
const edgeCacheExpiry = new Date(issuedAt + edgeCacheTtlSec * 1000).toISOString();
|
|
162
|
+
return wrapResult(value, edgeCacheExpiry);
|
|
160
163
|
}
|
|
161
164
|
return value;
|
|
162
|
-
}, {
|
|
165
|
+
}, {
|
|
166
|
+
ttlSec: 60,
|
|
167
|
+
...(forceRefresh !== undefined ? { forceRefresh } : {})
|
|
168
|
+
});
|
|
163
169
|
}
|
|
164
|
-
//
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Helpers
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
/** Strip whitespace and reject PEM-wrapped input. */
|
|
165
174
|
function normalizeCert(raw) {
|
|
166
|
-
if (raw.includes("BEGIN CERTIFICATE"))
|
|
175
|
+
if (raw.includes("BEGIN CERTIFICATE")) {
|
|
167
176
|
throw new InternalError("pem_not_allowed");
|
|
177
|
+
}
|
|
168
178
|
return raw.replace(/\s+/g, "");
|
|
169
179
|
}
|
|
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
|
+
*/
|
|
170
184
|
function parseCertSerialDec(normalizedCertBase64) {
|
|
171
185
|
try {
|
|
172
186
|
const der = base64ToBytes(normalizedCertBase64);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
function readLen(p) {
|
|
177
|
-
if (p[0] >= der.length)
|
|
187
|
+
let offset = 0;
|
|
188
|
+
function readLen() {
|
|
189
|
+
if (offset >= der.length)
|
|
178
190
|
throw new Error("DER overflow");
|
|
179
|
-
const b = der[
|
|
191
|
+
const b = der[offset++];
|
|
180
192
|
if ((b & 0x80) === 0)
|
|
181
193
|
return b;
|
|
182
194
|
const n = b & 0x7f;
|
|
183
195
|
let len = 0;
|
|
184
|
-
if (
|
|
196
|
+
if (offset + n > der.length)
|
|
185
197
|
throw new Error("DER overflow");
|
|
186
198
|
for (let i = 0; i < n; i++)
|
|
187
|
-
len = (len << 8) | der[
|
|
199
|
+
len = (len << 8) | der[offset++];
|
|
188
200
|
return len;
|
|
189
201
|
}
|
|
190
|
-
if (der[
|
|
202
|
+
if (der[offset++] !== 0x30)
|
|
191
203
|
throw new Error("bad cert");
|
|
192
|
-
readLen(
|
|
193
|
-
if (der[
|
|
204
|
+
readLen();
|
|
205
|
+
if (der[offset++] !== 0x30)
|
|
194
206
|
throw new Error("bad tbs");
|
|
195
|
-
readLen(
|
|
207
|
+
readLen();
|
|
196
208
|
// Skip optional [0] EXPLICIT version field.
|
|
197
|
-
if (der[
|
|
198
|
-
|
|
199
|
-
|
|
209
|
+
if (der[offset] === 0xa0) {
|
|
210
|
+
offset++;
|
|
211
|
+
const vLen = readLen();
|
|
212
|
+
offset += vLen;
|
|
200
213
|
}
|
|
201
|
-
if (der[
|
|
214
|
+
if (der[offset++] !== 0x02)
|
|
202
215
|
throw new Error("bad serial tag");
|
|
203
|
-
const serialLen = readLen(
|
|
204
|
-
if (
|
|
216
|
+
const serialLen = readLen();
|
|
217
|
+
if (offset + serialLen > der.length)
|
|
205
218
|
throw new Error("DER overflow");
|
|
206
|
-
let serial = der.slice(
|
|
207
|
-
|
|
219
|
+
let serial = der.slice(offset, offset + serialLen);
|
|
220
|
+
// Strip ASN.1 sign-extension padding byte.
|
|
221
|
+
if (serial.length > 1 && serial[0] === 0x00) {
|
|
208
222
|
serial = serial.slice(1);
|
|
223
|
+
}
|
|
224
|
+
// Accumulate directly to BigInt — avoids intermediate hex string allocation.
|
|
209
225
|
let serialBig = 0n;
|
|
210
|
-
for (let i = 0; i < serial.length; i++)
|
|
226
|
+
for (let i = 0; i < serial.length; i++) {
|
|
211
227
|
serialBig = (serialBig << 8n) | BigInt(serial[i]);
|
|
228
|
+
}
|
|
212
229
|
return serialBig.toString();
|
|
213
230
|
}
|
|
214
231
|
catch (e) {
|
|
@@ -216,16 +233,26 @@ function parseCertSerialDec(normalizedCertBase64) {
|
|
|
216
233
|
throw new InternalError("invalid_cert_der");
|
|
217
234
|
}
|
|
218
235
|
}
|
|
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
|
+
*/
|
|
219
240
|
function isoToAmzDate(iso) {
|
|
220
|
-
return (iso.slice(0, 4) +
|
|
241
|
+
return (iso.slice(0, 4) +
|
|
242
|
+
iso.slice(5, 7) +
|
|
243
|
+
iso.slice(8, 10) +
|
|
221
244
|
"T" +
|
|
222
|
-
iso.slice(11, 13) +
|
|
245
|
+
iso.slice(11, 13) +
|
|
246
|
+
iso.slice(14, 16) +
|
|
247
|
+
iso.slice(17, 19) +
|
|
223
248
|
"Z");
|
|
224
249
|
}
|
|
250
|
+
/** Faster base64 → Uint8Array using V8's vectorized atob path. */
|
|
225
251
|
function base64ToBytes(base64) {
|
|
226
252
|
const binary = atob(base64);
|
|
227
253
|
const bytes = new Uint8Array(binary.length);
|
|
228
|
-
for (let i = 0; i < binary.length; i++)
|
|
254
|
+
for (let i = 0; i < binary.length; i++) {
|
|
229
255
|
bytes[i] = binary.charCodeAt(i);
|
|
256
|
+
}
|
|
230
257
|
return bytes;
|
|
231
258
|
}
|