@vizamodo/aws-sts-core 0.3.3 → 0.3.8
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/crypto/sha256.d.ts +2 -0
- package/dist/crypto/sha256.js +22 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sigv4/request.d.ts +17 -0
- package/dist/sigv4/request.js +124 -0
- package/dist/sts/issue.js +26 -37
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const textEncoder = new TextEncoder();
|
|
2
|
+
const HEX_TABLE = new Array(256);
|
|
3
|
+
for (let i = 0; i < 256; i++) {
|
|
4
|
+
HEX_TABLE[i] = (i < 16 ? "0" : "") + i.toString(16);
|
|
5
|
+
}
|
|
6
|
+
export function hex(buf) {
|
|
7
|
+
const arr = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
8
|
+
const out = new Array(arr.length);
|
|
9
|
+
for (let i = 0; i < arr.length; i++) {
|
|
10
|
+
out[i] = HEX_TABLE[arr[i]];
|
|
11
|
+
}
|
|
12
|
+
return out.join("");
|
|
13
|
+
}
|
|
14
|
+
export async function sha256Hex(input) {
|
|
15
|
+
const hash = await crypto.subtle.digest("SHA-256", textEncoder.encode(input));
|
|
16
|
+
const bytes = new Uint8Array(hash);
|
|
17
|
+
const hexParts = new Array(32);
|
|
18
|
+
for (let i = 0; i < 32; i++) {
|
|
19
|
+
hexParts[i] = HEX_TABLE[bytes[i]];
|
|
20
|
+
}
|
|
21
|
+
return hexParts.join("");
|
|
22
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type AwsCredentials = {
|
|
2
|
+
accessKeyId: string;
|
|
3
|
+
secretAccessKey: string;
|
|
4
|
+
sessionToken?: string;
|
|
5
|
+
};
|
|
6
|
+
export type BuildSignedAwsRequestInput = {
|
|
7
|
+
service: string;
|
|
8
|
+
region: string;
|
|
9
|
+
target: string;
|
|
10
|
+
body: string;
|
|
11
|
+
contentType?: string;
|
|
12
|
+
credentials: AwsCredentials;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Build signed AWS JSON API request
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildSignedAwsRequest(input: BuildSignedAwsRequestInput): Promise<RequestInit>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/*
|
|
2
|
+
SigV4 signed AWS JSON request builder.
|
|
3
|
+
|
|
4
|
+
Designed for edge runtimes (Cloudflare Workers, Deno, Bun, Node 20+).
|
|
5
|
+
No AWS SDK dependency.
|
|
6
|
+
*/
|
|
7
|
+
import { sha256Hex, hex } from "../crypto/sha256";
|
|
8
|
+
const DEFAULT_CONTENT_TYPE = "application/x-amz-json-1.1";
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const signingKeyCache = new Map();
|
|
11
|
+
const MAX_SIGNING_KEY_CACHE = 64;
|
|
12
|
+
const hmacKeyCache = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* HMAC SHA256
|
|
15
|
+
*/
|
|
16
|
+
async function hmac(key, data) {
|
|
17
|
+
const rawKey = key instanceof Uint8Array ? key : new Uint8Array(key);
|
|
18
|
+
const keyId = hex(rawKey);
|
|
19
|
+
let cryptoKey = hmacKeyCache.get(keyId);
|
|
20
|
+
if (!cryptoKey) {
|
|
21
|
+
cryptoKey = await crypto.subtle.importKey("raw", rawKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
22
|
+
hmacKeyCache.set(keyId, cryptoKey);
|
|
23
|
+
}
|
|
24
|
+
return crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(data));
|
|
25
|
+
}
|
|
26
|
+
function toUint8(buf) {
|
|
27
|
+
return new Uint8Array(buf);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Derive SigV4 signing key
|
|
31
|
+
*/
|
|
32
|
+
async function getSignatureKey(secretKey, date, region, service) {
|
|
33
|
+
const kDate = await hmac(new TextEncoder().encode("AWS4" + secretKey), date);
|
|
34
|
+
const kRegion = await hmac(toUint8(kDate), region);
|
|
35
|
+
const kService = await hmac(toUint8(kRegion), service);
|
|
36
|
+
return hmac(toUint8(kService), "aws4_request");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build signed AWS JSON API request
|
|
40
|
+
*/
|
|
41
|
+
export async function buildSignedAwsRequest(input) {
|
|
42
|
+
const { service, region, target, body, credentials } = input;
|
|
43
|
+
const contentType = input.contentType ?? DEFAULT_CONTENT_TYPE;
|
|
44
|
+
const host = `${service}.${region}.amazonaws.com`;
|
|
45
|
+
const now = new Date();
|
|
46
|
+
const iso = now.toISOString();
|
|
47
|
+
const amzDate = iso.slice(0, 4) +
|
|
48
|
+
iso.slice(5, 7) +
|
|
49
|
+
iso.slice(8, 10) +
|
|
50
|
+
"T" +
|
|
51
|
+
iso.slice(11, 13) +
|
|
52
|
+
iso.slice(14, 16) +
|
|
53
|
+
iso.slice(17, 19) +
|
|
54
|
+
"Z";
|
|
55
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
56
|
+
const canonicalHeaders = `content-type:${contentType}\n` +
|
|
57
|
+
`host:${host}\n` +
|
|
58
|
+
`x-amz-date:${amzDate}\n` +
|
|
59
|
+
`x-amz-target:${target}\n` +
|
|
60
|
+
(credentials.sessionToken
|
|
61
|
+
? `x-amz-security-token:${credentials.sessionToken}\n`
|
|
62
|
+
: "");
|
|
63
|
+
const signedHeaders = credentials.sessionToken
|
|
64
|
+
? "content-type;host;x-amz-date;x-amz-security-token;x-amz-target"
|
|
65
|
+
: "content-type;host;x-amz-date;x-amz-target";
|
|
66
|
+
const payloadHash = await sha256Hex(body);
|
|
67
|
+
const canonicalRequest = "POST\n" +
|
|
68
|
+
"/\n" +
|
|
69
|
+
"\n" +
|
|
70
|
+
canonicalHeaders +
|
|
71
|
+
"\n" +
|
|
72
|
+
signedHeaders +
|
|
73
|
+
"\n" +
|
|
74
|
+
payloadHash;
|
|
75
|
+
const credentialScope = dateStamp +
|
|
76
|
+
"/" +
|
|
77
|
+
region +
|
|
78
|
+
"/" +
|
|
79
|
+
service +
|
|
80
|
+
"/aws4_request";
|
|
81
|
+
const stringToSign = "AWS4-HMAC-SHA256\n" +
|
|
82
|
+
amzDate +
|
|
83
|
+
"\n" +
|
|
84
|
+
credentialScope +
|
|
85
|
+
"\n" +
|
|
86
|
+
await sha256Hex(canonicalRequest);
|
|
87
|
+
const cacheKey = await sha256Hex(credentials.secretAccessKey +
|
|
88
|
+
"|" +
|
|
89
|
+
dateStamp +
|
|
90
|
+
"|" +
|
|
91
|
+
region +
|
|
92
|
+
"|" +
|
|
93
|
+
service);
|
|
94
|
+
let signingKey = signingKeyCache.get(cacheKey);
|
|
95
|
+
if (!signingKey) {
|
|
96
|
+
signingKey = await getSignatureKey(credentials.secretAccessKey, dateStamp, region, service);
|
|
97
|
+
if (signingKeyCache.size > MAX_SIGNING_KEY_CACHE) {
|
|
98
|
+
const firstKey = signingKeyCache.keys().next().value;
|
|
99
|
+
if (firstKey)
|
|
100
|
+
signingKeyCache.delete(firstKey);
|
|
101
|
+
}
|
|
102
|
+
signingKeyCache.set(cacheKey, signingKey);
|
|
103
|
+
}
|
|
104
|
+
const signature = hex(await hmac(signingKey, stringToSign));
|
|
105
|
+
const authorization = "AWS4-HMAC-SHA256 " +
|
|
106
|
+
`Credential=${credentials.accessKeyId}/${credentialScope}, ` +
|
|
107
|
+
`SignedHeaders=${signedHeaders}, ` +
|
|
108
|
+
`Signature=${signature}`;
|
|
109
|
+
const headers = {
|
|
110
|
+
"Content-Type": contentType,
|
|
111
|
+
"X-Amz-Date": amzDate,
|
|
112
|
+
"X-Amz-Target": target,
|
|
113
|
+
Authorization: authorization,
|
|
114
|
+
Host: host
|
|
115
|
+
};
|
|
116
|
+
if (credentials.sessionToken) {
|
|
117
|
+
headers["X-Amz-Security-Token"] = credentials.sessionToken;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers,
|
|
122
|
+
body
|
|
123
|
+
};
|
|
124
|
+
}
|
package/dist/sts/issue.js
CHANGED
|
@@ -3,12 +3,13 @@ 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
|
+
import { sha256Hex } from "../crypto/sha256";
|
|
6
7
|
// ---- constants (cleaner configuration, no runtime cost) ----
|
|
7
8
|
const ALGORITHM = "AWS4-X509-ECDSA-SHA256";
|
|
8
9
|
const SERVICE = "rolesanywhere";
|
|
9
10
|
const PATH = "/sessions";
|
|
10
11
|
const PROFILE_TTL = {
|
|
11
|
-
Runtime:
|
|
12
|
+
Runtime: 30 * 60,
|
|
12
13
|
ConsoleReadOnly: 12 * 60 * 60,
|
|
13
14
|
ConsoleViewOnlyProd: 12 * 60 * 60,
|
|
14
15
|
BillingReadOnly: 2 * 60 * 60,
|
|
@@ -26,13 +27,6 @@ let cachedPrivateKeyBase64 = null;
|
|
|
26
27
|
let cachedCertSerialDec = null;
|
|
27
28
|
let cachedCertSerialSource = null;
|
|
28
29
|
const stsCredentialCache = new Map();
|
|
29
|
-
// ---- shared encoder ----
|
|
30
|
-
const textEncoder = new TextEncoder();
|
|
31
|
-
// Precomputed hex table for fast byte→hex conversion (no per-byte toString alloc)
|
|
32
|
-
const HEX_TABLE = new Array(256);
|
|
33
|
-
for (let i = 0; i < 256; i++) {
|
|
34
|
-
HEX_TABLE[i] = (i < 16 ? "0" : "") + i.toString(16);
|
|
35
|
-
}
|
|
36
30
|
async function getSigningMaterial(input) {
|
|
37
31
|
if (cachedSigningKey &&
|
|
38
32
|
cachedCertBase64 === input.certBase64 &&
|
|
@@ -91,11 +85,15 @@ export async function issueAwsCredentials(input) {
|
|
|
91
85
|
const der = base64ToBytes(normalizedCert);
|
|
92
86
|
let offset = 0;
|
|
93
87
|
function readLen() {
|
|
88
|
+
if (offset >= der.length)
|
|
89
|
+
throw new Error("DER overflow");
|
|
94
90
|
const b = der[offset++];
|
|
95
91
|
if ((b & 0x80) === 0)
|
|
96
92
|
return b;
|
|
97
93
|
const n = b & 0x7f;
|
|
98
94
|
let len = 0;
|
|
95
|
+
if (offset + n > der.length)
|
|
96
|
+
throw new Error("DER overflow");
|
|
99
97
|
for (let i = 0; i < n; i++) {
|
|
100
98
|
len = (len << 8) | der[offset++];
|
|
101
99
|
}
|
|
@@ -115,6 +113,8 @@ export async function issueAwsCredentials(input) {
|
|
|
115
113
|
if (der[offset++] !== 0x02)
|
|
116
114
|
throw new Error("bad serial tag");
|
|
117
115
|
const serialLen = readLen();
|
|
116
|
+
if (offset + serialLen > der.length)
|
|
117
|
+
throw new Error("DER overflow");
|
|
118
118
|
let serial = der.slice(offset, offset + serialLen);
|
|
119
119
|
if (serial.length > 1 && serial[0] === 0x00) {
|
|
120
120
|
serial = serial.slice(1);
|
|
@@ -134,17 +134,17 @@ export async function issueAwsCredentials(input) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
// ---- STS credential cache lookup (using certificate serial) ----
|
|
137
|
-
cacheKey = `${region}|${roleArn}|${profileArn}|${certSerialDec}`;
|
|
137
|
+
cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
|
|
138
138
|
const cachedEntry = stsCredentialCache.get(cacheKey);
|
|
139
|
-
// Only reuse cached credentials if they still have >
|
|
140
|
-
const MIN_REMAINING_MS =
|
|
139
|
+
// Only reuse cached credentials if they still have >10p remaining
|
|
140
|
+
const MIN_REMAINING_MS = 5 * 60 * 1000;
|
|
141
141
|
if (cachedEntry) {
|
|
142
|
-
// If
|
|
143
|
-
if (cachedEntry.expiresAt
|
|
142
|
+
// If refresh already in-flight → await the same promise
|
|
143
|
+
if (cachedEntry.expiresAt === 0) {
|
|
144
144
|
return cachedEntry.promise;
|
|
145
145
|
}
|
|
146
|
-
// If
|
|
147
|
-
if (cachedEntry.expiresAt
|
|
146
|
+
// If credentials still valid with safe remaining window → reuse
|
|
147
|
+
if (cachedEntry.expiresAt > Date.now() + MIN_REMAINING_MS) {
|
|
148
148
|
return cachedEntry.promise;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
@@ -155,9 +155,9 @@ export async function issueAwsCredentials(input) {
|
|
|
155
155
|
// 2. Setup constants
|
|
156
156
|
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
157
157
|
const path = PATH;
|
|
158
|
-
const
|
|
159
|
-
const amzDate =
|
|
160
|
-
const dateStamp =
|
|
158
|
+
const iso = new Date().toISOString(); // e.g. 2026-03-07T12:00:00.000Z
|
|
159
|
+
const amzDate = `${iso.slice(0, 4)}${iso.slice(5, 7)}${iso.slice(8, 10)}T${iso.slice(11, 13)}${iso.slice(14, 16)}${iso.slice(17, 19)}Z`;
|
|
160
|
+
const dateStamp = iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10);
|
|
161
161
|
const service = SERVICE;
|
|
162
162
|
// 3. Chuẩn bị Body & Cert
|
|
163
163
|
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
@@ -204,20 +204,17 @@ export async function issueAwsCredentials(input) {
|
|
|
204
204
|
body,
|
|
205
205
|
});
|
|
206
206
|
if (!res.ok) {
|
|
207
|
-
|
|
208
|
-
console.error("[aws-rejected] Request details", {
|
|
207
|
+
console.warn("[aws-rejected]", {
|
|
209
208
|
status: res.status,
|
|
210
|
-
response: errorBody,
|
|
211
209
|
region,
|
|
212
|
-
profile
|
|
213
|
-
amzDate
|
|
210
|
+
profile
|
|
214
211
|
});
|
|
215
212
|
throw new InternalError("aws_rejected");
|
|
216
213
|
}
|
|
217
214
|
const json = await res.json();
|
|
218
215
|
const creds = json?.credentialSet?.[0]?.credentials;
|
|
219
216
|
if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
|
|
220
|
-
console.
|
|
217
|
+
console.warn("[issueAwsCredentials] malformed AWS credential response");
|
|
221
218
|
throw new InternalError("aws_malformed_credentials");
|
|
222
219
|
}
|
|
223
220
|
const result = {
|
|
@@ -226,7 +223,8 @@ export async function issueAwsCredentials(input) {
|
|
|
226
223
|
sessionToken: creds.sessionToken,
|
|
227
224
|
expiration: creds.expiration,
|
|
228
225
|
};
|
|
229
|
-
|
|
226
|
+
// subtract small clock‑skew safety window (5s)
|
|
227
|
+
const exp = new Date(creds.expiration).getTime() - 5000;
|
|
230
228
|
// update cache expiration after success
|
|
231
229
|
const entry = stsCredentialCache.get(cacheKey);
|
|
232
230
|
if (entry) {
|
|
@@ -254,17 +252,8 @@ function normalizeCert(raw) {
|
|
|
254
252
|
if (raw.includes("BEGIN CERTIFICATE")) {
|
|
255
253
|
throw new InternalError("pem_not_allowed");
|
|
256
254
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
async function sha256Hex(input) {
|
|
260
|
-
// Tận dụng textEncoder có sẵn
|
|
261
|
-
const hash = await crypto.subtle.digest("SHA-256", textEncoder.encode(input));
|
|
262
|
-
const bytes = new Uint8Array(hash);
|
|
263
|
-
const hexParts = new Array(32); // SHA-256 luôn là 32 bytes
|
|
264
|
-
for (let i = 0; i < 32; i++) {
|
|
265
|
-
hexParts[i] = HEX_TABLE[bytes[i]];
|
|
266
|
-
}
|
|
267
|
-
return hexParts.join("");
|
|
255
|
+
// remove whitespace and newlines to ensure stable cache keys
|
|
256
|
+
return raw.replace(/\s+/g, "");
|
|
268
257
|
}
|
|
269
258
|
async function getCanonicalRequestHash(canonicalRequest) {
|
|
270
259
|
return sha256Hex(canonicalRequest);
|
|
@@ -274,7 +263,7 @@ async function getPayloadHash(body) {
|
|
|
274
263
|
// so caching adds complexity without measurable benefit.
|
|
275
264
|
return sha256Hex(body);
|
|
276
265
|
}
|
|
277
|
-
// Faster base64 decode → Uint8Array (
|
|
266
|
+
// Faster base64 decode → Uint8Array (V8 optimized path using built‑in vectorized conversion)
|
|
278
267
|
function base64ToBytes(base64) {
|
|
279
268
|
const binary = atob(base64);
|
|
280
269
|
const len = binary.length;
|