From 51ea8c96612245f591faf74e94885e2468fdd691 Mon Sep 17 00:00:00 2001 From: George Fu Date: Thu, 4 Apr 2024 15:26:56 +0000 Subject: [PATCH 1/2] feat: give SigV4 its own header formatter to avoid import of entire eventstream-codec package --- .changeset/cuddly-ties-cover.md | 5 + packages/signature-v4/package.json | 1 - .../signature-v4/src/HeaderFormatter.spec.ts | 266 ++++++++++++++++++ packages/signature-v4/src/HeaderFormatter.ts | 153 ++++++++++ packages/signature-v4/src/SignatureV4.ts | 8 +- 5 files changed, 428 insertions(+), 5 deletions(-) create mode 100644 .changeset/cuddly-ties-cover.md create mode 100644 packages/signature-v4/src/HeaderFormatter.spec.ts create mode 100644 packages/signature-v4/src/HeaderFormatter.ts diff --git a/.changeset/cuddly-ties-cover.md b/.changeset/cuddly-ties-cover.md new file mode 100644 index 00000000000..1cd7ad3ff72 --- /dev/null +++ b/.changeset/cuddly-ties-cover.md @@ -0,0 +1,5 @@ +--- +"@smithy/signature-v4": patch +--- + +internalize header format function from eventstream-codec into signature-v4 diff --git a/packages/signature-v4/package.json b/packages/signature-v4/package.json index 9d7ab75456d..661710f66ad 100644 --- a/packages/signature-v4/package.json +++ b/packages/signature-v4/package.json @@ -24,7 +24,6 @@ }, "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "workspace:^", "@smithy/is-array-buffer": "workspace:^", "@smithy/types": "workspace:^", "@smithy/util-hex-encoding": "workspace:^", diff --git a/packages/signature-v4/src/HeaderFormatter.spec.ts b/packages/signature-v4/src/HeaderFormatter.spec.ts new file mode 100644 index 00000000000..f7e589a7a32 --- /dev/null +++ b/packages/signature-v4/src/HeaderFormatter.spec.ts @@ -0,0 +1,266 @@ +import type { MessageHeaders } from "@smithy/types"; + +import { HeaderFormatter, Int64 } from "./HeaderFormatter"; + +/** + * TODO: this test duplicates a test in HeaderMarshaller in eventstream-codec. + * TODO: when submodules are implemented this should be reunified/deduped. + */ +describe("HeaderFormatter", () => { + const marshaller = new HeaderFormatter(); + const name = [0x04, 0xf0, 0x9f, 0xa6, 0x84]; + + const testCases: Array<[string, Uint8Array, MessageHeaders]> = [ + [ + "boolean true headers", + Uint8Array.from([...name, 0]), + { + "🦄": { + type: "boolean", + value: true, + }, + }, + ], + [ + "boolean false headers", + Uint8Array.from([...name, 1]), + { + "🦄": { + type: "boolean", + value: false, + }, + }, + ], + [ + "byte headers", + Uint8Array.from([...name, 2, 0x7f]), + { + "🦄": { + type: "byte", + value: 127, + }, + }, + ], + [ + "short headers", + Uint8Array.from([...name, 3, 0x7f, 0xff]), + { + "🦄": { + type: "short", + value: 32767, + }, + }, + ], + [ + "integer headers", + Uint8Array.from([...name, 4, 0x7f, 0xff, 0xff, 0xff]), + { + "🦄": { + type: "integer", + value: 2147483647, + }, + }, + ], + [ + "long headers", + Uint8Array.from([...name, 5, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + { + "🦄": { + type: "long", + value: new Int64(Uint8Array.from([0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])), + }, + }, + ], + [ + "binary headers", + Uint8Array.from([...name, 6, 0x00, 0x08, 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe]), + { + "🦄": { + type: "binary", + value: Uint8Array.from([0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe]), + }, + }, + ], + [ + "string headers", + Uint8Array.from([ + ...name, + 7, + 0x00, + 0x2e, + 0xd8, + 0xaf, + 0xd8, + 0xb3, + 0xd8, + 0xaa, + 0xe2, + 0x80, + 0x8c, + 0xd9, + 0x86, + 0xd9, + 0x88, + 0xd8, + 0xb4, + 0xd8, + 0xaa, + 0xd9, + 0x87, + 0xe2, + 0x80, + 0x8c, + 0xd9, + 0x87, + 0xd8, + 0xa7, + 0x20, + 0xd9, + 0x86, + 0xd9, + 0x85, + 0xdb, + 0x8c, + 0xe2, + 0x80, + 0x8c, + 0xd8, + 0xb3, + 0xd9, + 0x88, + 0xd8, + 0xb2, + 0xd9, + 0x86, + 0xd8, + 0xaf, + ]), + { + "🦄": { + type: "string", + value: "دست‌نوشته‌ها نمی‌سوزند", + }, + }, + ], + [ + "timestamp headers", + Uint8Array.from([...name, 8, 0x00, 0x00, 0x01, 0x61, 0x97, 0x16, 0xac, 0xc2]), + { + "🦄": { + type: "timestamp", + value: new Date(1518658301122), + }, + }, + ], + [ + "UUID headers", + Uint8Array.from([ + ...name, + 9, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + ]), + { + "🦄": { + type: "uuid", + value: "ffffffff-ffff-ffff-ffff-ffffffffffff", + }, + }, + ], + [ + "a sequence of headers", + Uint8Array.from([ + 0x04, + 0xf0, + 0x9f, + 0xa6, + 0x84, + 0x06, + 0x00, + 0x04, + 0xde, + 0xad, + 0xbe, + 0xef, + 0x04, + 0xf0, + 0x9f, + 0x8f, + 0x87, + 0x00, + 0x04, + 0xf0, + 0x9f, + 0x90, + 0x8e, + 0x07, + 0x00, + 0x07, + 0xe2, + 0x98, + 0x83, + 0xf0, + 0x9f, + 0x92, + 0xa9, + 0x04, + 0xf0, + 0x9f, + 0x90, + 0xb4, + 0x01, + ]), + { + "🦄": { + type: "binary", + value: Uint8Array.from([0xde, 0xad, 0xbe, 0xef]), + }, + "🏇": { + type: "boolean", + value: true, + }, + "🐎": { + type: "string", + value: "☃💩", + }, + "🐴": { + type: "boolean", + value: false, + }, + }, + ], + ]; + + describe("#format", () => { + for (const [description, encoded, decoded] of testCases) { + it(`should format ${description}`, () => { + expect(marshaller.format(decoded)).toEqual(encoded); + }); + } + + it("should throw if it receives an invalid UUID", () => { + expect(() => + marshaller.format({ + uuid: { + type: "uuid", + value: "foo", + }, + }) + ).toThrowError("Invalid UUID received"); + }); + }); +}); diff --git a/packages/signature-v4/src/HeaderFormatter.ts b/packages/signature-v4/src/HeaderFormatter.ts new file mode 100644 index 00000000000..f0fe544e918 --- /dev/null +++ b/packages/signature-v4/src/HeaderFormatter.ts @@ -0,0 +1,153 @@ +import type { Int64 as IInt64, MessageHeaders, MessageHeaderValue } from "@smithy/types"; +import { fromHex, toHex } from "@smithy/util-hex-encoding"; +import { fromUtf8 } from "@smithy/util-utf8"; + +/** + * @internal + * TODO: duplicated from @smithy/eventstream-codec to break large dependency. + * TODO: This should be moved to its own deduped submodule in @smithy/core when submodules are implemented. + */ +export class HeaderFormatter { + public format(headers: MessageHeaders): Uint8Array { + const chunks: Array = []; + + for (const headerName of Object.keys(headers)) { + const bytes = fromUtf8(headerName); + chunks.push(Uint8Array.from([bytes.byteLength]), bytes, this.formatHeaderValue(headers[headerName])); + } + + const out = new Uint8Array(chunks.reduce((carry, bytes) => carry + bytes.byteLength, 0)); + let position = 0; + for (const chunk of chunks) { + out.set(chunk, position); + position += chunk.byteLength; + } + + return out; + } + + private formatHeaderValue(header: MessageHeaderValue): Uint8Array { + switch (header.type) { + case "boolean": + return Uint8Array.from([header.value ? HEADER_VALUE_TYPE.boolTrue : HEADER_VALUE_TYPE.boolFalse]); + case "byte": + return Uint8Array.from([HEADER_VALUE_TYPE.byte, header.value]); + case "short": + const shortView = new DataView(new ArrayBuffer(3)); + shortView.setUint8(0, HEADER_VALUE_TYPE.short); + shortView.setInt16(1, header.value, false); + return new Uint8Array(shortView.buffer); + case "integer": + const intView = new DataView(new ArrayBuffer(5)); + intView.setUint8(0, HEADER_VALUE_TYPE.integer); + intView.setInt32(1, header.value, false); + return new Uint8Array(intView.buffer); + case "long": + const longBytes = new Uint8Array(9); + longBytes[0] = HEADER_VALUE_TYPE.long; + longBytes.set(header.value.bytes, 1); + return longBytes; + case "binary": + const binView = new DataView(new ArrayBuffer(3 + header.value.byteLength)); + binView.setUint8(0, HEADER_VALUE_TYPE.byteArray); + binView.setUint16(1, header.value.byteLength, false); + const binBytes = new Uint8Array(binView.buffer); + binBytes.set(header.value, 3); + return binBytes; + case "string": + const utf8Bytes = fromUtf8(header.value); + const strView = new DataView(new ArrayBuffer(3 + utf8Bytes.byteLength)); + strView.setUint8(0, HEADER_VALUE_TYPE.string); + strView.setUint16(1, utf8Bytes.byteLength, false); + const strBytes = new Uint8Array(strView.buffer); + strBytes.set(utf8Bytes, 3); + return strBytes; + case "timestamp": + const tsBytes = new Uint8Array(9); + tsBytes[0] = HEADER_VALUE_TYPE.timestamp; + tsBytes.set(Int64.fromNumber(header.value.valueOf()).bytes, 1); + return tsBytes; + case "uuid": + if (!UUID_PATTERN.test(header.value)) { + throw new Error(`Invalid UUID received: ${header.value}`); + } + const uuidBytes = new Uint8Array(17); + uuidBytes[0] = HEADER_VALUE_TYPE.uuid; + uuidBytes.set(fromHex(header.value.replace(/\-/g, "")), 1); + return uuidBytes; + } + } +} + +const enum HEADER_VALUE_TYPE { + boolTrue = 0, + boolFalse, + byte, + short, + integer, + long, + byteArray, + string, + timestamp, + uuid, +} + +const UUID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/; + +/** + * TODO: duplicated from @smithy/eventstream-codec to break large dependency. + * TODO: This should be moved to its own deduped submodule in @smithy/core when submodules are implemented. + */ +export class Int64 implements IInt64 { + constructor(readonly bytes: Uint8Array) { + if (bytes.byteLength !== 8) { + throw new Error("Int64 buffers must be exactly 8 bytes"); + } + } + + static fromNumber(number: number): Int64 { + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + if (number > 9_223_372_036_854_775_807 || number < -9_223_372_036_854_775_808) { + throw new Error(`${number} is too large (or, if negative, too small) to represent as an Int64`); + } + + const bytes = new Uint8Array(8); + for (let i = 7, remaining = Math.abs(Math.round(number)); i > -1 && remaining > 0; i--, remaining /= 256) { + bytes[i] = remaining; + } + + if (number < 0) { + negate(bytes); + } + + return new Int64(bytes); + } + + /** + * Called implicitly by infix arithmetic operators. + */ + valueOf(): number { + const bytes = this.bytes.slice(0); + const negative = bytes[0] & 0b10000000; + if (negative) { + negate(bytes); + } + + return parseInt(toHex(bytes), 16) * (negative ? -1 : 1); + } + + toString() { + return String(this.valueOf()); + } +} + +function negate(bytes: Uint8Array): void { + for (let i = 0; i < 8; i++) { + bytes[i] ^= 0xff; + } + + for (let i = 7; i > -1; i--) { + bytes[i]++; + if (bytes[i] !== 0) break; + } +} diff --git a/packages/signature-v4/src/SignatureV4.ts b/packages/signature-v4/src/SignatureV4.ts index 840ef9750d2..c3b7bd1cfc8 100644 --- a/packages/signature-v4/src/SignatureV4.ts +++ b/packages/signature-v4/src/SignatureV4.ts @@ -1,4 +1,3 @@ -import { HeaderMarshaller } from "@smithy/eventstream-codec"; import { AwsCredentialIdentity, ChecksumConstructor, @@ -22,7 +21,7 @@ import { } from "@smithy/types"; import { toHex } from "@smithy/util-hex-encoding"; import { normalizeProvider } from "@smithy/util-middleware"; -import { fromUtf8, toUint8Array, toUtf8 } from "@smithy/util-utf8"; +import { toUint8Array } from "@smithy/util-utf8"; import { ALGORITHM_IDENTIFIER, @@ -44,6 +43,7 @@ import { createScope, getSigningKey } from "./credentialDerivation"; import { getCanonicalHeaders } from "./getCanonicalHeaders"; import { getCanonicalQuery } from "./getCanonicalQuery"; import { getPayloadHash } from "./getPayloadHash"; +import { HeaderFormatter } from "./HeaderFormatter"; import { hasHeader } from "./headerUtil"; import { moveHeadersToQuery } from "./moveHeadersToQuery"; import { prepareRequest } from "./prepareRequest"; @@ -104,7 +104,7 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne private readonly sha256: ChecksumConstructor | HashConstructor; private readonly uriEscapePath: boolean; private readonly applyChecksum: boolean; - private readonly headerMarshaller = new HeaderMarshaller(toUtf8, fromUtf8); + private readonly headerFormatter = new HeaderFormatter(); constructor({ applyChecksum, @@ -212,7 +212,7 @@ export class SignatureV4 implements RequestPresigner, RequestSigner, StringSigne ): Promise { const promise = this.signEvent( { - headers: this.headerMarshaller.format(signableMessage.message.headers), + headers: this.headerFormatter.format(signableMessage.message.headers), payload: signableMessage.message.body, }, { From 2fee6cbf52c7f927dd65121555e89060ad012a63 Mon Sep 17 00:00:00 2001 From: George Fu Date: Thu, 4 Apr 2024 15:44:21 +0000 Subject: [PATCH 2/2] update yarn lock --- yarn.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 62948852009..c520fa08cf9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2485,7 +2485,6 @@ __metadata: resolution: "@smithy/signature-v4@workspace:packages/signature-v4" dependencies: "@aws-crypto/sha256-js": 3.0.0 - "@smithy/eventstream-codec": "workspace:^" "@smithy/is-array-buffer": "workspace:^" "@smithy/protocol-http": "workspace:^" "@smithy/types": "workspace:^"