Skip to content

Commit eaa46d2

Browse files
kuheBraiden Maggiasiddsriv
authored
feat(signature-v4a): create SignatureV4a JavaScript implementation (#1319)
* Adding SignatureV4a implementation * add elliptic types, formatting * lint fix * export sigv4a * move sigv4a to own package * formatting * build elliptic * building elliptic * formatting * chore(signature-v4): remove duplicate script for bundling elliptic * chore(signature-v4a): formatting fix * test(signature-v4a): move testing from jest to vitest * chore(lockfile): fix build issues * test(signature-v4a): update expectation in buildFixedInputBuffer test case * chore(signature-v4): refactor to move sigv4a container here * chore(signature-v4a): newline for CI * chore(signature-v4a): whitespace fix for CI * chore(signature-v4a): deps updates and lockfile * chore(signature-v4a): constants access annotations * choer(signature-v4a): keep elliptic dep in signature-v4a for bundle * chore(lockfile): ci fix * chore(signature-v4a): formatting * chore(signature-v4): run CI * chore(signature-v4a): readme typo fix --------- Co-authored-by: Braiden Maggia <[email protected]> Co-authored-by: siddsriv <[email protected]> Co-authored-by: sid <[email protected]>
1 parent 8dec179 commit eaa46d2

25 files changed

+10756
-162
lines changed

.changeset/tall-pens-yell.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/signature-v4": minor
3+
---
4+
5+
Adding Signature V4a implementation
+25-162
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import {
22
AwsCredentialIdentity,
3-
ChecksumConstructor,
4-
DateInput,
53
EventSigner,
64
EventSigningArguments,
75
FormattedEvent,
8-
HashConstructor,
9-
HeaderBag,
106
HttpRequest,
117
MessageSigner,
12-
Provider,
138
RequestPresigner,
149
RequestPresigningArguments,
1510
RequestSigner,
@@ -20,8 +15,6 @@ import {
2015
StringSigner,
2116
} from "@smithy/types";
2217
import { toHex } from "@smithy/util-hex-encoding";
23-
import { normalizeProvider } from "@smithy/util-middleware";
24-
import { escapeUri } from "@smithy/util-uri-escape";
2518
import { toUint8Array } from "@smithy/util-utf8";
2619

2720
import {
@@ -42,78 +35,20 @@ import {
4235
} from "./constants";
4336
import { createScope, getSigningKey } from "./credentialDerivation";
4437
import { getCanonicalHeaders } from "./getCanonicalHeaders";
45-
import { getCanonicalQuery } from "./getCanonicalQuery";
4638
import { getPayloadHash } from "./getPayloadHash";
4739
import { HeaderFormatter } from "./HeaderFormatter";
4840
import { hasHeader } from "./headerUtil";
4941
import { moveHeadersToQuery } from "./moveHeadersToQuery";
5042
import { prepareRequest } from "./prepareRequest";
51-
import { iso8601 } from "./utilDate";
43+
import { SignatureV4Base, SignatureV4CryptoInit, SignatureV4Init } from "./SignatureV4Base";
5244

5345
/**
5446
* @public
5547
*/
56-
export interface SignatureV4Init {
57-
/**
58-
* The service signing name.
59-
*/
60-
service: string;
61-
62-
/**
63-
* The region name or a function that returns a promise that will be
64-
* resolved with the region name.
65-
*/
66-
region: string | Provider<string>;
67-
68-
/**
69-
* The credentials with which the request should be signed or a function
70-
* that returns a promise that will be resolved with credentials.
71-
*/
72-
credentials: AwsCredentialIdentity | Provider<AwsCredentialIdentity>;
73-
74-
/**
75-
* A constructor function for a hash object that will calculate SHA-256 HMAC
76-
* checksums.
77-
*/
78-
sha256: ChecksumConstructor | HashConstructor;
79-
80-
/**
81-
* Whether to uri-escape the request URI path as part of computing the
82-
* canonical request string. This is required for every AWS service, except
83-
* Amazon S3, as of late 2017.
84-
*
85-
* @default [true]
86-
*/
87-
uriEscapePath?: boolean;
88-
89-
/**
90-
* Whether to calculate a checksum of the request body and include it as
91-
* either a request header (when signing) or as a query string parameter
92-
* (when presigning). This is required for AWS Glacier and Amazon S3 and optional for
93-
* every other AWS service as of late 2017.
94-
*
95-
* @default [true]
96-
*/
97-
applyChecksum?: boolean;
98-
}
99-
100-
/**
101-
* @public
102-
*/
103-
export interface SignatureV4CryptoInit {
104-
sha256: ChecksumConstructor | HashConstructor;
105-
}
106-
107-
/**
108-
* @public
109-
*/
110-
export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigner, EventSigner, MessageSigner {
111-
private readonly service: string;
112-
private readonly regionProvider: Provider<string>;
113-
private readonly credentialProvider: Provider<AwsCredentialIdentity>;
114-
private readonly sha256: ChecksumConstructor | HashConstructor;
115-
private readonly uriEscapePath: boolean;
116-
private readonly applyChecksum: boolean;
48+
export class SignatureV4
49+
extends SignatureV4Base
50+
implements RequestPresigner, RequestSigner, StringSigner, EventSigner, MessageSigner
51+
{
11752
private readonly headerFormatter = new HeaderFormatter();
11853

11954
constructor({
@@ -124,13 +59,14 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
12459
sha256,
12560
uriEscapePath = true,
12661
}: SignatureV4Init & SignatureV4CryptoInit) {
127-
this.service = service;
128-
this.sha256 = sha256;
129-
this.uriEscapePath = uriEscapePath;
130-
// default to true if applyChecksum isn't set
131-
this.applyChecksum = typeof applyChecksum === "boolean" ? applyChecksum : true;
132-
this.regionProvider = normalizeProvider(region);
133-
this.credentialProvider = normalizeProvider(credentials);
62+
super({
63+
applyChecksum,
64+
credentials,
65+
region,
66+
service,
67+
sha256,
68+
uriEscapePath,
69+
});
13470
}
13571

13672
public async presign(originalRequest: HttpRequest, options: RequestPresigningArguments = {}): Promise<HttpRequest> {
@@ -148,7 +84,7 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
14884
this.validateResolvedCredentials(credentials);
14985
const region = signingRegion ?? (await this.regionProvider());
15086

151-
const { longDate, shortDate } = formatDate(signingDate);
87+
const { longDate, shortDate } = this.formatDate(signingDate);
15288
if (expiresIn > MAX_PRESIGNED_TTL) {
15389
return Promise.reject(
15490
"Signature version 4 presigned URLs" + " must have an expiration date less than one week in" + " the future"
@@ -167,7 +103,7 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
167103
request.query[EXPIRES_QUERY_PARAM] = expiresIn.toString(10);
168104

169105
const canonicalHeaders = getCanonicalHeaders(request, unsignableHeaders, signableHeaders);
170-
request.query[SIGNED_HEADERS_QUERY_PARAM] = getCanonicalHeaderList(canonicalHeaders);
106+
request.query[SIGNED_HEADERS_QUERY_PARAM] = this.getCanonicalHeaderList(canonicalHeaders);
171107

172108
request.query[SIGNATURE_QUERY_PARAM] = await this.getSignature(
173109
longDate,
@@ -200,7 +136,7 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
200136
{ signingDate = new Date(), priorSignature, signingRegion, signingService }: EventSigningArguments
201137
): Promise<string> {
202138
const region = signingRegion ?? (await this.regionProvider());
203-
const { shortDate, longDate } = formatDate(signingDate);
139+
const { shortDate, longDate } = this.formatDate(signingDate);
204140
const scope = createScope(shortDate, region, signingService ?? this.service);
205141
const hashedPayload = await getPayloadHash({ headers: {}, body: payload } as any, this.sha256);
206142
const hash = new this.sha256();
@@ -246,7 +182,7 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
246182
const credentials = await this.credentialProvider();
247183
this.validateResolvedCredentials(credentials);
248184
const region = signingRegion ?? (await this.regionProvider());
249-
const { shortDate } = formatDate(signingDate);
185+
const { shortDate } = this.formatDate(signingDate);
250186

251187
const hash = new this.sha256(await this.getSigningKey(credentials, region, shortDate, signingService));
252188
hash.update(toUint8Array(stringToSign));
@@ -267,7 +203,7 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
267203
this.validateResolvedCredentials(credentials);
268204
const region = signingRegion ?? (await this.regionProvider());
269205
const request = prepareRequest(requestToSign);
270-
const { longDate, shortDate } = formatDate(signingDate);
206+
const { longDate, shortDate } = this.formatDate(signingDate);
271207
const scope = createScope(shortDate, region, signingService ?? this.service);
272208

273209
request.headers[AMZ_DATE_HEADER] = longDate;
@@ -291,75 +227,24 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne
291227
request.headers[AUTH_HEADER] =
292228
`${ALGORITHM_IDENTIFIER} ` +
293229
`Credential=${credentials.accessKeyId}/${scope}, ` +
294-
`SignedHeaders=${getCanonicalHeaderList(canonicalHeaders)}, ` +
230+
`SignedHeaders=${this.getCanonicalHeaderList(canonicalHeaders)}, ` +
295231
`Signature=${signature}`;
296232

297233
return request;
298234
}
299235

300-
private createCanonicalRequest(request: HttpRequest, canonicalHeaders: HeaderBag, payloadHash: string): string {
301-
const sortedHeaders = Object.keys(canonicalHeaders).sort();
302-
return `${request.method}
303-
${this.getCanonicalPath(request)}
304-
${getCanonicalQuery(request)}
305-
${sortedHeaders.map((name) => `${name}:${canonicalHeaders[name]}`).join("\n")}
306-
307-
${sortedHeaders.join(";")}
308-
${payloadHash}`;
309-
}
310-
311-
private async createStringToSign(
312-
longDate: string,
313-
credentialScope: string,
314-
canonicalRequest: string
315-
): Promise<string> {
316-
const hash = new this.sha256();
317-
hash.update(toUint8Array(canonicalRequest));
318-
const hashedRequest = await hash.digest();
319-
320-
return `${ALGORITHM_IDENTIFIER}
321-
${longDate}
322-
${credentialScope}
323-
${toHex(hashedRequest)}`;
324-
}
325-
326-
private getCanonicalPath({ path }: HttpRequest): string {
327-
if (this.uriEscapePath) {
328-
// Non-S3 services, we normalize the path and then double URI encode it.
329-
// Ref: "Remove Dot Segments" https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
330-
const normalizedPathSegments = [];
331-
for (const pathSegment of path.split("/")) {
332-
if (pathSegment?.length === 0) continue;
333-
if (pathSegment === ".") continue;
334-
if (pathSegment === "..") {
335-
normalizedPathSegments.pop();
336-
} else {
337-
normalizedPathSegments.push(pathSegment);
338-
}
339-
}
340-
// Joining by single slashes to remove consecutive slashes.
341-
const normalizedPath = `${path?.startsWith("/") ? "/" : ""}${normalizedPathSegments.join("/")}${
342-
normalizedPathSegments.length > 0 && path?.endsWith("/") ? "/" : ""
343-
}`;
344-
345-
// Double encode and replace non-standard characters !'()* according to RFC 3986
346-
const doubleEncoded = escapeUri(normalizedPath);
347-
return doubleEncoded.replace(/%2F/g, "/");
348-
}
349-
350-
// For S3, we shouldn't normalize the path. For example, object name
351-
// my-object//example//photo.user should not be normalized to
352-
// my-object/example/photo.user
353-
return path;
354-
}
355-
356236
private async getSignature(
357237
longDate: string,
358238
credentialScope: string,
359239
keyPromise: Promise<Uint8Array>,
360240
canonicalRequest: string
361241
): Promise<string> {
362-
const stringToSign = await this.createStringToSign(longDate, credentialScope, canonicalRequest);
242+
const stringToSign = await this.createStringToSign(
243+
longDate,
244+
credentialScope,
245+
canonicalRequest,
246+
ALGORITHM_IDENTIFIER
247+
);
363248

364249
const hash = new this.sha256(await keyPromise);
365250
hash.update(toUint8Array(stringToSign));
@@ -374,26 +259,4 @@ ${toHex(hashedRequest)}`;
374259
): Promise<Uint8Array> {
375260
return getSigningKey(this.sha256, credentials, shortDate, region, service || this.service);
376261
}
377-
378-
private validateResolvedCredentials(credentials: unknown) {
379-
if (
380-
typeof credentials !== "object" ||
381-
// @ts-expect-error: Property 'accessKeyId' does not exist on type 'object'.ts(2339)
382-
typeof credentials.accessKeyId !== "string" ||
383-
// @ts-expect-error: Property 'secretAccessKey' does not exist on type 'object'.ts(2339)
384-
typeof credentials.secretAccessKey !== "string"
385-
) {
386-
throw new Error("Resolved credential object is not valid");
387-
}
388-
}
389262
}
390-
391-
const formatDate = (now: DateInput): { longDate: string; shortDate: string } => {
392-
const longDate = iso8601(now).replace(/[\-:]/g, "");
393-
return {
394-
longDate,
395-
shortDate: longDate.slice(0, 8),
396-
};
397-
};
398-
399-
const getCanonicalHeaderList = (headers: object): string => Object.keys(headers).sort().join(";");

0 commit comments

Comments
 (0)