Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Node): Implement getter and setter for initial session keys #641

Merged
merged 3 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}