Skip to content

Commit

Permalink
feat(Endpoint): Implement creation of channel with private peer (#565)
Browse files Browse the repository at this point in the history
`Channel`s are now initialised with `Node` and `Peer` objects.
  • Loading branch information
gnarea authored May 11, 2023
1 parent a350801 commit 77d404c
Show file tree
Hide file tree
Showing 36 changed files with 895 additions and 246 deletions.
39 changes: 39 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
"node": ">=12"
},
"dependencies": {
"@peculiar/webcrypto": "^1.4.3",
"@peculiar/asn1-schema": "^2.3.6",
"@peculiar/asn1-x509": "^2.3.6",
"@peculiar/webcrypto": "< 2",
"@stablelib/aes-kw": "^1.0.1",
"@types/verror": "^1.10.6",
"asn1js": "^3.0.5",
Expand All @@ -46,7 +48,7 @@
"smart-buffer": "^4.2.0",
"uuid4": "^2.0.3",
"verror": "^1.10.1",
"webcrypto-core": "^1.7.7"
"webcrypto-core": "< 2"
},
"peerDependencies": {
"@peculiar/webcrypto": "< 2",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/keyStores/testMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class MockPrivateKeyStore extends PrivateKeyStore {
export class MockPublicKeyStore extends PublicKeyStore {
public identityKeys: { [peerId: string]: Buffer } = {};

public sessionKeys: { [key: string]: SessionPublicKeyData } = {};
public sessionKeys: { [peerId: string]: SessionPublicKeyData } = {};

constructor(protected readonly failOnSave = false, protected fetchError?: Error) {
super();
Expand Down
159 changes: 159 additions & 0 deletions src/lib/nodes/Endpoint.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { addDays } from 'date-fns';

import { CertificationPath } from '../pki/CertificationPath';
import { MockKeyStoreSet } from '../keyStores/testMocks';
import { issueEndpointCertificate, issueGatewayCertificate } from '../pki/issuance';
import { getIdFromIdentityKey } from '../crypto/keys/digest';
import { generateRSAKeyPair } from '../crypto/keys/generation';
import { Certificate } from '../crypto/x509/Certificate';
import { reSerializeCertificate } from '../_test_utils';
import { StubNodeChannel } from './channels/_test_utils';
import { Endpoint } from './Endpoint';
import { PrivateEndpointConnParams } from './PrivateEndpointConnParams';
import { SessionKeyPair } from '../SessionKeyPair';
import { derSerializePublicKey } from '../crypto/keys/serialisation';
import { SessionPublicKeyData } from '../keyStores/PublicKeyStore';
import { InvalidNodeConnectionParams } from './errors';
import { Peer } from './peer';

const INTERNET_ADDRESS = 'example.com';

let peerId: string;
let peerIdentityKeyPair: CryptoKeyPair;
let peerCertificate: Certificate;
beforeAll(async () => {
peerIdentityKeyPair = await generateRSAKeyPair();
peerId = await getIdFromIdentityKey(peerIdentityKeyPair.publicKey);
peerCertificate = await issueEndpointCertificate({
issuerPrivateKey: peerIdentityKeyPair.privateKey,
subjectPublicKey: peerIdentityKeyPair.publicKey,
validityEndDate: addDays(new Date(), 1),
});
});

let nodeId: string;
let nodeKeyPair: CryptoKeyPair;
let nodeCertificate: Certificate;
beforeAll(async () => {
nodeKeyPair = await generateRSAKeyPair();
nodeCertificate = reSerializeCertificate(
await issueGatewayCertificate({
issuerCertificate: peerCertificate,
issuerPrivateKey: peerIdentityKeyPair.privateKey,
subjectPublicKey: nodeKeyPair.publicKey,
validityEndDate: peerCertificate.expiryDate,
}),
);
nodeId = await getIdFromIdentityKey(nodeKeyPair.publicKey);
});

const KEY_STORES = new MockKeyStoreSet();
beforeEach(async () => {
KEY_STORES.clear();
});

describe('savePrivateEndpointChannel', () => {
let connectionParams: PrivateEndpointConnParams;
beforeAll(async () => {
const deliveryAuth = new CertificationPath(nodeCertificate, [peerCertificate]);
connectionParams = new PrivateEndpointConnParams(
peerIdentityKeyPair.publicKey,
INTERNET_ADDRESS,
deliveryAuth,
);
});

test('Delivery authorization should be refused if granted to other node', async () => {
const node = new StubEndpoint(`not-${nodeId}`, nodeKeyPair, KEY_STORES, {});

await expect(node.savePrivateEndpointChannel(connectionParams)).rejects.toThrowWithMessage(
InvalidNodeConnectionParams,
`Delivery authorization was granted to another node (${nodeId})`,
);
});

test('Delivery authorization should be stored if valid', async () => {
const node = new StubEndpoint(nodeId, nodeKeyPair, KEY_STORES, {});

await node.savePrivateEndpointChannel(connectionParams);

const deliveryAuthorisations = await KEY_STORES.certificateStore.retrieveAll(nodeId, peerId);
expect(deliveryAuthorisations).toHaveLength(1);
const [deliveryAuthorisation] = deliveryAuthorisations;
expect(Buffer.from(deliveryAuthorisation.serialize())).toStrictEqual(
Buffer.from(connectionParams.deliveryAuth.serialize()),
);
});

test('Identity public key of peer should be stored', async () => {
const node = new StubEndpoint(nodeId, nodeKeyPair, KEY_STORES, {});

await node.savePrivateEndpointChannel(connectionParams);

expect(KEY_STORES.publicKeyStore.identityKeys).toHaveProperty(
peerId,
await derSerializePublicKey(peerIdentityKeyPair.publicKey),
);
});

test('Session public key of peer should be stored if set', async () => {
const node = new StubEndpoint(nodeId, nodeKeyPair, KEY_STORES, {});
const dateBeforeSave = new Date();
const { sessionKey } = await SessionKeyPair.generate();
const paramsWithSessionKey = new PrivateEndpointConnParams(
connectionParams.identityKey,
connectionParams.internetGatewayAddress,
connectionParams.deliveryAuth,
sessionKey,
);

await node.savePrivateEndpointChannel(paramsWithSessionKey);

expect(KEY_STORES.publicKeyStore.sessionKeys).toHaveProperty(
peerId,
expect.objectContaining<SessionPublicKeyData>({
publicKeyId: sessionKey.keyId,
publicKeyDer: await derSerializePublicKey(sessionKey.publicKey),
publicKeyCreationTime: expect.toSatisfy<Date>(
(date) => date <= new Date() && dateBeforeSave <= date,
),
}),
);
});

test('Resulting channel should be output', async () => {
const node = new StubEndpoint(nodeId, nodeKeyPair, KEY_STORES, {});

const channel = await node.savePrivateEndpointChannel(connectionParams);

expect(channel.node).toBe(node);
expect(channel.peer).toMatchObject<Peer<string>>({
id: peerId,
identityPublicKey: peerIdentityKeyPair.publicKey,
internetAddress: INTERNET_ADDRESS,
});
expect(channel.deliveryAuthPath).toBe(connectionParams.deliveryAuth);
});

test('Key store should be propagated', async () => {
const node = new StubEndpoint(nodeId, nodeKeyPair, KEY_STORES, {});

const channel = await node.savePrivateEndpointChannel(connectionParams);

expect(channel.keyStores).toBe(KEY_STORES);
});

test('Crypto options should be propagated', async () => {
const node = new StubEndpoint(nodeId, nodeKeyPair, KEY_STORES, {
encryption: { aesKeySize: 128 },
});

const channel = await node.savePrivateEndpointChannel(connectionParams);

expect(channel.cryptoOptions).toBe(node.cryptoOptions);
});
});

export class StubEndpoint extends Endpoint<undefined> {
protected readonly channelConstructor = StubNodeChannel;
}
49 changes: 48 additions & 1 deletion src/lib/nodes/Endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,51 @@
import { ServiceMessage } from '../messages/payloads/ServiceMessage';
import { Node } from './Node';
import { Peer, PeerInternetAddress } from './peer';
import { Channel } from './channels/Channel';
import { PrivateEndpointConnParams } from './PrivateEndpointConnParams';
import { InvalidNodeConnectionParams } from './errors';
import { getIdFromIdentityKey } from '../crypto/keys/digest';

export class Endpoint extends Node<ServiceMessage> {}
export abstract class Endpoint<PeerAddress extends PeerInternetAddress> extends Node<
ServiceMessage,
PeerAddress
> {
/**
* Create or update a channel with a private endpoint.
*/
public async savePrivateEndpointChannel(
connectionParams: PrivateEndpointConnParams,
): Promise<Channel<ServiceMessage, PeerAddress>> {
const authSubjectId = await connectionParams.deliveryAuth.leafCertificate.calculateSubjectId();
if (authSubjectId !== this.id) {
throw new InvalidNodeConnectionParams(
`Delivery authorization was granted to another node (${authSubjectId})`,
);
}

const peer: Peer<string> = {
id: await getIdFromIdentityKey(connectionParams.identityKey),
internetAddress: connectionParams.internetGatewayAddress,
identityPublicKey: connectionParams.identityKey,
};

await this.keyStores.certificateStore.save(connectionParams.deliveryAuth, peer.id);
await this.keyStores.publicKeyStore.saveIdentityKey(peer.identityPublicKey);

if (connectionParams.sessionKey) {
await this.keyStores.publicKeyStore.saveSessionKey(
connectionParams.sessionKey,
peer.id,
new Date(),
);
}

return new this.channelConstructor(
this,
peer,
connectionParams.deliveryAuth,
this.keyStores,
this.cryptoOptions,
);
}
}
4 changes: 1 addition & 3 deletions src/lib/nodes/Gateway.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { Certificate } from '../crypto/x509/Certificate';
import { MockKeyStoreSet } from '../keyStores/testMocks';
import { CertificationPath } from '../pki/CertificationPath';
import { issueGatewayCertificate } from '../pki/issuance';
import { Gateway } from './Gateway';
import { StubVerifier } from './signatures/_test_utils';
import { getIdFromIdentityKey } from '../crypto/keys/digest';
import { StubGateway } from './channels/_test_utils';

let nodeId: string;
let nodeKeyPair: CryptoKeyPair;
Expand Down Expand Up @@ -72,5 +72,3 @@ describe('getGSCVerifier', () => {
expect(nodeCertificate.isEqual(trustedCertificates[0])).toBeTrue();
});
});

class StubGateway extends Gateway {}
8 changes: 7 additions & 1 deletion src/lib/nodes/Gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import { CargoCollectionRequest } from '../messages/payloads/CargoCollectionRequ
import { CargoMessageSet } from '../messages/payloads/CargoMessageSet';
import { Node } from './Node';
import { Verifier } from './signatures/Verifier';
import { PeerInternetAddress } from './peer';

export abstract class Gateway extends Node<CargoMessageSet | CargoCollectionRequest> {
export type GatewayPayload = CargoMessageSet | CargoCollectionRequest;

export abstract class Gateway<PeerAddress extends PeerInternetAddress> extends Node<
GatewayPayload,
PeerAddress
> {
public async getGSCVerifier<V extends Verifier>(
peerId: string,
verifierClass: new (trustedCertificates: readonly Certificate[]) => V,
Expand Down
9 changes: 8 additions & 1 deletion src/lib/nodes/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ import { SessionKeyPair } from '../SessionKeyPair';
import { NodeCryptoOptions } from './NodeCryptoOptions';
import { Signer } from './signatures/Signer';
import { InvalidMessageError } from '../messages/InvalidMessageError';
import { PeerInternetAddress } from './peer';
import { ChannelConstructor } from './channels/ChannelConstructor';

export abstract class Node<
Payload extends PayloadPlaintext,
PeerAddress extends PeerInternetAddress,
> {
protected abstract readonly channelConstructor: ChannelConstructor<Payload, PeerAddress>;

export abstract class Node<Payload extends PayloadPlaintext> {
constructor(
public readonly id: string,
public readonly identityKeyPair: CryptoKeyPair,
Expand Down
Loading

0 comments on commit 77d404c

Please sign in to comment.