@vizamodo/aws-sts-core 0.1.23 → 0.1.30
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 +48 -68
- package/dist/sts/signer.js +37 -36
- package/package.json +1 -1
package/dist/sts/issue.js
CHANGED
|
@@ -42,49 +42,39 @@ 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 & Cert (base64-encoded DER, không marker, không xuống dòng)
|
|
57
|
+
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
70
58
|
const payloadHash = await sha256Hex(body);
|
|
71
|
-
|
|
59
|
+
// AWS Roles Anywhere requires base64-encoded DER (NO PEM markers, NO newlines)
|
|
60
|
+
const normalizedCert = certBase64.replace(/\s+/g, "");
|
|
61
|
+
// 4. Tính toán Signature (CẦN Host trong Canonical Request)
|
|
72
62
|
const baseHeaders = {
|
|
73
|
-
host,
|
|
74
63
|
"content-type": "application/json",
|
|
64
|
+
"host": host,
|
|
75
65
|
"x-amz-date": amzDate,
|
|
76
|
-
"x-amz-x509-chain":
|
|
66
|
+
"x-amz-x509-chain": normalizedCert,
|
|
77
67
|
};
|
|
78
68
|
const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
|
|
69
|
+
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
79
70
|
const canonicalRequest = buildCanonicalRequest({
|
|
80
|
-
method,
|
|
71
|
+
method: "POST",
|
|
81
72
|
canonicalUri: path,
|
|
82
73
|
query: "",
|
|
83
74
|
canonicalHeaders,
|
|
84
75
|
signedHeaders,
|
|
85
76
|
payloadHash,
|
|
86
77
|
});
|
|
87
|
-
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
88
78
|
const stringToSign = buildStringToSign({
|
|
89
79
|
algorithm: "AWS4-X509-ECDSA-SHA256",
|
|
90
80
|
amzDate,
|
|
@@ -92,56 +82,46 @@ export async function issueAwsCredentials(input) {
|
|
|
92
82
|
canonicalRequestHash: await sha256Hex(canonicalRequest),
|
|
93
83
|
});
|
|
94
84
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
let res;
|
|
85
|
+
// 5. Build Headers gửi đi (KHÔNG gửi Host header thủ công trong fetch)
|
|
86
|
+
const finalHeaders = new Headers({
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
"X-Amz-Date": amzDate,
|
|
89
|
+
"X-Amz-X509-Chain": normalizedCert,
|
|
90
|
+
"Authorization": `AWS4-X509-ECDSA-SHA256 Credential=${roleArn}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`
|
|
91
|
+
});
|
|
92
|
+
// 6. Execution (fetch)
|
|
104
93
|
try {
|
|
105
|
-
res = await fetch(`https://${host}${path}`, {
|
|
106
|
-
method,
|
|
107
|
-
headers,
|
|
94
|
+
const res = await fetch(`https://${host}${path}`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: finalHeaders,
|
|
108
97
|
body,
|
|
109
98
|
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const errorBody = await res.text();
|
|
101
|
+
// Log cực kỳ chi tiết để soi Signature của AWS vs của mình
|
|
102
|
+
console.error("[aws-rejected]", {
|
|
103
|
+
status: res.status,
|
|
104
|
+
awsResponse: errorBody,
|
|
105
|
+
myCanonicalRequest: canonicalRequest, // Copy cái này ra so với AWS
|
|
106
|
+
});
|
|
107
|
+
throw new InternalError("aws_rejected");
|
|
108
|
+
}
|
|
109
|
+
const json = await res.json();
|
|
110
|
+
const creds = json?.credentialSet?.[0]?.credentials;
|
|
111
|
+
return {
|
|
112
|
+
accessKeyId: creds.accessKeyId,
|
|
113
|
+
secretAccessKey: creds.secretAccessKey,
|
|
114
|
+
sessionToken: creds.sessionToken,
|
|
115
|
+
expiration: creds.expiration,
|
|
116
|
+
};
|
|
110
117
|
}
|
|
111
|
-
catch {
|
|
118
|
+
catch (e) {
|
|
119
|
+
if (e instanceof InternalError)
|
|
120
|
+
throw e;
|
|
112
121
|
throw new InternalError("aws_unreachable");
|
|
113
122
|
}
|
|
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
123
|
}
|
|
138
124
|
// ---- helpers ----
|
|
139
|
-
function toPemCertificate(base64) {
|
|
140
|
-
// Loại bỏ mọi khoảng trắng/xuống dòng trong base64
|
|
141
|
-
const normalized = base64.replace(/\s+/g, "");
|
|
142
|
-
// AWS Roles Anywhere Header: Không được dùng xuống dòng \n
|
|
143
|
-
return `-----BEGIN CERTIFICATE-----${normalized}-----END CERTIFICATE-----`;
|
|
144
|
-
}
|
|
145
125
|
async function sha256Hex(input) {
|
|
146
126
|
const data = new TextEncoder().encode(input);
|
|
147
127
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
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
|
/**
|