From 82cfa40df633da33248257623c1a3f118231e877 Mon Sep 17 00:00:00 2001 From: Gus Narea Date: Thu, 4 May 2023 14:58:31 +0100 Subject: [PATCH] feat(Node): Implement validation of incoming messages (#559) `Node.validateMessage()`. --- src/lib/nodes/Node.spec.ts | 78 +++++++++++++++++++++++++++++++------- src/lib/nodes/Node.ts | 22 +++++++++++ 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/src/lib/nodes/Node.spec.ts b/src/lib/nodes/Node.spec.ts index 5efff2275..319c5f488 100644 --- a/src/lib/nodes/Node.spec.ts +++ b/src/lib/nodes/Node.spec.ts @@ -1,4 +1,4 @@ -import { addDays, setMilliseconds } from 'date-fns'; +import { addDays, setMilliseconds, subDays } from 'date-fns'; import { arrayBufferFrom, expectArrayBuffersToEqual, reSerializeCertificate } from '../_test_utils'; import { SessionEnvelopedData } from '../crypto_wrappers/cms/envelopedData'; @@ -14,6 +14,7 @@ import { CertificationPath } from '../pki/CertificationPath'; import { issueGatewayCertificate } from '../pki/issuance'; import { StubMessage } from '../ramf/_test_utils'; import { StubNode } from './_test_utils'; +import InvalidMessageError from '../messages/InvalidMessageError'; let nodeId: string; let nodePrivateKey: CryptoKey; @@ -46,6 +47,19 @@ beforeAll(async () => { nodeId = await getIdFromIdentityKey(nodeKeyPair.publicKey); }); +let peerId: string; +let peerCertificate: Certificate; +beforeAll(async () => { + const peerKeyPair = await generateRSAKeyPair(); + peerId = await getIdFromIdentityKey(peerKeyPair.publicKey); + peerCertificate = await issueGatewayCertificate({ + issuerCertificate: nodeCertificate, + issuerPrivateKey: nodePrivateKey, + subjectPublicKey: peerKeyPair.publicKey, + validityEndDate: addDays(new Date(), 1), + }); +}); + const KEY_STORES = new MockKeyStoreSet(); beforeEach(async () => { KEY_STORES.clear(); @@ -113,7 +127,6 @@ describe('generateSessionKey', () => { test('Key should be bound to a peer if explicitly set', async () => { const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); - const peerId = '0deadbeef'; const sessionKey = await node.generateSessionKey(peerId); @@ -125,21 +138,60 @@ describe('generateSessionKey', () => { }); }); -describe('unwrapMessagePayload', () => { - const PAYLOAD_PLAINTEXT_CONTENT = arrayBufferFrom('payload content'); +describe('validateMessage', () => { + test('Invalid message should be refused', async () => { + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); + const expiredMessage = new StubMessage({ id: nodeId }, peerCertificate, Buffer.from([]), { + creationDate: subDays(new Date(), 1), + ttl: 1, + }); + + await expect(node.validateMessage(expiredMessage)).rejects.toThrowWithMessage( + InvalidMessageError, + /expired/, + ); + }); + + test('Valid message with untrusted sender should be refused', async () => { + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); + const message = new StubMessage({ id: nodeId }, peerCertificate, Buffer.from([])); + + await expect(node.validateMessage(message, [])).rejects.toThrowWithMessage( + InvalidMessageError, + /authorized/, + ); + }); - let peerId: string; - let peerCertificate: Certificate; - beforeAll(async () => { - const peerKeyPair = await generateRSAKeyPair(); - peerId = await getIdFromIdentityKey(peerKeyPair.publicKey); - peerCertificate = await issueGatewayCertificate({ - issuerPrivateKey: peerKeyPair.privateKey, - subjectPublicKey: peerKeyPair.publicKey, - validityEndDate: addDays(new Date(), 1), + test('Valid message with trusted sender should be allowed', async () => { + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); + const message = new StubMessage({ id: nodeId }, peerCertificate, Buffer.from([]), { + senderCaCertificateChain: [peerCertificate], }); + + await expect(node.validateMessage(message, [nodeCertificate])).toResolve(); + }); + + test('Message recipient should match node id', async () => { + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); + const message = new StubMessage({ id: `not${nodeId}` }, peerCertificate, Buffer.from([])); + + await expect(node.validateMessage(message)).rejects.toThrowWithMessage( + InvalidMessageError, + `Message is bound for another node (${message.recipient.id})`, + ); }); + test('Valid message should be allowed', async () => { + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); + const message = new StubMessage({ id: nodeId }, peerCertificate, Buffer.from([])); + + await expect(node.validateMessage(message)).toResolve(); + }); +}); + +describe('unwrapMessagePayload', () => { + const PAYLOAD_PLAINTEXT_CONTENT = arrayBufferFrom('payload content'); + test('Payload plaintext should be returned', async () => { const node = new StubNode(peerId, nodePrivateKey, KEY_STORES, {}); const sessionKey = await node.generateSessionKey(peerId); diff --git a/src/lib/nodes/Node.ts b/src/lib/nodes/Node.ts index a9a59bbf1..1aa089cd7 100644 --- a/src/lib/nodes/Node.ts +++ b/src/lib/nodes/Node.ts @@ -7,6 +7,7 @@ import { SessionKey } from '../SessionKey'; import { SessionKeyPair } from '../SessionKeyPair'; import { NodeCryptoOptions } from './NodeCryptoOptions'; import { Signer } from './signatures/Signer'; +import InvalidMessageError from '../messages/InvalidMessageError'; export abstract class Node { constructor( @@ -47,6 +48,27 @@ export abstract class Node { return new signerClass(path.leafCertificate, this.identityPrivateKey); } + /** + * Validate the `message` and report whether it's correctly bound for this node. + * @param message The message to validate + * @param trustedCertificates If authorisation should be verified + * @throws {InvalidMessageError} If the message is invalid + */ + public async validateMessage( + message: RAMFMessage, + trustedCertificates?: readonly Certificate[], + ): Promise { + if (trustedCertificates) { + await message.validate(trustedCertificates); + } else { + await message.validate(); + } + + if (message.recipient.id !== this.id) { + throw new InvalidMessageError(`Message is bound for another node (${message.recipient.id})`); + } + } + /** * Decrypt and return the payload in the `message`. *