@vizamodo/aws-sts-core 0.1.21 → 0.1.28
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 +51 -66
- package/dist/sts/signer.js +37 -36
- package/package.json +1 -1
package/dist/sts/issue.js
CHANGED
|
@@ -22,12 +22,12 @@ async function getSigningMaterial(input) {
|
|
|
22
22
|
}
|
|
23
23
|
function resolveSessionTtlByProfile(profile) {
|
|
24
24
|
switch (profile) {
|
|
25
|
-
case "
|
|
26
|
-
return
|
|
27
|
-
case "
|
|
28
|
-
case "
|
|
25
|
+
case "HubConsoleReadOnly":
|
|
26
|
+
return 1 * 60 * 60; // 12h
|
|
27
|
+
case "HubBillingReadonly":
|
|
28
|
+
case "HubBillingAdmin":
|
|
29
29
|
return 2 * 60 * 60; // 2h
|
|
30
|
-
case "
|
|
30
|
+
case "HubRuntime":
|
|
31
31
|
return 90 * 60; // 1h30m
|
|
32
32
|
default:
|
|
33
33
|
// fail-safe
|
|
@@ -42,49 +42,38 @@ function resolveSessionTtlByProfile(profile) {
|
|
|
42
42
|
*/
|
|
43
43
|
export async function issueAwsCredentials(input) {
|
|
44
44
|
const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, } = input;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
certBase64,
|
|
51
|
-
privateKeyPkcs8Base64,
|
|
52
|
-
});
|
|
53
|
-
const signingKey = signingMaterial.signingKey;
|
|
54
|
-
const method = "POST";
|
|
55
|
-
const service = "rolesanywhere";
|
|
45
|
+
// 1. Kiểm tra đầu vào & Fix TTL an toàn
|
|
46
|
+
// AWS mặc định chỉ cho phép 3600s, hãy fix cứng 1h để loại trừ lỗi IAM
|
|
47
|
+
const sessionTtl = Math.min(resolveSessionTtlByProfile(profile), 3600);
|
|
48
|
+
const { signingKey } = await getSigningMaterial({ certBase64, privateKeyPkcs8Base64 });
|
|
49
|
+
// 2. Setup constants
|
|
56
50
|
const host = `rolesanywhere.${region}.amazonaws.com`;
|
|
57
51
|
const path = "/sessions";
|
|
58
52
|
const now = new Date();
|
|
59
|
-
const amzDate = now
|
|
60
|
-
.toISOString()
|
|
61
|
-
.replace(/\.\d{3}Z$/, "Z")
|
|
62
|
-
.replace(/[:-]/g, "");
|
|
53
|
+
const amzDate = now.toISOString().replace(/\.\d{3}Z$/, "Z").replace(/[:-]/g, "");
|
|
63
54
|
const dateStamp = amzDate.slice(0, 8);
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
roleArn,
|
|
68
|
-
durationSeconds: sessionTtl,
|
|
69
|
-
});
|
|
55
|
+
const service = "rolesanywhere";
|
|
56
|
+
// 3. Chuẩn bị Body & PEM Cert (Flat - Không xuống dòng)
|
|
57
|
+
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
70
58
|
const payloadHash = await sha256Hex(body);
|
|
71
|
-
const pemCert =
|
|
59
|
+
const pemCert = `-----BEGIN CERTIFICATE-----${certBase64.replace(/\s+/g, "")}-----END CERTIFICATE-----`;
|
|
60
|
+
// 4. Tính toán Signature (CẦN Host trong Canonical Request)
|
|
72
61
|
const baseHeaders = {
|
|
73
|
-
host,
|
|
74
62
|
"content-type": "application/json",
|
|
63
|
+
"host": host,
|
|
75
64
|
"x-amz-date": amzDate,
|
|
76
65
|
"x-amz-x509-chain": pemCert,
|
|
77
66
|
};
|
|
78
67
|
const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
|
|
68
|
+
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
79
69
|
const canonicalRequest = buildCanonicalRequest({
|
|
80
|
-
method,
|
|
70
|
+
method: "POST",
|
|
81
71
|
canonicalUri: path,
|
|
82
72
|
query: "",
|
|
83
73
|
canonicalHeaders,
|
|
84
74
|
signedHeaders,
|
|
85
75
|
payloadHash,
|
|
86
76
|
});
|
|
87
|
-
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
88
77
|
const stringToSign = buildStringToSign({
|
|
89
78
|
algorithm: "AWS4-X509-ECDSA-SHA256",
|
|
90
79
|
amzDate,
|
|
@@ -92,48 +81,44 @@ export async function issueAwsCredentials(input) {
|
|
|
92
81
|
canonicalRequestHash: await sha256Hex(canonicalRequest),
|
|
93
82
|
});
|
|
94
83
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
let res;
|
|
84
|
+
// 5. Build Headers gửi đi (KHÔNG gửi Host header thủ công trong fetch)
|
|
85
|
+
const finalHeaders = new Headers({
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"X-Amz-Date": amzDate,
|
|
88
|
+
"X-Amz-X509-Chain": pemCert,
|
|
89
|
+
"Authorization": `AWS4-X509-ECDSA-SHA256 Credential=${roleArn}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`
|
|
90
|
+
});
|
|
91
|
+
// 6. Execution (fetch)
|
|
104
92
|
try {
|
|
105
|
-
res = await fetch(`https://${host}${path}`, {
|
|
106
|
-
method,
|
|
107
|
-
headers,
|
|
93
|
+
const res = await fetch(`https://${host}${path}`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: finalHeaders,
|
|
108
96
|
body,
|
|
109
97
|
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
const errorBody = await res.text();
|
|
100
|
+
// Log cực kỳ chi tiết để soi Signature của AWS vs của mình
|
|
101
|
+
console.error("[aws-rejected]", {
|
|
102
|
+
status: res.status,
|
|
103
|
+
awsResponse: errorBody,
|
|
104
|
+
myCanonicalRequest: canonicalRequest, // Copy cái này ra so với AWS
|
|
105
|
+
});
|
|
106
|
+
throw new InternalError("aws_rejected");
|
|
107
|
+
}
|
|
108
|
+
const json = await res.json();
|
|
109
|
+
const creds = json?.credentialSet?.[0]?.credentials;
|
|
110
|
+
return {
|
|
111
|
+
accessKeyId: creds.accessKeyId,
|
|
112
|
+
secretAccessKey: creds.secretAccessKey,
|
|
113
|
+
sessionToken: creds.sessionToken,
|
|
114
|
+
expiration: creds.expiration,
|
|
115
|
+
};
|
|
110
116
|
}
|
|
111
|
-
catch {
|
|
117
|
+
catch (e) {
|
|
118
|
+
if (e instanceof InternalError)
|
|
119
|
+
throw e;
|
|
112
120
|
throw new InternalError("aws_unreachable");
|
|
113
121
|
}
|
|
114
|
-
if (!res.ok) {
|
|
115
|
-
throw new InternalError("aws_rejected");
|
|
116
|
-
}
|
|
117
|
-
let json;
|
|
118
|
-
try {
|
|
119
|
-
json = await res.json();
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
throw new InternalError("invalid_aws_response");
|
|
123
|
-
}
|
|
124
|
-
const creds = json?.credentialSet?.[0]?.credentials;
|
|
125
|
-
if (!creds?.accessKeyId ||
|
|
126
|
-
!creds?.secretAccessKey ||
|
|
127
|
-
!creds?.sessionToken ||
|
|
128
|
-
!creds?.expiration) {
|
|
129
|
-
throw new InternalError("malformed_aws_credentials");
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
accessKeyId: creds.accessKeyId,
|
|
133
|
-
secretAccessKey: creds.secretAccessKey,
|
|
134
|
-
sessionToken: creds.sessionToken,
|
|
135
|
-
expiration: creds.expiration,
|
|
136
|
-
};
|
|
137
122
|
}
|
|
138
123
|
// ---- helpers ----
|
|
139
124
|
function toPemCertificate(base64) {
|
package/dist/sts/signer.js
CHANGED
|
@@ -10,54 +10,55 @@ import { InternalError } from "./errors";
|
|
|
10
10
|
export async function signStringToSign(stringToSign, signingKey) {
|
|
11
11
|
try {
|
|
12
12
|
const data = new TextEncoder().encode(stringToSign);
|
|
13
|
-
const
|
|
13
|
+
const signature = await crypto.subtle.sign({
|
|
14
14
|
name: "ECDSA",
|
|
15
15
|
hash: "SHA-256",
|
|
16
16
|
}, signingKey, data);
|
|
17
|
-
|
|
18
|
-
// Cần convert sang DER format cho AWS
|
|
19
|
-
const derSignature = rawToDer(new Uint8Array(signatureRaw));
|
|
17
|
+
const derSignature = rawEcdsaToDer(new Uint8Array(signature));
|
|
20
18
|
return uint8ArrayToHex(derSignature);
|
|
21
19
|
}
|
|
22
|
-
catch
|
|
23
|
-
console.error("[SIGNING_DEBUG]", e);
|
|
20
|
+
catch {
|
|
24
21
|
throw new InternalError("signing_failed");
|
|
25
22
|
}
|
|
26
23
|
}
|
|
27
24
|
/**
|
|
28
|
-
* Convert
|
|
25
|
+
* Convert WebCrypto ECDSA signature (IEEE P1363 raw format: r||s)
|
|
26
|
+
* to DER-encoded ASN.1 sequence required by AWS (SigV4 X.509 ECDSA).
|
|
29
27
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
function rawEcdsaToDer(raw) {
|
|
29
|
+
if (raw.length % 2 !== 0) {
|
|
30
|
+
throw new InternalError("invalid_raw_signature_length");
|
|
31
|
+
}
|
|
32
|
+
const len = raw.length / 2;
|
|
33
|
+
const r = raw.slice(0, len);
|
|
34
|
+
const s = raw.slice(len);
|
|
35
|
+
const encodeInt = (bytes) => {
|
|
36
|
+
// Remove leading zeros
|
|
37
|
+
let i = 0;
|
|
38
|
+
while (i < bytes.length - 1 && bytes[i] === 0)
|
|
39
|
+
i++;
|
|
40
|
+
let v = bytes.slice(i);
|
|
41
|
+
// If high bit is set, prepend 0x00 to indicate positive integer
|
|
42
|
+
if (v[0] & 0x80) {
|
|
43
|
+
const extended = new Uint8Array(v.length + 1);
|
|
44
|
+
extended[0] = 0;
|
|
45
|
+
extended.set(v, 1);
|
|
46
|
+
v = extended;
|
|
44
47
|
}
|
|
45
|
-
|
|
48
|
+
const result = new Uint8Array(2 + v.length);
|
|
49
|
+
result[0] = 0x02; // INTEGER tag
|
|
50
|
+
result[1] = v.length;
|
|
51
|
+
result.set(v, 2);
|
|
52
|
+
return result;
|
|
46
53
|
};
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const der = new Uint8Array(
|
|
51
|
-
der[0] = 0x30; //
|
|
52
|
-
der[1] =
|
|
53
|
-
|
|
54
|
-
der
|
|
55
|
-
der[offset++] = rAsn1.length;
|
|
56
|
-
der.set(rAsn1, offset);
|
|
57
|
-
offset += rAsn1.length;
|
|
58
|
-
der[offset++] = 0x02; // Integer tag
|
|
59
|
-
der[offset++] = sAsn1.length;
|
|
60
|
-
der.set(sAsn1, offset);
|
|
54
|
+
const rDer = encodeInt(r);
|
|
55
|
+
const sDer = encodeInt(s);
|
|
56
|
+
const sequenceLen = rDer.length + sDer.length;
|
|
57
|
+
const der = new Uint8Array(2 + sequenceLen);
|
|
58
|
+
der[0] = 0x30; // SEQUENCE tag
|
|
59
|
+
der[1] = sequenceLen;
|
|
60
|
+
der.set(rDer, 2);
|
|
61
|
+
der.set(sDer, 2 + rDer.length);
|
|
61
62
|
return der;
|
|
62
63
|
}
|
|
63
64
|
/**
|