1 | import { toHex } from "@aws-sdk/util-hex-encoding";
|
2 | import { normalizeProvider } from "@aws-sdk/util-middleware";
|
3 | import { toUint8Array } from "@aws-sdk/util-utf8";
|
4 | import { ALGORITHM_IDENTIFIER, ALGORITHM_QUERY_PARAM, AMZ_DATE_HEADER, AMZ_DATE_QUERY_PARAM, AUTH_HEADER, CREDENTIAL_QUERY_PARAM, EVENT_ALGORITHM_IDENTIFIER, EXPIRES_QUERY_PARAM, MAX_PRESIGNED_TTL, SHA256_HEADER, SIGNATURE_QUERY_PARAM, SIGNED_HEADERS_QUERY_PARAM, TOKEN_HEADER, TOKEN_QUERY_PARAM, } from "./constants";
|
5 | import { createScope, getSigningKey } from "./credentialDerivation";
|
6 | import { getCanonicalHeaders } from "./getCanonicalHeaders";
|
7 | import { getCanonicalQuery } from "./getCanonicalQuery";
|
8 | import { getPayloadHash } from "./getPayloadHash";
|
9 | import { hasHeader } from "./headerUtil";
|
10 | import { moveHeadersToQuery } from "./moveHeadersToQuery";
|
11 | import { prepareRequest } from "./prepareRequest";
|
12 | import { iso8601 } from "./utilDate";
|
13 | export class SignatureV4 {
|
14 | constructor({ applyChecksum, credentials, region, service, sha256, uriEscapePath = true, }) {
|
15 | this.service = service;
|
16 | this.sha256 = sha256;
|
17 | this.uriEscapePath = uriEscapePath;
|
18 | this.applyChecksum = typeof applyChecksum === "boolean" ? applyChecksum : true;
|
19 | this.regionProvider = normalizeProvider(region);
|
20 | this.credentialProvider = normalizeProvider(credentials);
|
21 | }
|
22 | async presign(originalRequest, options = {}) {
|
23 | const { signingDate = new Date(), expiresIn = 3600, unsignableHeaders, unhoistableHeaders, signableHeaders, signingRegion, signingService, } = options;
|
24 | const credentials = await this.credentialProvider();
|
25 | this.validateResolvedCredentials(credentials);
|
26 | const region = signingRegion ?? (await this.regionProvider());
|
27 | const { longDate, shortDate } = formatDate(signingDate);
|
28 | if (expiresIn > MAX_PRESIGNED_TTL) {
|
29 | return Promise.reject("Signature version 4 presigned URLs" + " must have an expiration date less than one week in" + " the future");
|
30 | }
|
31 | const scope = createScope(shortDate, region, signingService ?? this.service);
|
32 | const request = moveHeadersToQuery(prepareRequest(originalRequest), { unhoistableHeaders });
|
33 | if (credentials.sessionToken) {
|
34 | request.query[TOKEN_QUERY_PARAM] = credentials.sessionToken;
|
35 | }
|
36 | request.query[ALGORITHM_QUERY_PARAM] = ALGORITHM_IDENTIFIER;
|
37 | request.query[CREDENTIAL_QUERY_PARAM] = `${credentials.accessKeyId}/${scope}`;
|
38 | request.query[AMZ_DATE_QUERY_PARAM] = longDate;
|
39 | request.query[EXPIRES_QUERY_PARAM] = expiresIn.toString(10);
|
40 | const canonicalHeaders = getCanonicalHeaders(request, unsignableHeaders, signableHeaders);
|
41 | request.query[SIGNED_HEADERS_QUERY_PARAM] = getCanonicalHeaderList(canonicalHeaders);
|
42 | request.query[SIGNATURE_QUERY_PARAM] = await this.getSignature(longDate, scope, this.getSigningKey(credentials, region, shortDate, signingService), this.createCanonicalRequest(request, canonicalHeaders, await getPayloadHash(originalRequest, this.sha256)));
|
43 | return request;
|
44 | }
|
45 | async sign(toSign, options) {
|
46 | if (typeof toSign === "string") {
|
47 | return this.signString(toSign, options);
|
48 | }
|
49 | else if (toSign.headers && toSign.payload) {
|
50 | return this.signEvent(toSign, options);
|
51 | }
|
52 | else {
|
53 | return this.signRequest(toSign, options);
|
54 | }
|
55 | }
|
56 | async signEvent({ headers, payload }, { signingDate = new Date(), priorSignature, signingRegion, signingService }) {
|
57 | const region = signingRegion ?? (await this.regionProvider());
|
58 | const { shortDate, longDate } = formatDate(signingDate);
|
59 | const scope = createScope(shortDate, region, signingService ?? this.service);
|
60 | const hashedPayload = await getPayloadHash({ headers: {}, body: payload }, this.sha256);
|
61 | const hash = new this.sha256();
|
62 | hash.update(headers);
|
63 | const hashedHeaders = toHex(await hash.digest());
|
64 | const stringToSign = [
|
65 | EVENT_ALGORITHM_IDENTIFIER,
|
66 | longDate,
|
67 | scope,
|
68 | priorSignature,
|
69 | hashedHeaders,
|
70 | hashedPayload,
|
71 | ].join("\n");
|
72 | return this.signString(stringToSign, { signingDate, signingRegion: region, signingService });
|
73 | }
|
74 | async signString(stringToSign, { signingDate = new Date(), signingRegion, signingService } = {}) {
|
75 | const credentials = await this.credentialProvider();
|
76 | this.validateResolvedCredentials(credentials);
|
77 | const region = signingRegion ?? (await this.regionProvider());
|
78 | const { shortDate } = formatDate(signingDate);
|
79 | const hash = new this.sha256(await this.getSigningKey(credentials, region, shortDate, signingService));
|
80 | hash.update(toUint8Array(stringToSign));
|
81 | return toHex(await hash.digest());
|
82 | }
|
83 | async signRequest(requestToSign, { signingDate = new Date(), signableHeaders, unsignableHeaders, signingRegion, signingService, } = {}) {
|
84 | const credentials = await this.credentialProvider();
|
85 | this.validateResolvedCredentials(credentials);
|
86 | const region = signingRegion ?? (await this.regionProvider());
|
87 | const request = prepareRequest(requestToSign);
|
88 | const { longDate, shortDate } = formatDate(signingDate);
|
89 | const scope = createScope(shortDate, region, signingService ?? this.service);
|
90 | request.headers[AMZ_DATE_HEADER] = longDate;
|
91 | if (credentials.sessionToken) {
|
92 | request.headers[TOKEN_HEADER] = credentials.sessionToken;
|
93 | }
|
94 | const payloadHash = await getPayloadHash(request, this.sha256);
|
95 | if (!hasHeader(SHA256_HEADER, request.headers) && this.applyChecksum) {
|
96 | request.headers[SHA256_HEADER] = payloadHash;
|
97 | }
|
98 | const canonicalHeaders = getCanonicalHeaders(request, unsignableHeaders, signableHeaders);
|
99 | const signature = await this.getSignature(longDate, scope, this.getSigningKey(credentials, region, shortDate, signingService), this.createCanonicalRequest(request, canonicalHeaders, payloadHash));
|
100 | request.headers[AUTH_HEADER] =
|
101 | `${ALGORITHM_IDENTIFIER} ` +
|
102 | `Credential=${credentials.accessKeyId}/${scope}, ` +
|
103 | `SignedHeaders=${getCanonicalHeaderList(canonicalHeaders)}, ` +
|
104 | `Signature=${signature}`;
|
105 | return request;
|
106 | }
|
107 | createCanonicalRequest(request, canonicalHeaders, payloadHash) {
|
108 | const sortedHeaders = Object.keys(canonicalHeaders).sort();
|
109 | return `${request.method}
|
110 | ${this.getCanonicalPath(request)}
|
111 | ${getCanonicalQuery(request)}
|
112 | ${sortedHeaders.map((name) => `${name}:${canonicalHeaders[name]}`).join("\n")}
|
113 |
|
114 | ${sortedHeaders.join(";")}
|
115 | ${payloadHash}`;
|
116 | }
|
117 | async createStringToSign(longDate, credentialScope, canonicalRequest) {
|
118 | const hash = new this.sha256();
|
119 | hash.update(toUint8Array(canonicalRequest));
|
120 | const hashedRequest = await hash.digest();
|
121 | return `${ALGORITHM_IDENTIFIER}
|
122 | ${longDate}
|
123 | ${credentialScope}
|
124 | ${toHex(hashedRequest)}`;
|
125 | }
|
126 | getCanonicalPath({ path }) {
|
127 | if (this.uriEscapePath) {
|
128 | const normalizedPathSegments = [];
|
129 | for (const pathSegment of path.split("/")) {
|
130 | if (pathSegment?.length === 0)
|
131 | continue;
|
132 | if (pathSegment === ".")
|
133 | continue;
|
134 | if (pathSegment === "..") {
|
135 | normalizedPathSegments.pop();
|
136 | }
|
137 | else {
|
138 | normalizedPathSegments.push(pathSegment);
|
139 | }
|
140 | }
|
141 | const normalizedPath = `${path?.startsWith("/") ? "/" : ""}${normalizedPathSegments.join("/")}${normalizedPathSegments.length > 0 && path?.endsWith("/") ? "/" : ""}`;
|
142 | const doubleEncoded = encodeURIComponent(normalizedPath);
|
143 | return doubleEncoded.replace(/%2F/g, "/");
|
144 | }
|
145 | return path;
|
146 | }
|
147 | async getSignature(longDate, credentialScope, keyPromise, canonicalRequest) {
|
148 | const stringToSign = await this.createStringToSign(longDate, credentialScope, canonicalRequest);
|
149 | const hash = new this.sha256(await keyPromise);
|
150 | hash.update(toUint8Array(stringToSign));
|
151 | return toHex(await hash.digest());
|
152 | }
|
153 | getSigningKey(credentials, region, shortDate, service) {
|
154 | return getSigningKey(this.sha256, credentials, shortDate, region, service || this.service);
|
155 | }
|
156 | validateResolvedCredentials(credentials) {
|
157 | if (typeof credentials !== "object" ||
|
158 | typeof credentials.accessKeyId !== "string" ||
|
159 | typeof credentials.secretAccessKey !== "string") {
|
160 | throw new Error("Resolved credential object is not valid");
|
161 | }
|
162 | }
|
163 | }
|
164 | const formatDate = (now) => {
|
165 | const longDate = iso8601(now).replace(/[\-:]/g, "");
|
166 | return {
|
167 | longDate,
|
168 | shortDate: longDate.slice(0, 8),
|
169 | };
|
170 | };
|
171 | const getCanonicalHeaderList = (headers) => Object.keys(headers).sort().join(";");
|