From 470fb45c8febddefe158fb76315fc5e8015e21eb Mon Sep 17 00:00:00 2001 From: seebees Date: Thu, 21 May 2020 15:58:37 -0700 Subject: [PATCH] fix: resource exhaustion from an incomplete encrypted message Decrypt needs to actively verify that it has reached the end of the encrypted message. This fix ensures an error on an incomplete encrypted message Also, the signature_info parsing needed to be updated, to handle an incomplete signature block. Browsers were never impacted, but the tests were included for completeness. --- modules/decrypt-browser/test/decrypt.test.ts | 35 +- modules/decrypt-browser/test/fixtures.ts | 701 ++++++++++++++++++ .../decrypt-node/src/parse_header_stream.ts | 16 + modules/decrypt-node/src/verify_stream.ts | 37 +- modules/decrypt-node/test/decrypt.test.ts | 5 +- modules/decrypt-node/test/fixtures.ts | 9 + .../test/parse_header_stream.test.ts | 64 ++ .../decrypt-node/test/verify_stream.test.ts | 109 ++- modules/serialize/src/signature_info.ts | 2 +- modules/serialize/test/signature_info.test.ts | 3 + 10 files changed, 968 insertions(+), 13 deletions(-) create mode 100644 modules/decrypt-node/test/parse_header_stream.test.ts diff --git a/modules/decrypt-browser/test/decrypt.test.ts b/modules/decrypt-browser/test/decrypt.test.ts index 241b28837..796b94ef5 100644 --- a/modules/decrypt-browser/test/decrypt.test.ts +++ b/modules/decrypt-browser/test/decrypt.test.ts @@ -3,10 +3,13 @@ /* eslint-env mocha */ -import { expect } from 'chai' +import * as chai from 'chai' +import chaiAsPromised from 'chai-as-promised' import { decrypt } from '../src/index' import { AlgorithmSuiteIdentifier } from '@aws-crypto/material-management-browser' import * as fixtures from './fixtures' +chai.use(chaiAsPromised) +const { expect } = chai describe('decrypt', () => { it('buffer', async () => { @@ -52,4 +55,34 @@ describe('decrypt', () => { } ) }) + + it('verify incomplete chipertext will fail for an un-signed algorithm suite', async () => { + const data = fixtures.base64CiphertextAlgAes256GcmIv12Tag16HkdfWith4Frames() + const keyring = fixtures.decryptKeyring() + + // First we make sure that the test vector is well formed + await decrypt(keyring, data) + + // This is the real test: + // trying to decrypt + // on EVERY boundary + for (let i = 0; data.byteLength > i; i++) { + await expect(decrypt(keyring, data.slice(0, i))).to.rejectedWith(Error) + } + }) + + it('verify incomplete chipertext will fail for a signed algorithm suite', async () => { + const data = fixtures.base64CiphertextAlgAes256GcmIv12Tag16HkdfSha384EcdsaP384With4Frames() + const keyring = fixtures.decryptKeyring() + + // First we make sure that the test vector is well formed + await decrypt(keyring, data) + + // This is the real test: + // trying to decrypt + // on EVERY boundary + for (let i = 0; data.byteLength > i; i++) { + await expect(decrypt(keyring, data.slice(0, i))).to.rejectedWith(Error) + } + }) }) diff --git a/modules/decrypt-browser/test/fixtures.ts b/modules/decrypt-browser/test/fixtures.ts index f744233e8..1b76e9ed2 100644 --- a/modules/decrypt-browser/test/fixtures.ts +++ b/modules/decrypt-browser/test/fixtures.ts @@ -5123,6 +5123,707 @@ export function invalidSignatureCiphertextAlgAes256GcmIv12Tag16HkdfSha384EcdsaP3 ]) } +export function base64CiphertextAlgAes256GcmIv12Tag16HkdfWith4Frames() { + return new Uint8Array([ + 1, + 128, + 1, + 120, + 162, + 85, + 112, + 209, + 207, + 101, + 191, + 54, + 217, + 141, + 57, + 77, + 12, + 136, + 62, + 194, + 0, + 0, + 0, + 1, + 0, + 1, + 107, + 0, + 1, + 107, + 0, + 3, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 12, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 177, + 195, + 69, + 110, + 43, + 185, + 60, + 113, + 151, + 135, + 132, + 9, + 179, + 151, + 0, + 190, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 169, + 144, + 90, + 138, + 121, + 185, + 154, + 87, + 27, + 193, + 172, + 46, + 194, + 48, + 3, + 122, + 249, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 131, + 79, + 172, + 68, + 143, + 35, + 1, + 125, + 64, + 68, + 76, + 212, + 143, + 103, + 76, + 142, + 179, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 222, + 170, + 53, + 251, + 18, + 96, + 84, + 195, + 1, + 255, + 158, + 156, + 238, + 9, + 94, + 196, + 137, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 74, + 67, + 72, + 44, + 7, + 45, + 10, + 51, + 194, + 146, + 143, + 191, + 39, + 91, + 120, + 26, + 144, + 255, + 255, + 255, + 255, + 0, + 0, + 0, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 5, + 0, + 0, + 0, + 0, + 169, + 51, + 218, + 188, + 68, + 57, + 66, + 188, + 148, + 129, + 25, + 13, + 94, + 141, + 52, + 92, + ]) +} + +export function base64CiphertextAlgAes256GcmIv12Tag16HkdfSha384EcdsaP384With4Frames() { + return new Uint8Array([ + 1, + 128, + 3, + 120, + 180, + 108, + 104, + 165, + 205, + 70, + 109, + 42, + 49, + 24, + 87, + 166, + 105, + 216, + 187, + 112, + 0, + 95, + 0, + 1, + 0, + 21, + 97, + 119, + 115, + 45, + 99, + 114, + 121, + 112, + 116, + 111, + 45, + 112, + 117, + 98, + 108, + 105, + 99, + 45, + 107, + 101, + 121, + 0, + 68, + 65, + 109, + 73, + 85, + 107, + 68, + 118, + 121, + 74, + 97, + 70, + 113, + 81, + 52, + 89, + 70, + 53, + 50, + 110, + 65, + 43, + 76, + 105, + 109, + 50, + 121, + 88, + 49, + 89, + 47, + 117, + 78, + 85, + 97, + 112, + 89, + 77, + 65, + 77, + 102, + 111, + 122, + 50, + 89, + 82, + 99, + 49, + 104, + 121, + 113, + 105, + 73, + 102, + 101, + 65, + 78, + 49, + 88, + 81, + 113, + 85, + 117, + 70, + 114, + 56, + 103, + 61, + 61, + 0, + 1, + 0, + 1, + 107, + 0, + 1, + 107, + 0, + 3, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 12, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 231, + 97, + 68, + 44, + 133, + 181, + 244, + 73, + 240, + 102, + 160, + 92, + 109, + 129, + 205, + 149, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 166, + 192, + 143, + 210, + 126, + 44, + 253, + 192, + 241, + 44, + 69, + 129, + 17, + 80, + 169, + 216, + 165, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 118, + 217, + 235, + 160, + 243, + 28, + 164, + 174, + 79, + 232, + 179, + 195, + 2, + 31, + 127, + 137, + 214, + 0, + 0, + 0, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 117, + 33, + 49, + 207, + 136, + 172, + 205, + 0, + 71, + 195, + 77, + 136, + 194, + 72, + 105, + 238, + 105, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 209, + 239, + 113, + 107, + 12, + 151, + 235, + 35, + 30, + 232, + 208, + 236, + 192, + 122, + 79, + 41, + 235, + 255, + 255, + 255, + 255, + 0, + 0, + 0, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 5, + 0, + 0, + 0, + 0, + 102, + 204, + 37, + 17, + 125, + 56, + 169, + 127, + 199, + 21, + 168, + 211, + 152, + 164, + 70, + 49, + 0, + 104, + 48, + 102, + 2, + 49, + 0, + 229, + 54, + 229, + 83, + 208, + 130, + 186, + 172, + 141, + 72, + 110, + 52, + 126, + 160, + 19, + 191, + 12, + 156, + 46, + 4, + 222, + 53, + 113, + 35, + 88, + 67, + 158, + 63, + 20, + 201, + 219, + 196, + 70, + 31, + 78, + 135, + 26, + 187, + 65, + 229, + 199, + 203, + 129, + 183, + 208, + 13, + 109, + 51, + 2, + 49, + 0, + 161, + 240, + 139, + 74, + 184, + 235, + 141, + 46, + 142, + 100, + 41, + 109, + 191, + 139, + 45, + 70, + 66, + 236, + 109, + 82, + 141, + 208, + 214, + 168, + 228, + 72, + 1, + 6, + 101, + 11, + 191, + 245, + 8, + 86, + 13, + 221, + 188, + 190, + 15, + 208, + 229, + 155, + 122, + 58, + 16, + 181, + 212, + 155, + ]) +} + export function plaintext() { return new Uint8Array([ 221, diff --git a/modules/decrypt-node/src/parse_header_stream.ts b/modules/decrypt-node/src/parse_header_stream.ts index 2987d015e..a3a6aa9e0 100644 --- a/modules/decrypt-node/src/parse_header_stream.ts +++ b/modules/decrypt-node/src/parse_header_stream.ts @@ -21,6 +21,7 @@ const PortableTransformWithType = PortableTransform as new ( interface HeaderState { buffer: Buffer + headerParsed: boolean } export class ParseHeaderStream extends PortableTransformWithType { @@ -34,6 +35,7 @@ export class ParseHeaderStream extends PortableTransformWithType { }) this._headerState = { buffer: Buffer.alloc(0), + headerParsed: false, } } @@ -92,6 +94,8 @@ export class ParseHeaderStream extends PortableTransformWithType { this.emit('VerifyInfo', verifyInfo) this.emit('MessageHeader', headerInfo.messageHeader) + this._headerState.headerParsed = true + // The header is parsed, pass control const readPos = rawHeader.byteLength + headerIv.byteLength + headerAuthTag.byteLength @@ -112,4 +116,16 @@ export class ParseHeaderStream extends PortableTransformWithType { }) .catch((err) => callback(err)) } + + _flush(callback: Function) { + /* Postcondition: A completed header MUST have been processed. + * callback is an errBack function, + * so it expects either an error OR undefined + */ + callback( + this._headerState.headerParsed + ? undefined + : new Error('Incomplete Header') + ) + } } diff --git a/modules/decrypt-node/src/verify_stream.ts b/modules/decrypt-node/src/verify_stream.ts index 3bfbdc8be..476c96a25 100644 --- a/modules/decrypt-node/src/verify_stream.ts +++ b/modules/decrypt-node/src/verify_stream.ts @@ -40,6 +40,7 @@ interface VerifyState { currentFrame?: BodyHeader signatureInfo: Buffer sequenceNumber: number + finalAuthTagReceived: boolean } export class VerifyStream extends PortableTransformWithType { @@ -49,6 +50,7 @@ export class VerifyStream extends PortableTransformWithType { authTagBuffer: Buffer.alloc(0), signatureInfo: Buffer.alloc(0), sequenceNumber: 0, + finalAuthTagReceived: false, } private _verify?: AWSVerify private _maxBodySize?: number @@ -198,6 +200,11 @@ export class VerifyStream extends PortableTransformWithType { * After the final frame, just moving on to concatenate the signature is much simpler. */ if (currentFrame.isFinalFrame) { + /* Signal that the we are at the end of the ciphertext. + * See decodeBodyHeader, non-framed will set isFinalFrame + * for the single frame. + */ + this._verifyState.finalAuthTagReceived = true /* Overwriting the _transform function. * Data flow control is not handled here. */ @@ -244,16 +251,28 @@ export class VerifyStream extends PortableTransformWithType { } _flush(callback: Function) { + const { finalAuthTagReceived } = this._verifyState + /* Precondition: All ciphertext MUST have been received. + * The verify stream has ended, + * there will be no more data. + * Therefore we MUST have reached the end. + */ + if (!finalAuthTagReceived) return callback(new Error('Incomplete message')) /* Check for early return (Postcondition): If there is no verify stream do not attempt to verify. */ if (!this._verify) return callback() - const { signatureInfo } = this._verifyState - const { buffer, byteOffset, byteLength } = deserializeSignature( - signatureInfo - ) - const signature = Buffer.from(buffer, byteOffset, byteLength) - const isVerified = this._verify.awsCryptoVerify(signature) - /* Postcondition: The signature must be valid. */ - needs(isVerified, 'Invalid Signature') - callback() + try { + const { signatureInfo } = this._verifyState + /* Precondition: The signature must be well formed. */ + const { buffer, byteOffset, byteLength } = deserializeSignature( + signatureInfo + ) + const signature = Buffer.from(buffer, byteOffset, byteLength) + const isVerified = this._verify.awsCryptoVerify(signature) + /* Postcondition: The signature must be valid. */ + needs(isVerified, 'Invalid Signature') + callback() + } catch (e) { + callback(e) + } } } diff --git a/modules/decrypt-node/test/decrypt.test.ts b/modules/decrypt-node/test/decrypt.test.ts index 342824c42..a20684789 100644 --- a/modules/decrypt-node/test/decrypt.test.ts +++ b/modules/decrypt-node/test/decrypt.test.ts @@ -3,10 +3,13 @@ /* eslint-env mocha */ -import { expect } from 'chai' +import * as chai from 'chai' +import chaiAsPromised from 'chai-as-promised' import { AlgorithmSuiteIdentifier } from '@aws-crypto/material-management-node' import { decrypt } from '../src/index' import * as fixtures from './fixtures' +chai.use(chaiAsPromised) +const { expect } = chai import from from 'from2' describe('decrypt', () => { diff --git a/modules/decrypt-node/test/fixtures.ts b/modules/decrypt-node/test/fixtures.ts index 118c3bfa7..ba938879d 100644 --- a/modules/decrypt-node/test/fixtures.ts +++ b/modules/decrypt-node/test/fixtures.ts @@ -18,6 +18,15 @@ export function invalidSignatureCiphertextAlgAes256GcmIv12Tag16HkdfSha384EcdsaP3 return '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' } +export function base64CiphertextAlgAes256GcmIv12Tag16HkdfWith4Frames() { + return 'AYABeKJVcNHPZb822Y05TQyIPsIAAAABAAFrAAFrAAMAAAACAAAAAAwAAAABAAAAAAAAAAAAAAAAscNFbiu5PHGXh4QJs5cAvgAAAAEAAAAAAAAAAAAAAAGpkFqKebmaVxvBrC7CMAN6+QAAAAIAAAAAAAAAAAAAAAKDT6xEjyMBfUBETNSPZ0yOswAAAAMAAAAAAAAAAAAAAAPeqjX7EmBUwwH/npzuCV7EiQAAAAQAAAAAAAAAAAAAAARKQ0gsBy0KM8KSj78nW3gakP////8AAAAFAAAAAAAAAAAAAAAFAAAAAKkz2rxEOUK8lIEZDV6NNFw=' +} + +export function base64CiphertextAlgAes256GcmIv12Tag16HkdfSha384EcdsaP384With4Frames() { + return 'AYADeLRsaKXNRm0qMRhXpmnYu3AAXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREFtSVVrRHZ5SmFGcVE0WUY1Mm5BK0xpbTJ5WDFZL3VOVWFwWU1BTWZvejJZUmMxaHlxaUlmZUFOMVhRcVV1RnI4Zz09AAEAAWsAAWsAAwAAAAIAAAAADAAAAAEAAAAAAAAAAAAAAADnYUQshbX0SfBmoFxtgc2VAAAAAQAAAAAAAAAAAAAAAabAj9J+LP3A8SxFgRFQqdilAAAAAgAAAAAAAAAAAAAAAnbZ66DzHKSuT+izwwIff4nWAAAAAwAAAAAAAAAAAAAAA3UhMc+IrM0AR8NNiMJIae5pAAAABAAAAAAAAAAAAAAABNHvcWsMl+sjHujQ7MB6Tynr/////wAAAAUAAAAAAAAAAAAAAAUAAAAAZswlEX04qX/HFajTmKRGMQBoMGYCMQDlNuVT0IK6rI1IbjR+oBO/DJwuBN41cSNYQ54/FMnbxEYfTocau0Hlx8uBt9ANbTMCMQCh8ItKuOuNLo5kKW2/iy1GQuxtUo3Q1qjkSAEGZQu/9QhWDd28vg/Q5Zt6OhC11Js=' +} + +// This is AlgAes256GcmIv12Tag16HkdfSha384EcdsaP384 export function base64Ciphertext4BytesWith4KFrameLength() { return 'AYADeEpqVoZzS6rBmbCKSa4acg4AXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREE4blREMk5rYzRDRXhoQTNlR0E3M1kyWWY2N2c4TDQySjlwL2UzNHo5RjdUR1hlT2pJZGR4TjEyeitUemk4Wmpxdz09AAEAAWsAAWsAAwAAAAIAAAAADAAAEAAAAAAAAAAAAAAAAABggOHpalFEq+ovk6c4Ugaj/////wAAAAEAAAAAAAAAAAAAAAEAAAAEDyVA5PCXHZo4sqQuqprmzWXwXUsAZzBlAjAY3vK9flPg8WEnoxIjPuhbj4o+bKUsJzbZgtDBaqIaBZYo8yt7JQB4vFyzb/PwucoCMQCz2fzhOQBAM63n56vBdIjBVqbNPyszIy0QjE8OJ/X8mV48VNi5wCF4u7I4MmK91N0=' } diff --git a/modules/decrypt-node/test/parse_header_stream.test.ts b/modules/decrypt-node/test/parse_header_stream.test.ts new file mode 100644 index 000000000..b033f6129 --- /dev/null +++ b/modules/decrypt-node/test/parse_header_stream.test.ts @@ -0,0 +1,64 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import * as chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import * as util from 'util' +import * as stream from 'stream' +const pipeline = util.promisify(stream.pipeline) +import { ParseHeaderStream } from '../src/parse_header_stream' +import { + NodeDefaultCryptographicMaterialsManager, + needs, +} from '@aws-crypto/material-management-node' +import * as fixtures from './fixtures' +chai.use(chaiAsPromised) +const { expect } = chai + +describe('ParseHeaderStream', () => { + it('Postcondition: A completed header MUST have been processed.', async () => { + const completeHeaderLength = 73 + const data = Buffer.from( + fixtures.base64CiphertextAlgAes256GcmIv12Tag16HkdfWith4Frames(), + 'base64' + ) + const cmm = new NodeDefaultCryptographicMaterialsManager( + fixtures.decryptKeyring() + ) + + for (let i = 0; completeHeaderLength > i; i++) { + await expect(testStream(cmm, data.slice(0, i))).rejectedWith( + Error, + 'Incomplete Header' + ) + } + + for (let i = completeHeaderLength; data.byteLength > i; i++) { + await testStream(cmm, data.slice(0, i)) + } + }) +}) + +async function testStream( + cmm: NodeDefaultCryptographicMaterialsManager, + data: Buffer +) { + let VerifyInfoEmitted = false + let MessageHeaderEmitted = false + const parseHeader = new ParseHeaderStream(cmm) + .on('VerifyInfo', () => { + VerifyInfoEmitted = true + }) + .on('MessageHeader', () => { + MessageHeaderEmitted = true + }) + parseHeader.end(data) + return pipeline(parseHeader, new stream.PassThrough()).then(() => { + needs( + VerifyInfoEmitted && MessageHeaderEmitted, + 'Required events not emitted.' + ) + }) +} diff --git a/modules/decrypt-node/test/verify_stream.test.ts b/modules/decrypt-node/test/verify_stream.test.ts index 320c45d96..84c40e386 100644 --- a/modules/decrypt-node/test/verify_stream.test.ts +++ b/modules/decrypt-node/test/verify_stream.test.ts @@ -3,7 +3,11 @@ /* eslint-env mocha */ -import { expect } from 'chai' +import * as chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import * as util from 'util' +import * as stream from 'stream' +const pipeline = util.promisify(stream.pipeline) import { VerifyStream } from '../src/verify_stream' import { ParseHeaderStream } from '../src/parse_header_stream' import * as fixtures from './fixtures' @@ -12,7 +16,11 @@ import { ContentType } from '@aws-crypto/serialize' import { NodeAlgorithmSuite, AlgorithmSuiteIdentifier, + NodeDefaultCryptographicMaterialsManager, + needs, } from '@aws-crypto/material-management-node' +chai.use(chaiAsPromised) +const { expect } = chai describe('VerifyStream', () => { it('can be created', () => { @@ -71,10 +79,109 @@ describe('VerifyStream', () => { it('Check for early return (Postcondition): If there is no verify stream do not attempt to verify.', () => { // This works because there is no state, and any state would cause _flush to throw const test = new VerifyStream({}) + // test. let called = false test._flush(() => { called = true }) expect(called).to.equal(true) }) + + it('Precondition: All ciphertext MUST have been received.', async () => { + const cmm = new NodeDefaultCryptographicMaterialsManager( + fixtures.decryptKeyring() + ) + const data = Buffer.from( + fixtures.base64CiphertextAlgAes256GcmIv12Tag16HkdfWith4Frames(), + 'base64' + ) + const completeHeaderLength = 73 + + // First we make sure that the test vector is well formed + await testStream(cmm, data) + + // This make sure we still get nice errors when composed + for (let i = 0; completeHeaderLength > i; i++) { + await expect(testStream(cmm, data.slice(0, i))).rejectedWith( + Error, + 'Incomplete Header' + ) + } + + // This is the real test, after the header the body MUST be complete + for (let i = completeHeaderLength; data.byteLength > i; i++) { + await expect(testStream(cmm, data.slice(0, i))).rejectedWith( + Error, + 'Incomplete message' + ) + } + }) + + it('Precondition: The signature must be well formed.', async () => { + const cmm = new NodeDefaultCryptographicMaterialsManager( + fixtures.decryptKeyring() + ) + const data = Buffer.from( + fixtures.base64CiphertextAlgAes256GcmIv12Tag16HkdfSha384EcdsaP384With4Frames(), + 'base64' + ) + const completeHeaderLength = 168 + const lengthToFooter = 340 + + // First we make sure that the test vector is well formed + await testStream(cmm, data) + + // This make sure we still get nice errors when composed + for (let i = 0; completeHeaderLength > i; i++) { + await expect(testStream(cmm, data.slice(0, i))).rejectedWith( + Error, + 'Incomplete Header' + ) + } + + // This is similar to the test above, and in included for completeness + for (let i = completeHeaderLength; lengthToFooter > i; i++) { + await expect(testStream(cmm, data.slice(0, i))).rejectedWith( + Error, + 'Incomplete message' + ) + } + + // This is the real test, the signature must be well formed + for (let i = lengthToFooter; data.byteLength > i; i++) { + await expect(testStream(cmm, data.slice(0, i))).rejectedWith( + Error, + 'Invalid Signature' + ) + } + }) }) + +async function testStream( + cmm: NodeDefaultCryptographicMaterialsManager, + data: Buffer +) { + const source = new ParseHeaderStream(cmm) + const test = new VerifyStream({}) + let DecipherInfoEmitted = false + let BodyInfoEmitted = false + let AuthTagEmitted = false + test + .on('DecipherInfo', (d) => { + DecipherInfoEmitted = !!d + }) + .on('BodyInfo', (b) => { + BodyInfoEmitted = !!b + }) + .on('AuthTag', (a, next) => { + AuthTagEmitted = !!a + next() + }) + source.end(data) + return pipeline(source, test).then(() => { + needs( + DecipherInfoEmitted && BodyInfoEmitted && AuthTagEmitted, + 'Required events not emitted.' + ) + }) +} diff --git a/modules/serialize/src/signature_info.ts b/modules/serialize/src/signature_info.ts index a4aac2c1b..8e10e7a32 100644 --- a/modules/serialize/src/signature_info.ts +++ b/modules/serialize/src/signature_info.ts @@ -24,7 +24,7 @@ export function deserializeSignature({ byteLength, }: Uint8Array) { /* Precondition: There must be information for a signature. */ - needs(byteLength, 'Invalid Signature') + needs(byteLength && byteLength > 2, 'Invalid Signature') /* Uint8Array is a view on top of the underlying ArrayBuffer. * This means that raw underlying memory stored in the ArrayBuffer * may be larger than the Uint8Array. This is especially true of diff --git a/modules/serialize/test/signature_info.test.ts b/modules/serialize/test/signature_info.test.ts index 42d7c3ee5..1edb4e1d6 100644 --- a/modules/serialize/test/signature_info.test.ts +++ b/modules/serialize/test/signature_info.test.ts @@ -25,6 +25,9 @@ describe('deserializeSignature', () => { it('Precondition: There must be information for a signature.', () => { expect(() => deserializeSignature({} as any)).to.throw() + expect(() => deserializeSignature(new Uint8Array())).to.throw() + expect(() => deserializeSignature(new Uint8Array(1))).to.throw() + expect(() => deserializeSignature(new Uint8Array(2))).to.throw() }) it('Precondition: The signature length must be positive.', () => {