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.', () => {