Skip to content

Commit

Permalink
feat(Node): Implement getter and setter for initial session keys (#641)
Browse files Browse the repository at this point in the history
  • Loading branch information
gnarea committed Jan 17, 2024
1 parent 487eaf9 commit 3cfcb08
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 109 deletions.
51 changes: 7 additions & 44 deletions src/lib/keyStores/PrivateKeyStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,61 +125,24 @@ describe('Session keys', () => {
});
});

describe('retrieveUnboundSessionKey', () => {
describe('retrieveUnboundSessionPublicKey', () => {
test('Existing key should be returned', async () => {
await MOCK_STORE.saveSessionKey(
sessionKeyPair.privateKey,
sessionKeyPair.sessionKey.keyId,
NODE_ID,
);

const keySerialized = await MOCK_STORE.retrieveUnboundSessionKey(
sessionKeyPair.sessionKey.keyId,
NODE_ID,
);
const publicKey = await MOCK_STORE.retrieveUnboundSessionPublicKey(NODE_ID);

expect(await derSerializePrivateKey(keySerialized)).toEqual(
await derSerializePrivateKey(sessionKeyPair.privateKey),
expect(publicKey!.type).toBe('public');
expect(await derSerializePublicKey(publicKey!!)).toEqual(
await derSerializePublicKey(sessionKeyPair.sessionKey.publicKey),
);
});

test('UnknownKeyError should be thrown if key id does not exist', async () => {
await expect(
MOCK_STORE.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, NODE_ID),
).rejects.toThrowWithMessage(UnknownKeyError, `Key ${sessionKeyIdHex} does not exist`);
});

test('Key should not be returned if owned by different node', async () => {
await MOCK_STORE.saveSessionKey(
sessionKeyPair.privateKey,
sessionKeyPair.sessionKey.keyId,
NODE_ID,
);

await expect(
MOCK_STORE.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, `not-${NODE_ID}`),
).rejects.toThrowWithMessage(UnknownKeyError, 'Key is owned by a different node');
});

test('Subsequent session keys should not be returned', async () => {
await MOCK_STORE.saveSessionKey(
sessionKeyPair.privateKey,
sessionKeyPair.sessionKey.keyId,
NODE_ID,
PEER_ID,
);

await expect(
MOCK_STORE.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, NODE_ID),
).rejects.toThrowWithMessage(UnknownKeyError, `Key ${sessionKeyIdHex} is bound`);
});

test('Errors should be wrapped', async () => {
const store = new MockPrivateKeyStore(false, true);

await expect(
store.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, NODE_ID),
).rejects.toEqual(new KeyStoreError('Failed to retrieve key: Denied'));
test('Null should be returned if node has no unbound keys', async () => {
await expect(MOCK_STORE.retrieveUnboundSessionPublicKey(NODE_ID)).resolves.toBeNull();
});
});

Expand Down
33 changes: 24 additions & 9 deletions src/lib/keyStores/PrivateKeyStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { generateRSAKeyPair, RSAKeyGenOptions } from '../crypto/keys/generation'
import { IdentityKeyPair } from '../IdentityKeyPair';
import { KeyStoreError } from './KeyStoreError';
import { UnknownKeyError } from './UnknownKeyError';
import { derDeserializeECDHPrivateKey, derSerializePrivateKey } from '../crypto/keys/serialisation';
import {
derDeserializeECDHPrivateKey,
derDeserializeECDHPublicKey,
derSerializePrivateKey,
derSerializePublicKey,
} from '../crypto/keys/serialisation';
import { getIdFromIdentityKey } from '../crypto/keys/digest';

/**
Expand Down Expand Up @@ -59,23 +64,33 @@ export abstract class PrivateKeyStore {
}

/**
* Return the private component of an initial session key pair.
* Return the public key of the latest, unbound session key pair for the specified `nodeId`.
*
* @param keyId The key pair id (typically the serial number)
* @param nodeId The id of the node that owns the key
* @throws UnknownKeyError when the key does not exist
* @throws PrivateKeyStoreError when the look-up could not be done
* @return The public key if it exists or `null` otherwise
*/
public async retrieveUnboundSessionKey(keyId: Buffer, nodeId: string): Promise<CryptoKey> {
const keyData = await this.retrieveSessionKeyDataOrThrowError(keyId, nodeId);
public async retrieveUnboundSessionPublicKey(nodeId: string): Promise<CryptoKey | null> {
const privateKeySerialised = await this.retrieveLatestUnboundSessionKeySerialised(nodeId);

if (keyData.peerId) {
throw new UnknownKeyError(`Key ${keyId.toString('hex')} is bound`);
if (!privateKeySerialised) {
return null;
}

return derDeserializeECDHPrivateKey(keyData.keySerialized, 'P-256');
const privateKey = await derDeserializeECDHPrivateKey(privateKeySerialised);
const publicKeySerialised = await derSerializePublicKey(privateKey);
return derDeserializeECDHPublicKey(publicKeySerialised);
}

/**
* Return the data of the latest, unbound session key for the specified `nodeId`.
*
* @param nodeId The id of the node that owns the key
*/
protected abstract retrieveLatestUnboundSessionKeySerialised(
nodeId: string,
): Promise<Buffer | null>;

/**
* Retrieve private session key, regardless of whether it's an initial key or not.
*
Expand Down
9 changes: 9 additions & 0 deletions src/lib/keyStores/testMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ export class MockPrivateKeyStore extends PrivateKeyStore {

return this.sessionKeys[keyId] ?? null;
}

protected override async retrieveLatestUnboundSessionKeySerialised(
nodeId: string,
): Promise<Buffer | null> {
const keyData = Object.values(this.sessionKeys).find(
(data) => data.nodeId === nodeId && !data.peerId,
);
return keyData?.keySerialized ?? null;
}
}

export class MockPublicKeyStore extends PublicKeyStore {
Expand Down
17 changes: 6 additions & 11 deletions src/lib/nodes/Endpoint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@ describe('savePrivateEndpointChannel', () => {
let connectionParams: PrivateEndpointConnParams;
beforeAll(async () => {
const deliveryAuth = new CertificationPath(nodeCertificate, [peerCertificate]);
const { sessionKey } = await SessionKeyPair.generate();
connectionParams = new PrivateEndpointConnParams(
peerIdentityKeyPair.publicKey,
INTERNET_ADDRESS,
deliveryAuth,
sessionKey,
);
});

Expand Down Expand Up @@ -95,24 +97,17 @@ describe('savePrivateEndpointChannel', () => {
);
});

test('Session public key of peer should be stored if set', async () => {
test('Session public key of peer should be stored', 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);
await node.savePrivateEndpointChannel(connectionParams);

expect(KEY_STORES.publicKeyStore.sessionKeys).toHaveProperty(
peerId,
expect.objectContaining<SessionPublicKeyData>({
publicKeyId: sessionKey.keyId,
publicKeyDer: await derSerializePublicKey(sessionKey.publicKey),
publicKeyId: connectionParams.sessionKey.keyId,
publicKeyDer: await derSerializePublicKey(connectionParams.sessionKey.publicKey),
publicKeyCreationTime: expect.toSatisfy<Date>(
(date) => date <= new Date() && dateBeforeSave <= date,
),
Expand Down
12 changes: 5 additions & 7 deletions src/lib/nodes/Endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,11 @@ export abstract class Endpoint extends Node<ServiceMessage, string> {
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(),
);
}
await this.keyStores.publicKeyStore.saveSessionKey(
connectionParams.sessionKey,
peer.id,
new Date(),
);

return new this.channelConstructor(
this,
Expand Down
9 changes: 4 additions & 5 deletions src/lib/nodes/Node.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,10 @@ describe('generateSessionKey', () => {

const sessionKey = await node.generateSessionKey();

await expect(
derSerializePublicKey(
await KEY_STORES.privateKeyStore.retrieveUnboundSessionKey(sessionKey.keyId, node.id),
),
).resolves.toEqual(await derSerializePublicKey(sessionKey.publicKey));
const publicKey = await KEY_STORES.privateKeyStore.retrieveUnboundSessionPublicKey(node.id);
await expect(derSerializePublicKey(publicKey!!)).resolves.toEqual(
await derSerializePublicKey(sessionKey.publicKey),
);
});

test('Key should be bound to a peer if explicitly set', async () => {
Expand Down
30 changes: 2 additions & 28 deletions src/lib/nodes/PrivateEndpointConnParams.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe('PrivateEndpointConnParams', () => {
);
});

test('Session key should be serialised if present', async () => {
test('Session key should be serialised', async () => {
const params = new PrivateEndpointConnParams(
peerIdentityKeyPair.publicKey,
INTERNET_ADDRESS,
Expand All @@ -122,19 +122,6 @@ describe('PrivateEndpointConnParams', () => {
Buffer.from(AsnSerializer.serialize(paramsDeserialized.sessionKey!.publicKey)),
).toStrictEqual(await derSerializePublicKey(peerSessionKey.publicKey));
});

test('Session key should be skipped if missing', async () => {
const params = new PrivateEndpointConnParams(
peerIdentityKeyPair.publicKey,
INTERNET_ADDRESS,
deliveryAuth,
);

const serialisation = await params.serialize();

const paramsDeserialized = AsnParser.parse(serialisation, PrivateEndpointConnParamsSchema);
expect(paramsDeserialized.sessionKey).toBeUndefined();
});
});

describe('deserialize', () => {
Expand Down Expand Up @@ -184,26 +171,13 @@ describe('PrivateEndpointConnParams', () => {
).toBeTrue();
});

test('Session key should be output if present', async () => {
test('Session key should be output', async () => {
const params = await PrivateEndpointConnParams.deserialize(paramsSerialized);

expect(params.sessionKey!.keyId).toStrictEqual(peerSessionKey.keyId);
await expect(derSerializePublicKey(params.sessionKey!.publicKey)).resolves.toStrictEqual(
await derSerializePublicKey(peerSessionKey.publicKey),
);
});

test('Session should be skipped if absent', async () => {
const params = new PrivateEndpointConnParams(
peerIdentityKeyPair.publicKey,
INTERNET_ADDRESS,
deliveryAuth,
);
const serialization = await params.serialize();

const paramsDeserialized = await PrivateEndpointConnParams.deserialize(serialization);

expect(paramsDeserialized.sessionKey).toBeUndefined();
});
});
});
6 changes: 3 additions & 3 deletions src/lib/nodes/PrivateEndpointConnParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class PrivateEndpointConnParams {
const identityKey = await derDeserializeRSAPublicKey(
AsnSerializer.serialize(schema.identityKey),
);
const sessionKey = schema.sessionKey ? await decodeSessionKey(schema.sessionKey) : undefined;
const sessionKey = await decodeSessionKey(schema.sessionKey);
const leafCertificate = convertAsnToCertificate(schema.deliveryAuth.leaf);
const cas = schema.deliveryAuth.certificateAuthorities.map(convertAsnToCertificate);
return new PrivateEndpointConnParams(
Expand All @@ -42,7 +42,7 @@ export class PrivateEndpointConnParams {
public readonly identityKey: CryptoKey,
public readonly internetGatewayAddress: string,
public readonly deliveryAuth: CertificationPath,
public readonly sessionKey?: SessionKey,
public readonly sessionKey: SessionKey,
) {}

public async serialize(): Promise<ArrayBuffer> {
Expand All @@ -61,7 +61,7 @@ export class PrivateEndpointConnParams {
this.deliveryAuth.certificateAuthorities.map(convertCertificateToAsn),
);

schema.sessionKey = this.sessionKey ? await encodeSessionKey(this.sessionKey) : undefined;
schema.sessionKey = await encodeSessionKey(this.sessionKey);

return AsnSerializer.serialize(schema);
}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/schemas/PrivateEndpointConnParamsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ export class PrivateEndpointConnParamsSchema {
@AsnProp({ type: CertificationPathSchema, context: 2, implicit: true })
public deliveryAuth!: CertificationPathSchema;

@AsnProp({ type: SessionKeySchema, context: 3, implicit: true, optional: true })
public sessionKey?: SessionKeySchema;
@AsnProp({ type: SessionKeySchema, context: 3, implicit: true })
public sessionKey!: SessionKeySchema;
}

0 comments on commit 3cfcb08

Please sign in to comment.