@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.
Files changed (2) hide show
  1. package/dist/sts/issue.js +133 -110
  2. 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
- // ---- shared encoder + canonical request hash cache ----
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
- let lastCanonicalRequestKey = null;
26
- let lastCanonicalRequest = null;
27
- let lastCanonicalRequestHash = null;
28
- let lastBody = null;
29
- let lastPayloadHash = null;
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 = base64ToArrayBuffer(input.privateKeyPkcs8Base64);
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
- const { signingKey } = await getSigningMaterial({ certBase64, privateKeyPkcs8Base64 });
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
- try {
77
- const der = new Uint8Array(base64ToArrayBuffer(normalizedCert));
78
- let offset = 0;
79
- // helper: read DER length (short + long form)
80
- function readLen() {
81
- const b = der[offset++];
82
- if ((b & 0x80) === 0)
83
- return b;
84
- const n = b & 0x7f;
85
- let len = 0;
86
- for (let i = 0; i < n; i++) {
87
- len = (len << 8) | der[offset++];
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
- return len;
90
- }
91
- // Certificate SEQUENCE
92
- if (der[offset++] !== 0x30)
93
- throw new Error("bad cert");
94
- readLen();
95
- // TBSCertificate SEQUENCE
96
- if (der[offset++] !== 0x30)
97
- throw new Error("bad tbs");
98
- readLen();
99
- // Optional version [0] EXPLICIT (0xa0)
100
- if (der[offset] === 0xa0) {
101
- offset++; // tag
102
- const vLen = readLen(); // length
103
- offset += vLen; // skip full version block
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
- // SerialNumber INTEGER
106
- if (der[offset++] !== 0x02)
107
- throw new Error("bad serial tag");
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 } = getCanonicalizedHeaders(baseHeaders);
142
+ const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
131
143
  const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
132
- const canonicalRequest = getCanonicalRequest({
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
- const hex = new Array(bytes.length);
258
+ // build hex string with minimal allocations
259
+ let out = "";
198
260
  for (let i = 0; i < bytes.length; i++) {
199
- const h = bytes[i].toString(16);
200
- hex[i] = h.length === 1 ? "0" + h : h;
261
+ out += HEX_TABLE[bytes[i]];
201
262
  }
202
- return hex.join("");
263
+ return out;
203
264
  }
204
265
  async function getCanonicalRequestHash(canonicalRequest) {
205
- if (lastCanonicalRequest === canonicalRequest && lastCanonicalRequestHash) {
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
- if (lastBody === body && lastPayloadHash) {
215
- return lastPayloadHash;
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
- function base64ToArrayBuffer(base64) {
273
+ // Faster base64 decode → Uint8Array (avoids extra ArrayBuffer wrapping)
274
+ function base64ToBytes(base64) {
253
275
  const binary = atob(base64);
254
- const bytes = new Uint8Array(binary.length);
255
- for (let i = 0; i < binary.length; i++) {
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.buffer;
281
+ return bytes;
259
282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizamodo/aws-sts-core",
3
- "version": "0.2.34",
3
+ "version": "0.3.1",
4
4
  "description": "Pure AWS STS + SigV4 (X509 Roles Anywhere) core logic",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",