@vizamodo/aws-sts-core 0.2.34 → 0.3.1
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 +133 -110
- package/package.json +1 -1
package/dist/sts/issue.js
CHANGED
|
@@ -20,24 +20,26 @@ const DEFAULT_TTL = 2 * 60 * 60;
|
|
|
20
20
|
let cachedSigningKey = null;
|
|
21
21
|
let cachedCertBase64 = null;
|
|
22
22
|
let cachedPrivateKeyBase64 = null;
|
|
23
|
-
// ----
|
|
23
|
+
// ---- cached certificate serial (DER parse is expensive, cert rarely changes) ----
|
|
24
|
+
let cachedCertSerialDec = null;
|
|
25
|
+
let cachedCertSerialSource = null;
|
|
26
|
+
// ---- shared encoder ----
|
|
24
27
|
const textEncoder = new TextEncoder();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let lastBaseHeadersKey = null;
|
|
31
|
-
let lastCanonicalHeaders = null;
|
|
32
|
-
let lastSignedHeaders = null;
|
|
28
|
+
// Precomputed hex table for fast byte→hex conversion (no per-byte toString alloc)
|
|
29
|
+
const HEX_TABLE = new Array(256);
|
|
30
|
+
for (let i = 0; i < 256; i++) {
|
|
31
|
+
HEX_TABLE[i] = (i < 16 ? "0" : "") + i.toString(16);
|
|
32
|
+
}
|
|
33
33
|
async function getSigningMaterial(input) {
|
|
34
34
|
if (cachedSigningKey &&
|
|
35
35
|
cachedCertBase64 === input.certBase64 &&
|
|
36
36
|
cachedPrivateKeyBase64 === input.privateKeyPkcs8Base64) {
|
|
37
|
+
console.debug("[sts-cache] signingKey HIT");
|
|
37
38
|
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
38
39
|
}
|
|
40
|
+
console.debug("[sts-cache] signingKey MISS → importing key");
|
|
39
41
|
try {
|
|
40
|
-
const keyBuffer =
|
|
42
|
+
const keyBuffer = base64ToBytes(input.privateKeyPkcs8Base64);
|
|
41
43
|
cachedSigningKey = await crypto.subtle.importKey("pkcs8", keyBuffer, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
|
|
42
44
|
cachedCertBase64 = input.certBase64;
|
|
43
45
|
cachedPrivateKeyBase64 = input.privateKeyPkcs8Base64;
|
|
@@ -57,7 +59,13 @@ export async function issueAwsCredentials(input) {
|
|
|
57
59
|
const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, } = input;
|
|
58
60
|
// 1. Kiểm tra đầu vào & Fix TTL an toàn
|
|
59
61
|
const sessionTtl = resolveSessionTtlByProfile(profile);
|
|
60
|
-
|
|
62
|
+
// Normalize certificate once so every subsystem (cache, DER parse, headers)
|
|
63
|
+
// uses the exact same canonical string
|
|
64
|
+
const normalizedCert = normalizeCert(certBase64);
|
|
65
|
+
const { signingKey } = await getSigningMaterial({
|
|
66
|
+
certBase64: normalizedCert,
|
|
67
|
+
privateKeyPkcs8Base64,
|
|
68
|
+
});
|
|
61
69
|
// 2. Setup constants
|
|
62
70
|
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
63
71
|
const path = PATH;
|
|
@@ -69,56 +77,60 @@ export async function issueAwsCredentials(input) {
|
|
|
69
77
|
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
70
78
|
const payloadHash = await getPayloadHash(body);
|
|
71
79
|
// --- REFACTOR: Làm sạch chứng chỉ chặt chẽ ---
|
|
72
|
-
const normalizedCert = normalizeCert(certBase64);
|
|
80
|
+
// const normalizedCert = normalizeCert(certBase64);
|
|
73
81
|
// --- REFACTOR: Parser ASN.1 an toàn để lấy Serial Number (DECIMAL) ---
|
|
74
82
|
// --- MINIMAL DER WALK: extract serial number ---
|
|
75
83
|
let certSerialDec;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
if (cachedCertSerialDec && cachedCertSerialSource === normalizedCert) {
|
|
85
|
+
console.debug("[sts-cache] certSerial HIT");
|
|
86
|
+
certSerialDec = cachedCertSerialDec;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.debug("[sts-cache] certSerial MISS → parsing DER");
|
|
90
|
+
try {
|
|
91
|
+
const der = base64ToBytes(normalizedCert);
|
|
92
|
+
let offset = 0;
|
|
93
|
+
function readLen() {
|
|
94
|
+
const b = der[offset++];
|
|
95
|
+
if ((b & 0x80) === 0)
|
|
96
|
+
return b;
|
|
97
|
+
const n = b & 0x7f;
|
|
98
|
+
let len = 0;
|
|
99
|
+
for (let i = 0; i < n; i++) {
|
|
100
|
+
len = (len << 8) | der[offset++];
|
|
101
|
+
}
|
|
102
|
+
return len;
|
|
88
103
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
+
if (der[offset++] !== 0x30)
|
|
105
|
+
throw new Error("bad cert");
|
|
106
|
+
readLen();
|
|
107
|
+
if (der[offset++] !== 0x30)
|
|
108
|
+
throw new Error("bad tbs");
|
|
109
|
+
readLen();
|
|
110
|
+
if (der[offset] === 0xa0) {
|
|
111
|
+
offset++;
|
|
112
|
+
const vLen = readLen();
|
|
113
|
+
offset += vLen;
|
|
114
|
+
}
|
|
115
|
+
if (der[offset++] !== 0x02)
|
|
116
|
+
throw new Error("bad serial tag");
|
|
117
|
+
const serialLen = readLen();
|
|
118
|
+
let serial = der.slice(offset, offset + serialLen);
|
|
119
|
+
if (serial.length > 1 && serial[0] === 0x00) {
|
|
120
|
+
serial = serial.slice(1);
|
|
121
|
+
}
|
|
122
|
+
let hex = "";
|
|
123
|
+
for (let i = 0; i < serial.length; i++) {
|
|
124
|
+
hex += HEX_TABLE[serial[i]];
|
|
125
|
+
}
|
|
126
|
+
certSerialDec = BigInt("0x" + hex).toString();
|
|
127
|
+
cachedCertSerialDec = certSerialDec;
|
|
128
|
+
cachedCertSerialSource = normalizedCert;
|
|
104
129
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
throw new
|
|
108
|
-
const serialLen = readLen();
|
|
109
|
-
let serial = der.slice(offset, offset + serialLen);
|
|
110
|
-
// strip leading 0x00 padding if present (DER signed integer rule)
|
|
111
|
-
if (serial.length > 1 && serial[0] === 0x00) {
|
|
112
|
-
serial = serial.slice(1);
|
|
130
|
+
catch (e) {
|
|
131
|
+
console.error("[issueAwsCredentials] Failed to parse cert serial", e);
|
|
132
|
+
throw new InternalError("invalid_cert_der");
|
|
113
133
|
}
|
|
114
|
-
certSerialDec = BigInt("0x" +
|
|
115
|
-
Array.from(serial)
|
|
116
|
-
.map(b => b.toString(16).padStart(2, "0"))
|
|
117
|
-
.join("")).toString();
|
|
118
|
-
}
|
|
119
|
-
catch (e) {
|
|
120
|
-
console.error("[issueAwsCredentials] Failed to parse cert serial", e);
|
|
121
|
-
throw new InternalError("invalid_cert_der");
|
|
122
134
|
}
|
|
123
135
|
// 4. Tính toán Signature
|
|
124
136
|
const baseHeaders = {
|
|
@@ -127,9 +139,9 @@ export async function issueAwsCredentials(input) {
|
|
|
127
139
|
"x-amz-date": amzDate,
|
|
128
140
|
"x-amz-x509": normalizedCert,
|
|
129
141
|
};
|
|
130
|
-
const { canonicalHeaders, signedHeaders } =
|
|
142
|
+
const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
|
|
131
143
|
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
132
|
-
const canonicalRequest =
|
|
144
|
+
const canonicalRequest = buildCanonicalRequest({
|
|
133
145
|
method: "POST",
|
|
134
146
|
canonicalUri: path,
|
|
135
147
|
query: "",
|
|
@@ -137,14 +149,30 @@ export async function issueAwsCredentials(input) {
|
|
|
137
149
|
signedHeaders,
|
|
138
150
|
payloadHash,
|
|
139
151
|
});
|
|
152
|
+
console.debug("[sts-debug] canonicalRequest", {
|
|
153
|
+
amzDate,
|
|
154
|
+
signedHeaders,
|
|
155
|
+
payloadHash,
|
|
156
|
+
canonicalHeadersPreview: canonicalHeaders.slice(0, 120),
|
|
157
|
+
canonicalRequestPreview: canonicalRequest.slice(0, 200)
|
|
158
|
+
});
|
|
140
159
|
const canonicalRequestHash = await getCanonicalRequestHash(canonicalRequest);
|
|
160
|
+
console.debug("[sts-debug] canonicalRequestHash", canonicalRequestHash);
|
|
141
161
|
const stringToSign = buildStringToSign({
|
|
142
162
|
algorithm: ALGORITHM,
|
|
143
163
|
amzDate,
|
|
144
164
|
credentialScope,
|
|
145
165
|
canonicalRequestHash,
|
|
146
166
|
});
|
|
167
|
+
console.debug("[sts-debug] stringToSign", {
|
|
168
|
+
credentialScope,
|
|
169
|
+
stringPreview: stringToSign.slice(0, 200)
|
|
170
|
+
});
|
|
147
171
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
172
|
+
console.debug("[sts-debug] signature", {
|
|
173
|
+
length: signatureHex.length,
|
|
174
|
+
prefix: signatureHex.slice(0, 32)
|
|
175
|
+
});
|
|
148
176
|
// 5. Build Authorization Header với số Serial (DECIMAL)
|
|
149
177
|
const finalHeaders = new Headers({
|
|
150
178
|
"Content-Type": "application/json",
|
|
@@ -152,6 +180,24 @@ export async function issueAwsCredentials(input) {
|
|
|
152
180
|
"X-Amz-X509": normalizedCert,
|
|
153
181
|
"Authorization": `AWS4-X509-ECDSA-SHA256 Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`
|
|
154
182
|
});
|
|
183
|
+
console.debug("[sts-debug] requestHeaders", {
|
|
184
|
+
host,
|
|
185
|
+
amzDate,
|
|
186
|
+
signedHeaders,
|
|
187
|
+
certLen: normalizedCert.length
|
|
188
|
+
});
|
|
189
|
+
console.debug("[sts-request] issuing rolesanywhere session", {
|
|
190
|
+
region,
|
|
191
|
+
profile,
|
|
192
|
+
roleArn,
|
|
193
|
+
profileArn,
|
|
194
|
+
trustAnchorArn,
|
|
195
|
+
amzDate,
|
|
196
|
+
cacheState: {
|
|
197
|
+
hasSigningKey: !!cachedSigningKey,
|
|
198
|
+
cachedCertMatch: cachedCertBase64 === normalizedCert,
|
|
199
|
+
}
|
|
200
|
+
});
|
|
155
201
|
// 6. Execution
|
|
156
202
|
try {
|
|
157
203
|
const res = await fetch(`https://${host}${path}`, {
|
|
@@ -164,12 +210,19 @@ export async function issueAwsCredentials(input) {
|
|
|
164
210
|
// Debug logs
|
|
165
211
|
console.error("[aws-rejected] Request details", {
|
|
166
212
|
status: res.status,
|
|
167
|
-
response: errorBody
|
|
213
|
+
response: errorBody,
|
|
214
|
+
region,
|
|
215
|
+
profile,
|
|
216
|
+
amzDate
|
|
168
217
|
});
|
|
169
218
|
throw new InternalError("aws_rejected");
|
|
170
219
|
}
|
|
171
220
|
const json = await res.json();
|
|
172
221
|
const creds = json?.credentialSet?.[0]?.credentials;
|
|
222
|
+
if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
|
|
223
|
+
console.error("[issueAwsCredentials] malformed AWS credential response", { json });
|
|
224
|
+
throw new InternalError("aws_malformed_credentials");
|
|
225
|
+
}
|
|
173
226
|
return {
|
|
174
227
|
accessKeyId: creds.accessKeyId,
|
|
175
228
|
secretAccessKey: creds.secretAccessKey,
|
|
@@ -178,6 +231,14 @@ export async function issueAwsCredentials(input) {
|
|
|
178
231
|
};
|
|
179
232
|
}
|
|
180
233
|
catch (e) {
|
|
234
|
+
console.error("[sts-debug] fetch failure", {
|
|
235
|
+
error: String(e),
|
|
236
|
+
region,
|
|
237
|
+
roleArn,
|
|
238
|
+
profileArn,
|
|
239
|
+
trustAnchorArn,
|
|
240
|
+
amzDate
|
|
241
|
+
});
|
|
181
242
|
if (e instanceof InternalError)
|
|
182
243
|
throw e;
|
|
183
244
|
throw new InternalError("aws_unreachable");
|
|
@@ -194,66 +255,28 @@ async function sha256Hex(input) {
|
|
|
194
255
|
const data = textEncoder.encode(input);
|
|
195
256
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
196
257
|
const bytes = new Uint8Array(hash);
|
|
197
|
-
|
|
258
|
+
// build hex string with minimal allocations
|
|
259
|
+
let out = "";
|
|
198
260
|
for (let i = 0; i < bytes.length; i++) {
|
|
199
|
-
|
|
200
|
-
hex[i] = h.length === 1 ? "0" + h : h;
|
|
261
|
+
out += HEX_TABLE[bytes[i]];
|
|
201
262
|
}
|
|
202
|
-
return
|
|
263
|
+
return out;
|
|
203
264
|
}
|
|
204
265
|
async function getCanonicalRequestHash(canonicalRequest) {
|
|
205
|
-
|
|
206
|
-
return lastCanonicalRequestHash;
|
|
207
|
-
}
|
|
208
|
-
const hash = await sha256Hex(canonicalRequest);
|
|
209
|
-
lastCanonicalRequest = canonicalRequest;
|
|
210
|
-
lastCanonicalRequestHash = hash;
|
|
211
|
-
return hash;
|
|
266
|
+
return sha256Hex(canonicalRequest);
|
|
212
267
|
}
|
|
213
268
|
async function getPayloadHash(body) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const hash = await sha256Hex(body);
|
|
218
|
-
lastBody = body;
|
|
219
|
-
lastPayloadHash = hash;
|
|
220
|
-
return hash;
|
|
221
|
-
}
|
|
222
|
-
function getCanonicalizedHeaders(baseHeaders) {
|
|
223
|
-
// build deterministic cache key without JSON.stringify allocation
|
|
224
|
-
const key = baseHeaders["content-type"] + "|" +
|
|
225
|
-
baseHeaders["host"] + "|" +
|
|
226
|
-
baseHeaders["x-amz-date"] + "|" +
|
|
227
|
-
baseHeaders["x-amz-x509"];
|
|
228
|
-
if (lastBaseHeadersKey === key && lastCanonicalHeaders && lastSignedHeaders) {
|
|
229
|
-
return { canonicalHeaders: lastCanonicalHeaders, signedHeaders: lastSignedHeaders };
|
|
230
|
-
}
|
|
231
|
-
const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
|
|
232
|
-
lastBaseHeadersKey = key;
|
|
233
|
-
lastCanonicalHeaders = canonicalHeaders;
|
|
234
|
-
lastSignedHeaders = signedHeaders;
|
|
235
|
-
return { canonicalHeaders, signedHeaders };
|
|
236
|
-
}
|
|
237
|
-
function getCanonicalRequest(input) {
|
|
238
|
-
const key = input.method + "|" +
|
|
239
|
-
input.canonicalUri + "|" +
|
|
240
|
-
input.query + "|" +
|
|
241
|
-
input.canonicalHeaders + "|" +
|
|
242
|
-
input.signedHeaders + "|" +
|
|
243
|
-
input.payloadHash;
|
|
244
|
-
if (lastCanonicalRequestKey === key && lastCanonicalRequest) {
|
|
245
|
-
return lastCanonicalRequest;
|
|
246
|
-
}
|
|
247
|
-
const req = buildCanonicalRequest(input);
|
|
248
|
-
lastCanonicalRequestKey = key;
|
|
249
|
-
lastCanonicalRequest = req;
|
|
250
|
-
return req;
|
|
269
|
+
// Payload changes are rare but SHA‑256 here is extremely cheap (~tens of µs),
|
|
270
|
+
// so caching adds complexity without measurable benefit.
|
|
271
|
+
return sha256Hex(body);
|
|
251
272
|
}
|
|
252
|
-
|
|
273
|
+
// Faster base64 decode → Uint8Array (avoids extra ArrayBuffer wrapping)
|
|
274
|
+
function base64ToBytes(base64) {
|
|
253
275
|
const binary = atob(base64);
|
|
254
|
-
const
|
|
255
|
-
|
|
276
|
+
const len = binary.length;
|
|
277
|
+
const bytes = new Uint8Array(len);
|
|
278
|
+
for (let i = 0; i < len; i++) {
|
|
256
279
|
bytes[i] = binary.charCodeAt(i);
|
|
257
280
|
}
|
|
258
|
-
return bytes
|
|
281
|
+
return bytes;
|
|
259
282
|
}
|