Skip to content

Commit

Permalink
feat(Channel): Implement makeMessage() to encrypt and sign messages (
Browse files Browse the repository at this point in the history
  • Loading branch information
gnarea authored May 19, 2023
1 parent 5bf8221 commit ee1c89b
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 98 deletions.
3 changes: 3 additions & 0 deletions src/lib/_test_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export const CRYPTO_OIDS = {
AES_CBC_128: '2.16.840.1.101.3.4.1.2',
AES_CBC_192: '2.16.840.1.101.3.4.1.22',
AES_CBC_256: '2.16.840.1.101.3.4.1.42',

SHA_256: '2.16.840.1.101.3.4.2.1',
SHA_384: '2.16.840.1.101.3.4.2.2',
};

type PkijsValueType = pkijs.RelativeDistinguishedNames | pkijs.Certificate;
Expand Down
5 changes: 2 additions & 3 deletions src/lib/crypto/cms/envelopedData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import * as pkijs from 'pkijs';
import {
arrayBufferFrom,
CRYPTO_OIDS,
expectAsn1ValuesToBeEqual,
expectArrayBuffersToEqual,
expectAsn1ValuesToBeEqual,
expectPkijsValuesToBeEqual,
generateStubCert,
getMockContext,
Expand All @@ -28,7 +28,6 @@ import { assertPkiType } from './_utils';
import { derSerializePublicKey } from '../keys/serialisation';
import { NODE_ENGINE } from '../pkijs';

const OID_SHA256 = '2.16.840.1.101.3.4.2.1';
const OID_RSA_OAEP = '1.2.840.113549.1.1.7';
const OID_AWALA_ORIGINATOR_EPHEMERAL_CERT_SERIAL_NUMBER = '1.3.6.1.4.1.58708.0.1.0';

Expand Down Expand Up @@ -250,7 +249,7 @@ describe('SessionlessEnvelopedData', () => {
const algorithmParams = new pkijs.RSAESOAEPParams({
schema: keyTransRecipientInfo.keyEncryptionAlgorithm.algorithmParams,
});
expect(algorithmParams.hashAlgorithm.algorithmId).toEqual(OID_SHA256);
expect(algorithmParams.hashAlgorithm.algorithmId).toEqual(CRYPTO_OIDS.SHA_256);
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/lib/messages/RAMFMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Recipient } from './Recipient';

const DEFAULT_TTL_SECONDS = 5 * 60; // 5 minutes

interface MessageOptions {
export interface MessageOptions {
readonly id: string;
readonly creationDate: Date;
readonly ttl: number;
Expand Down
11 changes: 11 additions & 0 deletions src/lib/messages/RAMFMessageConstructor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MessageOptions, RAMFMessage } from './RAMFMessage';
import { PayloadPlaintext } from './payloads/PayloadPlaintext';
import { Recipient } from './Recipient';
import { Certificate } from '../crypto/x509/Certificate';

export type RAMFMessageConstructor<Payload extends PayloadPlaintext> = new (
recipient: Recipient,
senderCertificate: Certificate,
payloadSerialized: Buffer,
options?: Partial<MessageOptions>,
) => RAMFMessage<Payload>;
228 changes: 165 additions & 63 deletions src/lib/nodes/channels/Channel.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { addDays, setMilliseconds } from 'date-fns';
import { addDays, setMilliseconds, subSeconds } from 'date-fns';

import { arrayBufferFrom, CRYPTO_OIDS, reSerializeCertificate } from '../../_test_utils';
import { SessionEnvelopedData } from '../../crypto/cms/envelopedData';
import { generateECDHKeyPair, generateRSAKeyPair } from '../../crypto/keys/generation';
import { generateRSAKeyPair } from '../../crypto/keys/generation';
import { MockKeyStoreSet } from '../../keyStores/testMocks';
import { Recipient } from '../../messages/Recipient';
import { issueGatewayCertificate } from '../../pki/issuance';
import { StubPayload } from '../../ramf/_test_utils';
import { SessionKey } from '../../SessionKey';
import { StubMessage, StubPayload } from '../../ramf/_test_utils';
import { NodeError } from '../errors';
import { getIdFromIdentityKey } from '../../crypto/keys/digest';
import { StubNode } from '../_test_utils';
import { Peer } from '../peer';
import { StubNodeChannel } from './_test_utils';
import { CertificationPath } from '../../pki/CertificationPath';
import { SessionKeyPair } from '../../SessionKeyPair';
import { SignedData } from '../../crypto/cms/signedData';
import bufferToArray from 'buffer-to-arraybuffer';

const PAYLOAD_CONTENT = arrayBufferFrom('payload content');
const PAYLOAD = new StubPayload(PAYLOAD_CONTENT);

const KEY_STORES = new MockKeyStoreSet();
beforeEach(() => {
Expand Down Expand Up @@ -60,89 +65,186 @@ beforeAll(async () => {
);
});

const PAYLOAD_PLAINTEXT_CONTENT = arrayBufferFrom('payload content');

describe('wrapMessagePayload', () => {
const stubPayload = new StubPayload(PAYLOAD_PLAINTEXT_CONTENT);

let peerSessionKey: SessionKey;
let peerSessionPrivateKey: CryptoKey;
describe('makeMessage', () => {
let peerSessionKeyPair: SessionKeyPair;
beforeEach(async () => {
const recipientSessionKeyPair = await generateECDHKeyPair();
peerSessionPrivateKey = recipientSessionKeyPair.privateKey;
peerSessionKey = {
keyId: Buffer.from('key id'),
publicKey: recipientSessionKeyPair.publicKey,
};
await KEY_STORES.publicKeyStore.saveSessionKey(peerSessionKey, peer.id, new Date());
});

test('There should be a session key for the recipient', async () => {
const unknownPeerId = `not-${peer.id}`;
const channel = new StubNodeChannel(
node,
{ ...peer, id: unknownPeerId },
deliveryAuthPath,
KEY_STORES,
peerSessionKeyPair = await SessionKeyPair.generate();
await KEY_STORES.publicKeyStore.saveSessionKey(
peerSessionKeyPair.sessionKey,
peer.id,
new Date(),
);

await expect(channel.wrapMessagePayload(stubPayload)).rejects.toThrowWithMessage(
NodeError,
`Could not find session key for peer ${unknownPeerId}`,
);
await KEY_STORES.certificateStore.save(deliveryAuthPath, peer.id);
});

test('Payload should be encrypted with the session key of the recipient', async () => {
test('Recipient should be channel peer', async () => {
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES);

const payloadSerialized = await channel.wrapMessagePayload(stubPayload);
const messageSerialised = await channel.makeMessage(PAYLOAD, StubMessage);

const payloadEnvelopedData = await SessionEnvelopedData.deserialize(payloadSerialized);
expect(payloadEnvelopedData.getRecipientKeyId()).toEqual(peerSessionKey.keyId);
await expect(payloadEnvelopedData.decrypt(peerSessionPrivateKey)).resolves.toEqual(
stubPayload.serialize(),
);
const { recipient } = await StubMessage.deserialize(messageSerialised);
expect(recipient.id).toBe(peer.id);
expect(recipient.internetAddress).toBe(peer.internetAddress);
});

test('Passing the payload as an ArrayBuffer should be supported', async () => {
const payloadPlaintext = stubPayload.serialize();
test('Sender certificate should be delivery authorisation', async () => {
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES);

const payloadSerialized = await channel.wrapMessagePayload(stubPayload);
const messageSerialised = await channel.makeMessage(PAYLOAD, StubMessage);

const payloadEnvelopedData = await SessionEnvelopedData.deserialize(payloadSerialized);
await expect(payloadEnvelopedData.decrypt(peerSessionPrivateKey)).resolves.toEqual(
payloadPlaintext,
);
const { senderCertificate } = await StubMessage.deserialize(messageSerialised);
expect(senderCertificate.isEqual(deliveryAuthPath.leafCertificate)).toBeTrue();
});

test('The new ephemeral session key of the sender should be stored', async () => {
test('Sender certificate CAs should be delivery authorisation CAs', async () => {
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES);

const payloadSerialized = await channel.wrapMessagePayload(stubPayload);
const messageSerialised = await channel.makeMessage(PAYLOAD, StubMessage);

const payloadEnvelopedData = (await SessionEnvelopedData.deserialize(
payloadSerialized,
)) as SessionEnvelopedData;
const originatorSessionKey = await payloadEnvelopedData.getOriginatorKey();
await expect(
KEY_STORES.privateKeyStore.retrieveSessionKey(originatorSessionKey.keyId, node.id, peer.id),
).resolves.toBeTruthy();
const { senderCaCertificateChain } = await StubMessage.deserialize(messageSerialised);
expect(senderCaCertificateChain).toHaveLength(1);
expect(
senderCaCertificateChain[0].isEqual(deliveryAuthPath.certificateAuthorities[0]),
).toBeTrue();
});

test('Encryption options should be honoured if set', async () => {
const aesKeySize = 192;
describe('Payload', () => {
test('There should be a session key for the recipient', async () => {
const unknownPeerId = `not-${peer.id}`;
const channel = new StubNodeChannel(
node,
{ ...peer, id: unknownPeerId },
deliveryAuthPath,
KEY_STORES,
);

await expect(channel.makeMessage(PAYLOAD, StubMessage)).rejects.toThrowWithMessage(
NodeError,
`Could not find session key for peer ${unknownPeerId}`,
);
});

test('Payload should be encrypted with the session key of the recipient', async () => {
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES);

const messageSerialised = await channel.makeMessage(PAYLOAD, StubMessage);

const message = await StubMessage.deserialize(messageSerialised);
const payloadEnvelopedData = await SessionEnvelopedData.deserialize(
message.payloadSerialized,
);
expect(payloadEnvelopedData.getRecipientKeyId()).toEqual(peerSessionKeyPair.sessionKey.keyId);
await expect(payloadEnvelopedData.decrypt(peerSessionKeyPair.privateKey)).resolves.toEqual(
PAYLOAD.serialize(),
);
});

test('Passing the payload as an ArrayBuffer should be supported', async () => {
const payloadPlaintext = PAYLOAD.serialize();
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES);

const messageSerialized = await channel.makeMessage(PAYLOAD_CONTENT, StubMessage);

const message = await StubMessage.deserialize(messageSerialized);
const payloadEnvelopedData = await SessionEnvelopedData.deserialize(
message.payloadSerialized,
);
await expect(payloadEnvelopedData.decrypt(peerSessionKeyPair.privateKey)).resolves.toEqual(
payloadPlaintext,
);
});

test('The new ephemeral session key of the sender should be stored', async () => {
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES);

const messageSerialized = await channel.makeMessage(PAYLOAD, StubMessage);

const message = await StubMessage.deserialize(messageSerialized);
const payloadEnvelopedData = (await SessionEnvelopedData.deserialize(
message.payloadSerialized,
)) as SessionEnvelopedData;
const originatorSessionKey = await payloadEnvelopedData.getOriginatorKey();
await expect(
KEY_STORES.privateKeyStore.retrieveSessionKey(originatorSessionKey.keyId, node.id, peer.id),
).resolves.toBeTruthy();
});

test('Encryption options should be honoured if set', async () => {
const aesKeySize = 192;
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES, {
encryption: { aesKeySize },
});

const messageSerialized = await channel.makeMessage(PAYLOAD, StubMessage);

const message = await StubMessage.deserialize(messageSerialized);
const payloadEnvelopedData = await SessionEnvelopedData.deserialize(
message.payloadSerialized,
);
const encryptedContentInfo = payloadEnvelopedData.pkijsEnvelopedData.encryptedContentInfo;
expect(encryptedContentInfo.contentEncryptionAlgorithm.algorithmId).toEqual(
CRYPTO_OIDS.AES_CBC_192,
);
});
});

describe('Creation date', () => {
test('Creation date should default to now', async () => {
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES);
const dateBeforeMessage = setMilliseconds(new Date(), 0);

const messageSerialized = await channel.makeMessage(PAYLOAD, StubMessage);

const { creationDate } = await StubMessage.deserialize(messageSerialized);
expect(creationDate).toBeAfterOrEqualTo(dateBeforeMessage);
expect(creationDate).toBeBeforeOrEqualTo(new Date());
});

test('Creation date should be honoured if set', async () => {
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES);
const creationDate = setMilliseconds(subSeconds(new Date(), 15), 0);

const messageSerialized = await channel.makeMessage(PAYLOAD, StubMessage, { creationDate });

const message = await StubMessage.deserialize(messageSerialized);
expect(message.creationDate).toStrictEqual(creationDate);
});
});

describe('TTL', () => {
test('TTL should default to 5 minutes', async () => {
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES);

const messageSerialized = await channel.makeMessage(PAYLOAD, StubMessage);

const { ttl } = await StubMessage.deserialize(messageSerialized);
expect(ttl).toStrictEqual(300);
});

test('TTL should be honoured if set', async () => {
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES);
const ttl = 60;

const messageSerialized = await channel.makeMessage(PAYLOAD, StubMessage, { ttl });

const message = await StubMessage.deserialize(messageSerialized);
expect(message.ttl).toBe(ttl);
});
});

test('Signature options should be honoured if set', async () => {
const hashingAlgorithmName = 'SHA-384';
const channel = new StubNodeChannel(node, peer, deliveryAuthPath, KEY_STORES, {
encryption: { aesKeySize },
signature: { hashingAlgorithmName },
});

const payloadSerialized = await channel.wrapMessagePayload(stubPayload);
const messageSerialised = await channel.makeMessage(PAYLOAD, StubMessage);

const payloadEnvelopedData = await SessionEnvelopedData.deserialize(payloadSerialized);
const encryptedContentInfo = payloadEnvelopedData.pkijsEnvelopedData.encryptedContentInfo;
expect(encryptedContentInfo.contentEncryptionAlgorithm.algorithmId).toEqual(
CRYPTO_OIDS.AES_CBC_192,
);
const signedDataSerialised = bufferToArray(Buffer.from(messageSerialised).subarray(7));
const signedData = await SignedData.deserialize(signedDataSerialised);
const [signerInfo] = signedData.pkijsSignedData.signerInfos;
expect(signerInfo.digestAlgorithm.algorithmId).toBe(CRYPTO_OIDS.SHA_384);
});
});

Expand Down
32 changes: 31 additions & 1 deletion src/lib/nodes/channels/Channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { NodeCryptoOptions } from '../NodeCryptoOptions';
import { Node } from '../Node';
import { Peer, PeerInternetAddress } from '../peer';
import { CertificationPath } from '../../pki/CertificationPath';
import { RAMFMessageConstructor } from '../../messages/RAMFMessageConstructor';
import { MessageOptions } from '../../messages/RAMFMessage';

export abstract class Channel<
Payload extends PayloadPlaintext,
Expand All @@ -21,14 +23,42 @@ export abstract class Channel<
public cryptoOptions: Partial<NodeCryptoOptions> = {},
) {}

/**
* Generate and serialise a message with the given `payload`.
* @param payload The payload to encrypt and encapsulate
* @param messageConstructor The message class constructor
* @param options
*/
public async makeMessage(
payload: Payload | ArrayBuffer,
messageConstructor: RAMFMessageConstructor<Payload>,
options: Partial<Omit<MessageOptions, 'senderCaCertificateChain'>> = {},
): Promise<ArrayBuffer> {
const recipient: Recipient = {
id: this.peer.id,
internetAddress: this.peer.internetAddress,
};
const payloadSerialised = await this.wrapMessagePayload(payload);
const message = new messageConstructor(
recipient,
this.deliveryAuthPath.leafCertificate,
Buffer.from(payloadSerialised),
{
...options,
senderCaCertificateChain: this.deliveryAuthPath.certificateAuthorities,
},
);
return message.serialize(this.node.identityKeyPair.privateKey, this.cryptoOptions.signature);
}

/**
* Encrypt and serialize the `payload`.
*
* @param payload
*
* Also store the new ephemeral session key.
*/
public async wrapMessagePayload(payload: Payload | ArrayBuffer): Promise<ArrayBuffer> {
private async wrapMessagePayload(payload: Payload | ArrayBuffer): Promise<ArrayBuffer> {
const recipientSessionKey = await this.keyStores.publicKeyStore.retrieveLastSessionKey(
this.peer.id,
);
Expand Down
Loading

0 comments on commit ee1c89b

Please sign in to comment.