From f052f0344d211c9ea1acca07117e9bd90121c520 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Wed, 10 Jul 2024 18:22:52 +0000 Subject: [PATCH 01/15] feat(signature-v4-multi-region): add support for sigv4a package --- .../src/SignatureV4MultiRegion.ts | 53 ++++++++----------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts index 9fa7d410e1a10..bfa77802f1995 100644 --- a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts +++ b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts @@ -9,7 +9,16 @@ import { RequestSigningArguments, } from "@smithy/types"; -import { OptionalCrtSignerV4, signatureV4CrtContainer } from "./signature-v4-crt-container"; +/** + * @internal + */ +export interface SigV4aSigner extends RequestPresigner, RequestSigner { + signWithCredentials( + requestToSign: HttpRequest, + credentials: AwsCredentialIdentity, + options: RequestSigningArguments + ): Promise; +} /** * @internal @@ -24,11 +33,10 @@ export type SignatureV4MultiRegionInit = SignatureV4Init & * dynamically, the signer wraps native module SigV4a signer and JS SigV4 signer. It signs the request with SigV4a * algorithm if the request needs to be signed with `*` region. Otherwise, it signs the request with normal SigV4 * signer. - * Note that SigV4a signer is only supported in Node.js now because it depends on a native dependency. * @internal */ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { - private sigv4aSigner?: InstanceType; + private sigv4aSigner?: SigV4aSigner; private readonly sigv4Signer: SignatureV4S3Express; private readonly signerOptions: SignatureV4MultiRegionInit; @@ -39,9 +47,8 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { public async sign(requestToSign: HttpRequest, options: RequestSigningArguments = {}): Promise { if (options.signingRegion === "*") { - if (this.signerOptions.runtime !== "node") - throw new Error("This request requires signing with SigV4Asymmetric algorithm. It's only available in Node.js"); - return this.getSigv4aSigner().sign(requestToSign, options); + const signer = await this.getSigv4aSigner(); + return signer.sign(requestToSign, options); } return this.sigv4Signer.sign(requestToSign, options); } @@ -55,18 +62,16 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { options: RequestSigningArguments = {} ): Promise { if (options.signingRegion === "*") { - if (this.signerOptions.runtime !== "node") - throw new Error("This request requires signing with SigV4Asymmetric algorithm. It's only available in Node.js"); - return this.getSigv4aSigner().signWithCredentials(requestToSign, credentials, options); + const signer = await this.getSigv4aSigner(); + return signer.signWithCredentials(requestToSign, credentials, options); } return this.sigv4Signer.signWithCredentials(requestToSign, credentials, options); } public async presign(originalRequest: HttpRequest, options: RequestPresigningArguments = {}): Promise { if (options.signingRegion === "*") { - if (this.signerOptions.runtime !== "node") - throw new Error("This request requires signing with SigV4Asymmetric algorithm. It's only available in Node.js"); - return this.getSigv4aSigner().presign(originalRequest, options); + const signer = await this.getSigv4aSigner(); + return signer.presign(originalRequest, options); } return this.sigv4Signer.presign(originalRequest, options); } @@ -82,28 +87,12 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { return this.sigv4Signer.presignWithCredentials(originalRequest, credentials, options); } - private getSigv4aSigner(): InstanceType { + private async getSigv4aSigner(): Promise { if (!this.sigv4aSigner) { - let CrtSignerV4: OptionalCrtSignerV4 | null = null; - - try { - CrtSignerV4 = signatureV4CrtContainer.CrtSignerV4; - if (typeof CrtSignerV4 !== "function") throw new Error(); - } catch (e) { - e.message = - `${e.message}\n` + - `Please check whether you have installed the "@aws-sdk/signature-v4-crt" package explicitly. \n` + - `You must also register the package by calling [require("@aws-sdk/signature-v4-crt");] ` + - `or an ESM equivalent such as [import "@aws-sdk/signature-v4-crt";]. \n` + - "For more information please go to " + - "https://github.com/aws/aws-sdk-js-v3#functionality-requiring-aws-common-runtime-crt"; - throw e; - } - - this.sigv4aSigner = new CrtSignerV4({ + const { SignatureV4a } = await import("@smithy/signature-v4a"); + this.sigv4aSigner = new SignatureV4a({ ...this.signerOptions, - signingAlgorithm: 1, - }); + }) as SigV4aSigner; } return this.sigv4aSigner; } From 81ddf6c88233344f6f435f372232898e03a9d125 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Wed, 10 Jul 2024 18:33:37 +0000 Subject: [PATCH 02/15] chore(signature-v4-multi-region): package.json update for adding sigv4a package in deps --- packages/signature-v4-multi-region/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/signature-v4-multi-region/package.json b/packages/signature-v4-multi-region/package.json index f031c7c71cde7..068cf4cfbba8c 100644 --- a/packages/signature-v4-multi-region/package.json +++ b/packages/signature-v4-multi-region/package.json @@ -24,6 +24,7 @@ "@aws-sdk/types": "*", "@smithy/protocol-http": "^4.0.3", "@smithy/signature-v4": "^3.1.2", + "@smithy/signature-v4a": "*", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, From 9cd37b2dfe834589595c4d8787562ab6f53d9d98 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Wed, 10 Jul 2024 20:26:08 +0000 Subject: [PATCH 03/15] fix(signature-v4-multi-region): container based loading for CRT and JS Sigv4a --- .../src/SignatureV4MultiRegion.ts | 37 +++++++++++-------- .../src/signature-v4a-container.ts | 25 +++++++++++++ 2 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 packages/signature-v4-multi-region/src/signature-v4a-container.ts diff --git a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts index bfa77802f1995..ab51ee0c36d3a 100644 --- a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts +++ b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts @@ -9,16 +9,8 @@ import { RequestSigningArguments, } from "@smithy/types"; -/** - * @internal - */ -export interface SigV4aSigner extends RequestPresigner, RequestSigner { - signWithCredentials( - requestToSign: HttpRequest, - credentials: AwsCredentialIdentity, - options: RequestSigningArguments - ): Promise; -} +import { OptionalSigV4aSigner, signatureV4aContainer } from "./signature-v4a-container"; +import { OptionalCrtSignerV4, signatureV4CrtContainer } from "./signature-v4-crt-container"; /** * @internal @@ -36,7 +28,7 @@ export type SignatureV4MultiRegionInit = SignatureV4Init & * @internal */ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { - private sigv4aSigner?: SigV4aSigner; + private sigv4aSigner?: InstanceType | InstanceType; private readonly sigv4Signer: SignatureV4S3Express; private readonly signerOptions: SignatureV4MultiRegionInit; @@ -87,12 +79,25 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { return this.sigv4Signer.presignWithCredentials(originalRequest, credentials, options); } - private async getSigv4aSigner(): Promise { + private async getSigv4aSigner(): Promise | InstanceType> { if (!this.sigv4aSigner) { - const { SignatureV4a } = await import("@smithy/signature-v4a"); - this.sigv4aSigner = new SignatureV4a({ - ...this.signerOptions, - }) as SigV4aSigner; + if (signatureV4CrtContainer.CrtSignerV4) { + // CRT implementation + this.sigv4aSigner = new signatureV4CrtContainer.CrtSignerV4({ + ...this.signerOptions, + signingAlgorithm: 1, + }); + } else if (signatureV4aContainer.SignatureV4a) { + // SigV4a JS implementation + this.sigv4aSigner = new signatureV4aContainer.SignatureV4a({ + ...this.signerOptions, + }); + } else { + throw new Error( + "Neither CRT nor JS SigV4a implementation is available. " + + "Please load either @aws-sdk/signature-v4-crt or @smithy/signature-v4a." + ); + } } return this.sigv4aSigner; } diff --git a/packages/signature-v4-multi-region/src/signature-v4a-container.ts b/packages/signature-v4-multi-region/src/signature-v4a-container.ts new file mode 100644 index 0000000000000..88c16469cb181 --- /dev/null +++ b/packages/signature-v4-multi-region/src/signature-v4a-container.ts @@ -0,0 +1,25 @@ +import type { AwsCredentialIdentity } from "@aws-sdk/types"; +import type { HttpRequest, RequestPresigner, RequestSigner, RequestSigningArguments } from "@smithy/types"; + +/** + * @public + */ +export type OptionalSigV4aSigner = { + new (options: any): RequestPresigner & + RequestSigner & { + signWithCredentials( + requestToSign: HttpRequest, + credentials: AwsCredentialIdentity, + options: RequestSigningArguments + ): Promise; + }; +}; + +/** + * @public + */ +export const signatureV4aContainer: { + SignatureV4a: null | OptionalSigV4aSigner; +} = { + SignatureV4a: null, +}; From c4f2e54e733f925019c3eced3f20e577d37d1bd2 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Wed, 10 Jul 2024 20:32:27 +0000 Subject: [PATCH 04/15] chore(signature-v4-multi-region): de-async getSigV4a owing to containers --- .../src/SignatureV4MultiRegion.ts | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts index ab51ee0c36d3a..3296eb1253564 100644 --- a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts +++ b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts @@ -9,8 +9,8 @@ import { RequestSigningArguments, } from "@smithy/types"; -import { OptionalSigV4aSigner, signatureV4aContainer } from "./signature-v4a-container"; import { OptionalCrtSignerV4, signatureV4CrtContainer } from "./signature-v4-crt-container"; +import { OptionalSigV4aSigner, signatureV4aContainer } from "./signature-v4a-container"; /** * @internal @@ -39,8 +39,7 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { public async sign(requestToSign: HttpRequest, options: RequestSigningArguments = {}): Promise { if (options.signingRegion === "*") { - const signer = await this.getSigv4aSigner(); - return signer.sign(requestToSign, options); + return this.getSigv4aSigner().sign(requestToSign, options); } return this.sigv4Signer.sign(requestToSign, options); } @@ -54,16 +53,14 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { options: RequestSigningArguments = {} ): Promise { if (options.signingRegion === "*") { - const signer = await this.getSigv4aSigner(); - return signer.signWithCredentials(requestToSign, credentials, options); + return this.getSigv4aSigner().signWithCredentials(requestToSign, credentials, options); } return this.sigv4Signer.signWithCredentials(requestToSign, credentials, options); } public async presign(originalRequest: HttpRequest, options: RequestPresigningArguments = {}): Promise { if (options.signingRegion === "*") { - const signer = await this.getSigv4aSigner(); - return signer.presign(originalRequest, options); + return this.getSigv4aSigner().presign(originalRequest, options); } return this.sigv4Signer.presign(originalRequest, options); } @@ -79,17 +76,44 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { return this.sigv4Signer.presignWithCredentials(originalRequest, credentials, options); } - private async getSigv4aSigner(): Promise | InstanceType> { + private getSigv4aSigner(): InstanceType | InstanceType { if (!this.sigv4aSigner) { + let CrtSignerV4: OptionalCrtSignerV4 | null = null; + let JsSigV4a: OptionalSigV4aSigner | null = null; + if (signatureV4CrtContainer.CrtSignerV4) { - // CRT implementation - this.sigv4aSigner = new signatureV4CrtContainer.CrtSignerV4({ + try { + CrtSignerV4 = signatureV4CrtContainer.CrtSignerV4; + if (typeof CrtSignerV4 !== "function") throw new Error(); + } catch (e) { + e.message = + `${e.message}\n` + + `Please check whether you have installed the "@aws-sdk/signature-v4-crt" package explicitly. \n` + + `You must also register the package by calling [require("@aws-sdk/signature-v4-crt");] ` + + `or an ESM equivalent such as [import "@aws-sdk/signature-v4-crt";]. \n` + + "For more information please go to " + + "https://github.com/aws/aws-sdk-js-v3#functionality-requiring-aws-common-runtime-crt"; + throw e; + } + + this.sigv4aSigner = new CrtSignerV4({ ...this.signerOptions, signingAlgorithm: 1, }); } else if (signatureV4aContainer.SignatureV4a) { - // SigV4a JS implementation - this.sigv4aSigner = new signatureV4aContainer.SignatureV4a({ + try { + JsSigV4a = signatureV4aContainer.SignatureV4a; + if (typeof JsSigV4a !== "function") throw new Error(); + } catch (e) { + e.message = + `${e.message}\n` + + `Please check whether you have installed the "@smithy/signature-v4a" package explicitly. \n` + + `You must also register the package by calling [require("@smithy/signature-v4a");] ` + + `or an ESM equivalent such as [import "@smithy/signature-v4a";]. \n`; + throw e; + } + + this.sigv4aSigner = new JsSigV4a({ ...this.signerOptions, }); } else { From b4f86d99a15503d44bf616888b85970da67617c1 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Thu, 11 Jul 2024 17:16:30 +0000 Subject: [PATCH 05/15] chore(signature-v4-multi-region): cleanups and docstrings --- packages/signature-v4-multi-region/package.json | 1 - .../src/SignatureV4MultiRegion.ts | 10 +++++----- .../src/signature-v4a-container.ts | 11 +++++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/signature-v4-multi-region/package.json b/packages/signature-v4-multi-region/package.json index 068cf4cfbba8c..f031c7c71cde7 100644 --- a/packages/signature-v4-multi-region/package.json +++ b/packages/signature-v4-multi-region/package.json @@ -24,7 +24,6 @@ "@aws-sdk/types": "*", "@smithy/protocol-http": "^4.0.3", "@smithy/signature-v4": "^3.1.2", - "@smithy/signature-v4a": "*", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, diff --git a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts index 3296eb1253564..5297b5d6a311c 100644 --- a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts +++ b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts @@ -79,7 +79,7 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { private getSigv4aSigner(): InstanceType | InstanceType { if (!this.sigv4aSigner) { let CrtSignerV4: OptionalCrtSignerV4 | null = null; - let JsSigV4a: OptionalSigV4aSigner | null = null; + let JsSigV4aSigner: OptionalSigV4aSigner | null = null; if (signatureV4CrtContainer.CrtSignerV4) { try { @@ -102,8 +102,8 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { }); } else if (signatureV4aContainer.SignatureV4a) { try { - JsSigV4a = signatureV4aContainer.SignatureV4a; - if (typeof JsSigV4a !== "function") throw new Error(); + JsSigV4aSigner = signatureV4aContainer.SignatureV4a; + if (typeof JsSigV4aSigner !== "function") throw new Error(); } catch (e) { e.message = `${e.message}\n` + @@ -113,13 +113,13 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { throw e; } - this.sigv4aSigner = new JsSigV4a({ + this.sigv4aSigner = new JsSigV4aSigner({ ...this.signerOptions, }); } else { throw new Error( "Neither CRT nor JS SigV4a implementation is available. " + - "Please load either @aws-sdk/signature-v4-crt or @smithy/signature-v4a." + "Please load either @aws-sdk/signature-v4-crt or @smithy/signature-v4a." ); } } diff --git a/packages/signature-v4-multi-region/src/signature-v4a-container.ts b/packages/signature-v4-multi-region/src/signature-v4a-container.ts index 88c16469cb181..b0e24362a7a09 100644 --- a/packages/signature-v4-multi-region/src/signature-v4a-container.ts +++ b/packages/signature-v4-multi-region/src/signature-v4a-container.ts @@ -5,6 +5,12 @@ import type { HttpRequest, RequestPresigner, RequestSigner, RequestSigningArgume * @public */ export type OptionalSigV4aSigner = { + /** + * This constructor is not typed so as not to require a type import + * from the signature-v4a package. + * + * The true type is SignatureV4a from @smithy/signature-v4a. + */ new (options: any): RequestPresigner & RequestSigner & { signWithCredentials( @@ -17,6 +23,11 @@ export type OptionalSigV4aSigner = { /** * @public + * + * \@smithy/signature-v4a will install the constructor in this + * container if it is installed. + * + * This avoids a runtime-require being interpreted statically by bundlers. */ export const signatureV4aContainer: { SignatureV4a: null | OptionalSigV4aSigner; From 2a954d4b7a7e9b3c1d4cd0e91c64e927215c0de4 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Thu, 11 Jul 2024 17:55:04 +0000 Subject: [PATCH 06/15] test(signature-v4-multi-region): add unit tests --- .../src/SignatureV4MultiRegion.spec.ts | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.spec.ts b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.spec.ts index 32ca2575d1a1d..5e172bff7f275 100644 --- a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.spec.ts +++ b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.spec.ts @@ -1,13 +1,15 @@ import { HttpRequest } from "@smithy/protocol-http"; jest.mock("@smithy/signature-v4"); - jest.mock("@aws-sdk/signature-v4-crt"); +jest.mock("@smithy/signature-v4a"); import { CrtSignerV4 } from "@aws-sdk/signature-v4-crt"; import { SignatureV4 } from "@smithy/signature-v4"; +import { SignatureV4a } from "@smithy/signature-v4a"; import { signatureV4CrtContainer } from "./signature-v4-crt-container"; +import { signatureV4aContainer } from "./signature-v4a-container"; import { SignatureV4MultiRegion, SignatureV4MultiRegionInit } from "./SignatureV4MultiRegion"; describe("SignatureV4MultiRegion", () => { @@ -28,6 +30,7 @@ describe("SignatureV4MultiRegion", () => { beforeEach(() => { signatureV4CrtContainer.CrtSignerV4 = CrtSignerV4 as any; + signatureV4aContainer.SignatureV4a = SignatureV4a as any; jest.clearAllMocks(); }); @@ -59,33 +62,41 @@ describe("SignatureV4MultiRegion", () => { expect(CrtSignerV4.mock.instances[0].presign).toBeCalledTimes(1); }); - it("should throw if sign with SigV4a in unsupported runtime", async () => { - expect.assertions(1); - const signer = new SignatureV4MultiRegion({ ...params, runtime: "browser" }); - await expect(async () => await signer.sign(minimalRequest, { signingRegion: "*" })).rejects.toThrow( - "This request requires signing with SigV4Asymmetric algorithm. It's only available in Node.js" - ); + it("should presign with SigV4a signer if signingRegion is '*'", async () => { + const signer = new SignatureV4MultiRegion(params); + await signer.presign(minimalRequest, { signingRegion: "*" }); + //@ts-ignore + expect(SignatureV4a.mock.instances[0].presign).toBeCalledTimes(1); }); - it("should throw if preSign with SigV4a in unsupported runtime", async () => { - expect.assertions(1); - const signer = new SignatureV4MultiRegion({ ...params, runtime: "browser" }); - await expect(signer.presign(minimalRequest, { signingRegion: "*" })).rejects.toThrow( - "This request requires signing with SigV4Asymmetric algorithm. It's only available in Node.js" - ); + it("should sign with SigV4a signer if signingRegion is '*'", async () => { + const signer = new SignatureV4MultiRegion(params); + await signer.sign(minimalRequest, { signingRegion: "*" }); + //@ts-ignore + expect(SignatureV4a.mock.instances[0].sign).toBeCalledTimes(1); + }); + + it("should use CrtSignerV4 if available", async () => { + const signer = new SignatureV4MultiRegion(params); + await signer.sign(minimalRequest, { signingRegion: "*" }); + expect(CrtSignerV4).toHaveBeenCalledTimes(1); + }); + + it("should use SignatureV4a if CrtSignerV4 is not available", async () => { + signatureV4CrtContainer.CrtSignerV4 = null; + const signer = new SignatureV4MultiRegion(params); + await signer.sign(minimalRequest, { signingRegion: "*" }); + expect(SignatureV4a).toHaveBeenCalledTimes(1); }); - it("should throw if sign with SigV4a and signature-v4-crt is not installed", async () => { + it("should throw if neither CrtSignerV4 nor SignatureV4a is available", async () => { signatureV4CrtContainer.CrtSignerV4 = null; + signatureV4aContainer.SignatureV4a = null; expect.assertions(1); const signer = new SignatureV4MultiRegion({ ...params }); await expect(async () => await signer.sign(minimalRequest, { signingRegion: "*" })).rejects.toThrow( - "\n" + - `Please check whether you have installed the "@aws-sdk/signature-v4-crt" package explicitly. \n` + - `You must also register the package by calling [require("@aws-sdk/signature-v4-crt");] ` + - `or an ESM equivalent such as [import "@aws-sdk/signature-v4-crt";]. \n` + - "For more information please go to " + - "https://github.com/aws/aws-sdk-js-v3#functionality-requiring-aws-common-runtime-crt" + "Neither CRT nor JS SigV4a implementation is available. " + + "Please load either @aws-sdk/signature-v4-crt or @smithy/signature-v4a." ); }); }); From b3ce48ae0ceb7132ae7fc6f28054f65461dc9a7a Mon Sep 17 00:00:00 2001 From: siddsriv Date: Thu, 15 Aug 2024 19:48:35 +0000 Subject: [PATCH 07/15] chore(README): readme notes for using sigv4a package --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index e61e443390d91..7322d422c6f83 100644 --- a/README.md +++ b/README.md @@ -686,6 +686,48 @@ import "@aws-sdk/signature-v4-crt"; Only the import statement is needed. The implementation then registers itself with `@aws-sdk/signature-v4-multi-region` and becomes available for its use. You do not need to use any imported objects directly. +Note that you can also use the native JavaScript implementation of SigV4a that does not depend on AWS CRT and can be used in browsers (unlike the CRT version). The instructions for using that package are below. + +### Using JavaScript (non-CRT) implementation of SigV4a + +AWS SDK for JavaScript v3 now supports a native JavaScript version of SigV4a that can be used in browsers and Node, and does not depend on [AWS Common Runtime (CRT)](https://docs.aws.amazon.com/sdkref/latest/guide/common-runtime.html). This is an alternative to the AWS CRT version, so you don't need to have both packages together. + +If neither the CRT components nor the JavaScript SigV4a implementation are installed, you will receive an error like: + +```console +Neither CRT nor JS SigV4a implementation is available. +Please load either @aws-sdk/signature-v4-crt or @smithy/signature-v4a. +``` + +indicating that at least one of the required dependencies is missing to use the associated functionality. To install the JavaScript SigV4a dependency, follow the provided instructions. + +#### Installing the JavaScript SigV4a Dependency + +You can install the JavaScript SigV4a dependency with different commands depending on the package management tool you are using. +If you are using NPM: + +```console +npm install @smithy/signature-v4a +``` + +If you are using Yarn: + +```console +yarn add @smithy/signature-v4a +``` + +Additionally, load the signature-v4a package by importing it. + +```js +require("@smithy/signature-v4a"); +// or ESM +import "@smithy/signature-v4a"; +``` + +Only the import statement is needed. The implementation then registers itself with `@aws-sdk/signature-v4-multi-region` +and becomes available for its use. You do not need to use any imported objects directly. +Note that if both the CRT-based implementation and the JavaScript implementation are available, the SDK will prefer to use the CRT-based implementation for better performance. If you specifically want to use the JavaScript implementation, ensure that the CRT package is not installed or imported. + #### Related issues 1. [S3 Multi-Region Access Point(MRAP) is not available unless with additional dependency](https://github.com/aws/aws-sdk-js-v3/issues/2822) From 655ddbaeaf5cd9a4e9c2f8a0be656de846d2b39b Mon Sep 17 00:00:00 2001 From: siddsriv Date: Thu, 15 Aug 2024 20:08:20 +0000 Subject: [PATCH 08/15] fix(signature-v4-multi-region): add check for node and error messages --- .../src/SignatureV4MultiRegion.ts | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts index 5297b5d6a311c..30f14a745ac18 100644 --- a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts +++ b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.ts @@ -78,49 +78,51 @@ export class SignatureV4MultiRegion implements RequestPresigner, RequestSigner { private getSigv4aSigner(): InstanceType | InstanceType { if (!this.sigv4aSigner) { - let CrtSignerV4: OptionalCrtSignerV4 | null = null; - let JsSigV4aSigner: OptionalSigV4aSigner | null = null; + const CrtSignerV4 = signatureV4CrtContainer.CrtSignerV4; + const JsSigV4aSigner = signatureV4aContainer.SignatureV4a; - if (signatureV4CrtContainer.CrtSignerV4) { - try { - CrtSignerV4 = signatureV4CrtContainer.CrtSignerV4; - if (typeof CrtSignerV4 !== "function") throw new Error(); - } catch (e) { - e.message = - `${e.message}\n` + - `Please check whether you have installed the "@aws-sdk/signature-v4-crt" package explicitly. \n` + - `You must also register the package by calling [require("@aws-sdk/signature-v4-crt");] ` + - `or an ESM equivalent such as [import "@aws-sdk/signature-v4-crt";]. \n` + - "For more information please go to " + - "https://github.com/aws/aws-sdk-js-v3#functionality-requiring-aws-common-runtime-crt"; - throw e; + if (this.signerOptions.runtime === "node") { + if (!CrtSignerV4 && !JsSigV4aSigner) { + throw new Error( + "Neither CRT nor JS SigV4a implementation is available. " + + "Please load either @aws-sdk/signature-v4-crt or @smithy/signature-v4a. " + + "For more information please go to " + + "https://github.com/aws/aws-sdk-js-v3#functionality-requiring-aws-common-runtime-crt" + ); } - this.sigv4aSigner = new CrtSignerV4({ - ...this.signerOptions, - signingAlgorithm: 1, - }); - } else if (signatureV4aContainer.SignatureV4a) { - try { - JsSigV4aSigner = signatureV4aContainer.SignatureV4a; - if (typeof JsSigV4aSigner !== "function") throw new Error(); - } catch (e) { - e.message = - `${e.message}\n` + - `Please check whether you have installed the "@smithy/signature-v4a" package explicitly. \n` + - `You must also register the package by calling [require("@smithy/signature-v4a");] ` + - `or an ESM equivalent such as [import "@smithy/signature-v4a";]. \n`; - throw e; + if (CrtSignerV4 && typeof CrtSignerV4 === "function") { + this.sigv4aSigner = new CrtSignerV4({ + ...this.signerOptions, + signingAlgorithm: 1, + }); + } else if (JsSigV4aSigner && typeof JsSigV4aSigner === "function") { + this.sigv4aSigner = new JsSigV4aSigner({ + ...this.signerOptions, + }); + } else { + throw new Error( + "Available SigV4a implementation is not a valid constructor. " + + "Please ensure you've properly imported @aws-sdk/signature-v4-crt or @smithy/signature-v4a." + + "For more information please go to " + + "https://github.com/aws/aws-sdk-js-v3#functionality-requiring-aws-common-runtime-crt" + ); + } + } else { + if (!JsSigV4aSigner || typeof JsSigV4aSigner !== "function") { + throw new Error( + "JS SigV4a implementation is not available or not a valid constructor. " + + "Please check whether you have installed the @smithy/signature-v4a package explicitly. " + + "You must also register the package by calling [require('@smithy/signature-v4a');] " + + "or an ESM equivalent such as [import '@smithy/signature-v4a';]. " + + "For more information please go to " + + "https://github.com/aws/aws-sdk-js-v3#using-javascript-non-crt-implementation-of-sigv4a" + ); } this.sigv4aSigner = new JsSigV4aSigner({ ...this.signerOptions, }); - } else { - throw new Error( - "Neither CRT nor JS SigV4a implementation is available. " + - "Please load either @aws-sdk/signature-v4-crt or @smithy/signature-v4a." - ); } } return this.sigv4aSigner; From fcb8b20579f801896e1b0c11128554f7c9590bda Mon Sep 17 00:00:00 2001 From: siddsriv Date: Tue, 20 Aug 2024 16:05:13 +0000 Subject: [PATCH 09/15] test(signature-v4-multi-region): add unit tests --- .../src/SignatureV4MultiRegion.spec.ts | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.spec.ts b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.spec.ts index 5e172bff7f275..0b36e66d33af4 100644 --- a/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.spec.ts +++ b/packages/signature-v4-multi-region/src/SignatureV4MultiRegion.spec.ts @@ -89,14 +89,47 @@ describe("SignatureV4MultiRegion", () => { expect(SignatureV4a).toHaveBeenCalledTimes(1); }); - it("should throw if neither CrtSignerV4 nor SignatureV4a is available", async () => { + it("should throw an error for presignWithCredentials with star region", async () => { + const signer = new SignatureV4MultiRegion(params); + const testCredentials = { + accessKeyId: "test-access-key", + secretAccessKey: "test-secret-key", + }; + await expect( + signer.presignWithCredentials(minimalRequest, testCredentials, { signingRegion: "*" }) + ).rejects.toThrow("Method presignWithCredentials is not supported for [signingRegion=*]."); + }); + + it("should throw an error if neither CrtSignerV4 nor JsSigV4aSigner is available in node runtime", async () => { signatureV4CrtContainer.CrtSignerV4 = null; signatureV4aContainer.SignatureV4a = null; - expect.assertions(1); - const signer = new SignatureV4MultiRegion({ ...params }); - await expect(async () => await signer.sign(minimalRequest, { signingRegion: "*" })).rejects.toThrow( + const signer = new SignatureV4MultiRegion(params); + await expect(signer.sign(minimalRequest, { signingRegion: "*" })).rejects.toThrow( "Neither CRT nor JS SigV4a implementation is available. " + - "Please load either @aws-sdk/signature-v4-crt or @smithy/signature-v4a." + "Please load either @aws-sdk/signature-v4-crt or @smithy/signature-v4a. " + + "For more information please go to " + + "https://github.com/aws/aws-sdk-js-v3#functionality-requiring-aws-common-runtime-crt" + ); + }); + + it("should throw an error if JsSigV4aSigner is not available in non-node runtime", async () => { + const nonNodeParams = { ...params, runtime: "browser" }; + signatureV4aContainer.SignatureV4a = null; + const signer = new SignatureV4MultiRegion(nonNodeParams); + await expect(signer.sign(minimalRequest, { signingRegion: "*" })).rejects.toThrow( + "JS SigV4a implementation is not available or not a valid constructor. " + + "Please check whether you have installed the @smithy/signature-v4a package explicitly. " + + "You must also register the package by calling [require('@smithy/signature-v4a');] " + + "or an ESM equivalent such as [import '@smithy/signature-v4a';]. " + + "For more information please go to " + + "https://github.com/aws/aws-sdk-js-v3#using-javascript-non-crt-implementation-of-sigv4a" ); }); + + it("should use JsSigV4aSigner in non-node runtime", async () => { + const nonNodeParams = { ...params, runtime: "browser" }; + const signer = new SignatureV4MultiRegion(nonNodeParams); + await signer.sign(minimalRequest, { signingRegion: "*" }); + expect(SignatureV4a).toHaveBeenCalledTimes(1); + }); }); From 959eafc50822be5d05e3c18088ea2de7d4190cbf Mon Sep 17 00:00:00 2001 From: siddsriv Date: Tue, 20 Aug 2024 19:31:13 +0000 Subject: [PATCH 10/15] test(signature-v4-multi-region): add e2e test for cloudfront-keyvaluestore --- ...loudfront-keyvaluestore-sigv4a.e2e.spec.ts | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 packages/signature-v4-multi-region/src/cloudfront-keyvaluestore-sigv4a.e2e.spec.ts diff --git a/packages/signature-v4-multi-region/src/cloudfront-keyvaluestore-sigv4a.e2e.spec.ts b/packages/signature-v4-multi-region/src/cloudfront-keyvaluestore-sigv4a.e2e.spec.ts new file mode 100644 index 0000000000000..c6a9228ac3191 --- /dev/null +++ b/packages/signature-v4-multi-region/src/cloudfront-keyvaluestore-sigv4a.e2e.spec.ts @@ -0,0 +1,101 @@ +import "@smithy/signature-v4a"; + +import { Sha256 } from "@aws-crypto/sha256-js"; +import { CloudFrontClient, CreateKeyValueStoreCommand, DeleteKeyValueStoreCommand } from "@aws-sdk/client-cloudfront"; +import { CloudFrontKeyValueStoreClient, DescribeKeyValueStoreCommand } from "@aws-sdk/client-cloudfront-keyvaluestore"; +import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts"; +import { SignatureV4MultiRegion } from "@aws-sdk/signature-v4-multi-region"; +import { HttpRequest } from "@smithy/protocol-http"; + +jest.setTimeout(300000); // 5 minutes + +describe("CloudFront KeyValue Store with SignatureV4a (JS Implementation)", () => { + let cfClient: CloudFrontClient; + let kvsClient: CloudFrontKeyValueStoreClient; + let keyValueStoreName: string; + let keyValueStoreARN: string; + let keyValueStoreETag: string; + let signer: SignatureV4MultiRegion; + + beforeAll(async () => { + const stsClient = new STSClient({}); + const { Account } = await stsClient.send(new GetCallerIdentityCommand({})); + const timestamp = Date.now(); + keyValueStoreName = `test-store-${Account}-${timestamp}`; + + signer = new SignatureV4MultiRegion({ + service: "cloudfront-keyvaluestore", + region: "*", + sha256: Sha256, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, + sessionToken: process.env.AWS_SESSION_TOKEN, + }, + }); + + cfClient = new CloudFrontClient({ + region: "us-east-1", + }); + + kvsClient = new CloudFrontKeyValueStoreClient({ + region: "*", + signer, + }); + + // Create the KeyValueStore + const createResponse = await cfClient.send( + new CreateKeyValueStoreCommand({ + Name: keyValueStoreName, + }) + ); + keyValueStoreARN = createResponse.KeyValueStore!.ARN!; + keyValueStoreETag = createResponse.ETag!; + }); + + afterAll(async () => { + try { + await cfClient.send( + new DeleteKeyValueStoreCommand({ + Name: keyValueStoreName, + IfMatch: keyValueStoreETag, + }) + ); + } catch (error) { + console.error("Failed to delete key-value store:", error); + } + }); + + it("should use SignatureV4a JS implementation", async () => { + const mockRequest = new HttpRequest({ + method: "POST", + protocol: "https:", + hostname: "cloudfront-keyvaluestore.amazonaws.com", + headers: { + host: "cloudfront-keyvaluestore.amazonaws.com", + }, + path: "/", + }); + + const signSpy = jest.spyOn(signer, "sign"); + + await signer.sign(mockRequest, { signingRegion: "*" }); + + expect(signSpy).toHaveBeenCalled(); + const signArgs = signSpy.mock.calls[0]; + expect(signArgs[1]?.signingRegion).toBe("*"); + + // verify that signed request has the expected SigV4a headers + const signedRequest = await signSpy.mock.results[0].value; + expect(signedRequest.headers["x-amz-region-set"]).toBe("*"); + expect(signedRequest.headers["authorization"]).toContain("AWS4-ECDSA-P256-SHA256"); + + signSpy.mockRestore(); + }); + + it("should describe the key-value store using SignatureV4a", async () => { + const response = await kvsClient.send(new DescribeKeyValueStoreCommand({ KvsARN: keyValueStoreARN })); + expect(response.KvsARN).toBe(keyValueStoreARN); + expect(response.Status).toBe("Deployed"); + }); +}); From a9b7f8f380ceee302f4a82ab02372edf621b53c4 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Tue, 20 Aug 2024 19:44:29 +0000 Subject: [PATCH 11/15] test(signature-v4-multi-region): add e2e test for eventbridge --- ...loudfront-keyvaluestore-sigv4a.e2e.spec.ts | 3 +- .../src/eventbridge-sigv4a.e2e.spec.ts | 99 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 packages/signature-v4-multi-region/src/eventbridge-sigv4a.e2e.spec.ts diff --git a/packages/signature-v4-multi-region/src/cloudfront-keyvaluestore-sigv4a.e2e.spec.ts b/packages/signature-v4-multi-region/src/cloudfront-keyvaluestore-sigv4a.e2e.spec.ts index c6a9228ac3191..62bec7f4032cd 100644 --- a/packages/signature-v4-multi-region/src/cloudfront-keyvaluestore-sigv4a.e2e.spec.ts +++ b/packages/signature-v4-multi-region/src/cloudfront-keyvaluestore-sigv4a.e2e.spec.ts @@ -7,7 +7,7 @@ import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts"; import { SignatureV4MultiRegion } from "@aws-sdk/signature-v4-multi-region"; import { HttpRequest } from "@smithy/protocol-http"; -jest.setTimeout(300000); // 5 minutes +jest.setTimeout(300000); describe("CloudFront KeyValue Store with SignatureV4a (JS Implementation)", () => { let cfClient: CloudFrontClient; @@ -43,7 +43,6 @@ describe("CloudFront KeyValue Store with SignatureV4a (JS Implementation)", () = signer, }); - // Create the KeyValueStore const createResponse = await cfClient.send( new CreateKeyValueStoreCommand({ Name: keyValueStoreName, diff --git a/packages/signature-v4-multi-region/src/eventbridge-sigv4a.e2e.spec.ts b/packages/signature-v4-multi-region/src/eventbridge-sigv4a.e2e.spec.ts new file mode 100644 index 0000000000000..69b500888acc9 --- /dev/null +++ b/packages/signature-v4-multi-region/src/eventbridge-sigv4a.e2e.spec.ts @@ -0,0 +1,99 @@ +import "@smithy/signature-v4a"; + +import { Sha256 } from "@aws-crypto/sha256-js"; +import { EventBridgeClient, PutRuleCommand, DeleteRuleCommand, DescribeRuleCommand } from "@aws-sdk/client-eventbridge"; +import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts"; +import { SignatureV4MultiRegion } from "@aws-sdk/signature-v4-multi-region"; +import { HttpRequest } from "@smithy/protocol-http"; + +jest.setTimeout(300000); + +describe("EventBridge with SignatureV4a (JS Implementation)", () => { + let ebClient: EventBridgeClient; + let ruleName: string; + let signer: SignatureV4MultiRegion; + + beforeAll(async () => { + const stsClient = new STSClient({}); + const { Account } = await stsClient.send(new GetCallerIdentityCommand({})); + const timestamp = Date.now(); + ruleName = `test-rule-${Account}-${timestamp}`; + + signer = new SignatureV4MultiRegion({ + service: "eventbridge", + region: "*", + sha256: Sha256, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, + sessionToken: process.env.AWS_SESSION_TOKEN, + }, + }); + + ebClient = new EventBridgeClient({ + region: "*", + signer, + }); + }); + + afterAll(async () => { + try { + await ebClient.send( + new DeleteRuleCommand({ + Name: ruleName, + }) + ); + } catch (error) { + console.error("Failed to delete EventBridge rule:", error); + } + }); + + it("should use SignatureV4a JS implementation", async () => { + const mockRequest = new HttpRequest({ + method: "POST", + protocol: "https:", + hostname: "events.amazonaws.com", + headers: { + host: "events.amazonaws.com", + }, + path: "/", + }); + + const signSpy = jest.spyOn(signer, "sign"); + + await signer.sign(mockRequest, { signingRegion: "*" }); + + expect(signSpy).toHaveBeenCalled(); + const signArgs = signSpy.mock.calls[0]; + expect(signArgs[1]?.signingRegion).toBe("*"); + + // verify that signed request has the expected SigV4a headers + const signedRequest = await signSpy.mock.results[0].value; + expect(signedRequest.headers["x-amz-region-set"]).toBe("*"); + expect(signedRequest.headers["authorization"]).toContain("AWS4-ECDSA-P256-SHA256"); + + signSpy.mockRestore(); + }); + + it("should create, describe, and delete an EventBridge rule using SignatureV4a", async () => { + const createResponse = await ebClient.send( + new PutRuleCommand({ + Name: ruleName, + ScheduleExpression: "rate(5 minutes)", + State: "DISABLED", + }) + ); + expect(createResponse.RuleArn).toBeDefined(); + + const describeResponse = await ebClient.send( + new DescribeRuleCommand({ + Name: ruleName, + }) + ); + expect(describeResponse.Name).toBe(ruleName); + expect(describeResponse.ScheduleExpression).toBe("rate(5 minutes)"); + expect(describeResponse.State).toBe("DISABLED"); + + // delete is handled in afterAll + }); +}); \ No newline at end of file From 19b27502522afa7eab3d4869712e8133a36a33b1 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Tue, 20 Aug 2024 19:49:24 +0000 Subject: [PATCH 12/15] test(signature-v4-multi-region): e2e test runner configs --- packages/signature-v4-multi-region/jest.config.e2e.js | 4 ++++ packages/signature-v4-multi-region/package.json | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 packages/signature-v4-multi-region/jest.config.e2e.js diff --git a/packages/signature-v4-multi-region/jest.config.e2e.js b/packages/signature-v4-multi-region/jest.config.e2e.js new file mode 100644 index 0000000000000..b4d9bee23f482 --- /dev/null +++ b/packages/signature-v4-multi-region/jest.config.e2e.js @@ -0,0 +1,4 @@ +module.exports = { + preset: "ts-jest", + testMatch: ["**/*.e2e.spec.ts"], +}; diff --git a/packages/signature-v4-multi-region/package.json b/packages/signature-v4-multi-region/package.json index f031c7c71cde7..ffba30f602537 100644 --- a/packages/signature-v4-multi-region/package.json +++ b/packages/signature-v4-multi-region/package.json @@ -9,7 +9,8 @@ "build:types": "tsc -p tsconfig.types.json", "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4", "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo", - "test": "jest" + "test": "jest", + "test:e2e": "jest -c jest.config.e2e.js" }, "main": "./dist-cjs/index.js", "module": "./dist-es/index.js", From 7bd6a61b7ab669225533556be4ed4c3fcf586e84 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Tue, 20 Aug 2024 21:08:28 +0000 Subject: [PATCH 13/15] test(signature-v4-multi-region): events signingName fix --- .../src/eventbridge-sigv4a.e2e.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/signature-v4-multi-region/src/eventbridge-sigv4a.e2e.spec.ts b/packages/signature-v4-multi-region/src/eventbridge-sigv4a.e2e.spec.ts index 69b500888acc9..0452ca9625636 100644 --- a/packages/signature-v4-multi-region/src/eventbridge-sigv4a.e2e.spec.ts +++ b/packages/signature-v4-multi-region/src/eventbridge-sigv4a.e2e.spec.ts @@ -1,7 +1,7 @@ import "@smithy/signature-v4a"; import { Sha256 } from "@aws-crypto/sha256-js"; -import { EventBridgeClient, PutRuleCommand, DeleteRuleCommand, DescribeRuleCommand } from "@aws-sdk/client-eventbridge"; +import { DeleteRuleCommand, DescribeRuleCommand, EventBridgeClient, PutRuleCommand } from "@aws-sdk/client-eventbridge"; import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts"; import { SignatureV4MultiRegion } from "@aws-sdk/signature-v4-multi-region"; import { HttpRequest } from "@smithy/protocol-http"; @@ -20,7 +20,7 @@ describe("EventBridge with SignatureV4a (JS Implementation)", () => { ruleName = `test-rule-${Account}-${timestamp}`; signer = new SignatureV4MultiRegion({ - service: "eventbridge", + service: "events", region: "*", sha256: Sha256, credentials: { @@ -96,4 +96,4 @@ describe("EventBridge with SignatureV4a (JS Implementation)", () => { // delete is handled in afterAll }); -}); \ No newline at end of file +}); From 3dfab74320eec823a630acf24c6a52cd57cb8d64 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Wed, 21 Aug 2024 20:35:29 +0000 Subject: [PATCH 14/15] test(signature-v4-multi-region): add s3 e2e test --- .../src/s3-mrap-sigv4a.e2e.spec.ts | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 packages/signature-v4-multi-region/src/s3-mrap-sigv4a.e2e.spec.ts diff --git a/packages/signature-v4-multi-region/src/s3-mrap-sigv4a.e2e.spec.ts b/packages/signature-v4-multi-region/src/s3-mrap-sigv4a.e2e.spec.ts new file mode 100644 index 0000000000000..1e7eb8ca82c08 --- /dev/null +++ b/packages/signature-v4-multi-region/src/s3-mrap-sigv4a.e2e.spec.ts @@ -0,0 +1,193 @@ +import "@smithy/signature-v4a"; + +import { Sha256 } from "@aws-crypto/sha256-js"; +import { + S3Client, + CreateBucketCommand, + DeleteBucketCommand, + PutObjectCommand, + ListObjectsV2Command +} from "@aws-sdk/client-s3"; +import { + S3ControlClient, + CreateMultiRegionAccessPointCommand, + DeleteMultiRegionAccessPointCommand, + DescribeMultiRegionAccessPointOperationCommand, + GetMultiRegionAccessPointCommand +} from "@aws-sdk/client-s3-control"; +import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts"; +import { SignatureV4MultiRegion } from "@aws-sdk/signature-v4-multi-region"; +import { HttpRequest } from "@smithy/protocol-http"; + +jest.setTimeout(1800000); // 30 minutes (MRAP operations can take a while) + +describe("S3 Multi-Region Access Point with SignatureV4a (JS Implementation)", () => { + let s3Client: S3Client; + let s3ControlClient: S3ControlClient; + let accountId: string; + let signer: SignatureV4MultiRegion; + let mrapName: string; + let bucketName1: string; + let bucketName2: string; + let mrapArn: string; + + beforeAll(async () => { + const stsClient = new STSClient({}); + const { Account } = await stsClient.send(new GetCallerIdentityCommand({})); + accountId = Account!; + const timestamp = Date.now(); + mrapName = `test-mrap-${timestamp}`; + bucketName1 = `test-bucket1-${timestamp}`; + bucketName2 = `test-bucket2-${timestamp}`; + + signer = new SignatureV4MultiRegion({ + service: "s3", + region: "*", + sha256: Sha256, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, + sessionToken: process.env.AWS_SESSION_TOKEN, + }, + }); + + s3Client = new S3Client({ + region: "*", + useArnRegion: true, + signer, + }); + + s3ControlClient = new S3ControlClient({ + region: "*", + signer, + }); + + // Create buckets + await s3Client.send(new CreateBucketCommand({ Bucket: bucketName1, CreateBucketConfiguration: { LocationConstraint: "us-west-2" } })); + await s3Client.send(new CreateBucketCommand({ Bucket: bucketName2, CreateBucketConfiguration: { LocationConstraint: "us-east-2" } })); + + // Create MRAP + const createResponse = await s3ControlClient.send( + new CreateMultiRegionAccessPointCommand({ + AccountId: accountId, + ClientToken: `create-${timestamp}`, + Details: { + Name: mrapName, + PublicAccessBlock: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + Regions: [ + { Bucket: bucketName1, BucketAccountId: accountId }, + { Bucket: bucketName2, BucketAccountId: accountId }, + ], + }, + }) + ); + + // Wait for MRAP to be created + let mrapReady = false; + let retries = 0; + while (!mrapReady && retries < 60) { + const describeResponse = await s3ControlClient.send( + new DescribeMultiRegionAccessPointOperationCommand({ + AccountId: accountId, + RequestTokenARN: createResponse.RequestTokenARN, + }) + ); + + if (describeResponse.AsyncOperation?.RequestStatus === "SUCCESS") { + mrapReady = true; + } else { + await new Promise(resolve => setTimeout(resolve, 30000)); // Wait for 30 seconds before retrying + retries++; + } + } + + if (!mrapReady) { + throw new Error("MRAP creation timed out"); + } + + // Get MRAP ARN + const getResponse = await s3ControlClient.send( + new GetMultiRegionAccessPointCommand({ + AccountId: accountId, + Name: mrapName, + }) + ); + mrapArn = getResponse.AccessPoint!.Alias!; + + // Upload a small file to one of the buckets + await s3Client.send(new PutObjectCommand({ + Bucket: bucketName1, + Key: "testfile", + Body: Buffer.from("test", "utf-8") + })); + }); + + afterAll(async () => { + // Delete MRAP + try { + await s3ControlClient.send( + new DeleteMultiRegionAccessPointCommand({ + AccountId: accountId, + ClientToken: `delete-${Date.now()}`, + Details: { + Name: mrapName, + }, + }) + ); + } catch (error) { + console.error("Failed to initiate deletion of Multi-Region Access Point:", error); + } + + // Delete buckets + try { + await s3Client.send(new DeleteBucketCommand({ Bucket: bucketName1 })); + await s3Client.send(new DeleteBucketCommand({ Bucket: bucketName2 })); + } catch (error) { + console.error("Failed to delete buckets:", error); + } + }); + + it("should use SignatureV4a JS implementation", async () => { + const mockRequest = new HttpRequest({ + method: "GET", + protocol: "https:", + hostname: "s3-global.amazonaws.com", + headers: { + host: "s3-global.amazonaws.com", + }, + path: "/", + }); + + const signSpy = jest.spyOn(signer, "sign"); + + await signer.sign(mockRequest, { signingRegion: "*" }); + + expect(signSpy).toHaveBeenCalled(); + const signArgs = signSpy.mock.calls[0]; + expect(signArgs[1]?.signingRegion).toBe("*"); + + // verify that signed request has the expected SigV4a headers + const signedRequest = await signSpy.mock.results[0].value; + expect(signedRequest.headers["x-amz-region-set"]).toBe("*"); + expect(signedRequest.headers["authorization"]).toContain("AWS4-ECDSA-P256-SHA256"); + + signSpy.mockRestore(); + }); + + it("should list objects through MRAP using SignatureV4a", async () => { + const command = new ListObjectsV2Command({ + Bucket: mrapArn, + }); + + const response = await s3Client.send(command); + + expect(response.Contents).toBeDefined(); + expect(response.Contents?.length).toBeGreaterThan(0); + expect(response.Contents?.some(object => object.Key === "testfile")).toBe(true); + }); +}); \ No newline at end of file From aa227cccea2f1bfc056446b979df781fd5bdc651 Mon Sep 17 00:00:00 2001 From: siddsriv Date: Wed, 4 Sep 2024 15:57:58 +0000 Subject: [PATCH 15/15] test(signature-v4-multi-region): initial browser test --- .../signature-v4-multi-region/karma.conf.js | 72 +++++++++++++ .../signature-v4-multi-region/package.json | 3 +- .../src/sigv4a.browser.spec.ts | 102 ++++++++++++++++++ 3 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 packages/signature-v4-multi-region/karma.conf.js create mode 100644 packages/signature-v4-multi-region/src/sigv4a.browser.spec.ts diff --git a/packages/signature-v4-multi-region/karma.conf.js b/packages/signature-v4-multi-region/karma.conf.js new file mode 100644 index 0000000000000..a7e316a610b04 --- /dev/null +++ b/packages/signature-v4-multi-region/karma.conf.js @@ -0,0 +1,72 @@ +const webpack = require("webpack"); + +module.exports = function (config) { + config.set({ + basePath: "", + frameworks: ["mocha", "chai", "webpack"], + files: ["src/**/*.browser.spec.ts"], + processKillTimeout: 5000, + preprocessors: { + "src/**/*.browser.spec.ts": ["webpack", "sourcemap", "credentials", "env"], + }, + webpackMiddleware: { + stats: "minimal", + }, + webpack: { + resolve: { + extensions: [".ts", ".js"], + fallback: { + stream: false, + }, + }, + mode: "development", + module: { + rules: [ + { + test: /\.tsx?$/, + use: [ + { + loader: "ts-loader", + options: { + configFile: "tsconfig.json", + compilerOptions: { + rootDir: "./", + }, + }, + }, + ], + exclude: /node_modules/, + }, + ], + }, + plugins: [new webpack.NormalModuleReplacementPlugin(/\.\/runtimeConfig$/, "./runtimeConfig.browser")], + devtool: "inline-source-map", + }, + envPreprocessor: ["AWS_REGION", "AWS_SMOKE_TEST_MRAP_ARN", "AWS_SMOKE_TEST_CF_KVS_ARN"], + plugins: [ + "@aws-sdk/karma-credential-loader", + "karma-chrome-launcher", + "karma-mocha", + "karma-chai", + "karma-webpack", + "karma-coverage", + "karma-sourcemap-loader", + "karma-env-preprocessor", + ], + reporters: ["progress"], + port: 9876, + colors: true, + logLevel: config.LOG_WARN, + autoWatch: false, + browsers: ["ChromeHeadlessNoSandbox"], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox"], + }, + }, + singleRun: true, + concurrency: Infinity, + exclude: ["**/*.d.ts"], + }); +}; diff --git a/packages/signature-v4-multi-region/package.json b/packages/signature-v4-multi-region/package.json index ffba30f602537..7aa615e3043ad 100644 --- a/packages/signature-v4-multi-region/package.json +++ b/packages/signature-v4-multi-region/package.json @@ -10,7 +10,8 @@ "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4", "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo", "test": "jest", - "test:e2e": "jest -c jest.config.e2e.js" + "test:e2e": "jest -c jest.config.e2e.js", + "test:browser": "karma start karma.conf.js" }, "main": "./dist-cjs/index.js", "module": "./dist-es/index.js", diff --git a/packages/signature-v4-multi-region/src/sigv4a.browser.spec.ts b/packages/signature-v4-multi-region/src/sigv4a.browser.spec.ts new file mode 100644 index 0000000000000..502dca8a9e892 --- /dev/null +++ b/packages/signature-v4-multi-region/src/sigv4a.browser.spec.ts @@ -0,0 +1,102 @@ +import { Sha256 } from "@aws-crypto/sha256-js"; +import { CloudFrontKeyValueStoreClient, DescribeKeyValueStoreCommand } from "@aws-sdk/client-cloudfront-keyvaluestore"; +import { EventBridgeClient, ListRulesCommand } from "@aws-sdk/client-eventbridge"; +import { ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3"; +import { SignatureV4MultiRegion } from "@aws-sdk/signature-v4-multi-region"; +import { HttpRequest } from "@smithy/protocol-http"; +import chai from "chai"; +import chaiAsPromised from "chai-as-promised"; + +chai.use(chaiAsPromised); +const { expect } = chai; + +const region = "*"; +const credentials = { + accessKeyId: (window as any).__env__.AWS_ACCESS_KEY_ID, + secretAccessKey: (window as any).__env__.AWS_SECRET_ACCESS_KEY, + sessionToken: (window as any).__env__.AWS_SESSION_TOKEN, +}; + +const s3MrapArn = (window as any).__env__.AWS_SMOKE_TEST_MRAP_ARN; +const cfKvsArn = (window as any).__env__.AWS_SMOKE_TEST_CF_KVS_ARN; + +describe("SignatureV4a Browser Tests", function () { + this.timeout(30000); // Set timeout to 30 seconds + + const signer = new SignatureV4MultiRegion({ + credentials, + sha256: Sha256, + region: region, + service: "s3", + }); + + describe("S3 Multi-Region Access Point", () => { + const s3Client = new S3Client({ + region: region, + credentials, + signer, + }); + + it("should successfully list objects using MRAP", async () => { + const command = new ListObjectsV2Command({ + Bucket: s3MrapArn, + }); + + const response = await s3Client.send(command); + expect(response.$metadata.httpStatusCode).to.equal(200); + expect(response.Contents).to.be.an("array"); + }); + }); + + describe("EventBridge Global Endpoint", () => { + const ebClient = new EventBridgeClient({ + region: region, + credentials, + signer, + }); + + it("should successfully list rules using global endpoint", async () => { + const command = new ListRulesCommand({}); + + const response = await ebClient.send(command); + expect(response.$metadata.httpStatusCode).to.equal(200); + expect(response.Rules).to.be.an("array"); + }); + }); + + describe("CloudFront Key-Value Store", () => { + const cfKvsClient = new CloudFrontKeyValueStoreClient({ + region: region, + credentials, + signer, + }); + + it("should successfully describe a key-value store", async () => { + const command = new DescribeKeyValueStoreCommand({ + KvsARN: cfKvsArn, + }); + + const response = await cfKvsClient.send(command); + expect(response.$metadata.httpStatusCode).to.equal(200); + expect(response.KvsARN).to.equal(cfKvsArn); + }); + }); + + describe("SignatureV4MultiRegion", () => { + it("should use SignatureV4a for '*' region", async () => { + const mockRequest = new HttpRequest({ + method: "GET", + protocol: "https:", + hostname: "s3-global.amazonaws.com", + headers: { + host: "s3-global.amazonaws.com", + }, + path: "/", + }); + + const signedRequest = await signer.sign(mockRequest, { signingRegion: "*" }); + expect(signedRequest.headers["x-amz-region-set"]).to.equal("*"); + expect(signedRequest.headers["authorization"]).to.include("AWS4-ECDSA-P256-SHA256"); + }); + }); +});