@vizamodo/aws-sts-core 0.2.19 → 0.2.34
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 +108 -39
- package/package.json +3 -2
package/dist/sts/issue.js
CHANGED
|
@@ -3,17 +3,44 @@ 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 + canonical request hash cache ----
|
|
24
|
+
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;
|
|
9
33
|
async function getSigningMaterial(input) {
|
|
10
|
-
if (cachedSigningKey &&
|
|
34
|
+
if (cachedSigningKey &&
|
|
35
|
+
cachedCertBase64 === input.certBase64 &&
|
|
36
|
+
cachedPrivateKeyBase64 === input.privateKeyPkcs8Base64) {
|
|
11
37
|
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
12
38
|
}
|
|
13
39
|
try {
|
|
14
|
-
cachedCertBase64 = input.certBase64;
|
|
15
40
|
const keyBuffer = base64ToArrayBuffer(input.privateKeyPkcs8Base64);
|
|
16
41
|
cachedSigningKey = await crypto.subtle.importKey("pkcs8", keyBuffer, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
|
|
42
|
+
cachedCertBase64 = input.certBase64;
|
|
43
|
+
cachedPrivateKeyBase64 = input.privateKeyPkcs8Base64;
|
|
17
44
|
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
18
45
|
}
|
|
19
46
|
catch {
|
|
@@ -21,22 +48,7 @@ async function getSigningMaterial(input) {
|
|
|
21
48
|
}
|
|
22
49
|
}
|
|
23
50
|
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
|
-
}
|
|
51
|
+
return PROFILE_TTL[profile] ?? DEFAULT_TTL;
|
|
40
52
|
}
|
|
41
53
|
/**
|
|
42
54
|
* Issue short-lived AWS credentials via Roles Anywhere.
|
|
@@ -45,23 +57,19 @@ export async function issueAwsCredentials(input) {
|
|
|
45
57
|
const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, } = input;
|
|
46
58
|
// 1. Kiểm tra đầu vào & Fix TTL an toàn
|
|
47
59
|
const sessionTtl = resolveSessionTtlByProfile(profile);
|
|
48
|
-
console.log("profile:", profile, "sessionTtl:", sessionTtl);
|
|
49
60
|
const { signingKey } = await getSigningMaterial({ certBase64, privateKeyPkcs8Base64 });
|
|
50
61
|
// 2. Setup constants
|
|
51
|
-
const host =
|
|
52
|
-
const path =
|
|
62
|
+
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
63
|
+
const path = PATH;
|
|
53
64
|
const now = new Date();
|
|
54
|
-
const amzDate = now.toISOString().replace(
|
|
65
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
55
66
|
const dateStamp = amzDate.slice(0, 8);
|
|
56
|
-
const service =
|
|
67
|
+
const service = SERVICE;
|
|
57
68
|
// 3. Chuẩn bị Body & Cert
|
|
58
69
|
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
59
|
-
const payloadHash = await
|
|
70
|
+
const payloadHash = await getPayloadHash(body);
|
|
60
71
|
// --- 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, "");
|
|
72
|
+
const normalizedCert = normalizeCert(certBase64);
|
|
65
73
|
// --- REFACTOR: Parser ASN.1 an toàn để lấy Serial Number (DECIMAL) ---
|
|
66
74
|
// --- MINIMAL DER WALK: extract serial number ---
|
|
67
75
|
let certSerialDec;
|
|
@@ -98,7 +106,11 @@ export async function issueAwsCredentials(input) {
|
|
|
98
106
|
if (der[offset++] !== 0x02)
|
|
99
107
|
throw new Error("bad serial tag");
|
|
100
108
|
const serialLen = readLen();
|
|
101
|
-
|
|
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);
|
|
113
|
+
}
|
|
102
114
|
certSerialDec = BigInt("0x" +
|
|
103
115
|
Array.from(serial)
|
|
104
116
|
.map(b => b.toString(16).padStart(2, "0"))
|
|
@@ -115,9 +127,9 @@ export async function issueAwsCredentials(input) {
|
|
|
115
127
|
"x-amz-date": amzDate,
|
|
116
128
|
"x-amz-x509": normalizedCert,
|
|
117
129
|
};
|
|
118
|
-
const { canonicalHeaders, signedHeaders } =
|
|
130
|
+
const { canonicalHeaders, signedHeaders } = getCanonicalizedHeaders(baseHeaders);
|
|
119
131
|
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
120
|
-
const canonicalRequest =
|
|
132
|
+
const canonicalRequest = getCanonicalRequest({
|
|
121
133
|
method: "POST",
|
|
122
134
|
canonicalUri: path,
|
|
123
135
|
query: "",
|
|
@@ -125,11 +137,12 @@ export async function issueAwsCredentials(input) {
|
|
|
125
137
|
signedHeaders,
|
|
126
138
|
payloadHash,
|
|
127
139
|
});
|
|
140
|
+
const canonicalRequestHash = await getCanonicalRequestHash(canonicalRequest);
|
|
128
141
|
const stringToSign = buildStringToSign({
|
|
129
|
-
algorithm:
|
|
142
|
+
algorithm: ALGORITHM,
|
|
130
143
|
amzDate,
|
|
131
144
|
credentialScope,
|
|
132
|
-
canonicalRequestHash
|
|
145
|
+
canonicalRequestHash,
|
|
133
146
|
});
|
|
134
147
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
135
148
|
// 5. Build Authorization Header với số Serial (DECIMAL)
|
|
@@ -151,8 +164,7 @@ export async function issueAwsCredentials(input) {
|
|
|
151
164
|
// Debug logs
|
|
152
165
|
console.error("[aws-rejected] Request details", {
|
|
153
166
|
status: res.status,
|
|
154
|
-
response: errorBody
|
|
155
|
-
serial: certSerialDec
|
|
167
|
+
response: errorBody
|
|
156
168
|
});
|
|
157
169
|
throw new InternalError("aws_rejected");
|
|
158
170
|
}
|
|
@@ -172,13 +184,70 @@ export async function issueAwsCredentials(input) {
|
|
|
172
184
|
}
|
|
173
185
|
}
|
|
174
186
|
// ---- helpers ----
|
|
187
|
+
function normalizeCert(raw) {
|
|
188
|
+
if (raw.includes("BEGIN CERTIFICATE")) {
|
|
189
|
+
throw new InternalError("pem_not_allowed");
|
|
190
|
+
}
|
|
191
|
+
return raw.trim();
|
|
192
|
+
}
|
|
175
193
|
async function sha256Hex(input) {
|
|
176
|
-
const data =
|
|
194
|
+
const data = textEncoder.encode(input);
|
|
177
195
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
178
196
|
const bytes = new Uint8Array(hash);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.
|
|
197
|
+
const hex = new Array(bytes.length);
|
|
198
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
199
|
+
const h = bytes[i].toString(16);
|
|
200
|
+
hex[i] = h.length === 1 ? "0" + h : h;
|
|
201
|
+
}
|
|
202
|
+
return hex.join("");
|
|
203
|
+
}
|
|
204
|
+
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;
|
|
212
|
+
}
|
|
213
|
+
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;
|
|
182
251
|
}
|
|
183
252
|
function base64ToArrayBuffer(base64) {
|
|
184
253
|
const binary = atob(base64);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizamodo/aws-sts-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.34",
|
|
4
4
|
"description": "Pure AWS STS + SigV4 (X509 Roles Anywhere) core logic",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"clean": "rm -rf dist",
|
|
17
17
|
"prepublishOnly": "npm run build",
|
|
18
18
|
"dev": "ts-node bin/viza.ts",
|
|
19
|
-
"release:prod": "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
|
|
19
|
+
"release:prod": "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 publish --tag latest --access public && git push",
|
|
20
|
+
"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"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"typescript": "^5.9.3"
|