@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.
Files changed (2) hide show
  1. package/dist/sts/issue.js +101 -132
  2. 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
- // ---- shared encoder + payload hash cache ----
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
- let lastBody = null;
26
- let lastPayloadHash = null;
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
- console.debug("[sts-cache] signingKey MISS importing key");
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
- const keyBuffer = base64ToArrayBuffer(input.privateKeyPkcs8Base64);
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
- const { signingKey } = await getSigningMaterial({ certBase64, privateKeyPkcs8Base64 });
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
- try {
73
- const der = new Uint8Array(base64ToArrayBuffer(normalizedCert));
74
- let offset = 0;
75
- // helper: read DER length (short + long form)
76
- function readLen() {
77
- const b = der[offset++];
78
- if ((b & 0x80) === 0)
79
- return b;
80
- const n = b & 0x7f;
81
- let len = 0;
82
- for (let i = 0; i < n; i++) {
83
- len = (len << 8) | der[offset++];
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
- return len;
86
- }
87
- // Certificate SEQUENCE
88
- if (der[offset++] !== 0x30)
89
- throw new Error("bad cert");
90
- readLen();
91
- // TBSCertificate SEQUENCE
92
- if (der[offset++] !== 0x30)
93
- throw new Error("bad tbs");
94
- readLen();
95
- // Optional version [0] EXPLICIT (0xa0)
96
- if (der[offset] === 0xa0) {
97
- offset++; // tag
98
- const vLen = readLen(); // length
99
- offset += vLen; // skip full version block
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
- // SerialNumber INTEGER
102
- if (der[offset++] !== 0x02)
103
- throw new Error("bad serial tag");
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 } = getCanonicalizedHeaders(baseHeaders);
156
+ const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
127
157
  const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
128
- const canonicalRequest = getCanonicalRequest({
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
- const hex = new Array(bytes.length);
229
+ // build hex string with minimal allocations
230
+ let out = "";
240
231
  for (let i = 0; i < bytes.length; i++) {
241
- const h = bytes[i].toString(16);
242
- hex[i] = h.length === 1 ? "0" + h : h;
232
+ out += HEX_TABLE[bytes[i]];
243
233
  }
244
- return hex.join("");
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
- if (lastBody === body && lastPayloadHash) {
251
- console.debug("[sts-cache] payloadHash HIT");
252
- return lastPayloadHash;
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
- function base64ToArrayBuffer(base64) {
244
+ // Faster base64 decode → Uint8Array (avoids extra ArrayBuffer wrapping)
245
+ function base64ToBytes(base64) {
278
246
  const binary = atob(base64);
279
- const bytes = new Uint8Array(binary.length);
280
- for (let i = 0; i < binary.length; i++) {
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.buffer;
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.39",
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
- "typescript": "^5.9.3"
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
  }