@vizamodo/aws-sts-core 0.2.21 → 0.2.39
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 +132 -38
- package/package.json +1 -1
package/dist/sts/issue.js
CHANGED
|
@@ -3,17 +3,40 @@ import { buildCanonicalRequest } from "../sigv4/canonical";
|
|
|
3
3
|
import { buildStringToSign } from "../sigv4/string-to-sign";
|
|
4
4
|
import { signStringToSign } from "./signer";
|
|
5
5
|
import { InternalError } from "./errors";
|
|
6
|
+
// ---- constants (cleaner configuration, no runtime cost) ----
|
|
7
|
+
const ALGORITHM = "AWS4-X509-ECDSA-SHA256";
|
|
8
|
+
const SERVICE = "rolesanywhere";
|
|
9
|
+
const PATH = "/sessions";
|
|
10
|
+
const PROFILE_TTL = {
|
|
11
|
+
Runtime: 90 * 60,
|
|
12
|
+
ConsoleReadOnly: 12 * 60 * 60,
|
|
13
|
+
ConsoleViewOnlyProd: 12 * 60 * 60,
|
|
14
|
+
BillingReadOnly: 2 * 60 * 60,
|
|
15
|
+
BillingAdmin: 2 * 60 * 60,
|
|
16
|
+
BillingAccountant: 2 * 60 * 60,
|
|
17
|
+
};
|
|
18
|
+
const DEFAULT_TTL = 2 * 60 * 60;
|
|
6
19
|
// ---- isolate-level cached signing material ----
|
|
7
20
|
let cachedSigningKey = null;
|
|
8
21
|
let cachedCertBase64 = null;
|
|
22
|
+
let cachedPrivateKeyBase64 = null;
|
|
23
|
+
// ---- shared encoder + payload hash cache ----
|
|
24
|
+
const textEncoder = new TextEncoder();
|
|
25
|
+
let lastBody = null;
|
|
26
|
+
let lastPayloadHash = null;
|
|
9
27
|
async function getSigningMaterial(input) {
|
|
10
|
-
if (cachedSigningKey &&
|
|
28
|
+
if (cachedSigningKey &&
|
|
29
|
+
cachedCertBase64 === input.certBase64 &&
|
|
30
|
+
cachedPrivateKeyBase64 === input.privateKeyPkcs8Base64) {
|
|
31
|
+
console.debug("[sts-cache] signingKey HIT");
|
|
11
32
|
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
12
33
|
}
|
|
34
|
+
console.debug("[sts-cache] signingKey MISS → importing key");
|
|
13
35
|
try {
|
|
14
|
-
cachedCertBase64 = input.certBase64;
|
|
15
36
|
const keyBuffer = base64ToArrayBuffer(input.privateKeyPkcs8Base64);
|
|
16
37
|
cachedSigningKey = await crypto.subtle.importKey("pkcs8", keyBuffer, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
|
|
38
|
+
cachedCertBase64 = input.certBase64;
|
|
39
|
+
cachedPrivateKeyBase64 = input.privateKeyPkcs8Base64;
|
|
17
40
|
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
18
41
|
}
|
|
19
42
|
catch {
|
|
@@ -21,22 +44,7 @@ async function getSigningMaterial(input) {
|
|
|
21
44
|
}
|
|
22
45
|
}
|
|
23
46
|
function resolveSessionTtlByProfile(profile) {
|
|
24
|
-
|
|
25
|
-
// Runtime
|
|
26
|
-
case "Runtime":
|
|
27
|
-
return 90 * 60; // 90 minutes
|
|
28
|
-
// Console profiles
|
|
29
|
-
case "ConsoleReadOnly":
|
|
30
|
-
case "ConsoleViewOnlyProd":
|
|
31
|
-
return 12 * 60 * 60; // 12 hours
|
|
32
|
-
// Billing profiles
|
|
33
|
-
case "BillingReadOnly":
|
|
34
|
-
case "BillingAdmin":
|
|
35
|
-
case "BillingAccountant":
|
|
36
|
-
return 2 * 60 * 60; // 2 hours
|
|
37
|
-
default:
|
|
38
|
-
return 2 * 60 * 60; // fail-safe
|
|
39
|
-
}
|
|
47
|
+
return PROFILE_TTL[profile] ?? DEFAULT_TTL;
|
|
40
48
|
}
|
|
41
49
|
/**
|
|
42
50
|
* Issue short-lived AWS credentials via Roles Anywhere.
|
|
@@ -45,23 +53,19 @@ export async function issueAwsCredentials(input) {
|
|
|
45
53
|
const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, } = input;
|
|
46
54
|
// 1. Kiểm tra đầu vào & Fix TTL an toàn
|
|
47
55
|
const sessionTtl = resolveSessionTtlByProfile(profile);
|
|
48
|
-
console.log("profile:", profile, "sessionTtl:", sessionTtl);
|
|
49
56
|
const { signingKey } = await getSigningMaterial({ certBase64, privateKeyPkcs8Base64 });
|
|
50
57
|
// 2. Setup constants
|
|
51
|
-
const host =
|
|
52
|
-
const path =
|
|
58
|
+
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
59
|
+
const path = PATH;
|
|
53
60
|
const now = new Date();
|
|
54
|
-
const amzDate = now.toISOString().replace(
|
|
61
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
55
62
|
const dateStamp = amzDate.slice(0, 8);
|
|
56
|
-
const service =
|
|
63
|
+
const service = SERVICE;
|
|
57
64
|
// 3. Chuẩn bị Body & Cert
|
|
58
65
|
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
59
|
-
const payloadHash = await
|
|
66
|
+
const payloadHash = await getPayloadHash(body);
|
|
60
67
|
// --- REFACTOR: Làm sạch chứng chỉ chặt chẽ ---
|
|
61
|
-
const normalizedCert = certBase64
|
|
62
|
-
.replace(/-----BEGIN CERTIFICATE-----/g, "")
|
|
63
|
-
.replace(/-----END CERTIFICATE-----/g, "")
|
|
64
|
-
.replace(/\s+/g, "");
|
|
68
|
+
const normalizedCert = normalizeCert(certBase64);
|
|
65
69
|
// --- REFACTOR: Parser ASN.1 an toàn để lấy Serial Number (DECIMAL) ---
|
|
66
70
|
// --- MINIMAL DER WALK: extract serial number ---
|
|
67
71
|
let certSerialDec;
|
|
@@ -98,7 +102,11 @@ export async function issueAwsCredentials(input) {
|
|
|
98
102
|
if (der[offset++] !== 0x02)
|
|
99
103
|
throw new Error("bad serial tag");
|
|
100
104
|
const serialLen = readLen();
|
|
101
|
-
|
|
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);
|
|
109
|
+
}
|
|
102
110
|
certSerialDec = BigInt("0x" +
|
|
103
111
|
Array.from(serial)
|
|
104
112
|
.map(b => b.toString(16).padStart(2, "0"))
|
|
@@ -115,9 +123,9 @@ export async function issueAwsCredentials(input) {
|
|
|
115
123
|
"x-amz-date": amzDate,
|
|
116
124
|
"x-amz-x509": normalizedCert,
|
|
117
125
|
};
|
|
118
|
-
const { canonicalHeaders, signedHeaders } =
|
|
126
|
+
const { canonicalHeaders, signedHeaders } = getCanonicalizedHeaders(baseHeaders);
|
|
119
127
|
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
120
|
-
const canonicalRequest =
|
|
128
|
+
const canonicalRequest = getCanonicalRequest({
|
|
121
129
|
method: "POST",
|
|
122
130
|
canonicalUri: path,
|
|
123
131
|
query: "",
|
|
@@ -125,20 +133,56 @@ export async function issueAwsCredentials(input) {
|
|
|
125
133
|
signedHeaders,
|
|
126
134
|
payloadHash,
|
|
127
135
|
});
|
|
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
|
+
const canonicalRequestHash = await getCanonicalRequestHash(canonicalRequest);
|
|
144
|
+
console.debug("[sts-debug] canonicalRequestHash", canonicalRequestHash);
|
|
128
145
|
const stringToSign = buildStringToSign({
|
|
129
|
-
algorithm:
|
|
146
|
+
algorithm: ALGORITHM,
|
|
130
147
|
amzDate,
|
|
131
148
|
credentialScope,
|
|
132
|
-
canonicalRequestHash
|
|
149
|
+
canonicalRequestHash,
|
|
150
|
+
});
|
|
151
|
+
console.debug("[sts-debug] stringToSign", {
|
|
152
|
+
credentialScope,
|
|
153
|
+
stringPreview: stringToSign.slice(0, 200)
|
|
133
154
|
});
|
|
134
155
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
156
|
+
console.debug("[sts-debug] signature", {
|
|
157
|
+
length: signatureHex.length,
|
|
158
|
+
prefix: signatureHex.slice(0, 32)
|
|
159
|
+
});
|
|
135
160
|
// 5. Build Authorization Header với số Serial (DECIMAL)
|
|
136
161
|
const finalHeaders = new Headers({
|
|
137
162
|
"Content-Type": "application/json",
|
|
163
|
+
"Host": host,
|
|
138
164
|
"X-Amz-Date": amzDate,
|
|
139
165
|
"X-Amz-X509": normalizedCert,
|
|
140
166
|
"Authorization": `AWS4-X509-ECDSA-SHA256 Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`
|
|
141
167
|
});
|
|
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
|
+
});
|
|
142
186
|
// 6. Execution
|
|
143
187
|
try {
|
|
144
188
|
const res = await fetch(`https://${host}${path}`, {
|
|
@@ -152,7 +196,9 @@ export async function issueAwsCredentials(input) {
|
|
|
152
196
|
console.error("[aws-rejected] Request details", {
|
|
153
197
|
status: res.status,
|
|
154
198
|
response: errorBody,
|
|
155
|
-
|
|
199
|
+
region,
|
|
200
|
+
profile,
|
|
201
|
+
amzDate
|
|
156
202
|
});
|
|
157
203
|
throw new InternalError("aws_rejected");
|
|
158
204
|
}
|
|
@@ -166,19 +212,67 @@ export async function issueAwsCredentials(input) {
|
|
|
166
212
|
};
|
|
167
213
|
}
|
|
168
214
|
catch (e) {
|
|
215
|
+
console.error("[sts-debug] fetch failure", {
|
|
216
|
+
error: String(e),
|
|
217
|
+
region,
|
|
218
|
+
roleArn,
|
|
219
|
+
profileArn,
|
|
220
|
+
trustAnchorArn,
|
|
221
|
+
amzDate
|
|
222
|
+
});
|
|
169
223
|
if (e instanceof InternalError)
|
|
170
224
|
throw e;
|
|
171
225
|
throw new InternalError("aws_unreachable");
|
|
172
226
|
}
|
|
173
227
|
}
|
|
174
228
|
// ---- helpers ----
|
|
229
|
+
function normalizeCert(raw) {
|
|
230
|
+
if (raw.includes("BEGIN CERTIFICATE")) {
|
|
231
|
+
throw new InternalError("pem_not_allowed");
|
|
232
|
+
}
|
|
233
|
+
return raw.trim();
|
|
234
|
+
}
|
|
175
235
|
async function sha256Hex(input) {
|
|
176
|
-
const data =
|
|
236
|
+
const data = textEncoder.encode(input);
|
|
177
237
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
178
238
|
const bytes = new Uint8Array(hash);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.
|
|
239
|
+
const hex = new Array(bytes.length);
|
|
240
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
241
|
+
const h = bytes[i].toString(16);
|
|
242
|
+
hex[i] = h.length === 1 ? "0" + h : h;
|
|
243
|
+
}
|
|
244
|
+
return hex.join("");
|
|
245
|
+
}
|
|
246
|
+
async function getCanonicalRequestHash(canonicalRequest) {
|
|
247
|
+
return sha256Hex(canonicalRequest);
|
|
248
|
+
}
|
|
249
|
+
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);
|
|
182
276
|
}
|
|
183
277
|
function base64ToArrayBuffer(base64) {
|
|
184
278
|
const binary = atob(base64);
|