From 8990d2bd61d3c1c39666980a3ee979d3044fee76 Mon Sep 17 00:00:00 2001 From: Gus Narea Date: Tue, 15 Dec 2020 15:52:44 +0000 Subject: [PATCH] feat(Node): Implement unwrapping of message payload (#201) Needed for https://github.com/relaycorp/relaynet-internet-gateway/pull/328 --- src/lib/nodes/BaseNode.spec.ts | 115 +++++++++++++++++++++++++++++++++ src/lib/nodes/BaseNode.ts | 35 ++++++++++ src/lib/nodes/NodeOptions.ts | 7 ++ src/lib/nodes/baseNode.ts | 17 ----- src/lib/nodes/gateway.ts | 5 +- 5 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 src/lib/nodes/BaseNode.spec.ts create mode 100644 src/lib/nodes/BaseNode.ts create mode 100644 src/lib/nodes/NodeOptions.ts delete mode 100644 src/lib/nodes/baseNode.ts diff --git a/src/lib/nodes/BaseNode.spec.ts b/src/lib/nodes/BaseNode.spec.ts new file mode 100644 index 000000000..336ab09cf --- /dev/null +++ b/src/lib/nodes/BaseNode.spec.ts @@ -0,0 +1,115 @@ +import { arrayBufferFrom, expectBuffersToEqual } from '../_test_utils'; +import { + SessionEnvelopedData, + SessionlessEnvelopedData, +} from '../crypto_wrappers/cms/envelopedData'; +import { + derSerializePublicKey, + generateECDHKeyPair, + generateRSAKeyPair, +} from '../crypto_wrappers/keys'; +import Certificate from '../crypto_wrappers/x509/Certificate'; +import { MockPrivateKeyStore, MockPublicKeyStore } from '../keyStores/testMocks'; +import { issueGatewayCertificate, issueInitialDHKeyCertificate } from '../pki'; +import { StubMessage, StubPayload } from '../ramf/_test_utils'; +import { BaseNode } from './BaseNode'; + +const TOMORROW = new Date(); +TOMORROW.setDate(TOMORROW.getDate() + 1); +TOMORROW.setMilliseconds(0); + +let senderCertificate: Certificate; +beforeAll(async () => { + const senderKeyPair = await generateRSAKeyPair(); + senderCertificate = await issueGatewayCertificate({ + issuerPrivateKey: senderKeyPair.privateKey, + subjectPublicKey: senderKeyPair.publicKey, + validityEndDate: TOMORROW, + }); +}); + +let recipientCertificate: Certificate; +let recipientPrivateKey: CryptoKey; +let privateKeyStore: MockPrivateKeyStore; +beforeAll(async () => { + const recipientKeyPair = await generateRSAKeyPair(); + recipientPrivateKey = recipientKeyPair.privateKey; + + recipientCertificate = await issueGatewayCertificate({ + issuerPrivateKey: recipientKeyPair.privateKey, + subjectPublicKey: recipientKeyPair.publicKey, + validityEndDate: TOMORROW, + }); + + privateKeyStore = new MockPrivateKeyStore(); + await privateKeyStore.registerNodeKey(recipientKeyPair.privateKey, recipientCertificate); +}); + +describe('unwrapMessagePayload', () => { + const payloadPlaintextContent = arrayBufferFrom('content'); + + let publicKeyStore: MockPublicKeyStore; + beforeEach(() => { + publicKeyStore = new MockPublicKeyStore(); + }); + + const RECIPIENT_ADDRESS = 'https://example.com'; + + test('Payload plaintext should be returned when using sessionless encryption', async () => { + const payload = await SessionlessEnvelopedData.encrypt( + payloadPlaintextContent, + recipientCertificate, + ); + const message = new StubMessage( + RECIPIENT_ADDRESS, + senderCertificate, + Buffer.from(payload.serialize()), + ); + const node = new StubNode(privateKeyStore, publicKeyStore); + + const payloadPlaintext = await node.unwrapMessagePayload(message); + + expectBuffersToEqual(payloadPlaintext.content, payloadPlaintextContent); + expect(publicKeyStore.keys).toBeEmpty(); + }); + + test('Payload plaintext should be returned and session key stored when using a session', async () => { + const sessionKeyPair = await generateECDHKeyPair(); + const initialSessionCertificate = await issueInitialDHKeyCertificate({ + issuerCertificate: recipientCertificate, + issuerPrivateKey: recipientPrivateKey, + subjectPublicKey: sessionKeyPair.publicKey, + validityEndDate: TOMORROW, + }); + await privateKeyStore.registerInitialSessionKey( + sessionKeyPair.privateKey, + initialSessionCertificate, + ); + const encryptionResult = await SessionEnvelopedData.encrypt( + payloadPlaintextContent, + initialSessionCertificate, + ); + const message = new StubMessage( + RECIPIENT_ADDRESS, + senderCertificate, + Buffer.from(encryptionResult.envelopedData.serialize()), + ); + const node = new StubNode(privateKeyStore, publicKeyStore); + + const payloadPlaintext = await node.unwrapMessagePayload(message); + + expectBuffersToEqual(payloadPlaintext.content, payloadPlaintextContent); + + const storedKey = publicKeyStore.keys[await senderCertificate.calculateSubjectPrivateAddress()]; + expect(storedKey.publicKeyCreationTime).toEqual(message.creationDate); + expectBuffersToEqual(Buffer.from(encryptionResult.dhKeyId), storedKey.publicKeyId); + expectBuffersToEqual( + await derSerializePublicKey( + (await encryptionResult.envelopedData.getOriginatorKey()).publicKey, + ), + storedKey.publicKeyDer, + ); + }); +}); + +class StubNode extends BaseNode {} diff --git a/src/lib/nodes/BaseNode.ts b/src/lib/nodes/BaseNode.ts new file mode 100644 index 000000000..ec8212f36 --- /dev/null +++ b/src/lib/nodes/BaseNode.ts @@ -0,0 +1,35 @@ +import { PrivateKeyStore } from '../keyStores/privateKeyStore'; +import { PublicKeyStore } from '../keyStores/publicKeyStore'; +import PayloadPlaintext from '../messages/payloads/PayloadPlaintext'; +import RAMFMessage from '../messages/RAMFMessage'; +import { NodeOptions } from './NodeOptions'; + +export abstract class BaseNode { + constructor( + protected privateKeyStore: PrivateKeyStore, + protected publicKeyStore: PublicKeyStore, + protected cryptoOptions: Partial = {}, + ) {} + + /** + * Decrypt and return the payload in the `message`. + * + * Also store the session key, if using the channel session protocol. + * + * @param message + */ + public async unwrapMessagePayload

(message: RAMFMessage

): Promise

{ + const unwrapResult = await message.unwrapPayload(this.privateKeyStore); + + // If the sender uses channel session, store its public key for later use. + if (unwrapResult.senderSessionKey) { + await this.publicKeyStore.saveSessionKey( + unwrapResult.senderSessionKey, + message.senderCertificate, + message.creationDate, + ); + } + + return unwrapResult.payload; + } +} diff --git a/src/lib/nodes/NodeOptions.ts b/src/lib/nodes/NodeOptions.ts new file mode 100644 index 000000000..bcd93498f --- /dev/null +++ b/src/lib/nodes/NodeOptions.ts @@ -0,0 +1,7 @@ +import { EncryptionOptions } from '../crypto_wrappers/cms/envelopedData'; +import { SignatureOptions } from '../crypto_wrappers/cms/SignatureOptions'; + +export interface NodeOptions { + readonly encryption: Partial; + readonly signature: Partial; +} diff --git a/src/lib/nodes/baseNode.ts b/src/lib/nodes/baseNode.ts deleted file mode 100644 index 6c483340c..000000000 --- a/src/lib/nodes/baseNode.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SignatureOptions } from '../..'; -import { EncryptionOptions } from '../crypto_wrappers/cms/envelopedData'; -import { PrivateKeyStore } from '../keyStores/privateKeyStore'; -import { PublicKeyStore } from '../keyStores/publicKeyStore'; - -export interface NodeOptions { - readonly encryption: Partial; - readonly signature: Partial; -} - -export abstract class BaseNode { - constructor( - protected privateKeyStore: PrivateKeyStore, - protected publicKeyStore: PublicKeyStore, - protected cryptoOptions: Partial = {}, - ) {} -} diff --git a/src/lib/nodes/gateway.ts b/src/lib/nodes/gateway.ts index fefe0d849..4e4598950 100644 --- a/src/lib/nodes/gateway.ts +++ b/src/lib/nodes/gateway.ts @@ -8,15 +8,16 @@ import { } from '../crypto_wrappers/cms/envelopedData'; import Certificate from '../crypto_wrappers/x509/Certificate'; import Cargo from '../messages/Cargo'; +import { CargoCollectionRequest } from '../messages/payloads/CargoCollectionRequest'; import CargoMessageSet, { MessageWithExpiryDate } from '../messages/payloads/CargoMessageSet'; -import { BaseNode } from './baseNode'; +import { BaseNode } from './BaseNode'; export type CargoMessageStream = AsyncIterable<{ readonly message: Buffer; readonly expiryDate: Date; }>; -export class Gateway extends BaseNode { +export class Gateway extends BaseNode { public async *generateCargoes( messages: CargoMessageStream, recipientCertificate: Certificate,