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