Skip to content

Commit

Permalink
feat(Node): Implement validation of incoming messages (#559)
Browse files Browse the repository at this point in the history
`Node.validateMessage()`.
  • Loading branch information
gnarea authored May 4, 2023
1 parent 2a40700 commit 82cfa40
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 13 deletions.
78 changes: 65 additions & 13 deletions src/lib/nodes/Node.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions src/lib/nodes/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Payload extends PayloadPlaintext> {
constructor(
Expand Down Expand Up @@ -47,6 +48,27 @@ export abstract class Node<Payload extends PayloadPlaintext> {
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<Payload>,
trustedCertificates?: readonly Certificate[],
): Promise<void> {
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`.
*
Expand Down

0 comments on commit 82cfa40

Please sign in to comment.