From 53e67ef0b0e3a6ab5c61157fa306e73723a301f3 Mon Sep 17 00:00:00 2001 From: Gus Narea Date: Thu, 28 Jul 2022 16:31:39 +0100 Subject: [PATCH] fix(RAMF): Require private address and support public address of recipient (#491) Part of https://github.com/relaycorp/relayverse/issues/19 - [x] Implement `RecipientAddressing` class. - [x] Reimplement `RAMFMessage.recipient` as a `RecipientAddressing` - [x] Remove `RAMFMessage.isRecipientAddressPrivate` - [x] Remove `https://` prefix from Internet addresses. --- src/index.ts | 8 +- ...ing.test.ts => internetAddressing.test.ts} | 22 +- src/integration_tests/pki.test.ts | 57 +- src/lib/IdentityKeyPair.ts | 2 +- src/lib/_test_utils.ts | 30 +- src/lib/bindings/gsc/ParcelCollection.spec.ts | 39 +- src/lib/bindings/gsc/ParcelCollection.ts | 3 +- .../gsc/PrivateNodeRegistration.spec.ts | 85 ++- .../bindings/gsc/PrivateNodeRegistration.ts | 36 +- ...ivateNodeRegistrationAuthorization.spec.ts | 1 - .../PrivateNodeRegistrationRequest.spec.ts | 8 +- src/lib/crypto_wrappers/cms/signedData.ts | 1 - src/lib/crypto_wrappers/keys.spec.ts | 12 +- src/lib/crypto_wrappers/keys.ts | 4 +- src/lib/crypto_wrappers/rsaSigning.spec.ts | 1 - .../crypto_wrappers/x509/Certificate.spec.ts | 28 +- src/lib/crypto_wrappers/x509/Certificate.ts | 14 +- ...ing.spec.ts => internetAddressing.spec.ts} | 50 +- ...licAddressing.ts => internetAddressing.ts} | 14 +- src/lib/keyStores/CertificateStore.spec.ts | 106 +-- src/lib/keyStores/CertificateStore.ts | 40 +- src/lib/keyStores/PrivateKeyStore.spec.ts | 105 ++- src/lib/keyStores/PrivateKeyStore.ts | 68 +- src/lib/keyStores/PublicKeyStore.spec.ts | 43 +- src/lib/keyStores/PublicKeyStore.ts | 40 +- src/lib/keyStores/testMocks.ts | 96 ++- src/lib/messages/CertificateRotation.spec.ts | 6 +- src/lib/messages/ParcelCollectionAck.spec.ts | 30 +- src/lib/messages/ParcelCollectionAck.ts | 18 +- src/lib/messages/RAMFMessage.spec.ts | 411 +++-------- src/lib/messages/RAMFMessage.ts | 86 +-- src/lib/messages/Recipient.ts | 4 + src/lib/messages/RecipientAddressType.ts | 4 - src/lib/messages/_test_utils.ts | 2 - src/lib/messages/formatSignature.ts | 6 +- .../payloads/CargoCollectionRequest.spec.ts | 4 +- .../messages/payloads/CargoMessageSet.spec.ts | 8 +- src/lib/messages/payloads/CargoMessageSet.ts | 7 +- .../messages/payloads/ServiceMessage.spec.ts | 10 +- src/lib/nodes/Gateway.spec.ts | 29 +- src/lib/nodes/Gateway.ts | 7 +- src/lib/nodes/Node.spec.ts | 72 +- src/lib/nodes/Node.ts | 24 +- src/lib/nodes/PrivateGateway.spec.ts | 164 +++-- src/lib/nodes/PrivateGateway.ts | 60 +- .../nodes/PublicNodeConnectionParams.spec.ts | 12 +- src/lib/nodes/PublicNodeConnectionParams.ts | 20 +- src/lib/nodes/channels/Channel.spec.ts | 29 +- src/lib/nodes/channels/Channel.ts | 19 +- src/lib/nodes/channels/GatewayChannel.spec.ts | 36 +- src/lib/nodes/channels/GatewayChannel.ts | 4 +- .../channels/PrivateGatewayChannel.spec.ts | 54 +- .../nodes/channels/PrivateGatewayChannel.ts | 21 +- ...el.ts => PrivateInternetGatewayChannel.ts} | 27 +- .../PrivatePublicGatewayChannel.spec.ts | 97 +-- .../nodes/managers/EndpointManager.spec.ts | 4 +- src/lib/nodes/managers/NodeConstructor.ts | 2 +- src/lib/nodes/managers/NodeManager.spec.ts | 27 +- src/lib/nodes/managers/NodeManager.ts | 24 +- .../managers/PrivateGatewayManager.spec.ts | 4 +- src/lib/pki/issuance.spec.ts | 6 +- src/lib/ramf/serialization.spec.ts | 640 +++++++++++------- src/lib/ramf/serialization.ts | 132 ++-- 63 files changed, 1435 insertions(+), 1588 deletions(-) rename src/integration_tests/{publicAddressing.test.ts => internetAddressing.test.ts} (51%) rename src/lib/{publicAddressing.spec.ts => internetAddressing.spec.ts} (76%) rename src/lib/{publicAddressing.ts => internetAddressing.ts} (82%) create mode 100644 src/lib/messages/Recipient.ts delete mode 100644 src/lib/messages/RecipientAddressType.ts rename src/lib/nodes/channels/{PrivatePublicGatewayChannel.ts => PrivateInternetGatewayChannel.ts} (83%) diff --git a/src/index.ts b/src/index.ts index ec28b8772..61c9c8769 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,7 @@ export { getPublicKeyDigest, getPublicKeyDigestHex, getRSAPublicKeyFromPrivate, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, RSAKeyGenOptions, } from './lib/crypto_wrappers/keys'; export { PrivateKey, RsaPssPrivateKey } from './lib/crypto_wrappers/PrivateKey'; @@ -63,7 +63,7 @@ export { default as RAMFError } from './lib/ramf/RAMFError'; export { default as RAMFSyntaxError } from './lib/ramf/RAMFSyntaxError'; export { MAX_RAMF_MESSAGE_LENGTH } from './lib/ramf/serialization'; export { default as RAMFMessage } from './lib/messages/RAMFMessage'; -export { RecipientAddressType } from './lib/messages/RecipientAddressType'; +export { Recipient } from './lib/messages/Recipient'; export { default as Parcel } from './lib/messages/Parcel'; export { default as ServiceMessage } from './lib/messages/payloads/ServiceMessage'; export { default as Cargo } from './lib/messages/Cargo'; @@ -98,11 +98,11 @@ export { PrivateGateway } from './lib/nodes/PrivateGateway'; export { CargoMessageStream } from './lib/nodes/CargoMessageStream'; export { Channel } from './lib/nodes/channels/Channel'; export { GatewayChannel } from './lib/nodes/channels/GatewayChannel'; -export { PrivatePublicGatewayChannel } from './lib/nodes/channels/PrivatePublicGatewayChannel'; +export { PrivateInternetGatewayChannel } from './lib/nodes/channels/PrivateInternetGatewayChannel'; //endregion export { NodeCryptoOptions } from './lib/nodes/NodeCryptoOptions'; export { PublicNodeConnectionParams } from './lib/nodes/PublicNodeConnectionParams'; export * from './lib/nodes/errors'; -export * from './lib/publicAddressing'; +export * from './lib/internetAddressing'; diff --git a/src/integration_tests/publicAddressing.test.ts b/src/integration_tests/internetAddressing.test.ts similarity index 51% rename from src/integration_tests/publicAddressing.test.ts rename to src/integration_tests/internetAddressing.test.ts index 09afb867f..c78adafc5 100644 --- a/src/integration_tests/publicAddressing.test.ts +++ b/src/integration_tests/internetAddressing.test.ts @@ -1,11 +1,11 @@ -import { BindingType, resolvePublicAddress } from '..'; +import { BindingType, resolveInternetAddress } from '..'; -describe('resolvePublicAddress', () => { - const EXISTING_PUBLIC_ADDRESS = 'frankfurt.relaycorp.cloud'; - const NON_EXISTING_ADDRESS = 'unlikely-to-ever-exist.relaycorp.cloud'; +describe('resolveInternetAddress', () => { + const EXISTING_INTERNET_ADDRESS = 'frankfurt.relaycorp.cloud'; + const NON_EXISTING_ADDRESS = 'unlikely-to-ever-exist-f5e6yht34.relaycorp.cloud'; test('Existing address should be resolved', async () => { - const address = await resolvePublicAddress(EXISTING_PUBLIC_ADDRESS, BindingType.PDC); + const address = await resolveInternetAddress(EXISTING_INTERNET_ADDRESS, BindingType.PDC); expect(address?.host).toBeString(); expect(address?.port).toBeNumber(); @@ -15,9 +15,9 @@ describe('resolvePublicAddress', () => { // This is important to check because CloudFlare and Google DNS resolvers are slightly // different. For example, Google's adds a trailing dot to the target host. - const cfAddress = await resolvePublicAddress(EXISTING_PUBLIC_ADDRESS, BindingType.PDC); - const gAddress = await resolvePublicAddress( - EXISTING_PUBLIC_ADDRESS, + const cfAddress = await resolveInternetAddress(EXISTING_INTERNET_ADDRESS, BindingType.PDC); + const gAddress = await resolveInternetAddress( + EXISTING_INTERNET_ADDRESS, BindingType.PDC, 'https://dns.google/dns-query', ); @@ -26,17 +26,17 @@ describe('resolvePublicAddress', () => { }); test('Invalid DNSSEC configuration should be refused', async () => { - await expect(resolvePublicAddress('dnssec-failed.org', BindingType.PDC)).toReject(); + await expect(resolveInternetAddress('dnssec-failed.org', BindingType.PDC)).toReject(); }); test('Non-existing addresses should not be resolved', async () => { - await expect(resolvePublicAddress(NON_EXISTING_ADDRESS, BindingType.PDC)).resolves.toBeNull(); + await expect(resolveInternetAddress(NON_EXISTING_ADDRESS, BindingType.PDC)).resolves.toBeNull(); }); test('Non-existing address should resolve if port is contained', async () => { const port = 1234; await expect( - resolvePublicAddress(`${NON_EXISTING_ADDRESS}:${port}`, BindingType.PDC), + resolveInternetAddress(`${NON_EXISTING_ADDRESS}:${port}`, BindingType.PDC), ).resolves.toEqual({ host: NON_EXISTING_ADDRESS, port }); }); }); diff --git a/src/integration_tests/pki.test.ts b/src/integration_tests/pki.test.ts index dff93c0ce..17b5aec9a 100644 --- a/src/integration_tests/pki.test.ts +++ b/src/integration_tests/pki.test.ts @@ -1,3 +1,5 @@ +import { addDays, subSeconds } from 'date-fns'; + import { Certificate, generateRSAKeyPair, @@ -8,22 +10,21 @@ import { } from '..'; import { reSerializeCertificate } from '../lib/_test_utils'; -const ONE_SECOND_AGO = new Date(); -ONE_SECOND_AGO.setSeconds(ONE_SECOND_AGO.getSeconds() - 1, 0); +const ONE_SECOND_AGO = subSeconds(new Date(), 1); -const TOMORROW = new Date(); -TOMORROW.setDate(TOMORROW.getDate() + 1); +const TOMORROW = addDays(new Date(), 1); -let publicGatewayCert: Certificate; +let internetGatewayCert: Certificate; let privateGatewayCert: Certificate; +let peerEndpointId: string; let peerEndpointCert: Certificate; let endpointPdaCert: Certificate; beforeAll(async () => { - const publicGatewayKeyPair = await generateRSAKeyPair(); - publicGatewayCert = reSerializeCertificate( + const internetGatewayKeyPair = await generateRSAKeyPair(); + internetGatewayCert = reSerializeCertificate( await issueGatewayCertificate({ - issuerPrivateKey: publicGatewayKeyPair.privateKey, - subjectPublicKey: publicGatewayKeyPair.publicKey, + issuerPrivateKey: internetGatewayKeyPair.privateKey, + subjectPublicKey: internetGatewayKeyPair.publicKey, validityEndDate: TOMORROW, validityStartDate: ONE_SECOND_AGO, }), @@ -32,8 +33,8 @@ beforeAll(async () => { const localGatewayKeyPair = await generateRSAKeyPair(); privateGatewayCert = reSerializeCertificate( await issueGatewayCertificate({ - issuerCertificate: publicGatewayCert, - issuerPrivateKey: publicGatewayKeyPair.privateKey, + issuerCertificate: internetGatewayCert, + issuerPrivateKey: internetGatewayKeyPair.privateKey, subjectPublicKey: localGatewayKeyPair.publicKey, validityEndDate: TOMORROW, validityStartDate: ONE_SECOND_AGO, @@ -51,6 +52,8 @@ beforeAll(async () => { }), ); + peerEndpointId = await peerEndpointCert.calculateSubjectId(); + const endpointKeyPair = await generateRSAKeyPair(); endpointPdaCert = reSerializeCertificate( await issueDeliveryAuthorization({ @@ -64,32 +67,24 @@ beforeAll(async () => { }); test('Messages by authorized senders should be accepted', async () => { - const parcel = new Parcel( - await peerEndpointCert.calculateSubjectPrivateAddress(), - endpointPdaCert, - Buffer.from('hey'), - { - creationDate: ONE_SECOND_AGO, - senderCaCertificateChain: [peerEndpointCert, privateGatewayCert], - }, - ); + const parcel = new Parcel({ id: peerEndpointId }, endpointPdaCert, Buffer.from('hey'), { + creationDate: ONE_SECOND_AGO, + senderCaCertificateChain: [peerEndpointCert, privateGatewayCert], + }); - await parcel.validate(undefined, [publicGatewayCert]); + await parcel.validate([internetGatewayCert]); }); test('Certificate chain should be computed corrected', async () => { - const parcel = new Parcel( - await peerEndpointCert.calculateSubjectPrivateAddress(), - endpointPdaCert, - Buffer.from('hey'), - { senderCaCertificateChain: [peerEndpointCert, privateGatewayCert] }, - ); + const parcel = new Parcel({ id: peerEndpointId }, endpointPdaCert, Buffer.from('hey'), { + senderCaCertificateChain: [peerEndpointCert, privateGatewayCert], + }); - await expect(parcel.getSenderCertificationPath([publicGatewayCert])).resolves.toEqual([ + await expect(parcel.getSenderCertificationPath([internetGatewayCert])).resolves.toEqual([ expect.toSatisfy((c) => c.isEqual(endpointPdaCert)), expect.toSatisfy((c) => c.isEqual(peerEndpointCert)), expect.toSatisfy((c) => c.isEqual(privateGatewayCert)), - expect.toSatisfy((c) => c.isEqual(publicGatewayCert)), + expect.toSatisfy((c) => c.isEqual(internetGatewayCert)), ]); }); @@ -104,7 +99,7 @@ test('Messages by unauthorized senders should be refused', async () => { }), ); const parcel = new Parcel( - await peerEndpointCert.calculateSubjectPrivateAddress(), + { id: peerEndpointId }, unauthorizedSenderCertificate, Buffer.from('hey'), { @@ -113,7 +108,7 @@ test('Messages by unauthorized senders should be refused', async () => { }, ); - await expect(parcel.validate(undefined, [publicGatewayCert])).rejects.toHaveProperty( + await expect(parcel.validate([internetGatewayCert])).rejects.toHaveProperty( 'message', 'Sender is not authorized: No valid certificate paths found', ); diff --git a/src/lib/IdentityKeyPair.ts b/src/lib/IdentityKeyPair.ts index 5f048a6dd..942047369 100644 --- a/src/lib/IdentityKeyPair.ts +++ b/src/lib/IdentityKeyPair.ts @@ -1,3 +1,3 @@ export interface IdentityKeyPair extends CryptoKeyPair { - readonly privateAddress: string; + readonly id: string; } diff --git a/src/lib/_test_utils.ts b/src/lib/_test_utils.ts index e9ceec0cd..d48b78ca5 100644 --- a/src/lib/_test_utils.ts +++ b/src/lib/_test_utils.ts @@ -1,4 +1,4 @@ -import * as asn1js from 'asn1js'; +import { BaseBlock, Constructed, Primitive } from 'asn1js'; import bufferToArray from 'buffer-to-arraybuffer'; import { createHash } from 'crypto'; import * as pkijs from 'pkijs'; @@ -169,14 +169,28 @@ export async function asyncIterableToArray(iterable: AsyncIterable): Promi return values; } -export function getAsn1SequenceItem( - fields: asn1js.Sequence | asn1js.BaseBlock, +export function getPrimitiveItemFromConstructed( + fields: BaseBlock, itemIndex: number, -): asn1js.Primitive { - expect(fields).toBeInstanceOf(asn1js.Sequence); - const itemBlock = (fields as asn1js.Sequence).valueBlock.value[itemIndex] as asn1js.Primitive; - expect(itemBlock).toBeInstanceOf(asn1js.Primitive); +): Primitive { + const itemBlock = getItemFromConstructed(fields, itemIndex); + expect(itemBlock).toBeInstanceOf(Primitive); + return itemBlock as Primitive; +} + +export function getConstructedItemFromConstructed( + fields: BaseBlock, + itemIndex: number, +): Constructed { + const itemBlock = getItemFromConstructed(fields, itemIndex); + expect(itemBlock).toBeInstanceOf(Constructed); + return itemBlock as Constructed; +} + +function getItemFromConstructed(fields: BaseBlock, itemIndex: number): BaseBlock { + expect(fields).toBeInstanceOf(Constructed); + const itemBlock = fields.valueBlock.value[itemIndex]; expect(itemBlock.idBlock.tagClass).toEqual(3); // Context-specific expect(itemBlock.idBlock.tagNumber).toEqual(itemIndex); - return itemBlock as any; + return itemBlock; } diff --git a/src/lib/bindings/gsc/ParcelCollection.spec.ts b/src/lib/bindings/gsc/ParcelCollection.spec.ts index c067310e1..810d2a245 100644 --- a/src/lib/bindings/gsc/ParcelCollection.spec.ts +++ b/src/lib/bindings/gsc/ParcelCollection.spec.ts @@ -1,3 +1,5 @@ +import { addDays } from 'date-fns'; + import { arrayBufferFrom, expectArrayBuffersToEqual, @@ -8,6 +10,7 @@ import { generateRSAKeyPair } from '../../crypto_wrappers/keys'; import Certificate from '../../crypto_wrappers/x509/Certificate'; import InvalidMessageError from '../../messages/InvalidMessageError'; import Parcel from '../../messages/Parcel'; +import { Recipient } from '../../messages/Recipient'; import { issueDeliveryAuthorization, issueEndpointCertificate, @@ -23,8 +26,7 @@ let pdaCertificate: Certificate; let recipientCertificate: Certificate; let gatewayCertificate: Certificate; beforeAll(async () => { - const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); + const tomorrow = addDays(new Date(), 1); const caKeyPair = await generateRSAKeyPair(); gatewayCertificate = reSerializeCertificate( @@ -56,6 +58,13 @@ beforeAll(async () => { ); }); +let recipient: Recipient; +beforeAll(async () => { + recipient = { + id: await recipientCertificate.calculateSubjectId(), + }; +}); + test('Parcel serialized should be honored', () => { const collection = new ParcelCollection(PARCEL_SERIALIZED, [gatewayCertificate], jest.fn()); @@ -88,27 +97,12 @@ describe('deserializeAndValidateParcel', () => { await expect(collection.deserializeAndValidateParcel()).rejects.toBeInstanceOf(RAMFSyntaxError); }); - test('Parcels bound for public endpoints should be refused', async () => { - const parcel = new Parcel('https://example.com', pdaCertificate, Buffer.from([]), { - senderCaCertificateChain: [gatewayCertificate], - }); - const collection = new ParcelCollection( - await parcel.serialize(pdaGranteeKeyPair.privateKey), - [gatewayCertificate], - jest.fn(), - ); - - await expect(collection.deserializeAndValidateParcel()).rejects.toBeInstanceOf( - InvalidMessageError, - ); - }); - test('Parcels from unauthorized senders should be refused', async () => { const unauthorizedSenderCertificate = await generateStubCert({ issuerPrivateKey: pdaGranteeKeyPair.privateKey, subjectPublicKey: pdaGranteeKeyPair.publicKey, }); - const parcel = new Parcel('0deadbeef', unauthorizedSenderCertificate, Buffer.from([])); + const parcel = new Parcel(recipient, unauthorizedSenderCertificate, Buffer.from([])); const collection = new ParcelCollection( await parcel.serialize(pdaGranteeKeyPair.privateKey), [gatewayCertificate], @@ -121,12 +115,9 @@ describe('deserializeAndValidateParcel', () => { }); test('Valid parcels should be returned', async () => { - const parcel = new Parcel( - await recipientCertificate.calculateSubjectPrivateAddress(), - pdaCertificate, - Buffer.from([]), - { senderCaCertificateChain: [recipientCertificate] }, - ); + const parcel = new Parcel(recipient, pdaCertificate, Buffer.from([]), { + senderCaCertificateChain: [recipientCertificate], + }); const collection = new ParcelCollection( await parcel.serialize(pdaGranteeKeyPair.privateKey), [gatewayCertificate], diff --git a/src/lib/bindings/gsc/ParcelCollection.ts b/src/lib/bindings/gsc/ParcelCollection.ts index 7dd53c53e..5715c1cba 100644 --- a/src/lib/bindings/gsc/ParcelCollection.ts +++ b/src/lib/bindings/gsc/ParcelCollection.ts @@ -1,6 +1,5 @@ import Certificate from '../../crypto_wrappers/x509/Certificate'; import Parcel from '../../messages/Parcel'; -import { RecipientAddressType } from '../../messages/RecipientAddressType'; export class ParcelCollection { constructor( @@ -15,7 +14,7 @@ export class ParcelCollection { public async deserializeAndValidateParcel(): Promise { const parcel = await Parcel.deserialize(this.parcelSerialized); - await parcel.validate(RecipientAddressType.PRIVATE, this.trustedCertificates); + await parcel.validate(this.trustedCertificates); return parcel; } } diff --git a/src/lib/bindings/gsc/PrivateNodeRegistration.spec.ts b/src/lib/bindings/gsc/PrivateNodeRegistration.spec.ts index d15c1c52f..785e7e375 100644 --- a/src/lib/bindings/gsc/PrivateNodeRegistration.spec.ts +++ b/src/lib/bindings/gsc/PrivateNodeRegistration.spec.ts @@ -1,4 +1,4 @@ -import { Constructed, OctetString, Sequence } from 'asn1js'; +import { Constructed, OctetString, Primitive, Sequence, VisibleString } from 'asn1js'; import bufferToArray from 'buffer-to-arraybuffer'; import { arrayBufferFrom, generateStubCert } from '../../_test_utils'; @@ -20,9 +20,15 @@ beforeAll(async () => { sessionKey = (await SessionKeyPair.generate()).sessionKey; }); +const INTERNET_GATEWAY_INTERNET_ADDRESS = 'westeros.relaycorp.cloud'; + describe('serialize', () => { test('Private node certificate should be serialized', async () => { - const registration = new PrivateNodeRegistration(privateNodeCertificate, gatewayCertificate); + const registration = new PrivateNodeRegistration( + privateNodeCertificate, + gatewayCertificate, + INTERNET_GATEWAY_INTERNET_ADDRESS, + ); const serialization = await registration.serialize(); @@ -35,7 +41,11 @@ describe('serialize', () => { }); test('Gateway certificate should be serialized', async () => { - const registration = new PrivateNodeRegistration(privateNodeCertificate, gatewayCertificate); + const registration = new PrivateNodeRegistration( + privateNodeCertificate, + gatewayCertificate, + INTERNET_GATEWAY_INTERNET_ADDRESS, + ); const serialization = await registration.serialize(); @@ -47,27 +57,48 @@ describe('serialize', () => { ); }); + test('Internet address of Internet gateway should be serialized', async () => { + const registration = new PrivateNodeRegistration( + privateNodeCertificate, + gatewayCertificate, + INTERNET_GATEWAY_INTERNET_ADDRESS, + ); + + const serialization = await registration.serialize(); + + const sequence = derDeserialize(serialization); + const addressPrimitive = (sequence as Sequence).valueBlock.value[2] as Primitive; + expect(Buffer.from(addressPrimitive.valueBlock.valueHexView).toString()).toEqual( + INTERNET_GATEWAY_INTERNET_ADDRESS, + ); + }); + describe('Session key', () => { test('Session key should be absent from serialization if it does not exist', async () => { - const registration = new PrivateNodeRegistration(privateNodeCertificate, gatewayCertificate); + const registration = new PrivateNodeRegistration( + privateNodeCertificate, + gatewayCertificate, + INTERNET_GATEWAY_INTERNET_ADDRESS, + ); const serialization = await registration.serialize(); const sequence = derDeserialize(serialization); - expect((sequence as Sequence).valueBlock.value).toHaveLength(2); + expect((sequence as Sequence).valueBlock.value).toHaveLength(3); }); test('Session key should be a CONSTRUCTED value', async () => { const registration = new PrivateNodeRegistration( privateNodeCertificate, gatewayCertificate, + INTERNET_GATEWAY_INTERNET_ADDRESS, sessionKey, ); const serialization = await registration.serialize(); const sequence = derDeserialize(serialization); - const sessionKeySequence = (sequence as Sequence).valueBlock.value[2]; + const sessionKeySequence = (sequence as Sequence).valueBlock.value[3]; expect(sessionKeySequence).toBeInstanceOf(Constructed); }); @@ -75,6 +106,7 @@ describe('serialize', () => { const registration = new PrivateNodeRegistration( privateNodeCertificate, gatewayCertificate, + INTERNET_GATEWAY_INTERNET_ADDRESS, sessionKey, ); @@ -82,7 +114,7 @@ describe('serialize', () => { const sequence = derDeserialize(serialization); expect( - ((sequence as Sequence).valueBlock.value[2] as Sequence).valueBlock.value[0], + ((sequence as Sequence).valueBlock.value[3] as Sequence).valueBlock.value[0], ).toHaveProperty('valueBlock.valueHex', bufferToArray(sessionKey.keyId)); }); @@ -90,6 +122,7 @@ describe('serialize', () => { const registration = new PrivateNodeRegistration( privateNodeCertificate, gatewayCertificate, + INTERNET_GATEWAY_INTERNET_ADDRESS, sessionKey, ); @@ -97,7 +130,7 @@ describe('serialize', () => { const sequence = await derDeserialize(serialization); expect( - ((sequence as Sequence).valueBlock.value[2] as Sequence).valueBlock.value[1], + ((sequence as Sequence).valueBlock.value[3] as Sequence).valueBlock.value[1], ).toHaveProperty( 'valueBlock.valueHex', bufferToArray(await derSerializePublicKey(sessionKey.publicKey)), @@ -118,9 +151,10 @@ describe('deserialize', () => { ); }); - test('Sequence should have at least two items', async () => { + test('Sequence should have at least 3 items', async () => { const invalidSerialization = makeImplicitlyTaggedSequence( new OctetString({ valueHex: arrayBufferFrom('nope.jpg') }), + new OctetString({ valueHex: arrayBufferFrom('nope.png') }), ).toBER(); await expect(() => @@ -135,6 +169,7 @@ describe('deserialize', () => { const invalidSerialization = makeImplicitlyTaggedSequence( new OctetString({ valueHex: arrayBufferFrom('not a certificate') }), new OctetString({ valueHex: gatewayCertificate.serialize() }), + new VisibleString({ value: INTERNET_GATEWAY_INTERNET_ADDRESS }), ).toBER(); await expect(() => @@ -146,6 +181,7 @@ describe('deserialize', () => { const invalidSerialization = makeImplicitlyTaggedSequence( new OctetString({ valueHex: gatewayCertificate.serialize() }), new OctetString({ valueHex: arrayBufferFrom('not a certificate') }), + new VisibleString({ value: INTERNET_GATEWAY_INTERNET_ADDRESS }), ).toBER(); await expect(() => @@ -153,11 +189,28 @@ describe('deserialize', () => { ).rejects.toThrowWithMessage(InvalidMessageError, /^Gateway certificate is invalid:/); }); + test('Malformed Internet address of Internet gateway should be refused', async () => { + const invalidAddress = `${INTERNET_GATEWAY_INTERNET_ADDRESS}-`; + const invalidSerialization = makeImplicitlyTaggedSequence( + new OctetString({ valueHex: gatewayCertificate.serialize() }), + new OctetString({ valueHex: privateNodeCertificate.serialize() }), + new VisibleString({ value: invalidAddress }), + ).toBER(); + + await expect(() => + PrivateNodeRegistration.deserialize(invalidSerialization), + ).rejects.toThrowWithMessage( + InvalidMessageError, + `Malformed Internet gateway address (${invalidAddress})`, + ); + }); + describe('Session key', () => { test('SEQUENCE should contain at least two items', async () => { const invalidSerialization = makeImplicitlyTaggedSequence( new OctetString({ valueHex: gatewayCertificate.serialize() }), new OctetString({ valueHex: privateNodeCertificate.serialize() }), + new VisibleString({ value: INTERNET_GATEWAY_INTERNET_ADDRESS }), makeImplicitlyTaggedSequence( new OctetString({ valueHex: bufferToArray(sessionKey.keyId) }), ), @@ -175,6 +228,7 @@ describe('deserialize', () => { const invalidRegistration = new PrivateNodeRegistration( privateNodeCertificate, gatewayCertificate, + INTERNET_GATEWAY_INTERNET_ADDRESS, { keyId: sessionKey.keyId, publicKey: await gatewayCertificate.getPublicKey(), // Invalid key type (RSA) @@ -195,6 +249,7 @@ describe('deserialize', () => { const registration = new PrivateNodeRegistration( privateNodeCertificate, gatewayCertificate, + INTERNET_GATEWAY_INTERNET_ADDRESS, sessionKey, ); @@ -205,6 +260,9 @@ describe('deserialize', () => { registrationDeserialized.privateNodeCertificate.isEqual(privateNodeCertificate), ).toBeTrue(); expect(registrationDeserialized.gatewayCertificate.isEqual(gatewayCertificate)).toBeTrue(); + expect(registrationDeserialized.internetGatewayInternetAddress).toEqual( + INTERNET_GATEWAY_INTERNET_ADDRESS, + ); expect(registrationDeserialized.sessionKey!!.keyId).toEqual(sessionKey.keyId); await expect( derSerializePublicKey(registrationDeserialized.sessionKey!!.publicKey), @@ -212,7 +270,11 @@ describe('deserialize', () => { }); test('Valid registration without session key should be accepted', async () => { - const registration = new PrivateNodeRegistration(privateNodeCertificate, gatewayCertificate); + const registration = new PrivateNodeRegistration( + privateNodeCertificate, + gatewayCertificate, + INTERNET_GATEWAY_INTERNET_ADDRESS, + ); const serialization = await registration.serialize(); @@ -221,5 +283,8 @@ describe('deserialize', () => { registrationDeserialized.privateNodeCertificate.isEqual(privateNodeCertificate), ).toBeTrue(); expect(registrationDeserialized.gatewayCertificate.isEqual(gatewayCertificate)).toBeTrue(); + expect(registrationDeserialized.internetGatewayInternetAddress).toEqual( + INTERNET_GATEWAY_INTERNET_ADDRESS, + ); }); }); diff --git a/src/lib/bindings/gsc/PrivateNodeRegistration.ts b/src/lib/bindings/gsc/PrivateNodeRegistration.ts index cd02bbf88..adce7ca64 100644 --- a/src/lib/bindings/gsc/PrivateNodeRegistration.ts +++ b/src/lib/bindings/gsc/PrivateNodeRegistration.ts @@ -1,5 +1,7 @@ -import { Constructed, OctetString, Primitive, verifySchema } from 'asn1js'; +import { Constructed, OctetString, Primitive, verifySchema, VisibleString } from 'asn1js'; import bufferToArray from 'buffer-to-arraybuffer'; +import isValidDomain from 'is-valid-domain'; +import { TextDecoder } from 'util'; import { makeHeterogeneousSequenceSchema, makeImplicitlyTaggedSequence } from '../../asn1'; import { derDeserializeECDHPublicKey, derSerializePublicKey } from '../../crypto_wrappers/keys'; @@ -33,27 +35,44 @@ export class PrivateNodeRegistration { throw new InvalidMessageError(err as Error, 'Gateway certificate is invalid'); } + const textDecoder = new TextDecoder(); + const internetGatewayInternetAddress = textDecoder.decode( + registrationASN1.internetGatewayInternetAddress.valueBlock.valueHex, + ); + if (!isValidDomain(internetGatewayInternetAddress)) { + throw new InvalidMessageError( + `Malformed Internet gateway address (${internetGatewayInternetAddress})`, + ); + } + const sessionKey = await deserializeSessionKey(registrationASN1.sessionKey); - return new PrivateNodeRegistration(privateNodeCertificate, gatewayCertificate, sessionKey); + return new PrivateNodeRegistration( + privateNodeCertificate, + gatewayCertificate, + internetGatewayInternetAddress, + sessionKey, + ); } private static readonly SCHEMA = makeHeterogeneousSequenceSchema('PrivateNodeRegistration', [ new Primitive({ name: 'privateNodeCertificate' }), new Primitive({ name: 'gatewayCertificate' }), + new Primitive({ name: 'internetGatewayInternetAddress' }), new Constructed({ name: 'sessionKey', optional: true, value: [ - new Primitive({ idBlock: { tagClass: 3, tagNumber: 0 } } as any), - new Primitive({ idBlock: { tagClass: 3, tagNumber: 1 } } as any), + new Primitive({ idBlock: { tagClass: 3, tagNumber: 0 } }), + new Primitive({ idBlock: { tagClass: 3, tagNumber: 1 } }), ], - } as any), + }), ]); constructor( public readonly privateNodeCertificate: Certificate, public readonly gatewayCertificate: Certificate, + public readonly internetGatewayInternetAddress: string, public readonly sessionKey: SessionKey | null = null, ) {} @@ -70,6 +89,7 @@ export class PrivateNodeRegistration { return makeImplicitlyTaggedSequence( new OctetString({ valueHex: this.privateNodeCertificate.serialize() }), new OctetString({ valueHex: this.gatewayCertificate.serialize() }), + new VisibleString({ value: this.internetGatewayInternetAddress }), ...(sessionKeySequence ? [sessionKeySequence] : []), ).toBER(); } @@ -86,7 +106,9 @@ async function deserializeSessionKey(sessionKeySequence: any): Promise { const expiryDate = moment().millisecond(0).add(1, 'days').toDate(); const gatewayData = arrayBufferFrom('This is the gateway data'); - // tslint:disable-next-line:no-let let gatewayKeyPair: CryptoKeyPair; beforeAll(async () => { gatewayKeyPair = await generateRSAKeyPair(); diff --git a/src/lib/bindings/gsc/PrivateNodeRegistrationRequest.spec.ts b/src/lib/bindings/gsc/PrivateNodeRegistrationRequest.spec.ts index 89fdb620d..4c341ba8d 100644 --- a/src/lib/bindings/gsc/PrivateNodeRegistrationRequest.spec.ts +++ b/src/lib/bindings/gsc/PrivateNodeRegistrationRequest.spec.ts @@ -4,7 +4,7 @@ import { generateRSAKeyPair, PrivateNodeRegistrationRequest, } from '../../../index'; -import { arrayBufferFrom, getAsn1SequenceItem } from '../../_test_utils'; +import { arrayBufferFrom, getPrimitiveItemFromConstructed } from '../../_test_utils'; import { makeImplicitlyTaggedSequence } from '../../asn1'; import { derDeserialize } from '../../crypto_wrappers/_utils'; import { verify } from '../../crypto_wrappers/rsaSigning'; @@ -27,7 +27,7 @@ describe('serialize', () => { const serialization = await request.serialize(privateNodeKeyPair.privateKey); const sequence = derDeserialize(serialization); - expect(getAsn1SequenceItem(sequence, 0)).toHaveProperty( + expect(getPrimitiveItemFromConstructed(sequence, 0)).toHaveProperty( 'valueBlock.valueHex', arrayBufferFrom(await derSerializePublicKey(privateNodeKeyPair.publicKey)), ); @@ -42,7 +42,7 @@ describe('serialize', () => { const serialization = await request.serialize(privateNodeKeyPair.privateKey); const sequence = derDeserialize(serialization); - expect(getAsn1SequenceItem(sequence, 1)).toHaveProperty( + expect(getPrimitiveItemFromConstructed(sequence, 1)).toHaveProperty( 'valueBlock.valueHex', authorizationSerialized, ); @@ -57,7 +57,7 @@ describe('serialize', () => { const serialization = await request.serialize(privateNodeKeyPair.privateKey); const sequence = derDeserialize(serialization); - const signature = getAsn1SequenceItem(sequence, 2).valueBlock.valueHex; + const signature = getPrimitiveItemFromConstructed(sequence, 2).valueBlock.valueHex; const expectedPNRACountersignature = makeImplicitlyTaggedSequence( new ObjectIdentifier({ value: RELAYNET_OIDS.NODE_REGISTRATION.AUTHORIZATION_COUNTERSIGNATURE, diff --git a/src/lib/crypto_wrappers/cms/signedData.ts b/src/lib/crypto_wrappers/cms/signedData.ts index b2dab725e..07821392e 100644 --- a/src/lib/crypto_wrappers/cms/signedData.ts +++ b/src/lib/crypto_wrappers/cms/signedData.ts @@ -109,7 +109,6 @@ export class SignedData { public static deserialize(signedDataSerialized: ArrayBuffer): SignedData { const contentInfo = deserializeContentInfo(signedDataSerialized); - // tslint:disable-next-line:no-let let pkijsSignedData: pkijs.SignedData; try { pkijsSignedData = new pkijs.SignedData({ schema: contentInfo.content }); diff --git a/src/lib/crypto_wrappers/keys.spec.ts b/src/lib/crypto_wrappers/keys.spec.ts index df7825d83..282c003d1 100644 --- a/src/lib/crypto_wrappers/keys.spec.ts +++ b/src/lib/crypto_wrappers/keys.spec.ts @@ -13,7 +13,7 @@ import { derSerializePublicKey, generateECDHKeyPair, generateRSAKeyPair, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, getPublicKeyDigest, getPublicKeyDigestHex, getRSAPublicKeyFromPrivate, @@ -426,19 +426,19 @@ test('getPublicKeyDigestHex should return the SHA-256 hex digest of the public k expect(digestHex).toEqual(sha256Hex(await derSerializePublicKey(keyPair.publicKey))); }); -describe('getPrivateAddressFromIdentityKey', () => { - test('Private address should be computed from identity key', async () => { +describe('getIdFromIdentityKey', () => { + test('Id should be computed from identity key', async () => { const keyPair = await generateRSAKeyPair(); - const privateAddress = await getPrivateAddressFromIdentityKey(keyPair.publicKey); + const id = await getIdFromIdentityKey(keyPair.publicKey); - expect(privateAddress).toEqual('0' + sha256Hex(await derSerializePublicKey(keyPair.publicKey))); + expect(id).toEqual('0' + sha256Hex(await derSerializePublicKey(keyPair.publicKey))); }); test('DH keys should be refused', async () => { const keyPair = await generateECDHKeyPair(); - await expect(getPrivateAddressFromIdentityKey(keyPair.publicKey)).rejects.toThrowWithMessage( + await expect(getIdFromIdentityKey(keyPair.publicKey)).rejects.toThrowWithMessage( Error, 'Only RSA keys are supported (got ECDH)', ); diff --git a/src/lib/crypto_wrappers/keys.ts b/src/lib/crypto_wrappers/keys.ts index f8ca4b62f..8ee2bb35d 100644 --- a/src/lib/crypto_wrappers/keys.ts +++ b/src/lib/crypto_wrappers/keys.ts @@ -183,9 +183,7 @@ export async function getPublicKeyDigestHex(publicKey: CryptoKey): Promise { +export async function getIdFromIdentityKey(identityPublicKey: CryptoKey): Promise { const algorithmName = identityPublicKey.algorithm.name; if (!algorithmName.startsWith('RSA-')) { throw new Error(`Only RSA keys are supported (got ${algorithmName})`); diff --git a/src/lib/crypto_wrappers/rsaSigning.spec.ts b/src/lib/crypto_wrappers/rsaSigning.spec.ts index f7c43f2d8..8500f9ad5 100644 --- a/src/lib/crypto_wrappers/rsaSigning.spec.ts +++ b/src/lib/crypto_wrappers/rsaSigning.spec.ts @@ -9,7 +9,6 @@ const plaintext = arrayBufferFrom('the plaintext'); const pkijsCrypto = utils.getPkijsCrypto(); -// tslint:disable-next-line:no-let let keyPair: CryptoKeyPair; beforeAll(async () => { keyPair = await generateRSAKeyPair(); diff --git a/src/lib/crypto_wrappers/x509/Certificate.spec.ts b/src/lib/crypto_wrappers/x509/Certificate.spec.ts index 475ab9986..5a599ddd5 100644 --- a/src/lib/crypto_wrappers/x509/Certificate.spec.ts +++ b/src/lib/crypto_wrappers/x509/Certificate.spec.ts @@ -7,11 +7,7 @@ import * as pkijs from 'pkijs'; import { generateStubCert, reSerializeCertificate, sha256Hex } from '../../_test_utils'; import * as oids from '../../oids'; import { derDeserialize, getPkijsCrypto } from '../_utils'; -import { - derSerializePublicKey, - generateRSAKeyPair, - getPrivateAddressFromIdentityKey, -} from '../keys'; +import { derSerializePublicKey, generateRSAKeyPair, getIdFromIdentityKey } from '../keys'; import { RsaPssPrivateKey } from '../PrivateKey'; import { MockRsaPssProvider } from '../webcrypto/_test_utils'; import { getEngineForPrivateKey } from '../webcrypto/engine'; @@ -655,7 +651,7 @@ describe('getCommonName()', () => { }); }); -describe('calculateSubjectPrivateAddress', () => { +describe('calculateSubjectId', () => { test('Private node address should be returned', async () => { const nodeKeyPair = await generateRSAKeyPair(); const nodeCertificate = await generateStubCert({ @@ -663,8 +659,8 @@ describe('calculateSubjectPrivateAddress', () => { subjectPublicKey: nodeKeyPair.publicKey, }); - await expect(nodeCertificate.calculateSubjectPrivateAddress()).resolves.toEqual( - await getPrivateAddressFromIdentityKey(nodeKeyPair.publicKey), + await expect(nodeCertificate.calculateSubjectId()).resolves.toEqual( + await getIdFromIdentityKey(nodeKeyPair.publicKey), ); }); @@ -676,20 +672,20 @@ describe('calculateSubjectPrivateAddress', () => { }); const getPublicKeySpy = jest.spyOn(nodeCertificate, 'getPublicKey'); - const address = await nodeCertificate.calculateSubjectPrivateAddress(); - await expect(nodeCertificate.calculateSubjectPrivateAddress()).resolves.toEqual(address); + const address = await nodeCertificate.calculateSubjectId(); + await expect(nodeCertificate.calculateSubjectId()).resolves.toEqual(address); expect(getPublicKeySpy).toBeCalledTimes(1); }); }); -describe('getIssuerPrivateAddress', () => { +describe('getIssuerId', () => { test('Nothing should be output if there are no extensions', async () => { const certificate = await generateStubCert({}); // tslint:disable-next-line:no-delete no-object-mutation delete certificate.pkijsCertificate.extensions; - expect(certificate.getIssuerPrivateAddress()).toBeNull(); + expect(certificate.getIssuerId()).toBeNull(); }); test('Nothing should be output if extension is missing', async () => { @@ -699,18 +695,16 @@ describe('getIssuerPrivateAddress', () => { (e) => e.extnID !== oids.AUTHORITY_KEY, ); - expect(certificate.getIssuerPrivateAddress()).toBeNull(); + expect(certificate.getIssuerId()).toBeNull(); }); - test('Private address of issuer should be output if extension is present', async () => { + test('Issuer id should be output if extension is present', async () => { const certificate = await generateStubCert({ issuerCertificate, issuerPrivateKey: issuerKeyPair.privateKey, }); - expect(certificate.getIssuerPrivateAddress()).toEqual( - await issuerCertificate.calculateSubjectPrivateAddress(), - ); + expect(certificate.getIssuerId()).toEqual(await issuerCertificate.calculateSubjectId()); }); }); diff --git a/src/lib/crypto_wrappers/x509/Certificate.ts b/src/lib/crypto_wrappers/x509/Certificate.ts index 591e8e98c..58fa6a7db 100644 --- a/src/lib/crypto_wrappers/x509/Certificate.ts +++ b/src/lib/crypto_wrappers/x509/Certificate.ts @@ -4,7 +4,7 @@ import * as pkijs from 'pkijs'; import * as oids from '../../oids'; import { derDeserialize, generateRandom64BitValue } from '../_utils'; -import { getPrivateAddressFromIdentityKey, getPublicKeyDigest } from '../keys'; +import { getIdFromIdentityKey, getPublicKeyDigest } from '../keys'; import { getEngineForPrivateKey } from '../webcrypto/engine'; import CertificateError from './CertificateError'; import FullCertificateIssuanceOptions from './FullCertificateIssuanceOptions'; @@ -121,7 +121,7 @@ export default class Certificate { public readonly pkijsCertificate: pkijs.Certificate; // tslint:disable-next-line:readonly-keyword - protected privateAddressCache: string | null = null; + protected subjectIdCache: string | null = null; /** * @internal @@ -199,15 +199,15 @@ export default class Certificate { } } - public async calculateSubjectPrivateAddress(): Promise { - if (!this.privateAddressCache) { + public async calculateSubjectId(): Promise { + if (!this.subjectIdCache) { // tslint:disable-next-line:no-object-mutation - this.privateAddressCache = await getPrivateAddressFromIdentityKey(await this.getPublicKey()); + this.subjectIdCache = await getIdFromIdentityKey(await this.getPublicKey()); } - return this.privateAddressCache; + return this.subjectIdCache; } - public getIssuerPrivateAddress(): string | null { + public getIssuerId(): string | null { const authorityKeyAttribute = this.pkijsCertificate.extensions?.find( (attr) => attr.extnID === oids.AUTHORITY_KEY, ); diff --git a/src/lib/publicAddressing.spec.ts b/src/lib/internetAddressing.spec.ts similarity index 76% rename from src/lib/publicAddressing.spec.ts rename to src/lib/internetAddressing.spec.ts index ae1a1deef..ee27d2e8e 100644 --- a/src/lib/publicAddressing.spec.ts +++ b/src/lib/internetAddressing.spec.ts @@ -28,16 +28,16 @@ jest.mock('dohdec', () => ({ })); import { BindingType, - PublicAddressingError, - resolvePublicAddress, + InternetAddressingError, + resolveInternetAddress, UnreachableResolverError, -} from './publicAddressing'; +} from './internetAddressing'; -describe('resolvePublicAddress', () => { - const MALFORMED_ANSWER_ERROR = new PublicAddressingError('DNS answer is malformed'); +describe('resolveInternetAddress', () => { + const MALFORMED_ANSWER_ERROR = new InternetAddressingError('DNS answer is malformed'); test('DNS resolution should be skipped if host name contains port', async () => { - const address = await resolvePublicAddress(`${HOST}:${TARGET_PORT}`, BindingType.PDC); + const address = await resolveInternetAddress(`${HOST}:${TARGET_PORT}`, BindingType.PDC); expect(address).toHaveProperty('host', HOST); expect(address).toHaveProperty('port', TARGET_PORT); @@ -45,7 +45,7 @@ describe('resolvePublicAddress', () => { }); test('Specified domain name should be requested', async () => { - await resolvePublicAddress(HOST, BindingType.PDC); + await resolveInternetAddress(HOST, BindingType.PDC); expect(mockGetDNS).toBeCalledWith( expect.objectContaining({ name: `_${BindingType.PDC}._tcp.${HOST}` }), @@ -53,13 +53,13 @@ describe('resolvePublicAddress', () => { }); test('DNSSEC verification should be requested', async () => { - await resolvePublicAddress(HOST, BindingType.PDC); + await resolveInternetAddress(HOST, BindingType.PDC); expect(mockGetDNS).toBeCalledWith(expect.objectContaining({ dnssec: true })); }); test('SRV record should be requested', async () => { - await resolvePublicAddress(HOST, BindingType.PDC); + await resolveInternetAddress(HOST, BindingType.PDC); expect(mockGetDNS).toBeCalledWith( expect.objectContaining({ name: HOST_SRV_NAME, rrtype: 'SRV' }), @@ -67,7 +67,7 @@ describe('resolvePublicAddress', () => { }); test('CloudFlare resolver should be used by default', async () => { - await resolvePublicAddress(HOST, BindingType.PDC); + await resolveInternetAddress(HOST, BindingType.PDC); expect(mockDOH).toBeCalledWith( expect.objectContaining({ url: 'https://cloudflare-dns.com/dns-query' }), @@ -77,7 +77,7 @@ describe('resolvePublicAddress', () => { test('DNS resolver should be customizable', async () => { const resolverURL = 'https://dns.example.com/dns-query'; - await resolvePublicAddress(HOST, BindingType.PDC, resolverURL); + await resolveInternetAddress(HOST, BindingType.PDC, resolverURL); expect(mockDOH).toBeCalledWith(expect.objectContaining({ url: resolverURL })); }); @@ -85,22 +85,22 @@ describe('resolvePublicAddress', () => { test('Null should be returned if domain does not exist', async () => { mockGetDNS.mockReturnValue({ ...SUCCESSFUL_RESPONSE, rcode: 'NXDOMAIN' }); - await expect(resolvePublicAddress(HOST, BindingType.PDC)).resolves.toBeNull(); + await expect(resolveInternetAddress(HOST, BindingType.PDC)).resolves.toBeNull(); }); test('An error should be thrown if DNS lookup status is not NOERROR', async () => { const status = 'SERVFAIL'; mockGetDNS.mockReturnValue({ ...SUCCESSFUL_RESPONSE, rcode: status }); - await expect(resolvePublicAddress(HOST, BindingType.PDC)).rejects.toEqual( - new PublicAddressingError(`SRV lookup for ${HOST_SRV_NAME} failed with status ${status}`), + await expect(resolveInternetAddress(HOST, BindingType.PDC)).rejects.toEqual( + new InternetAddressingError(`SRV lookup for ${HOST_SRV_NAME} failed with status ${status}`), ); }); test('An error should be thrown if the Answer is empty', async () => { mockGetDNS.mockReturnValue({ ...SUCCESSFUL_RESPONSE, answers: [] }); - await expect(resolvePublicAddress(HOST, BindingType.PDC)).rejects.toEqual( + await expect(resolveInternetAddress(HOST, BindingType.PDC)).rejects.toEqual( MALFORMED_ANSWER_ERROR, ); }); @@ -116,7 +116,7 @@ describe('resolvePublicAddress', () => { ], }); - await expect(resolvePublicAddress(HOST, BindingType.PDC)).rejects.toEqual( + await expect(resolveInternetAddress(HOST, BindingType.PDC)).rejects.toEqual( MALFORMED_ANSWER_ERROR, ); }); @@ -132,7 +132,7 @@ describe('resolvePublicAddress', () => { ], }); - await expect(resolvePublicAddress(HOST, BindingType.PDC)).rejects.toEqual( + await expect(resolveInternetAddress(HOST, BindingType.PDC)).rejects.toEqual( MALFORMED_ANSWER_ERROR, ); }); @@ -148,7 +148,7 @@ describe('resolvePublicAddress', () => { ], }); - await expect(resolvePublicAddress(HOST, BindingType.PDC)).rejects.toEqual( + await expect(resolveInternetAddress(HOST, BindingType.PDC)).rejects.toEqual( MALFORMED_ANSWER_ERROR, ); }); @@ -159,7 +159,7 @@ describe('resolvePublicAddress', () => { (networkError as any).errno = 'ENOTFOUND'; mockGetDNS.mockRejectedValue(networkError); - const error = await getPromiseRejection(resolvePublicAddress(HOST, BindingType.PDC)); + const error = await getPromiseRejection(resolveInternetAddress(HOST, BindingType.PDC)); expect(error).toBeInstanceOf(UnreachableResolverError); expect(error.message).toMatch(/^Failed to reach DoH resolver:/); @@ -170,21 +170,21 @@ describe('resolvePublicAddress', () => { const dnsLookupError = new Error('This is unexpected'); mockGetDNS.mockRejectedValue(dnsLookupError); - await expect(resolvePublicAddress(HOST, BindingType.PDC)).rejects.toBe(dnsLookupError); + await expect(resolveInternetAddress(HOST, BindingType.PDC)).rejects.toBe(dnsLookupError); }); test('An error should be thrown if DNSSEC verification fails', async () => { mockGetDNS.mockResolvedValue({ ...SUCCESSFUL_RESPONSE, flag_ad: false }); - await expect(resolvePublicAddress(HOST, BindingType.PDC)).rejects.toEqual( - new PublicAddressingError( + await expect(resolveInternetAddress(HOST, BindingType.PDC)).rejects.toEqual( + new InternetAddressingError( `DNSSEC verification for SRV _${BindingType.PDC}._tcp.${HOST} failed`, ), ); }); test('Address should be returned if record exists and is valid', async () => { - const address = await resolvePublicAddress(HOST, BindingType.PDC); + const address = await resolveInternetAddress(HOST, BindingType.PDC); expect(address).toHaveProperty('host', TARGET_HOST); expect(address).toHaveProperty('port', TARGET_PORT); @@ -196,7 +196,7 @@ describe('resolvePublicAddress', () => { answers: [{ type: 'RRSIG' }, ...SUCCESSFUL_RESPONSE.answers], }); - const address = await resolvePublicAddress(HOST, BindingType.PDC); + const address = await resolveInternetAddress(HOST, BindingType.PDC); expect(address).toHaveProperty('host', TARGET_HOST); expect(address).toHaveProperty('port', TARGET_PORT); @@ -213,7 +213,7 @@ describe('resolvePublicAddress', () => { ], }); - const address = await resolvePublicAddress(HOST, BindingType.PDC); + const address = await resolveInternetAddress(HOST, BindingType.PDC); expect(address).toHaveProperty('host', TARGET_HOST); }); diff --git a/src/lib/publicAddressing.ts b/src/lib/internetAddressing.ts similarity index 82% rename from src/lib/publicAddressing.ts rename to src/lib/internetAddressing.ts index d32f27b21..db22df026 100644 --- a/src/lib/publicAddressing.ts +++ b/src/lib/internetAddressing.ts @@ -17,7 +17,7 @@ export enum BindingType { PDC = 'awala-pdc', } -export class PublicAddressingError extends RelaynetError {} +export class InternetAddressingError extends RelaynetError {} export class UnreachableResolverError extends RelaynetError {} @@ -27,8 +27,8 @@ export class UnreachableResolverError extends RelaynetError {} * @param hostName The host name to look up * @param bindingType The SRV service to look up * @param resolverURL The URL for the DNS-over-HTTPS resolver - * @throws PublicAddressingError If DNSSEC verification failed - * @throws UnreachableResolverError If the DNS resolver was unreachable + * @throws {InternetAddressingError} If DNSSEC verification failed + * @throws {UnreachableResolverError} If the DNS resolver was unreachable * * `null` is returned when `hostName` is an IP address or a non-existing SRV record for the service * in `bindingType`. @@ -38,7 +38,7 @@ export class UnreachableResolverError extends RelaynetError {} * * DNS resolution is done with DNS-over-HTTPS. */ -export async function resolvePublicAddress( +export async function resolveInternetAddress( hostName: string, bindingType: BindingType, resolverURL = CLOUDFLARE_RESOLVER_URL, @@ -64,16 +64,16 @@ export async function resolvePublicAddress( return null; } if (result.rcode !== 'NOERROR') { - throw new PublicAddressingError(`SRV lookup for ${name} failed with status ${result.rcode}`); + throw new InternetAddressingError(`SRV lookup for ${name} failed with status ${result.rcode}`); } if (!result.flag_ad) { - throw new PublicAddressingError(`DNSSEC verification for SRV ${name} failed`); + throw new InternetAddressingError(`DNSSEC verification for SRV ${name} failed`); } const srvAnswers = result.answers.filter((a) => a.type === 'SRV'); // TODO: Pick the best answer based on its weight and priority fields const answer = srvAnswers[0]; if (!answer || !answer.data || !answer.data.target || !answer.data.port) { - throw new PublicAddressingError('DNS answer is malformed'); + throw new InternetAddressingError('DNS answer is malformed'); } return { host: removeTrailingDot(answer.data.target), port: answer.data.port }; } diff --git a/src/lib/keyStores/CertificateStore.spec.ts b/src/lib/keyStores/CertificateStore.spec.ts index 18d81f7d9..e65c55043 100644 --- a/src/lib/keyStores/CertificateStore.spec.ts +++ b/src/lib/keyStores/CertificateStore.spec.ts @@ -1,7 +1,7 @@ import { addSeconds, setMilliseconds, subSeconds } from 'date-fns'; import { expectArrayBuffersToEqual } from '../_test_utils'; -import { generateRSAKeyPair, getPrivateAddressFromIdentityKey } from '../crypto_wrappers/keys'; +import { generateRSAKeyPair, getIdFromIdentityKey } from '../crypto_wrappers/keys'; import Certificate from '../crypto_wrappers/x509/Certificate'; import { CertificationPath } from '../pki/CertificationPath'; import { issueGatewayCertificate } from '../pki/issuance'; @@ -12,15 +12,15 @@ beforeEach(() => { store.clear(); }); -let issuerPrivateAddress: string; +let issuerId: string; let issuerCertificate: Certificate; let subjectKeyPair: CryptoKeyPair; -let subjectPrivateAddress: string; +let subjectId: string; let subjectCertificate: Certificate; let certificationPath: CertificationPath; beforeAll(async () => { const issuerKeyPair = await generateRSAKeyPair(); - issuerPrivateAddress = await getPrivateAddressFromIdentityKey(issuerKeyPair.publicKey); + issuerId = await getIdFromIdentityKey(issuerKeyPair.publicKey); issuerCertificate = await issueGatewayCertificate({ subjectPublicKey: issuerKeyPair.publicKey, issuerPrivateKey: issuerKeyPair.privateKey, @@ -28,7 +28,7 @@ beforeAll(async () => { }); subjectKeyPair = await generateRSAKeyPair(); - subjectPrivateAddress = await getPrivateAddressFromIdentityKey(subjectKeyPair.publicKey); + subjectId = await getIdFromIdentityKey(subjectKeyPair.publicKey); subjectCertificate = await issueGatewayCertificate({ issuerCertificate, issuerPrivateKey: issuerKeyPair.privateKey, @@ -43,84 +43,64 @@ describe('save', () => { test('Expired certificate should not be saved', async () => { const certificate = await generateSubjectCertificate(subSeconds(new Date(), 1)); - await store.save( - new CertificationPath(certificate, [issuerCertificate]), - subjectPrivateAddress, - ); + await store.save(new CertificationPath(certificate, [issuerCertificate]), subjectId); - expect(store.dataByPrivateAddress).toBeEmpty(); + expect(store.dataBySubjectId).toBeEmpty(); }); test('Certification path should be stored', async () => { - await store.save(certificationPath, issuerPrivateAddress); + await store.save(certificationPath, issuerId); - expect(store.dataByPrivateAddress).toHaveProperty(subjectPrivateAddress); - const serialization = store.dataByPrivateAddress[subjectPrivateAddress][0].serialization; + expect(store.dataBySubjectId).toHaveProperty(subjectId); + const serialization = store.dataBySubjectId[subjectId][0].serialization; expectArrayBuffersToEqual(certificationPath.serialize(), serialization); }); test('Expiry date should be taken from certificate', async () => { - await store.save(certificationPath, issuerPrivateAddress); + await store.save(certificationPath, issuerId); - expect(store.dataByPrivateAddress).toHaveProperty(subjectPrivateAddress); - expect(store.dataByPrivateAddress[subjectPrivateAddress][0].expiryDate).toEqual( + expect(store.dataBySubjectId).toHaveProperty(subjectId); + expect(store.dataBySubjectId[subjectId][0].expiryDate).toEqual( setMilliseconds(subjectCertificate.expiryDate, 0), ); }); - test('Specified issuer private address should be honoured', async () => { - const differentIssuerPrivateAddress = `not-${subjectPrivateAddress}`; + test('Specified issuer id should be honoured', async () => { + const differentIssuerId = `not-${subjectId}`; - await store.save(certificationPath, differentIssuerPrivateAddress); + await store.save(certificationPath, differentIssuerId); - expect(store.dataByPrivateAddress).toHaveProperty(subjectPrivateAddress); - expect(store.dataByPrivateAddress[subjectPrivateAddress][0].issuerPrivateAddress).toEqual( - differentIssuerPrivateAddress, - ); + expect(store.dataBySubjectId).toHaveProperty(subjectId); + expect(store.dataBySubjectId[subjectId][0].issuerId).toEqual(differentIssuerId); }); }); describe('retrieveLatest', () => { test('Nothing should be returned if certificate does not exist', async () => { - await expect( - store.retrieveLatest(subjectPrivateAddress, issuerPrivateAddress), - ).resolves.toBeNull(); + await expect(store.retrieveLatest(subjectId, issuerId)).resolves.toBeNull(); }); test('Expired certificate should be ignored', async () => { const expiredCertificate = await generateSubjectCertificate(subSeconds(new Date(), 1)); - await store.forceSave( - new CertificationPath(expiredCertificate, [issuerCertificate]), - issuerPrivateAddress, - ); + await store.forceSave(new CertificationPath(expiredCertificate, [issuerCertificate]), issuerId); - await expect( - store.retrieveLatest(subjectPrivateAddress, issuerPrivateAddress), - ).resolves.toBeNull(); + await expect(store.retrieveLatest(subjectId, issuerId)).resolves.toBeNull(); }); test('Certificates from another issuer should be ignored', async () => { - await store.save(certificationPath, `not-${issuerPrivateAddress}`); + await store.save(certificationPath, `not-${issuerId}`); - await expect( - store.retrieveLatest(subjectPrivateAddress, issuerPrivateAddress), - ).resolves.toBeNull(); + await expect(store.retrieveLatest(subjectId, issuerId)).resolves.toBeNull(); }); test('Latest path should be returned', async () => { const now = new Date(); const olderCertificate = await generateSubjectCertificate(addSeconds(now, 5)); - await store.save( - new CertificationPath(olderCertificate, [issuerCertificate]), - issuerPrivateAddress, - ); + await store.save(new CertificationPath(olderCertificate, [issuerCertificate]), issuerId); const newerCertificate = await generateSubjectCertificate(addSeconds(now, 10)); - await store.save( - new CertificationPath(newerCertificate, [issuerCertificate]), - issuerPrivateAddress, - ); + await store.save(new CertificationPath(newerCertificate, [issuerCertificate]), issuerId); - const path = await store.retrieveLatest(subjectPrivateAddress, issuerPrivateAddress); + const path = await store.retrieveLatest(subjectId, issuerId); expect(path!.leafCertificate.isEqual(newerCertificate)).toBeTrue(); expect(path!.certificateAuthorities).toHaveLength(1); @@ -130,24 +110,16 @@ describe('retrieveLatest', () => { describe('retrieveAll', () => { test('Nothing should be returned if no certificate exists', async () => { - await expect( - store.retrieveAll(subjectPrivateAddress, issuerPrivateAddress), - ).resolves.toBeEmpty(); + await expect(store.retrieveAll(subjectId, issuerId)).resolves.toBeEmpty(); }); test('Expired certificates should be ignored', async () => { const validCertificate = await generateSubjectCertificate(addSeconds(new Date(), 3)); - await store.save( - new CertificationPath(validCertificate, [issuerCertificate]), - issuerPrivateAddress, - ); + await store.save(new CertificationPath(validCertificate, [issuerCertificate]), issuerId); const expiredCertificate = await generateSubjectCertificate(subSeconds(new Date(), 1)); - await store.forceSave( - new CertificationPath(expiredCertificate, [issuerCertificate]), - issuerPrivateAddress, - ); + await store.forceSave(new CertificationPath(expiredCertificate, [issuerCertificate]), issuerId); - const allPaths = await store.retrieveAll(subjectPrivateAddress, issuerPrivateAddress); + const allPaths = await store.retrieveAll(subjectId, issuerId); expect(allPaths).toHaveLength(1); expect(validCertificate.isEqual(allPaths[0].leafCertificate)).toBeTrue(); @@ -155,17 +127,11 @@ describe('retrieveAll', () => { test('All valid certificates should be returned', async () => { const certificate1 = await generateSubjectCertificate(addSeconds(new Date(), 3)); - await store.save( - new CertificationPath(certificate1, [issuerCertificate]), - issuerPrivateAddress, - ); + await store.save(new CertificationPath(certificate1, [issuerCertificate]), issuerId); const certificate2 = await generateSubjectCertificate(addSeconds(new Date(), 5)); - await store.save( - new CertificationPath(certificate2, [issuerCertificate]), - issuerPrivateAddress, - ); + await store.save(new CertificationPath(certificate2, [issuerCertificate]), issuerId); - const allCertificates = await store.retrieveAll(subjectPrivateAddress, issuerPrivateAddress); + const allCertificates = await store.retrieveAll(subjectId, issuerId); expect(allCertificates).toHaveLength(2); expect(allCertificates.filter((p) => certificate1.isEqual(p.leafCertificate))).toHaveLength(1); @@ -178,11 +144,9 @@ describe('retrieveAll', () => { test('Certificates from another issuer should be ignored', async () => { const certificate = await generateSubjectCertificate(addSeconds(new Date(), 3)); - await store.save(new CertificationPath(certificate, []), `not-${issuerPrivateAddress}`); + await store.save(new CertificationPath(certificate, []), `not-${issuerId}`); - await expect( - store.retrieveAll(subjectPrivateAddress, issuerPrivateAddress), - ).resolves.toBeEmpty(); + await expect(store.retrieveAll(subjectId, issuerId)).resolves.toBeEmpty(); }); }); diff --git a/src/lib/keyStores/CertificateStore.ts b/src/lib/keyStores/CertificateStore.ts index f1c9ef8d9..5ddc47cc4 100644 --- a/src/lib/keyStores/CertificateStore.ts +++ b/src/lib/keyStores/CertificateStore.ts @@ -8,32 +8,29 @@ export abstract class CertificateStore { * Store `subjectCertificate` as long as it's still valid. * * @param path - * @param issuerPrivateAddress + * @param issuerId * - * Whilst we could take the {issuerPrivateAddress} from the leaf certificate in the {path}, we + * Whilst we could take the {issuerId} from the leaf certificate in the {path}, we * must not rely on it because we don't have enough information/context here to be certain that * the value is legitimate. Additionally, the value has to be present in an X.509 extension, * which could be absent if produced by a non-compliant implementation. */ - public async save(path: CertificationPath, issuerPrivateAddress: string): Promise { + public async save(path: CertificationPath, issuerId: string): Promise { if (new Date() < path.leafCertificate.expiryDate) { await this.saveData( path.serialize(), - await path.leafCertificate.calculateSubjectPrivateAddress(), + await path.leafCertificate.calculateSubjectId(), path.leafCertificate.expiryDate, - issuerPrivateAddress, + issuerId, ); } } public async retrieveLatest( - subjectPrivateAddress: string, - issuerPrivateAddress: string, + subjectId: string, + issuerId: string, ): Promise { - const serialization = await this.retrieveLatestSerialization( - subjectPrivateAddress, - issuerPrivateAddress, - ); + const serialization = await this.retrieveLatestSerialization(subjectId, issuerId); if (!serialization) { return null; } @@ -42,13 +39,10 @@ export abstract class CertificateStore { } public async retrieveAll( - subjectPrivateAddress: string, - issuerPrivateAddress: string, + subjectId: string, + issuerId: string, ): Promise { - const allSerializations = await this.retrieveAllSerializations( - subjectPrivateAddress, - issuerPrivateAddress, - ); + const allSerializations = await this.retrieveAllSerializations(subjectId, issuerId); return allSerializations .map(CertificationPath.deserialize) .filter((p) => new Date() < p.leafCertificate.expiryDate); @@ -58,18 +52,18 @@ export abstract class CertificateStore { protected abstract saveData( serialization: ArrayBuffer, - subjectPrivateAddress: string, + subjectId: string, subjectCertificateExpiryDate: Date, - issuerPrivateAddress: string, + issuerId: string, ): Promise; protected abstract retrieveLatestSerialization( - subjectPrivateAddress: string, - issuerPrivateAddress: string, + subjectId: string, + issuerId: string, ): Promise; protected abstract retrieveAllSerializations( - subjectPrivateAddress: string, - issuerPrivateAddress: string, + subjectId: string, + issuerId: string, ): Promise; } diff --git a/src/lib/keyStores/PrivateKeyStore.spec.ts b/src/lib/keyStores/PrivateKeyStore.spec.ts index 37dc8c06b..f981a8278 100644 --- a/src/lib/keyStores/PrivateKeyStore.spec.ts +++ b/src/lib/keyStores/PrivateKeyStore.spec.ts @@ -2,7 +2,7 @@ import { HashingAlgorithm, RSAModulus } from '../crypto_wrappers/algorithms'; import { derSerializePrivateKey, derSerializePublicKey, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, } from '../crypto_wrappers/keys'; import { SessionKeyPair } from '../SessionKeyPair'; import { KeyStoreError } from './KeyStoreError'; @@ -47,12 +47,10 @@ describe('Identity keys', () => { }, ); - test('Private address should be returned', async () => { + test('Id should be returned', async () => { const keyPair = await MOCK_STORE.generateIdentityKeyPair(); - expect(keyPair.privateAddress).toEqual( - await getPrivateAddressFromIdentityKey(keyPair.publicKey), - ); + expect(keyPair.id).toEqual(await getIdFromIdentityKey(keyPair.publicKey)); }); test('Public key should correspond to private key', async () => { @@ -65,9 +63,9 @@ describe('Identity keys', () => { }); test('Key should be stored', async () => { - const { privateAddress, privateKey } = await MOCK_STORE.generateIdentityKeyPair(); + const { id, privateKey } = await MOCK_STORE.generateIdentityKeyPair(); - expect(MOCK_STORE.identityKeys).toHaveProperty(privateAddress, privateKey); + expect(MOCK_STORE.identityKeys).toHaveProperty(id, privateKey); }); test('Errors should be wrapped', async () => { @@ -89,20 +87,20 @@ describe('Session keys', () => { sessionKeyIdHex = sessionKeyPair.sessionKey.keyId.toString('hex'); }); - const PRIVATE_ADDRESS = '0deadc0de'; - const PEER_PRIVATE_ADDRESS = '0deadbeef'; + const NODE_ID = '0deadc0de'; + const PEER_ID = '0deadbeef'; describe('saveSessionKey', () => { test('Unbound key should be stored', async () => { await MOCK_STORE.saveSessionKey( sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, + NODE_ID, ); expect(MOCK_STORE.sessionKeys).toHaveProperty(sessionKeyIdHex, { keySerialized: await derSerializePrivateKey(sessionKeyPair.privateKey), - privateAddress: PRIVATE_ADDRESS, + nodeId: NODE_ID, }); }); @@ -110,14 +108,14 @@ describe('Session keys', () => { await MOCK_STORE.saveSessionKey( sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - PEER_PRIVATE_ADDRESS, + NODE_ID, + PEER_ID, ); expect(MOCK_STORE.sessionKeys).toHaveProperty(sessionKeyIdHex, { keySerialized: await derSerializePrivateKey(sessionKeyPair.privateKey), - peerPrivateAddress: PEER_PRIVATE_ADDRESS, - privateAddress: PRIVATE_ADDRESS, + peerId: PEER_ID, + nodeId: NODE_ID, }); }); @@ -125,11 +123,7 @@ describe('Session keys', () => { const store = new MockPrivateKeyStore(true); await expect( - store.saveSessionKey( - sessionKeyPair.privateKey, - sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - ), + store.saveSessionKey(sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, NODE_ID), ).rejects.toThrowWithMessage(KeyStoreError, `Failed to save key ${sessionKeyIdHex}: Denied`); }); }); @@ -139,12 +133,12 @@ describe('Session keys', () => { await MOCK_STORE.saveSessionKey( sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, + NODE_ID, ); const keySerialized = await MOCK_STORE.retrieveUnboundSessionKey( sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, + NODE_ID, ); expect(await derSerializePrivateKey(keySerialized)).toEqual( @@ -154,7 +148,7 @@ describe('Session keys', () => { test('UnknownKeyError should be thrown if key id does not exist', async () => { await expect( - MOCK_STORE.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, PRIVATE_ADDRESS), + MOCK_STORE.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, NODE_ID), ).rejects.toThrowWithMessage(UnknownKeyError, `Key ${sessionKeyIdHex} does not exist`); }); @@ -162,14 +156,11 @@ describe('Session keys', () => { await MOCK_STORE.saveSessionKey( sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, + NODE_ID, ); await expect( - MOCK_STORE.retrieveUnboundSessionKey( - sessionKeyPair.sessionKey.keyId, - `not-${PRIVATE_ADDRESS}`, - ), + MOCK_STORE.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, `not-${NODE_ID}`), ).rejects.toThrowWithMessage(UnknownKeyError, 'Key is owned by a different node'); }); @@ -177,12 +168,12 @@ describe('Session keys', () => { await MOCK_STORE.saveSessionKey( sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - PEER_PRIVATE_ADDRESS, + NODE_ID, + PEER_ID, ); await expect( - MOCK_STORE.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, PRIVATE_ADDRESS), + MOCK_STORE.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, NODE_ID), ).rejects.toThrowWithMessage(UnknownKeyError, `Key ${sessionKeyIdHex} is bound`); }); @@ -190,7 +181,7 @@ describe('Session keys', () => { const store = new MockPrivateKeyStore(false, true); await expect( - store.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, PRIVATE_ADDRESS), + store.retrieveUnboundSessionKey(sessionKeyPair.sessionKey.keyId, NODE_ID), ).rejects.toEqual(new KeyStoreError('Failed to retrieve key: Denied')); }); }); @@ -200,13 +191,13 @@ describe('Session keys', () => { await MOCK_STORE.saveSessionKey( sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, + NODE_ID, ); const privateKey = await MOCK_STORE.retrieveSessionKey( sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - PEER_PRIVATE_ADDRESS, + NODE_ID, + PEER_ID, ); expect(await derSerializePrivateKey(privateKey)).toEqual( @@ -218,14 +209,14 @@ describe('Session keys', () => { await MOCK_STORE.saveSessionKey( sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - PEER_PRIVATE_ADDRESS, + NODE_ID, + PEER_ID, ); const privateKey = await MOCK_STORE.retrieveSessionKey( sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - PEER_PRIVATE_ADDRESS, + NODE_ID, + PEER_ID, ); expect(await derSerializePrivateKey(privateKey)).toEqual( @@ -235,11 +226,7 @@ describe('Session keys', () => { test('UnknownKeyError should be thrown if key pair does not exist', async () => { await expect( - MOCK_STORE.retrieveSessionKey( - sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - PEER_PRIVATE_ADDRESS, - ), + MOCK_STORE.retrieveSessionKey(sessionKeyPair.sessionKey.keyId, NODE_ID, PEER_ID), ).rejects.toThrowWithMessage(UnknownKeyError, `Key ${sessionKeyIdHex} does not exist`); }); @@ -247,16 +234,12 @@ describe('Session keys', () => { await MOCK_STORE.saveSessionKey( sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - PEER_PRIVATE_ADDRESS, + NODE_ID, + PEER_ID, ); await expect( - MOCK_STORE.retrieveSessionKey( - sessionKeyPair.sessionKey.keyId, - `not-${PRIVATE_ADDRESS}`, - PEER_PRIVATE_ADDRESS, - ), + MOCK_STORE.retrieveSessionKey(sessionKeyPair.sessionKey.keyId, `not-${NODE_ID}`, PEER_ID), ).rejects.toThrowWithMessage(UnknownKeyError, 'Key is owned by a different node'); }); @@ -264,21 +247,17 @@ describe('Session keys', () => { await MOCK_STORE.saveSessionKey( sessionKeyPair.privateKey, sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - PEER_PRIVATE_ADDRESS, + NODE_ID, + PEER_ID, ); - const invalidPeerPrivateAddress = `not ${PEER_PRIVATE_ADDRESS}`; + const invalidPeerId = `not ${PEER_ID}`; await expect( - MOCK_STORE.retrieveSessionKey( - sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - invalidPeerPrivateAddress, - ), + MOCK_STORE.retrieveSessionKey(sessionKeyPair.sessionKey.keyId, NODE_ID, invalidPeerId), ).rejects.toThrowWithMessage( UnknownKeyError, `Session key ${sessionKeyIdHex} is bound to another recipient ` + - `(${PEER_PRIVATE_ADDRESS}, not ${invalidPeerPrivateAddress})`, + `(${PEER_ID}, not ${invalidPeerId})`, ); }); @@ -286,11 +265,7 @@ describe('Session keys', () => { const store = new MockPrivateKeyStore(false, true); await expect( - store.retrieveSessionKey( - sessionKeyPair.sessionKey.keyId, - PRIVATE_ADDRESS, - PEER_PRIVATE_ADDRESS, - ), + store.retrieveSessionKey(sessionKeyPair.sessionKey.keyId, NODE_ID, PEER_ID), ).rejects.toEqual(new KeyStoreError('Failed to retrieve key: Denied')); }); }); diff --git a/src/lib/keyStores/PrivateKeyStore.ts b/src/lib/keyStores/PrivateKeyStore.ts index 240364ad6..dda667107 100644 --- a/src/lib/keyStores/PrivateKeyStore.ts +++ b/src/lib/keyStores/PrivateKeyStore.ts @@ -4,7 +4,7 @@ import { derDeserializeECDHPrivateKey, derSerializePrivateKey, generateRSAKeyPair, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, RSAKeyGenOptions, } from '../crypto_wrappers/keys'; import { IdentityKeyPair } from '../IdentityKeyPair'; @@ -16,8 +16,8 @@ import UnknownKeyError from './UnknownKeyError'; */ export interface SessionPrivateKeyData { readonly keySerialized: Buffer; - readonly privateAddress: string; - readonly peerPrivateAddress?: string; + readonly nodeId: string; + readonly peerId?: string; } export abstract class PrivateKeyStore { @@ -27,22 +27,22 @@ export abstract class PrivateKeyStore { keyOptions: Partial = {}, ): Promise { const keyPair = await this.generateRSAKeyPair(keyOptions); - const privateAddress = await getPrivateAddressFromIdentityKey(keyPair.publicKey); + const id = await getIdFromIdentityKey(keyPair.publicKey); try { - await this.saveIdentityKey(privateAddress, keyPair.privateKey); + await this.saveIdentityKey(id, keyPair.privateKey); } catch (err) { - throw new KeyStoreError(err as Error, `Failed to save key for ${privateAddress}`); + throw new KeyStoreError(err as Error, `Failed to save key for ${id}`); } - return { ...keyPair, privateAddress }; + return { ...keyPair, id }; } /** * Return the private component of a node key pair if it exists. * - * @param privateAddress + * @param nodeId * @throws {KeyStoreError} if the backend failed to retrieve the key due to an error */ - public abstract retrieveIdentityKey(privateAddress: string): Promise; + public abstract retrieveIdentityKey(nodeId: string): Promise; //endregion //region Session keys @@ -50,18 +50,13 @@ export abstract class PrivateKeyStore { public async saveSessionKey( privateKey: CryptoKey, keyId: Buffer, - privateAddress: string, - peerPrivateAddress?: string, + nodeId: string, + peerId?: string, ): Promise { const keyIdString = keyId.toString('hex'); const privateKeyDer = await derSerializePrivateKey(privateKey); try { - await this.saveSessionKeySerialized( - keyIdString, - privateKeyDer, - privateAddress, - peerPrivateAddress, - ); + await this.saveSessionKeySerialized(keyIdString, privateKeyDer, nodeId, peerId); } catch (error) { throw new KeyStoreError(error as Error, `Failed to save key ${keyIdString}`); } @@ -71,17 +66,14 @@ export abstract class PrivateKeyStore { * Return the private component of an initial session key pair. * * @param keyId The key pair id (typically the serial number) - * @param privateAddress The private address of the node that owns the key + * @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 + * @throws PrivateKeyStoreError when the look-up could not be done */ - public async retrieveUnboundSessionKey( - keyId: Buffer, - privateAddress: string, - ): Promise { - const keyData = await this.retrieveSessionKeyDataOrThrowError(keyId, privateAddress); + public async retrieveUnboundSessionKey(keyId: Buffer, nodeId: string): Promise { + const keyData = await this.retrieveSessionKeyDataOrThrowError(keyId, nodeId); - if (keyData.peerPrivateAddress) { + if (keyData.peerId) { throw new UnknownKeyError(`Key ${keyId.toString('hex')} is bound`); } @@ -92,24 +84,24 @@ export abstract class PrivateKeyStore { * Retrieve private session key, regardless of whether it's an initial key or not. * * @param keyId The key pair id (typically the serial number) - * @param privateAddress The private address of the node that owns the key - * @param peerPrivateAddress The private address of the recipient, in case the key is bound to + * @param nodeId The id of the node that owns the key + * @param peerId The id of the recipient, in case the key is bound to * a recipient * @throws UnknownKeyError when the key does not exist - * @throws PrivateKeyStoreError when the look up could not be done + * @throws PrivateKeyStoreError when the look-up could not be done */ public async retrieveSessionKey( keyId: Buffer, - privateAddress: string, - peerPrivateAddress: string, + nodeId: string, + peerId: string, ): Promise { - const keyData = await this.retrieveSessionKeyDataOrThrowError(keyId, privateAddress); + const keyData = await this.retrieveSessionKeyDataOrThrowError(keyId, nodeId); const keyIdHex = keyId.toString('hex'); - if (keyData.peerPrivateAddress && peerPrivateAddress !== keyData.peerPrivateAddress) { + if (keyData.peerId && peerId !== keyData.peerId) { throw new UnknownKeyError( `Session key ${keyIdHex} is bound to another recipient ` + - `(${keyData.peerPrivateAddress}, not ${peerPrivateAddress})`, + `(${keyData.peerId}, not ${peerId})`, ); } @@ -118,19 +110,19 @@ export abstract class PrivateKeyStore { //endregion - public abstract saveIdentityKey(privateAddress: string, privateKey: CryptoKey): Promise; + public abstract saveIdentityKey(nodeId: string, privateKey: CryptoKey): Promise; protected abstract saveSessionKeySerialized( keyId: string, keySerialized: Buffer, - privateAddress: string, - peerPrivateAddress?: string, + nodeId: string, + peerId?: string, ): Promise; protected abstract retrieveSessionKeyData(keyId: string): Promise; private async retrieveSessionKeyDataOrThrowError( keyId: Buffer, - privateAddress: string, + nodeId: string, ): Promise { const keyIdHex = keyId.toString('hex'); let keyData: SessionPrivateKeyData | null; @@ -142,7 +134,7 @@ export abstract class PrivateKeyStore { if (keyData === null) { throw new UnknownKeyError(`Key ${keyIdHex} does not exist`); } - if (keyData.privateAddress !== privateAddress) { + if (keyData.nodeId !== nodeId) { throw new UnknownKeyError('Key is owned by a different node'); } return keyData; diff --git a/src/lib/keyStores/PublicKeyStore.spec.ts b/src/lib/keyStores/PublicKeyStore.spec.ts index 7f4e8bb5b..de57ecc90 100644 --- a/src/lib/keyStores/PublicKeyStore.spec.ts +++ b/src/lib/keyStores/PublicKeyStore.spec.ts @@ -2,7 +2,7 @@ import { derSerializePublicKey, generateECDHKeyPair, generateRSAKeyPair, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, } from '../crypto_wrappers/keys'; import { KeyStoreError } from './KeyStoreError'; import { SessionPublicKeyData } from './PublicKeyStore'; @@ -15,21 +15,18 @@ beforeEach(() => { describe('Identity keys', () => { let publicKey: CryptoKey; - let peerPrivateAddress: string; + let peerId: string; beforeAll(async () => { const keyPair = await generateRSAKeyPair(); publicKey = keyPair.publicKey; - peerPrivateAddress = await getPrivateAddressFromIdentityKey(publicKey); + peerId = await getIdFromIdentityKey(publicKey); }); describe('saveIdentityKey', () => { test('Key should be stored', async () => { await STORE.saveIdentityKey(publicKey); - expect(STORE.identityKeys).toHaveProperty( - peerPrivateAddress, - await derSerializePublicKey(publicKey), - ); + expect(STORE.identityKeys).toHaveProperty(peerId, await derSerializePublicKey(publicKey)); }); }); @@ -37,7 +34,7 @@ describe('Identity keys', () => { test('Key should be returned if it exists', async () => { await STORE.saveIdentityKey(publicKey); - const publicKeyRetrieved = await STORE.retrieveIdentityKey(peerPrivateAddress); + const publicKeyRetrieved = await STORE.retrieveIdentityKey(peerId); await expect(derSerializePublicKey(publicKeyRetrieved!)).resolves.toEqual( await derSerializePublicKey(publicKey), @@ -45,7 +42,7 @@ describe('Identity keys', () => { }); test('Null should be returned if it does not exist', async () => { - await expect(STORE.retrieveIdentityKey(peerPrivateAddress)).resolves.toBeNull(); + await expect(STORE.retrieveIdentityKey(peerId)).resolves.toBeNull(); }); }); }); @@ -55,7 +52,7 @@ describe('Session keys', () => { const sessionKeyId = Buffer.from([1, 3, 5, 7, 9]); let sessionPublicKey: CryptoKey; - const peerPrivateAddress = '0deadbeef'; + const peerId = '0deadbeef'; beforeAll(async () => { const keyPair = await generateECDHKeyPair(); sessionPublicKey = keyPair.publicKey; @@ -68,15 +65,15 @@ describe('Session keys', () => { publicKeyDer: await derSerializePublicKey(sessionPublicKey), publicKeyId: sessionKeyId, }; - STORE.registerSessionKey(keyData, peerPrivateAddress); + STORE.registerSessionKey(keyData, peerId); - const key = await STORE.retrieveLastSessionKey(peerPrivateAddress); + const key = await STORE.retrieveLastSessionKey(peerId); expect(key?.keyId).toEqual(sessionKeyId); expect(await derSerializePublicKey(key!.publicKey)).toEqual(keyData.publicKeyDer); }); test('Null should be returned if key for recipient does not exist', async () => { - await expect(STORE.retrieveLastSessionKey(peerPrivateAddress)).resolves.toBeNull(); + await expect(STORE.retrieveLastSessionKey(peerId)).resolves.toBeNull(); }); test('Retrieval errors should be wrapped', async () => { @@ -84,7 +81,7 @@ describe('Session keys', () => { const bogusStore = new MockPublicKeyStore(false, fetchError); - await expect(bogusStore.retrieveLastSessionKey(peerPrivateAddress)).rejects.toEqual( + await expect(bogusStore.retrieveLastSessionKey(peerId)).rejects.toEqual( new KeyStoreError(fetchError, 'Failed to retrieve key'), ); }); @@ -94,11 +91,11 @@ describe('Session keys', () => { test('Key data should be saved if there is no prior key for recipient', async () => { await STORE.saveSessionKey( { keyId: sessionKeyId, publicKey: sessionPublicKey }, - peerPrivateAddress, + peerId, CREATION_DATE, ); - const keyData = STORE.sessionKeys[peerPrivateAddress]; + const keyData = STORE.sessionKeys[peerId]; const expectedKeyData: SessionPublicKeyData = { publicKeyCreationTime: CREATION_DATE, publicKeyDer: await derSerializePublicKey(sessionPublicKey), @@ -113,7 +110,7 @@ describe('Session keys', () => { publicKeyDer: await derSerializePublicKey(sessionPublicKey), publicKeyId: sessionKeyId, }; - STORE.registerSessionKey(oldKeyData, peerPrivateAddress); + STORE.registerSessionKey(oldKeyData, peerId); const newPublicKeyId = Buffer.concat([sessionKeyId, Buffer.from([1, 0])]); const newPublicKey = (await generateECDHKeyPair()).publicKey; @@ -121,11 +118,11 @@ describe('Session keys', () => { newPublicKeyDate.setHours(newPublicKeyDate.getHours() + 1); await STORE.saveSessionKey( { publicKey: newPublicKey, keyId: newPublicKeyId }, - peerPrivateAddress, + peerId, newPublicKeyDate, ); - const keyData = STORE.sessionKeys[peerPrivateAddress]; + const keyData = STORE.sessionKeys[peerId]; const expectedKeyData: SessionPublicKeyData = { publicKeyCreationTime: newPublicKeyDate, publicKeyDer: await derSerializePublicKey(newPublicKey), @@ -140,7 +137,7 @@ describe('Session keys', () => { publicKeyDer: await derSerializePublicKey(sessionPublicKey), publicKeyId: sessionKeyId, }; - STORE.registerSessionKey(currentKeyData, peerPrivateAddress); + STORE.registerSessionKey(currentKeyData, peerId); const olderPublicKeyId = Buffer.concat([sessionKeyId, sessionKeyId]); const olderPublicKey = (await generateECDHKeyPair()).publicKey; @@ -148,11 +145,11 @@ describe('Session keys', () => { olderPublicKeyDate.setHours(olderPublicKeyDate.getHours() - 1); await STORE.saveSessionKey( { publicKey: olderPublicKey, keyId: olderPublicKeyId }, - peerPrivateAddress, + peerId, olderPublicKeyDate, ); - const keyData = STORE.sessionKeys[peerPrivateAddress]; + const keyData = STORE.sessionKeys[peerId]; expect(keyData).toEqual(currentKeyData); }); @@ -162,7 +159,7 @@ describe('Session keys', () => { await expect( bogusStore.saveSessionKey( { keyId: sessionKeyId, publicKey: sessionPublicKey }, - peerPrivateAddress, + peerId, CREATION_DATE, ), ).rejects.toEqual(new KeyStoreError('Failed to save public session key: Denied')); diff --git a/src/lib/keyStores/PublicKeyStore.ts b/src/lib/keyStores/PublicKeyStore.ts index e7aee2cc0..53b95b110 100644 --- a/src/lib/keyStores/PublicKeyStore.ts +++ b/src/lib/keyStores/PublicKeyStore.ts @@ -2,7 +2,7 @@ import { derDeserializeECDHPublicKey, derDeserializeRSAPublicKey, derSerializePublicKey, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, } from '../crypto_wrappers/keys'; import { SessionKey } from '../SessionKey'; import { KeyStoreError } from './KeyStoreError'; @@ -17,25 +17,21 @@ export abstract class PublicKeyStore { //region Identity keys public async saveIdentityKey(key: CryptoKey): Promise { - const peerPrivateAddress = await getPrivateAddressFromIdentityKey(key); + const peerId = await getIdFromIdentityKey(key); const keySerialized = await derSerializePublicKey(key); - await this.saveIdentityKeySerialized(keySerialized, peerPrivateAddress); + await this.saveIdentityKeySerialized(keySerialized, peerId); } - public async retrieveIdentityKey(peerPrivateAddress: string): Promise { - const keySerialized = await this.retrieveIdentityKeySerialized(peerPrivateAddress); + public async retrieveIdentityKey(peerId: string): Promise { + const keySerialized = await this.retrieveIdentityKeySerialized(peerId); return keySerialized ? derDeserializeRSAPublicKey(keySerialized) : null; } //endregion //region Session keys - public async saveSessionKey( - key: SessionKey, - peerPrivateAddress: string, - creationTime: Date, - ): Promise { - const priorKeyData = await this.fetchSessionKeyDataOrWrapError(peerPrivateAddress); + public async saveSessionKey(key: SessionKey, peerId: string, creationTime: Date): Promise { + const priorKeyData = await this.fetchSessionKeyDataOrWrapError(peerId); if (priorKeyData && creationTime <= priorKeyData.publicKeyCreationTime) { return; } @@ -46,14 +42,14 @@ export abstract class PublicKeyStore { publicKeyId: key.keyId, }; try { - await this.saveSessionKeyData(keyData, peerPrivateAddress); + await this.saveSessionKeyData(keyData, peerId); } catch (error) { throw new KeyStoreError(error as Error, 'Failed to save public session key'); } } - public async retrieveLastSessionKey(peerPrivateAddress: string): Promise { - const keyData = await this.fetchSessionKeyDataOrWrapError(peerPrivateAddress); + public async retrieveLastSessionKey(peerId: string): Promise { + const keyData = await this.fetchSessionKeyDataOrWrapError(peerId); if (!keyData) { return null; } @@ -63,27 +59,23 @@ export abstract class PublicKeyStore { //endregion - protected abstract retrieveIdentityKeySerialized( - peerPrivateAddress: string, - ): Promise; - protected abstract retrieveSessionKeyData( - peerPrivateAddress: string, - ): Promise; + protected abstract retrieveIdentityKeySerialized(peerId: string): Promise; + protected abstract retrieveSessionKeyData(peerId: string): Promise; protected abstract saveIdentityKeySerialized( keySerialized: Buffer, - peerPrivateAddress: string, + peerId: string, ): Promise; protected abstract saveSessionKeyData( keyData: SessionPublicKeyData, - peerPrivateAddress: string, + peerId: string, ): Promise; private async fetchSessionKeyDataOrWrapError( - peerPrivateAddress: string, + peerId: string, ): Promise { try { - return await this.retrieveSessionKeyData(peerPrivateAddress); + return await this.retrieveSessionKeyData(peerId); } catch (error) { throw new KeyStoreError(error as Error, 'Failed to retrieve key'); } diff --git a/src/lib/keyStores/testMocks.ts b/src/lib/keyStores/testMocks.ts index a9c3e9399..0d74cf5f0 100644 --- a/src/lib/keyStores/testMocks.ts +++ b/src/lib/keyStores/testMocks.ts @@ -7,7 +7,7 @@ import { PrivateKeyStore, SessionPrivateKeyData } from './PrivateKeyStore'; import { PublicKeyStore, SessionPublicKeyData } from './PublicKeyStore'; export class MockPrivateKeyStore extends PrivateKeyStore { - public identityKeys: { [privateAddress: string]: CryptoKey } = {}; + public identityKeys: { [nodeId: string]: CryptoKey } = {}; public sessionKeys: { [keyId: string]: SessionPrivateKeyData } = {}; @@ -20,32 +20,28 @@ export class MockPrivateKeyStore extends PrivateKeyStore { this.sessionKeys = {}; } - public async retrieveIdentityKey(privateAddress: string): Promise { - return this.identityKeys[privateAddress] ?? null; + public async retrieveIdentityKey(nodeId: string): Promise { + return this.identityKeys[nodeId] ?? null; } - public async saveIdentityKey(privateAddress: string, privateKey: CryptoKey): Promise { + public async saveIdentityKey(nodeId: string, privateKey: CryptoKey): Promise { if (this.failOnSave) { throw new Error('Denied'); } - this.identityKeys[privateAddress] = privateKey; + this.identityKeys[nodeId] = privateKey; } protected async saveSessionKeySerialized( keyId: string, keySerialized: Buffer, - privateAddress: string, - peerPrivateAddress?: string, + nodeId: string, + peerId?: string, ): Promise { if (this.failOnSave) { throw new Error('Denied'); } - this.sessionKeys[keyId] = { - keySerialized, - peerPrivateAddress, - privateAddress, - }; + this.sessionKeys[keyId] = { keySerialized, peerId, nodeId }; } protected async retrieveSessionKeyData(keyId: string): Promise { @@ -58,7 +54,7 @@ export class MockPrivateKeyStore extends PrivateKeyStore { } export class MockPublicKeyStore extends PublicKeyStore { - public identityKeys: { [peerPrivateAddress: string]: Buffer } = {}; + public identityKeys: { [peerId: string]: Buffer } = {}; public sessionKeys: { [key: string]: SessionPublicKeyData } = {}; @@ -71,65 +67,55 @@ export class MockPublicKeyStore extends PublicKeyStore { this.identityKeys = {}; } - public registerSessionKey(keyData: SessionPublicKeyData, peerPrivateAddress: string): void { - this.sessionKeys[peerPrivateAddress] = keyData; + public registerSessionKey(keyData: SessionPublicKeyData, peerId: string): void { + this.sessionKeys[peerId] = keyData; } - protected async retrieveIdentityKeySerialized( - peerPrivateAddress: string, - ): Promise { - return this.identityKeys[peerPrivateAddress] ?? null; + protected async retrieveIdentityKeySerialized(peerId: string): Promise { + return this.identityKeys[peerId] ?? null; } - protected async retrieveSessionKeyData( - peerPrivateAddress: string, - ): Promise { + protected async retrieveSessionKeyData(peerId: string): Promise { if (this.fetchError) { throw this.fetchError; } - const keyData = this.sessionKeys[peerPrivateAddress]; + const keyData = this.sessionKeys[peerId]; return keyData ?? null; } - protected async saveIdentityKeySerialized( - keySerialized: Buffer, - peerPrivateAddress: string, - ): Promise { - this.identityKeys[peerPrivateAddress] = keySerialized; + protected async saveIdentityKeySerialized(keySerialized: Buffer, peerId: string): Promise { + this.identityKeys[peerId] = keySerialized; } - protected async saveSessionKeyData( - keyData: SessionPublicKeyData, - peerPrivateAddress: string, - ): Promise { + protected async saveSessionKeyData(keyData: SessionPublicKeyData, peerId: string): Promise { if (this.failOnSave) { throw new Error('Denied'); } - this.sessionKeys[peerPrivateAddress] = keyData; + this.sessionKeys[peerId] = keyData; } } interface MockStoredCertificateData { readonly expiryDate: Date; readonly serialization: ArrayBuffer; - readonly issuerPrivateAddress: string; + readonly issuerId: string; } export class MockCertificateStore extends CertificateStore { - public dataByPrivateAddress: { - [privateAddress: string]: MockStoredCertificateData[]; + public dataBySubjectId: { + [nodeId: string]: MockStoredCertificateData[]; } = {}; public clear(): void { - this.dataByPrivateAddress = {}; + this.dataBySubjectId = {}; } - public async forceSave(path: CertificationPath, issuerPrivateAddress: string): Promise { + public async forceSave(path: CertificationPath, issuerId: string): Promise { await this.saveData( path.serialize(), - await path.leafCertificate.calculateSubjectPrivateAddress(), + await path.leafCertificate.calculateSubjectId(), path.leafCertificate.expiryDate, - issuerPrivateAddress, + issuerId, ); } @@ -138,13 +124,11 @@ export class MockCertificateStore extends CertificateStore { } protected async retrieveAllSerializations( - subjectPrivateAddress: string, - issuerPrivateAddress: string, + subjectId: string, + issuerId: string, ): Promise { - const certificateData = this.dataByPrivateAddress[subjectPrivateAddress] ?? []; - const matchingCertificateData = certificateData.filter( - (d) => d.issuerPrivateAddress === issuerPrivateAddress, - ); + const certificateData = this.dataBySubjectId[subjectId] ?? []; + const matchingCertificateData = certificateData.filter((d) => d.issuerId === issuerId); if (matchingCertificateData.length === 0) { return []; } @@ -152,13 +136,11 @@ export class MockCertificateStore extends CertificateStore { } protected async retrieveLatestSerialization( - subjectPrivateAddress: string, - issuerPrivateAddress: string, + subjectId: string, + issuerId: string, ): Promise { - const certificateData = this.dataByPrivateAddress[subjectPrivateAddress] ?? []; - const matchingCertificateData = certificateData.filter( - (d) => d.issuerPrivateAddress === issuerPrivateAddress, - ); + const certificateData = this.dataBySubjectId[subjectId] ?? []; + const matchingCertificateData = certificateData.filter((d) => d.issuerId === issuerId); if (matchingCertificateData.length === 0) { return null; } @@ -170,17 +152,17 @@ export class MockCertificateStore extends CertificateStore { protected async saveData( serialization: ArrayBuffer, - subjectPrivateAddress: string, + subjectId: string, subjectCertificateExpiryDate: Date, - issuerPrivateAddress: string, + issuerId: string, ): Promise { const mockData: MockStoredCertificateData = { serialization, expiryDate: subjectCertificateExpiryDate, - issuerPrivateAddress, + issuerId, }; - const originalCertificateData = this.dataByPrivateAddress[subjectPrivateAddress] ?? []; - this.dataByPrivateAddress[subjectPrivateAddress] = [...originalCertificateData, mockData]; + const originalCertificateData = this.dataBySubjectId[subjectId] ?? []; + this.dataBySubjectId[subjectId] = [...originalCertificateData, mockData]; } } diff --git a/src/lib/messages/CertificateRotation.spec.ts b/src/lib/messages/CertificateRotation.spec.ts index ba6415826..c35a62a6c 100644 --- a/src/lib/messages/CertificateRotation.spec.ts +++ b/src/lib/messages/CertificateRotation.spec.ts @@ -24,16 +24,16 @@ describe('CertificateRotation', () => { const serialization = rotation.serialize(); const expectedFormatSignature = Buffer.concat([ - Buffer.from('Relaynet'), + Buffer.from('Awala'), Buffer.from([0x10, 0x00]), ]); - expect(Buffer.from(serialization).slice(0, 10)).toEqual(expectedFormatSignature); + expect(Buffer.from(serialization).slice(0, 7)).toEqual(expectedFormatSignature); }); test('Serialization should contain CertificationPath', async () => { const rotation = new CertificateRotation(certificationPath); - const pathSerialized = rotation.serialize().slice(10); + const pathSerialized = rotation.serialize().slice(7); expect(pathSerialized).toEqual(certificationPath.serialize()); }); diff --git a/src/lib/messages/ParcelCollectionAck.spec.ts b/src/lib/messages/ParcelCollectionAck.spec.ts index 483eae535..25d2dc028 100644 --- a/src/lib/messages/ParcelCollectionAck.spec.ts +++ b/src/lib/messages/ParcelCollectionAck.spec.ts @@ -6,31 +6,31 @@ import InvalidMessageError from './InvalidMessageError'; import { ParcelCollectionAck } from './ParcelCollectionAck'; describe('ParcelCollectionAck', () => { - const SENDER_ENDPOINT_PRIVATE_ADDRESS = '0deadbeef'; - const RECIPIENT_ENDPOINT_ADDRESS = 'https://example.com'; + const SENDER_ENDPOINT_ID = '0deadbeef'; + const RECIPIENT_ENDPOINT_INTERNET_ADDRESS = 'example.com'; const PARCEL_ID = 'the-parcel-id'; describe('serialize', () => { test('Serialization should start with format signature', () => { const pca = new ParcelCollectionAck( - SENDER_ENDPOINT_PRIVATE_ADDRESS, - RECIPIENT_ENDPOINT_ADDRESS, + SENDER_ENDPOINT_ID, + RECIPIENT_ENDPOINT_INTERNET_ADDRESS, PARCEL_ID, ); const pcaSerialized = pca.serialize(); const expectedFormatSignature = Buffer.concat([ - Buffer.from('Relaynet'), + Buffer.from('Awala'), Buffer.from([0x51, 0x00]), ]); - expect(Buffer.from(pcaSerialized).slice(0, 10)).toEqual(expectedFormatSignature); + expect(Buffer.from(pcaSerialized).slice(0, 7)).toEqual(expectedFormatSignature); }); test('An ACK should be serialized as a 3-item sequence', () => { const pca = new ParcelCollectionAck( - SENDER_ENDPOINT_PRIVATE_ADDRESS, - RECIPIENT_ENDPOINT_ADDRESS, + SENDER_ENDPOINT_ID, + RECIPIENT_ENDPOINT_INTERNET_ADDRESS, PARCEL_ID, ); @@ -41,10 +41,10 @@ describe('ParcelCollectionAck', () => { const pcaSequenceItems = pcaSequenceBlock.valueBlock.value; expect(pcaSequenceItems).toHaveLength(3); expect((pcaSequenceItems[0] as asn1js.Primitive).valueBlock.valueHex).toEqual( - arrayBufferFrom(SENDER_ENDPOINT_PRIVATE_ADDRESS), + arrayBufferFrom(SENDER_ENDPOINT_ID), ); expect((pcaSequenceItems[1] as asn1js.Primitive).valueBlock.valueHex).toEqual( - arrayBufferFrom(RECIPIENT_ENDPOINT_ADDRESS), + arrayBufferFrom(RECIPIENT_ENDPOINT_INTERNET_ADDRESS), ); expect((pcaSequenceItems[2] as asn1js.Primitive).valueBlock.valueHex).toEqual( arrayBufferFrom(PARCEL_ID), @@ -52,7 +52,7 @@ describe('ParcelCollectionAck', () => { }); function skipFormatSignatureFromSerialization(pcaSerialized: ArrayBuffer): ArrayBuffer { - return pcaSerialized.slice(10); + return pcaSerialized.slice(7); } function parsePCA(pcaSerialized: ArrayBuffer): asn1js.Sequence { @@ -110,15 +110,15 @@ describe('ParcelCollectionAck', () => { test('A new instance should be returned if serialization is valid', () => { const pca = new ParcelCollectionAck( - SENDER_ENDPOINT_PRIVATE_ADDRESS, - RECIPIENT_ENDPOINT_ADDRESS, + SENDER_ENDPOINT_ID, + RECIPIENT_ENDPOINT_INTERNET_ADDRESS, PARCEL_ID, ); const pcaDeserialized = ParcelCollectionAck.deserialize(pca.serialize()); - expect(pcaDeserialized.senderEndpointPrivateAddress).toEqual(SENDER_ENDPOINT_PRIVATE_ADDRESS); - expect(pcaDeserialized.recipientEndpointAddress).toEqual(RECIPIENT_ENDPOINT_ADDRESS); + expect(pcaDeserialized.senderEndpointId).toEqual(SENDER_ENDPOINT_ID); + expect(pcaDeserialized.recipientEndpointId).toEqual(RECIPIENT_ENDPOINT_INTERNET_ADDRESS); expect(pcaDeserialized.parcelId).toEqual(PARCEL_ID); }); }); diff --git a/src/lib/messages/ParcelCollectionAck.ts b/src/lib/messages/ParcelCollectionAck.ts index a1a04889a..33b67decc 100644 --- a/src/lib/messages/ParcelCollectionAck.ts +++ b/src/lib/messages/ParcelCollectionAck.ts @@ -16,7 +16,7 @@ export class ParcelCollectionAck { throw new InvalidMessageError('Format signature should be that of a PCA'); } - const pcaSequenceSerialized = pcaSerialized.slice(10); + const pcaSequenceSerialized = pcaSerialized.slice(7); const result = verifySchema(pcaSequenceSerialized, ParcelCollectionAck.SCHEMA); if (!result.verified) { throw new InvalidMessageError('PCA did not meet required structure'); @@ -25,28 +25,28 @@ export class ParcelCollectionAck { const textDecoder = new TextDecoder(); const pcaBlock: any = (result.result as any).ParcelCollectionAck; return new ParcelCollectionAck( - textDecoder.decode(pcaBlock.senderEndpointPrivateAddress.valueBlock.valueHex), - textDecoder.decode(pcaBlock.recipientEndpointAddress.valueBlock.valueHex), + textDecoder.decode(pcaBlock.senderEndpointId.valueBlock.valueHex), + textDecoder.decode(pcaBlock.recipientEndpointId.valueBlock.valueHex), textDecoder.decode(pcaBlock.parcelId.valueBlock.valueHex), ); } private static readonly SCHEMA = makeHeterogeneousSequenceSchema('ParcelCollectionAck', [ - new Primitive({ name: 'senderEndpointPrivateAddress' }), - new Primitive({ name: 'recipientEndpointAddress' }), + new Primitive({ name: 'senderEndpointId' }), + new Primitive({ name: 'recipientEndpointId' }), new Primitive({ name: 'parcelId' }), ]); constructor( - public readonly senderEndpointPrivateAddress: string, - public readonly recipientEndpointAddress: string, + public readonly senderEndpointId: string, + public readonly recipientEndpointId: string, public readonly parcelId: string, ) {} public serialize(): ArrayBuffer { const ackSerialized = makeImplicitlyTaggedSequence( - new VisibleString({ value: this.senderEndpointPrivateAddress }), - new VisibleString({ value: this.recipientEndpointAddress }), + new VisibleString({ value: this.senderEndpointId }), + new VisibleString({ value: this.recipientEndpointId }), new VisibleString({ value: this.parcelId }), ).toBER(); const serialization = new ArrayBuffer( diff --git a/src/lib/messages/RAMFMessage.spec.ts b/src/lib/messages/RAMFMessage.spec.ts index 2d6e185aa..3c501524f 100644 --- a/src/lib/messages/RAMFMessage.spec.ts +++ b/src/lib/messages/RAMFMessage.spec.ts @@ -1,8 +1,9 @@ // tslint:disable:max-classes-per-file +import { addMinutes, addSeconds, setMilliseconds, subSeconds } from 'date-fns'; import * as jestDateMock from 'jest-date-mock'; -import { generateStubCert, getPromiseRejection, reSerializeCertificate } from '../_test_utils'; +import { generateStubCert, reSerializeCertificate } from '../_test_utils'; import { SessionEnvelopedData, SessionlessEnvelopedData, @@ -15,7 +16,7 @@ import { StubMessage, StubPayload } from '../ramf/_test_utils'; import RAMFError from '../ramf/RAMFError'; import { SessionKeyPair } from '../SessionKeyPair'; import InvalidMessageError from './InvalidMessageError'; -import { RecipientAddressType } from './RecipientAddressType'; +import { Recipient } from './Recipient'; const mockStubUuid4 = '56e95d8a-6be2-4020-bb36-5dd0da36c181'; jest.mock('uuid4', () => { @@ -33,38 +34,24 @@ afterEach(() => { }); describe('RAMFMessage', () => { - let recipientPrivateAddress: string; - let recipientCertificate: Certificate; + let rootCertificate: Certificate; let senderCertificate: Certificate; + let recipientCertificate: Certificate; + let recipient: Recipient; beforeAll(async () => { - const recipientKeyPair = await generateRSAKeyPair(); - recipientCertificate = await generateStubCert({ - attributes: { isCA: true }, - subjectPublicKey: recipientKeyPair.publicKey, - }); - recipientPrivateAddress = recipientCertificate.getCommonName(); + const stubSenderChain = await generateAuthorizedSenderChain(); - const senderKeyPair = await generateRSAKeyPair(); - senderCertificate = reSerializeCertificate( - await generateStubCert({ - subjectPublicKey: senderKeyPair.publicKey, - }), - ); - }); + rootCertificate = stubSenderChain.rootCert; + recipientCertificate = stubSenderChain.recipientCert; + senderCertificate = stubSenderChain.senderCert; - let stubSenderChain: AuthorizedSenderChain; - beforeAll(async () => { - stubSenderChain = await generateAuthorizedSenderChain(); + recipient = { id: recipientCertificate.getCommonName() }; }); describe('constructor', () => { describe('Id', () => { test('Id should fall back to UUID4 when left unspecified', () => { - const message = new StubMessage( - recipientPrivateAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - ); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT); expect(message.id).toEqual(mockStubUuid4); }); @@ -75,26 +62,18 @@ describe('RAMFMessage', () => { const now = new Date(2019, 1, 1, 1, 1, 1, 1); jestDateMock.advanceTo(now); - const message = new StubMessage( - recipientPrivateAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - ); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT); - const expectedDate = new Date(now.getTime()); - expectedDate.setMilliseconds(0); + const expectedDate = setMilliseconds(now, 0); expect(message.creationDate).toEqual(expectedDate); }); test('A custom date should be accepted', () => { const date = new Date(2020, 1, 1, 1, 1, 1, 1); - const message = new StubMessage( - recipientPrivateAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - { creationDate: date }, - ); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT, { + creationDate: date, + }); const expectedDate = new Date(date.getTime()); expectedDate.setMilliseconds(0); @@ -104,23 +83,16 @@ describe('RAMFMessage', () => { describe('TTL', () => { test('TTL should be 5 minutes by default', () => { - const message = new StubMessage( - recipientPrivateAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - ); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT); expect(message.ttl).toEqual(5 * 60); }); test('A custom TTL under 2^24 should be accepted', () => { const ttl = 2 ** 24 - 1; - const message = new StubMessage( - recipientPrivateAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - { ttl }, - ); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT, { + ttl, + }); expect(message.ttl).toEqual(ttl); }); @@ -128,37 +100,25 @@ describe('RAMFMessage', () => { describe('Sender CA certificate chain', () => { test('CA certificate chain should be empty by default', () => { - const message = new StubMessage( - recipientPrivateAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - ); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT); expect(message.senderCaCertificateChain).toEqual([]); }); test('A custom sender certificate chain should be accepted', async () => { const chain: readonly Certificate[] = [await generateStubCert(), await generateStubCert()]; - const message = new StubMessage( - recipientPrivateAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - { senderCaCertificateChain: chain }, - ); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT, { + senderCaCertificateChain: chain, + }); expect(message.senderCaCertificateChain).toEqual(chain); }); test('Sender certificate should be excluded from chain if included', async () => { const chain: readonly Certificate[] = [await generateStubCert()]; - const message = new StubMessage( - recipientPrivateAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - { - senderCaCertificateChain: [...chain, senderCertificate], - }, - ); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT, { + senderCaCertificateChain: [...chain, senderCertificate], + }); expect(message.senderCaCertificateChain).toEqual(chain); }); @@ -167,31 +127,26 @@ describe('RAMFMessage', () => { test('getSenderCertificationPath should return certification path', async () => { const message = new StubMessage( - await stubSenderChain.recipientCert.calculateSubjectPrivateAddress(), - stubSenderChain.senderCert, + { id: await recipientCertificate.calculateSubjectId() }, + senderCertificate, STUB_PAYLOAD_PLAINTEXT, { - senderCaCertificateChain: [stubSenderChain.recipientCert], + senderCaCertificateChain: [recipientCertificate], }, ); - await expect(message.getSenderCertificationPath([stubSenderChain.rootCert])).resolves.toEqual([ - expect.toSatisfy((c) => c.isEqual(stubSenderChain.senderCert)), - expect.toSatisfy((c) => c.isEqual(stubSenderChain.recipientCert)), - expect.toSatisfy((c) => c.isEqual(stubSenderChain.rootCert)), + await expect(message.getSenderCertificationPath([rootCertificate])).resolves.toEqual([ + expect.toSatisfy((c) => c.isEqual(senderCertificate)), + expect.toSatisfy((c) => c.isEqual(recipientCertificate)), + expect.toSatisfy((c) => c.isEqual(rootCertificate)), ]); }); test('expiryDate field should calculate expiry date from creation date and TTL', () => { - const message = new StubMessage( - 'some-address', - stubSenderChain.senderCert, - STUB_PAYLOAD_PLAINTEXT, - { - creationDate: new Date('2020-04-07T21:00:00Z'), - ttl: 5, - }, - ); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT, { + creationDate: new Date('2020-04-07T21:00:00Z'), + ttl: 5, + }); const expectedExpiryDate = new Date(message.creationDate.getTime()); expectedExpiryDate.setSeconds(expectedExpiryDate.getSeconds() + message.ttl); @@ -199,75 +154,13 @@ describe('RAMFMessage', () => { }); describe('validate', () => { - describe('Recipient address', () => { - const PRIVATE_ADDRESS = '0deadbeef'; - const PUBLIC_ADDRESS = 'https://example.com'; - - test('Public address should be allowed if no specific type is required', async () => { - const message = new StubMessage(PUBLIC_ADDRESS, senderCertificate, STUB_PAYLOAD_PLAINTEXT); - - await message.validate(); - }); - - test('Private address should be allowed if no specific type is required', async () => { - const message = new StubMessage(PRIVATE_ADDRESS, senderCertificate, STUB_PAYLOAD_PLAINTEXT); - - await message.validate(); - }); - - test('Syntactically-invalid addresses should be refused', async () => { - const message = new StubMessage( - 'this is an invalid address', - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - ); - - const error = await getPromiseRejection(message.validate()); - - expect(error).toBeInstanceOf(InvalidMessageError); - expect(error.message).toEqual('Recipient address is malformed'); - }); - - test('Private address should be refused if a public one is required', async () => { - const message = new StubMessage(PRIVATE_ADDRESS, senderCertificate, STUB_PAYLOAD_PLAINTEXT); - - const error = await getPromiseRejection(message.validate(RecipientAddressType.PUBLIC)); - - expect(error).toBeInstanceOf(InvalidMessageError); - expect(error.message).toEqual('Recipient address should be public but got a private one'); - }); - - test('Public address should be refused if a private one is required', async () => { - const message = new StubMessage(PUBLIC_ADDRESS, senderCertificate, STUB_PAYLOAD_PLAINTEXT); - - const error = await getPromiseRejection(message.validate(RecipientAddressType.PRIVATE)); - - expect(error).toBeInstanceOf(InvalidMessageError); - expect(error.message).toEqual('Recipient address should be private but got a public one'); - }); - - test('Private address should be allowed if a private one is required', async () => { - const message = new StubMessage(PRIVATE_ADDRESS, senderCertificate, STUB_PAYLOAD_PLAINTEXT); - - await message.validate(RecipientAddressType.PRIVATE); - }); - - test('Public address should be allowed if a public one is required', async () => { - const message = new StubMessage(PUBLIC_ADDRESS, senderCertificate, STUB_PAYLOAD_PLAINTEXT); - - await message.validate(RecipientAddressType.PUBLIC); - }); - }); - describe('Authorization without trusted certificates', () => { test('Invalid sender certificate should be refused', async () => { - const validityStartDate = new Date(); - validityStartDate.setMinutes(validityStartDate.getMinutes() + 1); const invalidSenderCertificate = await generateStubCert({ - attributes: { validityStartDate }, + attributes: { validityStartDate: addMinutes(new Date(), 1) }, }); const message = new StubMessage( - '0deadbeef', + recipient, invalidSenderCertificate, STUB_PAYLOAD_PLAINTEXT, ); @@ -276,113 +169,76 @@ describe('RAMFMessage', () => { }); test('Valid sender certificate should be allowed', async () => { - const message = new StubMessage('0deadbeef', senderCertificate, STUB_PAYLOAD_PLAINTEXT); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT); await expect(message.validate()).resolves.toBeNull(); }); - }); - describe('Authorization with trusted certificates', () => { - test('Message should be refused if sender is not trusted', async () => { + test('Mismatching recipient should be allowed', async () => { const message = new StubMessage( - await stubSenderChain.recipientCert.calculateSubjectPrivateAddress(), + { id: `not-${recipient.id}` }, senderCertificate, STUB_PAYLOAD_PLAINTEXT, ); - jestDateMock.advanceBy(1_000); + await expect(message.validate()).resolves.toBeNull(); + }); + }); + + describe('Authorization with trusted certificates', () => { + test('Message should be refused if sender is not trusted', async () => { + // The intermediate certificate is missing + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT); - await expect(message.validate(undefined, [stubSenderChain.rootCert])).rejects.toEqual( + await expect(message.validate([rootCertificate])).rejects.toEqual( new InvalidMessageError('Sender is not authorized: No valid certificate paths found'), ); }); test('Message should be accepted if sender is trusted', async () => { - const message = new StubMessage( - await stubSenderChain.recipientCert.calculateSubjectPrivateAddress(), - stubSenderChain.senderCert, - STUB_PAYLOAD_PLAINTEXT, - { - senderCaCertificateChain: [stubSenderChain.recipientCert], - }, - ); - - jestDateMock.advanceBy(1_000); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT, { + senderCaCertificateChain: [recipientCertificate], + }); - const certificationPath = await message.validate(undefined, [stubSenderChain.rootCert]); + const certificationPath = await message.validate([rootCertificate]); expect(certificationPath).toHaveLength(3); expect(certificationPath!![0].isEqual(message.senderCertificate)).toBeTrue(); - expect(certificationPath!![1].isEqual(stubSenderChain.recipientCert)).toBeTrue(); - expect(certificationPath!![2].isEqual(stubSenderChain.rootCert)).toBeTrue(); + expect(certificationPath!![1].isEqual(recipientCertificate)).toBeTrue(); + expect(certificationPath!![2].isEqual(rootCertificate)).toBeTrue(); }); - test('Message should be refused if recipient is private and did not authorize', async () => { + test('Message should be refused if recipient does not match issuer of sender', async () => { const message = new StubMessage( - '0deadbeef', - stubSenderChain.senderCert, + { id: `not-${recipient.id}` }, + senderCertificate, STUB_PAYLOAD_PLAINTEXT, { - senderCaCertificateChain: [stubSenderChain.recipientCert], + senderCaCertificateChain: [recipientCertificate], }, ); - jestDateMock.advanceBy(1_000); - - await expect(message.validate(undefined, [stubSenderChain.rootCert])).rejects.toEqual( - new InvalidMessageError(`Sender is not authorized to reach ${message.recipientAddress}`), + await expect(message.validate([rootCertificate])).rejects.toEqual( + new InvalidMessageError(`Sender is not authorized to reach ${message.recipient}`), ); }); - - test('Message should be accepted if recipient address is public', async () => { - const message = new StubMessage( - 'https://example.com', - stubSenderChain.senderCert, - STUB_PAYLOAD_PLAINTEXT, - { - senderCaCertificateChain: [stubSenderChain.recipientCert], - }, - ); - - jestDateMock.advanceBy(1_000); - - const certificationPath = await message.validate(undefined, [stubSenderChain.rootCert]); - expect(certificationPath).toHaveLength(3); - expect(certificationPath!![2].isEqual(stubSenderChain.rootCert)).toBeTrue(); - expect(certificationPath!![1].isEqual(stubSenderChain.recipientCert)).toBeTrue(); - expect(certificationPath!![0].isEqual(message.senderCertificate)).toBeTrue(); - }); }); describe('Validity period', () => { - const recipientPublicAddress = 'https://example.com'; - test('Date equal to the current date should be accepted', async () => { - const stubDate = new Date( - senderCertificate.pkijsCertificate.notAfter.value.getTime() - 1_000, - ); - stubDate.setSeconds(0, 0); - const message = new StubMessage( - recipientPublicAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - { creationDate: stubDate }, - ); + const stubDate = setMilliseconds(subSeconds(senderCertificate.expiryDate, 1), 0); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT, { + creationDate: stubDate, + }); jestDateMock.advanceTo(stubDate); await message.validate(); }); test('Date should not be in the future', async () => { - const message = new StubMessage( - recipientPublicAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - ); - message.creationDate.setMilliseconds(0); - - const oneSecondAgo = new Date(message.creationDate); - oneSecondAgo.setDate(oneSecondAgo.getDate() - 1_000); - jestDateMock.advanceTo(oneSecondAgo); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT, { + creationDate: setMilliseconds(new Date(), 0), + }); + jestDateMock.advanceTo(subSeconds(message.creationDate, 1)); await expect(message.validate()).rejects.toEqual( new InvalidMessageError('Message date is in the future'), @@ -390,30 +246,21 @@ describe('RAMFMessage', () => { }); test('TTL matching current time should be accepted', async () => { - const message = new StubMessage( - recipientPublicAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - { - creationDate: senderCertificate.startDate, - ttl: 1, - }, - ); - + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT, { + creationDate: senderCertificate.startDate, + ttl: 1, + }); jestDateMock.advanceTo(message.expiryDate); await message.validate(); }); test('TTL in the past should not be accepted', async () => { - const message = new StubMessage( - recipientPublicAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - { ttl: 1 }, - ); + const message = new StubMessage(recipient, senderCertificate, STUB_PAYLOAD_PLAINTEXT, { + ttl: 1, + }); + jestDateMock.advanceTo(addSeconds(message.expiryDate, 1)); - jestDateMock.advanceTo(message.creationDate.getTime() + (message.ttl + 1) * 1_000); await expect(message.validate()).rejects.toEqual( new InvalidMessageError('Message already expired'), ); @@ -431,7 +278,7 @@ describe('RAMFMessage', () => { const recipientKeyStore = new MockPrivateKeyStore(); const stubMessage = new StubMessage( - recipientPrivateAddress, + recipient, senderCertificate, Buffer.from(envelopedData.serialize()), ); @@ -442,14 +289,14 @@ describe('RAMFMessage', () => { ); }); - test('Payload for private recipient should be decrypted with key store', async () => { + test('Payload should be decrypted with key store', async () => { const recipientSessionKeyPair = await SessionKeyPair.generate(); const { envelopedData } = await SessionEnvelopedData.encrypt( STUB_PAYLOAD_PLAINTEXT, recipientSessionKeyPair.sessionKey, ); const stubMessage = new StubMessage( - recipientPrivateAddress, + recipient, senderCertificate, Buffer.from(envelopedData.serialize()), ); @@ -457,7 +304,7 @@ describe('RAMFMessage', () => { await recipientKeyStore.saveSessionKey( recipientSessionKeyPair.privateKey, recipientSessionKeyPair.sessionKey.keyId, - stubMessage.recipientAddress, + recipient.id, ); const { payload, senderSessionKey } = await stubMessage.unwrapPayload(recipientKeyStore); @@ -468,69 +315,7 @@ describe('RAMFMessage', () => { expect(senderSessionKey).toEqual(await envelopedData.getOriginatorKey()); }); - test('Payload for public recipient should be decrypted with key store', async () => { - const recipientSessionKeyPair = await SessionKeyPair.generate(); - const { envelopedData } = await SessionEnvelopedData.encrypt( - STUB_PAYLOAD_PLAINTEXT, - recipientSessionKeyPair.sessionKey, - ); - const stubMessage = new StubMessage( - 'https://example.com', - senderCertificate, - Buffer.from(envelopedData.serialize()), - ); - const recipientKeyStore = new MockPrivateKeyStore(); - await recipientKeyStore.saveSessionKey( - recipientSessionKeyPair.privateKey, - recipientSessionKeyPair.sessionKey.keyId, - recipientPrivateAddress, - ); - - const { payload, senderSessionKey } = await stubMessage.unwrapPayload( - recipientKeyStore, - recipientPrivateAddress, - ); - - expect(payload).toBeInstanceOf(StubPayload); - expect(Buffer.from(payload.content)).toEqual(STUB_PAYLOAD_PLAINTEXT); - - expect(senderSessionKey).toEqual(await envelopedData.getOriginatorKey()); - }); - - test('Recipient private address should be passed if only public is available', async () => { - const stubMessage = new StubMessage( - 'https://example.com', - senderCertificate, - Buffer.from([]), - ); - const recipientKeyStore = new MockPrivateKeyStore(); - - await expect(stubMessage.unwrapPayload(recipientKeyStore)).rejects.toThrowWithMessage( - RAMFError, - 'Recipient private address should be passed because message uses public address', - ); - }); - - test('Payload for private recipient should be decrypted with private key', async () => { - const recipientSessionKeyPair = await SessionKeyPair.generate(); - const { envelopedData } = await SessionEnvelopedData.encrypt( - STUB_PAYLOAD_PLAINTEXT, - recipientSessionKeyPair.sessionKey, - ); - - const stubMessage = new StubMessage( - '0123', - senderCertificate, - Buffer.from(envelopedData.serialize()), - ); - - const { payload } = await stubMessage.unwrapPayload(recipientSessionKeyPair.privateKey); - - expect(payload).toBeInstanceOf(StubPayload); - expect(Buffer.from(payload.content)).toEqual(STUB_PAYLOAD_PLAINTEXT); - }); - - test('Payload for public recipient should be decrypted with private key', async () => { + test('Payload should be decrypted with private key', async () => { const recipientSessionKeyPair = await SessionKeyPair.generate(); const { envelopedData } = await SessionEnvelopedData.encrypt( STUB_PAYLOAD_PLAINTEXT, @@ -538,7 +323,7 @@ describe('RAMFMessage', () => { ); const stubMessage = new StubMessage( - 'https://example.com', + recipient, senderCertificate, Buffer.from(envelopedData.serialize()), ); @@ -549,28 +334,6 @@ describe('RAMFMessage', () => { expect(Buffer.from(payload.content)).toEqual(STUB_PAYLOAD_PLAINTEXT); }); }); - - describe('isRecipientAddressPrivate', () => { - test('True should be returned when address is private', () => { - const message = new StubMessage( - recipientPrivateAddress, - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - ); - - expect(message.isRecipientAddressPrivate).toBeTrue(); - }); - - test('False should be returned when address is public', () => { - const message = new StubMessage( - 'https://example.com', - senderCertificate, - STUB_PAYLOAD_PLAINTEXT, - ); - - expect(message.isRecipientAddressPrivate).toBeFalse(); - }); - }); }); interface AuthorizedSenderChain { diff --git a/src/lib/messages/RAMFMessage.ts b/src/lib/messages/RAMFMessage.ts index be2d9b4b1..d20608867 100644 --- a/src/lib/messages/RAMFMessage.ts +++ b/src/lib/messages/RAMFMessage.ts @@ -10,12 +10,10 @@ import RAMFError from '../ramf/RAMFError'; import { SessionKey } from '../SessionKey'; import InvalidMessageError from './InvalidMessageError'; import PayloadPlaintext from './payloads/PayloadPlaintext'; -import { RecipientAddressType } from './RecipientAddressType'; +import { Recipient } from './Recipient'; const DEFAULT_TTL_SECONDS = 5 * 60; // 5 minutes -const PRIVATE_ADDRESS_REGEX = /^0[a-f\d]+$/; - interface MessageOptions { readonly id: string; readonly creationDate: Date; @@ -38,7 +36,7 @@ export default abstract class RAMFMessage { public readonly senderCaCertificateChain: readonly Certificate[]; constructor( - readonly recipientAddress: string, + readonly recipient: Recipient, readonly senderCertificate: Certificate, readonly payloadSerialized: Buffer, options: Partial = {}, @@ -56,16 +54,6 @@ export default abstract class RAMFMessage { return new Date(creationDateTimestamp + this.ttl * 1_000); } - get isRecipientAddressPrivate(): boolean { - try { - // tslint:disable-next-line:no-unused-expression - new URL(this.recipientAddress); - } catch (_) { - return true; - } - return false; - } - /** * Return RAMF serialization of message. * @@ -94,35 +82,15 @@ export default abstract class RAMFMessage { ); } - public async unwrapPayload(privateKey: CryptoKey): Promise>; - public async unwrapPayload( - privateKeyStore: PrivateKeyStore, - privateAddress?: string, - ): Promise>; public async unwrapPayload( privateKeyOrStore: CryptoKey | PrivateKeyStore, - privateAddress?: string, ): Promise> { - if ( - privateKeyOrStore instanceof PrivateKeyStore && - !this.isRecipientAddressPrivate && - !privateAddress - ) { - throw new RAMFError( - 'Recipient private address should be passed because message uses public address', - ); - } - const payloadEnvelopedData = EnvelopedData.deserialize(bufferToArray(this.payloadSerialized)); if (!(payloadEnvelopedData instanceof SessionEnvelopedData)) { throw new RAMFError('Sessionless payloads are no longer supported'); } - const payloadPlaintext = await this.decryptPayload( - payloadEnvelopedData, - privateKeyOrStore, - privateAddress, - ); + const payloadPlaintext = await this.decryptPayload(payloadEnvelopedData, privateKeyOrStore); const payload = await this.deserializePayload(payloadPlaintext); const senderSessionKey = await ( @@ -135,17 +103,13 @@ export default abstract class RAMFMessage { /** * Report whether the message is valid. * - * @param recipientAddressType The expected type of recipient address, if one is required - * @param trustedCertificates If present, will check that the sender is authorized to send + * @param trustedCertificates? If present, will check that the sender is authorized to send * the message based on the trusted certificates. * @return The certification path from the sender to one of the `trustedCertificates` (if present) */ public async validate( - recipientAddressType?: RecipientAddressType, trustedCertificates?: readonly Certificate[], ): Promise { - await this.validateRecipientAddress(recipientAddressType); - await this.validateTiming(); if (trustedCertificates) { @@ -159,30 +123,20 @@ export default abstract class RAMFMessage { protected async decryptPayload( payloadEnvelopedData: EnvelopedData, privateKeyOrStore: CryptoKey | PrivateKeyStore, - privateAddress?: string, ): Promise { - const privateKey = await this.fetchPrivateKey( - payloadEnvelopedData, - privateKeyOrStore, - privateAddress, - ); + const privateKey = await this.fetchPrivateKey(payloadEnvelopedData, privateKeyOrStore); return payloadEnvelopedData.decrypt(privateKey); } protected async fetchPrivateKey( payloadEnvelopedData: EnvelopedData, privateKeyOrStore: CryptoKey | PrivateKeyStore, - privateAddress?: string, ): Promise { const keyId = payloadEnvelopedData.getRecipientKeyId(); let privateKey: CryptoKey; if (privateKeyOrStore instanceof PrivateKeyStore) { - const peerPrivateAddress = await this.senderCertificate.calculateSubjectPrivateAddress(); - privateKey = await privateKeyOrStore.retrieveSessionKey( - keyId, - privateAddress ?? this.recipientAddress, - peerPrivateAddress, - ); + const peerId = await this.senderCertificate.calculateSubjectId(); + privateKey = await privateKeyOrStore.retrieveSessionKey(keyId, this.recipient.id, peerId); } else { privateKey = privateKeyOrStore; } @@ -191,22 +145,6 @@ export default abstract class RAMFMessage { protected abstract deserializePayload(payloadPlaintext: ArrayBuffer): Payload; - private async validateRecipientAddress( - requiredRecipientAddressType?: RecipientAddressType, - ): Promise { - const isAddressPrivate = this.isRecipientAddressPrivate; - if (isAddressPrivate && !PRIVATE_ADDRESS_REGEX[Symbol.match](this.recipientAddress)) { - throw new InvalidMessageError('Recipient address is malformed'); - } - - if (requiredRecipientAddressType === RecipientAddressType.PUBLIC && isAddressPrivate) { - throw new InvalidMessageError('Recipient address should be public but got a private one'); - } - if (requiredRecipientAddressType === RecipientAddressType.PRIVATE && !isAddressPrivate) { - throw new InvalidMessageError('Recipient address should be private but got a public one'); - } - } - private async validateAuthorization( trustedCertificates: readonly Certificate[], ): Promise { @@ -217,12 +155,10 @@ export default abstract class RAMFMessage { throw new InvalidMessageError(error as Error, 'Sender is not authorized'); } - if (this.isRecipientAddressPrivate) { - const recipientCertificate = certificationPath[1]; - const recipientPrivateAddress = await recipientCertificate.calculateSubjectPrivateAddress(); - if (recipientPrivateAddress !== this.recipientAddress) { - throw new InvalidMessageError(`Sender is not authorized to reach ${this.recipientAddress}`); - } + const recipientCertificate = certificationPath[1]; + const recipientId = await recipientCertificate.calculateSubjectId(); + if (recipientId !== this.recipient.id) { + throw new InvalidMessageError(`Sender is not authorized to reach ${this.recipient}`); } return certificationPath; diff --git a/src/lib/messages/Recipient.ts b/src/lib/messages/Recipient.ts new file mode 100644 index 000000000..b79a1f5b7 --- /dev/null +++ b/src/lib/messages/Recipient.ts @@ -0,0 +1,4 @@ +export interface Recipient { + readonly id: string; + readonly internetAddress?: string; +} diff --git a/src/lib/messages/RecipientAddressType.ts b/src/lib/messages/RecipientAddressType.ts deleted file mode 100644 index b186136b8..000000000 --- a/src/lib/messages/RecipientAddressType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum RecipientAddressType { - PRIVATE, - PUBLIC, -} diff --git a/src/lib/messages/_test_utils.ts b/src/lib/messages/_test_utils.ts index 5b19250a1..63a5a018b 100644 --- a/src/lib/messages/_test_utils.ts +++ b/src/lib/messages/_test_utils.ts @@ -1,5 +1,3 @@ -/* tslint:disable:no-let */ - import { arrayBufferFrom, generateStubCert, getMockContext, mockSpy } from '../_test_utils'; import { HashingAlgorithm } from '../crypto_wrappers/algorithms'; import { generateRSAKeyPair } from '../crypto_wrappers/keys'; diff --git a/src/lib/messages/formatSignature.ts b/src/lib/messages/formatSignature.ts index 3b57bf62e..47b4470cc 100644 --- a/src/lib/messages/formatSignature.ts +++ b/src/lib/messages/formatSignature.ts @@ -1,11 +1,11 @@ -const SIGNATURE_PREFIX = Buffer.from('Relaynet'); +const SIGNATURE_PREFIX = Buffer.from('Awala'); export function generateFormatSignature( concreteMessageType: number, concreteMessageVersion: number, ): Uint8Array { - const formatSignature = new Uint8Array(10); + const formatSignature = new Uint8Array(SIGNATURE_PREFIX.byteLength + 2); formatSignature.set(SIGNATURE_PREFIX, 0); - formatSignature.set([concreteMessageType, concreteMessageVersion], 8); + formatSignature.set([concreteMessageType, concreteMessageVersion], SIGNATURE_PREFIX.byteLength); return formatSignature; } diff --git a/src/lib/messages/payloads/CargoCollectionRequest.spec.ts b/src/lib/messages/payloads/CargoCollectionRequest.spec.ts index a9a62f537..1393164dd 100644 --- a/src/lib/messages/payloads/CargoCollectionRequest.spec.ts +++ b/src/lib/messages/payloads/CargoCollectionRequest.spec.ts @@ -4,7 +4,7 @@ import { arrayBufferFrom, expectArrayBuffersToEqual, generateStubCert, - getAsn1SequenceItem, + getPrimitiveItemFromConstructed, } from '../../_test_utils'; import { makeImplicitlyTaggedSequence } from '../../asn1'; import { derDeserialize } from '../../crypto_wrappers/_utils'; @@ -24,7 +24,7 @@ describe('serialize', () => { const serialization = request.serialize(); const sequence = derDeserialize(serialization); - const cdaSerialized = getAsn1SequenceItem(sequence, 0).valueBlock.valueHex; + const cdaSerialized = getPrimitiveItemFromConstructed(sequence, 0).valueBlock.valueHex; expectArrayBuffersToEqual(cargoDeliveryAuthorization.serialize(), cdaSerialized); }); }); diff --git a/src/lib/messages/payloads/CargoMessageSet.spec.ts b/src/lib/messages/payloads/CargoMessageSet.spec.ts index 82167ef2f..2e0588263 100644 --- a/src/lib/messages/payloads/CargoMessageSet.spec.ts +++ b/src/lib/messages/payloads/CargoMessageSet.spec.ts @@ -1,4 +1,4 @@ -/* tslint:disable:no-let no-object-mutation */ +/* tslint:disable:no-object-mutation */ import * as asn1js from 'asn1js'; import bufferToArray from 'buffer-to-arraybuffer'; @@ -97,7 +97,7 @@ describe('CargoMessageSet', () => { let privateKey: CryptoKey; let certificate: Certificate; - const PCA = new ParcelCollectionAck('https://sender.endpoint/', 'deadbeef', 'parcel-id'); + const PCA = new ParcelCollectionAck('0deadc0de', 'deadbeef', 'parcel-id'); beforeAll(async () => { const senderKeyPair = await generateRSAKeyPair(); @@ -109,7 +109,7 @@ describe('CargoMessageSet', () => { }); test('Parcels should be returned', async () => { - const parcel = new Parcel('0deadbeef', certificate, Buffer.from('hi')); + const parcel = new Parcel({ id: '0deadbeef' }, certificate, Buffer.from('hi')); const parcelSerialization = await parcel.serialize(privateKey); const item = await CargoMessageSet.deserializeItem(parcelSerialization); @@ -148,7 +148,7 @@ describe('CargoMessageSet', () => { }); test('An error should be yielded when unsupported RAMF message is found', async () => { - const innerCargo = new Cargo('address', certificate, Buffer.from('hi')); + const innerCargo = new Cargo({ id: 'address' }, certificate, Buffer.from('hi')); const cargoSerialization = await innerCargo.serialize(privateKey); await expect(CargoMessageSet.deserializeItem(cargoSerialization)).rejects.toThrow( diff --git a/src/lib/messages/payloads/CargoMessageSet.ts b/src/lib/messages/payloads/CargoMessageSet.ts index 30a0b262f..d919000c9 100644 --- a/src/lib/messages/payloads/CargoMessageSet.ts +++ b/src/lib/messages/payloads/CargoMessageSet.ts @@ -62,11 +62,10 @@ export default class CargoMessageSet implements PayloadPlaintext { public static async *batchMessagesSerialized( messagesWithExpiryDate: AsyncIterable, ): AsyncIterable { - // tslint:disable-next-line:readonly-array no-let + // tslint:disable-next-line:readonly-array let currentBatch: ArrayBuffer[] = []; - // tslint:disable-next-line:no-let no-unnecessary-initializer + // tslint:disable-next-line:no-unnecessary-initializer let currentBatchExpiryDate: Date | undefined = undefined; - // tslint:disable-next-line:no-let let availableOctetsInCurrentBatch = MAX_SDU_PLAINTEXT_LENGTH - DER_TL_OVERHEAD_OCTETS; for await (const { messageSerialized, expiryDate } of messagesWithExpiryDate) { @@ -133,7 +132,7 @@ export default class CargoMessageSet implements PayloadPlaintext { function getItemClass(itemSerialized: ArrayBuffer): { readonly deserialize: (s: ArrayBuffer) => CargoMessageSetItem | Promise; } { - const messageFormatSignature = Buffer.from(itemSerialized.slice(0, 10)); + const messageFormatSignature = Buffer.from(itemSerialized.slice(0, 7)); if (messageFormatSignature.equals(ParcelCollectionAck.FORMAT_SIGNATURE)) { return ParcelCollectionAck; diff --git a/src/lib/messages/payloads/ServiceMessage.spec.ts b/src/lib/messages/payloads/ServiceMessage.spec.ts index b6f6e4a18..12f2e171c 100644 --- a/src/lib/messages/payloads/ServiceMessage.spec.ts +++ b/src/lib/messages/payloads/ServiceMessage.spec.ts @@ -1,7 +1,11 @@ import * as asn1js from 'asn1js'; import bufferToArray from 'buffer-to-arraybuffer'; -import { arrayBufferFrom, expectArrayBuffersToEqual, getAsn1SequenceItem } from '../../_test_utils'; +import { + arrayBufferFrom, + expectArrayBuffersToEqual, + getPrimitiveItemFromConstructed, +} from '../../_test_utils'; import { makeImplicitlyTaggedSequence } from '../../asn1'; import { derDeserialize } from '../../crypto_wrappers/_utils'; import InvalidMessageError from '../InvalidMessageError'; @@ -19,7 +23,7 @@ describe('ServiceMessage', () => { const sequence = derDeserialize(serialization); expect(sequence).toBeInstanceOf(asn1js.Sequence); - const typeASN1 = getAsn1SequenceItem(sequence, 0); + const typeASN1 = getPrimitiveItemFromConstructed(sequence, 0); expect(typeASN1.valueBlock.valueHex).toEqual(arrayBufferFrom(TYPE)); }); @@ -30,7 +34,7 @@ describe('ServiceMessage', () => { const sequence = derDeserialize(serialization); expect(sequence).toBeInstanceOf(asn1js.Sequence); - const contentASN1 = getAsn1SequenceItem(sequence, 1); + const contentASN1 = getPrimitiveItemFromConstructed(sequence, 1); expectArrayBuffersToEqual(bufferToArray(CONTENT), contentASN1.valueBlock.valueHex); }); }); diff --git a/src/lib/nodes/Gateway.spec.ts b/src/lib/nodes/Gateway.spec.ts index 7d66fdefd..fe6de8a6e 100644 --- a/src/lib/nodes/Gateway.spec.ts +++ b/src/lib/nodes/Gateway.spec.ts @@ -1,7 +1,7 @@ import { addDays, setMilliseconds } from 'date-fns'; import { reSerializeCertificate } from '../_test_utils'; -import { generateRSAKeyPair, getPrivateAddressFromIdentityKey } from '../crypto_wrappers/keys'; +import { generateRSAKeyPair, getIdFromIdentityKey } from '../crypto_wrappers/keys'; import Certificate from '../crypto_wrappers/x509/Certificate'; import { MockKeyStoreSet } from '../keyStores/testMocks'; import { CertificationPath } from '../pki/CertificationPath'; @@ -9,11 +9,11 @@ import { issueGatewayCertificate } from '../pki/issuance'; import { Gateway } from './Gateway'; import { StubVerifier } from './signatures/_test_utils'; -let nodePrivateAddress: string; +let nodeId: string; let nodePrivateKey: CryptoKey; let nodeCertificate: Certificate; let nodeCertificateIssuer: Certificate; -let nodeCertificateIssuerPrivateAddress: string; +let nodeCertificateIssuerId: string; beforeAll(async () => { const tomorrow = setMilliseconds(addDays(new Date(), 1), 0); @@ -25,8 +25,7 @@ beforeAll(async () => { validityEndDate: tomorrow, }), ); - nodeCertificateIssuerPrivateAddress = - await nodeCertificateIssuer.calculateSubjectPrivateAddress(); + nodeCertificateIssuerId = await nodeCertificateIssuer.calculateSubjectId(); const nodeKeyPair = await generateRSAKeyPair(); nodePrivateKey = nodeKeyPair.privateKey; @@ -38,7 +37,7 @@ beforeAll(async () => { validityEndDate: tomorrow, }), ); - nodePrivateAddress = await getPrivateAddressFromIdentityKey(nodeKeyPair.publicKey); + nodeId = await getIdFromIdentityKey(nodeKeyPair.publicKey); }); const KEY_STORES = new MockKeyStoreSet(); @@ -48,31 +47,25 @@ beforeEach(async () => { describe('getGSCVerifier', () => { test('Certificates from a different issuer should be ignored', async () => { - const gateway = new StubGateway(nodePrivateAddress, nodePrivateKey, KEY_STORES, {}); + const gateway = new StubGateway(nodeId, nodePrivateKey, KEY_STORES, {}); await KEY_STORES.certificateStore.save( new CertificationPath(nodeCertificate, []), - nodeCertificateIssuerPrivateAddress, + nodeCertificateIssuerId, ); - const verifier = await gateway.getGSCVerifier( - `not-${nodeCertificateIssuerPrivateAddress}`, - StubVerifier, - ); + const verifier = await gateway.getGSCVerifier(`not-${nodeCertificateIssuerId}`, StubVerifier); expect(verifier.getTrustedCertificates()).toBeEmpty(); }); test('All certificates should be set as trusted', async () => { - const gateway = new StubGateway(nodePrivateAddress, nodePrivateKey, KEY_STORES, {}); + const gateway = new StubGateway(nodeId, nodePrivateKey, KEY_STORES, {}); await KEY_STORES.certificateStore.save( new CertificationPath(nodeCertificate, []), - nodeCertificateIssuerPrivateAddress, + nodeCertificateIssuerId, ); - const verifier = await gateway.getGSCVerifier( - nodeCertificateIssuerPrivateAddress, - StubVerifier, - ); + const verifier = await gateway.getGSCVerifier(nodeCertificateIssuerId, StubVerifier); const trustedCertificates = verifier!.getTrustedCertificates(); expect(trustedCertificates).toHaveLength(1); diff --git a/src/lib/nodes/Gateway.ts b/src/lib/nodes/Gateway.ts index c48b81a6b..214083b61 100644 --- a/src/lib/nodes/Gateway.ts +++ b/src/lib/nodes/Gateway.ts @@ -6,13 +6,10 @@ import { Verifier } from './signatures/Verifier'; export abstract class Gateway extends Node { public async getGSCVerifier( - peerPrivateAddress: string, + peerId: string, verifierClass: new (trustedCertificates: readonly Certificate[]) => V, ): Promise { - const trustedPaths = await this.keyStores.certificateStore.retrieveAll( - this.privateAddress, - peerPrivateAddress, - ); + const trustedPaths = await this.keyStores.certificateStore.retrieveAll(this.id, peerId); return new verifierClass(trustedPaths.map((p) => p.leafCertificate)); } } diff --git a/src/lib/nodes/Node.spec.ts b/src/lib/nodes/Node.spec.ts index 013dbbe69..5efff2275 100644 --- a/src/lib/nodes/Node.spec.ts +++ b/src/lib/nodes/Node.spec.ts @@ -5,7 +5,7 @@ import { SessionEnvelopedData } from '../crypto_wrappers/cms/envelopedData'; import { derSerializePublicKey, generateRSAKeyPair, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, } from '../crypto_wrappers/keys'; import Certificate from '../crypto_wrappers/x509/Certificate'; import { MockKeyStoreSet } from '../keyStores/testMocks'; @@ -15,11 +15,11 @@ import { issueGatewayCertificate } from '../pki/issuance'; import { StubMessage } from '../ramf/_test_utils'; import { StubNode } from './_test_utils'; -let nodePrivateAddress: string; +let nodeId: string; let nodePrivateKey: CryptoKey; let nodeCertificate: Certificate; let nodeCertificateIssuer: Certificate; -let nodeCertificateIssuerPrivateAddress: string; +let nodeCertificateIssuerId: string; beforeAll(async () => { const tomorrow = setMilliseconds(addDays(new Date(), 1), 0); @@ -31,8 +31,7 @@ beforeAll(async () => { validityEndDate: tomorrow, }), ); - nodeCertificateIssuerPrivateAddress = - await nodeCertificateIssuer.calculateSubjectPrivateAddress(); + nodeCertificateIssuerId = await nodeCertificateIssuer.calculateSubjectId(); const nodeKeyPair = await generateRSAKeyPair(); nodePrivateKey = nodeKeyPair.privateKey; @@ -44,7 +43,7 @@ beforeAll(async () => { validityEndDate: tomorrow, }), ); - nodePrivateAddress = await getPrivateAddressFromIdentityKey(nodeKeyPair.publicKey); + nodeId = await getIdFromIdentityKey(nodeKeyPair.publicKey); }); const KEY_STORES = new MockKeyStoreSet(); @@ -54,7 +53,7 @@ beforeEach(async () => { describe('getIdentityPublicKey', () => { test('Public key should be returned', async () => { - const node = new StubNode(nodePrivateAddress, nodePrivateKey, KEY_STORES, {}); + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); await expect(derSerializePublicKey(await node.getIdentityPublicKey())).resolves.toEqual( await derSerializePublicKey(nodePrivateKey), @@ -64,39 +63,33 @@ describe('getIdentityPublicKey', () => { describe('getGSCSigner', () => { test('Nothing should be returned if certificate does not exist', async () => { - const node = new StubNode(nodePrivateAddress, nodePrivateKey, KEY_STORES, {}); + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); await expect( - node.getGSCSigner(nodeCertificateIssuerPrivateAddress, ParcelDeliverySigner), + node.getGSCSigner(nodeCertificateIssuerId, ParcelDeliverySigner), ).resolves.toBeNull(); }); test('Signer should be of the type requested if certificate exists', async () => { - const node = new StubNode(nodePrivateAddress, nodePrivateKey, KEY_STORES, {}); + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); await KEY_STORES.certificateStore.save( new CertificationPath(nodeCertificate, []), - nodeCertificateIssuerPrivateAddress, + nodeCertificateIssuerId, ); - const signer = await node.getGSCSigner( - nodeCertificateIssuerPrivateAddress, - ParcelDeliverySigner, - ); + const signer = await node.getGSCSigner(nodeCertificateIssuerId, ParcelDeliverySigner); expect(signer).toBeInstanceOf(ParcelDeliverySigner); }); test('Signer should receive the certificate and private key of the node', async () => { - const node = new StubNode(nodePrivateAddress, nodePrivateKey, KEY_STORES, {}); + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); await KEY_STORES.certificateStore.save( new CertificationPath(nodeCertificate, []), - nodeCertificateIssuerPrivateAddress, + nodeCertificateIssuerId, ); - const signer = await node.getGSCSigner( - nodeCertificateIssuerPrivateAddress, - ParcelDeliverySigner, - ); + const signer = await node.getGSCSigner(nodeCertificateIssuerId, ParcelDeliverySigner); const plaintext = arrayBufferFrom('hiya'); const verifier = new ParcelDeliveryVerifier([nodeCertificateIssuer]); @@ -106,35 +99,27 @@ describe('getGSCSigner', () => { }); describe('generateSessionKey', () => { - const PRIVATE_ADDRESS = '0deadbeef'; test('Key should not be bound to any peer by default', async () => { - const node = new StubNode(nodePrivateAddress, nodePrivateKey, KEY_STORES, {}); + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); const sessionKey = await node.generateSessionKey(); await expect( derSerializePublicKey( - await KEY_STORES.privateKeyStore.retrieveUnboundSessionKey( - sessionKey.keyId, - node.privateAddress, - ), + await KEY_STORES.privateKeyStore.retrieveUnboundSessionKey(sessionKey.keyId, node.id), ), ).resolves.toEqual(await derSerializePublicKey(sessionKey.publicKey)); }); test('Key should be bound to a peer if explicitly set', async () => { - const node = new StubNode(nodePrivateAddress, nodePrivateKey, KEY_STORES, {}); - const peerPrivateAddress = `${PRIVATE_ADDRESS}cousin`; + const node = new StubNode(nodeId, nodePrivateKey, KEY_STORES, {}); + const peerId = '0deadbeef'; - const sessionKey = await node.generateSessionKey(peerPrivateAddress); + const sessionKey = await node.generateSessionKey(peerId); await expect( derSerializePublicKey( - await KEY_STORES.privateKeyStore.retrieveSessionKey( - sessionKey.keyId, - node.privateAddress, - peerPrivateAddress, - ), + await KEY_STORES.privateKeyStore.retrieveSessionKey(sessionKey.keyId, node.id, peerId), ), ).resolves.toEqual(await derSerializePublicKey(sessionKey.publicKey)); }); @@ -142,11 +127,12 @@ describe('generateSessionKey', () => { describe('unwrapMessagePayload', () => { const PAYLOAD_PLAINTEXT_CONTENT = arrayBufferFrom('payload content'); - const RECIPIENT_ADDRESS = 'https://example.com'; + let peerId: string; let peerCertificate: Certificate; beforeAll(async () => { const peerKeyPair = await generateRSAKeyPair(); + peerId = await getIdFromIdentityKey(peerKeyPair.publicKey); peerCertificate = await issueGatewayCertificate({ issuerPrivateKey: peerKeyPair.privateKey, subjectPublicKey: peerKeyPair.publicKey, @@ -155,14 +141,14 @@ describe('unwrapMessagePayload', () => { }); test('Payload plaintext should be returned', async () => { - const node = new StubNode(nodePrivateAddress, nodePrivateKey, KEY_STORES, {}); - const sessionKey = await node.generateSessionKey(); + const node = new StubNode(peerId, nodePrivateKey, KEY_STORES, {}); + const sessionKey = await node.generateSessionKey(peerId); const { envelopedData } = await SessionEnvelopedData.encrypt( PAYLOAD_PLAINTEXT_CONTENT, sessionKey, ); const message = new StubMessage( - RECIPIENT_ADDRESS, + { id: peerId }, peerCertificate, Buffer.from(envelopedData.serialize()), ); @@ -173,14 +159,14 @@ describe('unwrapMessagePayload', () => { }); test('Originator session key should be stored', async () => { - const node = new StubNode(nodePrivateAddress, nodePrivateKey, KEY_STORES, {}); - const sessionKey = await node.generateSessionKey(); + const node = new StubNode(peerId, nodePrivateKey, KEY_STORES, {}); + const sessionKey = await node.generateSessionKey(peerId); const { envelopedData, dhKeyId } = await SessionEnvelopedData.encrypt( PAYLOAD_PLAINTEXT_CONTENT, sessionKey, ); const message = new StubMessage( - RECIPIENT_ADDRESS, + { id: peerId }, peerCertificate, Buffer.from(envelopedData.serialize()), ); @@ -188,7 +174,7 @@ describe('unwrapMessagePayload', () => { await node.unwrapMessagePayload(message); const storedKey = - KEY_STORES.publicKeyStore.sessionKeys[await peerCertificate.calculateSubjectPrivateAddress()]; + KEY_STORES.publicKeyStore.sessionKeys[await peerCertificate.calculateSubjectId()]; expect(storedKey.publicKeyCreationTime).toEqual(message.creationDate); expect(storedKey.publicKeyId).toEqual(Buffer.from(dhKeyId)); expect(storedKey.publicKeyDer).toEqual( diff --git a/src/lib/nodes/Node.ts b/src/lib/nodes/Node.ts index c65fe7f13..e4e885e0f 100644 --- a/src/lib/nodes/Node.ts +++ b/src/lib/nodes/Node.ts @@ -10,7 +10,7 @@ import { Signer } from './signatures/Signer'; export abstract class Node { constructor( - public readonly privateAddress: string, + public readonly id: string, protected readonly identityPrivateKey: CryptoKey, protected readonly keyStores: KeyStoreSet, protected readonly cryptoOptions: Partial, @@ -23,27 +23,24 @@ export abstract class Node { /** * Generate and store a new session key. * - * @param peerPrivateAddress The peer to bind the key to, unless it's an initial key + * @param peerId The peer to bind the key to, unless it's an initial key */ - public async generateSessionKey(peerPrivateAddress?: string): Promise { + public async generateSessionKey(peerId?: string): Promise { const { sessionKey, privateKey } = await SessionKeyPair.generate(); await this.keyStores.privateKeyStore.saveSessionKey( privateKey, sessionKey.keyId, - this.privateAddress, - peerPrivateAddress, + this.id, + peerId, ); return sessionKey; } public async getGSCSigner( - peerPrivateAddress: string, + peerId: string, signerClass: new (certificate: Certificate, privateKey: CryptoKey) => S, ): Promise { - const path = await this.keyStores.certificateStore.retrieveLatest( - this.privateAddress, - peerPrivateAddress, - ); + const path = await this.keyStores.certificateStore.retrieveLatest(this.id, peerId); if (!path) { return null; } @@ -58,14 +55,11 @@ export abstract class Node { * @param message */ public async unwrapMessagePayload

(message: RAMFMessage

): Promise

{ - const unwrapResult = await message.unwrapPayload( - this.keyStores.privateKeyStore, - this.privateAddress, - ); + const unwrapResult = await message.unwrapPayload(this.keyStores.privateKeyStore); await this.keyStores.publicKeyStore.saveSessionKey( unwrapResult.senderSessionKey, - await message.senderCertificate.calculateSubjectPrivateAddress(), + await message.senderCertificate.calculateSubjectId(), message.creationDate, ); diff --git a/src/lib/nodes/PrivateGateway.spec.ts b/src/lib/nodes/PrivateGateway.spec.ts index 9b2ac5b30..0d810ca55 100644 --- a/src/lib/nodes/PrivateGateway.spec.ts +++ b/src/lib/nodes/PrivateGateway.spec.ts @@ -5,7 +5,7 @@ import { PrivateNodeRegistrationRequest } from '../bindings/gsc/PrivateNodeRegis import { derSerializePublicKey, generateRSAKeyPair, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, getRSAPublicKeyFromPrivate, } from '../crypto_wrappers/keys'; import Certificate from '../crypto_wrappers/x509/Certificate'; @@ -17,25 +17,25 @@ import { SessionKeyPair } from '../SessionKeyPair'; import { NodeError } from './errors'; import { PrivateGateway } from './PrivateGateway'; -const PUBLIC_GATEWAY_PUBLIC_ADDRESS = 'example.com'; +const INTERNET_GATEWAY_INTERNET_ADDRESS = 'example.com'; -let publicGatewayPrivateAddress: string; -let publicGatewayPublicKey: CryptoKey; -let publicGatewayCertificate: Certificate; -let privateGatewayPrivateAddress: string; +let internetGatewayId: string; +let internetGatewayPublicKey: CryptoKey; +let internetGatewayCertificate: Certificate; +let privateGatewayId: string; let privateGatewayPrivateKey: CryptoKey; let privateGatewayPDCCertificate: Certificate; beforeAll(async () => { const tomorrow = setMilliseconds(addDays(new Date(), 1), 0); - // Public gateway - const publicGatewayKeyPair = await generateRSAKeyPair(); - publicGatewayPublicKey = publicGatewayKeyPair.publicKey; - publicGatewayPrivateAddress = await getPrivateAddressFromIdentityKey(publicGatewayPublicKey); - publicGatewayCertificate = reSerializeCertificate( + // Internet gateway + const internetGatewayKeyPair = await generateRSAKeyPair(); + internetGatewayPublicKey = internetGatewayKeyPair.publicKey; + internetGatewayId = await getIdFromIdentityKey(internetGatewayPublicKey); + internetGatewayCertificate = reSerializeCertificate( await issueGatewayCertificate({ - issuerPrivateKey: publicGatewayKeyPair.privateKey, - subjectPublicKey: publicGatewayPublicKey, + issuerPrivateKey: internetGatewayKeyPair.privateKey, + subjectPublicKey: internetGatewayPublicKey, validityEndDate: tomorrow, }), ); @@ -43,13 +43,11 @@ beforeAll(async () => { // Private gateway const privateGatewayKeyPair = await generateRSAKeyPair(); privateGatewayPrivateKey = privateGatewayKeyPair.privateKey; - privateGatewayPrivateAddress = await getPrivateAddressFromIdentityKey( - privateGatewayKeyPair.publicKey, - ); + privateGatewayId = await getIdFromIdentityKey(privateGatewayKeyPair.publicKey); privateGatewayPDCCertificate = reSerializeCertificate( await issueGatewayCertificate({ - issuerCertificate: publicGatewayCertificate, - issuerPrivateKey: publicGatewayKeyPair.privateKey, + issuerCertificate: internetGatewayCertificate, + issuerPrivateKey: internetGatewayKeyPair.privateKey, subjectPublicKey: privateGatewayKeyPair.publicKey, validityEndDate: tomorrow, }), @@ -61,18 +59,18 @@ afterEach(() => { KEY_STORES.clear(); }); -describe('requestPublicGatewayRegistration', () => { +describe('requestInternetGatewayRegistration', () => { const AUTHORIZATION_SERIALIZED = arrayBufferFrom('Go ahead'); test('Registration authorization should be honoured', async () => { const privateGateway = new PrivateGateway( - privateGatewayPrivateAddress, + privateGatewayId, privateGatewayPrivateKey, KEY_STORES, {}, ); - const requestSerialized = await privateGateway.requestPublicGatewayRegistration( + const requestSerialized = await privateGateway.requestInternetGatewayRegistration( AUTHORIZATION_SERIALIZED, ); @@ -82,13 +80,13 @@ describe('requestPublicGatewayRegistration', () => { test('Public key should be honoured', async () => { const privateGateway = new PrivateGateway( - privateGatewayPrivateAddress, + privateGatewayId, privateGatewayPrivateKey, KEY_STORES, {}, ); - const requestSerialized = await privateGateway.requestPublicGatewayRegistration( + const requestSerialized = await privateGateway.requestInternetGatewayRegistration( AUTHORIZATION_SERIALIZED, ); @@ -99,136 +97,136 @@ describe('requestPublicGatewayRegistration', () => { }); }); -describe('savePublicGatewayChannel', () => { - let publicGatewaySessionPublicKey: SessionKey; +describe('saveInternetGatewayChannel', () => { + let internetGatewaySessionPublicKey: SessionKey; beforeAll(async () => { - const publicGatewaySessionKeyPair = await SessionKeyPair.generate(); - publicGatewaySessionPublicKey = publicGatewaySessionKeyPair.sessionKey; + const internetGatewaySessionKeyPair = await SessionKeyPair.generate(); + internetGatewaySessionPublicKey = internetGatewaySessionKeyPair.sessionKey; }); - test('Registration should be refused if public gateway did not issue authorization', async () => { + test('Registration should be refused if Internet gateway did not issue authorization', async () => { const privateGateway = new PrivateGateway( - privateGatewayPrivateAddress, + privateGatewayId, privateGatewayPrivateKey, KEY_STORES, {}, ); await expect( - privateGateway.savePublicGatewayChannel( + privateGateway.saveInternetGatewayChannel( privateGatewayPDCCertificate, privateGatewayPDCCertificate, // Invalid - publicGatewaySessionPublicKey, + internetGatewaySessionPublicKey, ), ).rejects.toThrowWithMessage( NodeError, - 'Delivery authorization was not issued by public gateway', + 'Delivery authorization was not issued by Internet gateway', ); }); test('Delivery authorisation should be stored', async () => { const privateGateway = new PrivateGateway( - privateGatewayPrivateAddress, + privateGatewayId, privateGatewayPrivateKey, KEY_STORES, {}, ); - await privateGateway.savePublicGatewayChannel( + await privateGateway.saveInternetGatewayChannel( privateGatewayPDCCertificate, - publicGatewayCertificate, - publicGatewaySessionPublicKey, + internetGatewayCertificate, + internetGatewaySessionPublicKey, ); const path = await KEY_STORES.certificateStore.retrieveLatest( - privateGatewayPrivateAddress, - publicGatewayPrivateAddress, + privateGatewayId, + internetGatewayId, ); expect(path!.leafCertificate.isEqual(privateGatewayPDCCertificate)); expect(path!.certificateAuthorities).toHaveLength(0); }); - test('Public key of public gateway should be stored', async () => { + test('Public key of Internet gateway should be stored', async () => { const privateGateway = new PrivateGateway( - privateGatewayPrivateAddress, + privateGatewayId, privateGatewayPrivateKey, KEY_STORES, {}, ); - await privateGateway.savePublicGatewayChannel( + await privateGateway.saveInternetGatewayChannel( privateGatewayPDCCertificate, - publicGatewayCertificate, - publicGatewaySessionPublicKey, + internetGatewayCertificate, + internetGatewaySessionPublicKey, ); - const publicGatewayPublicKeyRetrieved = await KEY_STORES.publicKeyStore.retrieveIdentityKey( - publicGatewayPrivateAddress, + const internetGatewayPublicKeyRetrieved = await KEY_STORES.publicKeyStore.retrieveIdentityKey( + internetGatewayId, ); - expect(publicGatewayPublicKeyRetrieved).toBeTruthy(); - await expect(derSerializePublicKey(publicGatewayPublicKeyRetrieved!)).resolves.toEqual( - await derSerializePublicKey(publicGatewayPublicKey), + expect(internetGatewayPublicKeyRetrieved).toBeTruthy(); + await expect(derSerializePublicKey(internetGatewayPublicKeyRetrieved!)).resolves.toEqual( + await derSerializePublicKey(internetGatewayPublicKey), ); }); - test('Session public key of public gateway should be stored', async () => { + test('Session public key of Internet gateway should be stored', async () => { const privateGateway = new PrivateGateway( - privateGatewayPrivateAddress, + privateGatewayId, privateGatewayPrivateKey, KEY_STORES, {}, ); - await privateGateway.savePublicGatewayChannel( + await privateGateway.saveInternetGatewayChannel( privateGatewayPDCCertificate, - publicGatewayCertificate, - publicGatewaySessionPublicKey, + internetGatewayCertificate, + internetGatewaySessionPublicKey, ); - const keyData = KEY_STORES.publicKeyStore.sessionKeys[publicGatewayPrivateAddress]; + const keyData = KEY_STORES.publicKeyStore.sessionKeys[internetGatewayId]; expect(keyData.publicKeyDer).toEqual( - await derSerializePublicKey(publicGatewaySessionPublicKey.publicKey), + await derSerializePublicKey(internetGatewaySessionPublicKey.publicKey), ); - expect(keyData.publicKeyId).toEqual(publicGatewaySessionPublicKey.keyId); + expect(keyData.publicKeyId).toEqual(internetGatewaySessionPublicKey.keyId); expect(keyData.publicKeyCreationTime).toBeBeforeOrEqualTo(new Date()); expect(keyData.publicKeyCreationTime).toBeAfter(subSeconds(new Date(), 10)); }); }); -describe('retrievePublicGatewayChannel', () => { - test('Null should be returned if public gateway public key is not found', async () => { +describe('retrieveInternetGatewayChannel', () => { + test('Null should be returned if Internet gateway public key is not found', async () => { await KEY_STORES.certificateStore.save( new CertificationPath(privateGatewayPDCCertificate, []), - publicGatewayPrivateAddress, + internetGatewayId, ); const privateGateway = new PrivateGateway( - privateGatewayPrivateAddress, + privateGatewayId, privateGatewayPrivateKey, KEY_STORES, {}, ); await expect( - privateGateway.retrievePublicGatewayChannel( - publicGatewayPrivateAddress, - PUBLIC_GATEWAY_PUBLIC_ADDRESS, + privateGateway.retrieveInternetGatewayChannel( + internetGatewayId, + INTERNET_GATEWAY_INTERNET_ADDRESS, ), ).resolves.toBeNull(); }); test('Null should be returned if delivery authorization is not found', async () => { - await KEY_STORES.publicKeyStore.saveIdentityKey(publicGatewayPublicKey); + await KEY_STORES.publicKeyStore.saveIdentityKey(internetGatewayPublicKey); const privateGateway = new PrivateGateway( - privateGatewayPrivateAddress, + privateGatewayId, privateGatewayPrivateKey, KEY_STORES, {}, ); await expect( - privateGateway.retrievePublicGatewayChannel( - publicGatewayPrivateAddress, - PUBLIC_GATEWAY_PUBLIC_ADDRESS, + privateGateway.retrieveInternetGatewayChannel( + internetGatewayId, + INTERNET_GATEWAY_INTERNET_ADDRESS, ), ).resolves.toBeNull(); }); @@ -236,46 +234,46 @@ describe('retrievePublicGatewayChannel', () => { test('Channel should be returned if it exists', async () => { await KEY_STORES.certificateStore.save( new CertificationPath(privateGatewayPDCCertificate, []), - publicGatewayPrivateAddress, + internetGatewayId, ); - await KEY_STORES.publicKeyStore.saveIdentityKey(publicGatewayPublicKey); + await KEY_STORES.publicKeyStore.saveIdentityKey(internetGatewayPublicKey); const privateGateway = new PrivateGateway( - privateGatewayPrivateAddress, + privateGatewayId, privateGatewayPrivateKey, KEY_STORES, {}, ); - const channel = await privateGateway.retrievePublicGatewayChannel( - publicGatewayPrivateAddress, - PUBLIC_GATEWAY_PUBLIC_ADDRESS, + const channel = await privateGateway.retrieveInternetGatewayChannel( + internetGatewayId, + INTERNET_GATEWAY_INTERNET_ADDRESS, ); - expect(channel!.publicGatewayPublicAddress).toEqual(PUBLIC_GATEWAY_PUBLIC_ADDRESS); + expect(channel!.internetGatewayInternetAddress).toEqual(INTERNET_GATEWAY_INTERNET_ADDRESS); expect(channel!.nodeDeliveryAuth.isEqual(privateGatewayPDCCertificate)).toBeTrue(); - expect(channel!.peerPrivateAddress).toEqual(publicGatewayPrivateAddress); + expect(channel!.peerId).toEqual(internetGatewayId); await expect(derSerializePublicKey(channel!.peerPublicKey)).resolves.toEqual( - await derSerializePublicKey(publicGatewayPublicKey), + await derSerializePublicKey(internetGatewayPublicKey), ); }); test('Crypto options should be passed', async () => { await KEY_STORES.certificateStore.save( new CertificationPath(privateGatewayPDCCertificate, []), - publicGatewayPrivateAddress, + internetGatewayId, ); - await KEY_STORES.publicKeyStore.saveIdentityKey(publicGatewayPublicKey); + await KEY_STORES.publicKeyStore.saveIdentityKey(internetGatewayPublicKey); const cryptoOptions = { encryption: { aesKeySize: 256 } }; const privateGateway = new PrivateGateway( - privateGatewayPrivateAddress, + privateGatewayId, privateGatewayPrivateKey, KEY_STORES, cryptoOptions, ); - const channel = await privateGateway.retrievePublicGatewayChannel( - publicGatewayPrivateAddress, - PUBLIC_GATEWAY_PUBLIC_ADDRESS, + const channel = await privateGateway.retrieveInternetGatewayChannel( + internetGatewayId, + INTERNET_GATEWAY_INTERNET_ADDRESS, ); expect(channel?.cryptoOptions).toEqual(cryptoOptions); diff --git a/src/lib/nodes/PrivateGateway.ts b/src/lib/nodes/PrivateGateway.ts index 7564746a7..939962725 100644 --- a/src/lib/nodes/PrivateGateway.ts +++ b/src/lib/nodes/PrivateGateway.ts @@ -2,17 +2,17 @@ import { PrivateNodeRegistrationRequest } from '../bindings/gsc/PrivateNodeRegis import Certificate from '../crypto_wrappers/x509/Certificate'; import { CertificationPath } from '../pki/CertificationPath'; import { SessionKey } from '../SessionKey'; -import { PrivatePublicGatewayChannel } from './channels/PrivatePublicGatewayChannel'; +import { PrivateInternetGatewayChannel } from './channels/PrivateInternetGatewayChannel'; import { NodeError } from './errors'; import { Gateway } from './Gateway'; export class PrivateGateway extends Gateway { /** - * Produce a `PrivateNodeRegistrationRequest` to register with a public gateway. + * Produce a `PrivateNodeRegistrationRequest` to register with a Internet gateway. * * @param authorizationSerialized */ - public async requestPublicGatewayRegistration( + public async requestInternetGatewayRegistration( authorizationSerialized: ArrayBuffer, ): Promise { const request = new PrivateNodeRegistrationRequest( @@ -23,65 +23,65 @@ export class PrivateGateway extends Gateway { } /** - * Create channel with public gateway using registration details. + * Create channel with Internet gateway using registration details. * * @param deliveryAuthorization - * @param publicGatewayIdentityCertificate - * @param publicGatewaySessionPublicKey - * @throws NodeError if the `publicGatewayIdentityCertificate` didn't issue + * @param internetGatewayIdentityCertificate + * @param internetGatewaySessionPublicKey + * @throws NodeError if the `internetGatewayIdentityCertificate` didn't issue * `deliveryAuthorization` */ - public async savePublicGatewayChannel( + public async saveInternetGatewayChannel( deliveryAuthorization: Certificate, - publicGatewayIdentityCertificate: Certificate, - publicGatewaySessionPublicKey: SessionKey, + internetGatewayIdentityCertificate: Certificate, + internetGatewaySessionPublicKey: SessionKey, ): Promise { try { - await deliveryAuthorization.getCertificationPath([], [publicGatewayIdentityCertificate]); + await deliveryAuthorization.getCertificationPath([], [internetGatewayIdentityCertificate]); } catch (_) { - throw new NodeError('Delivery authorization was not issued by public gateway'); + throw new NodeError('Delivery authorization was not issued by Internet gateway'); } - const publicGatewayPrivateAddress = deliveryAuthorization.getIssuerPrivateAddress()!; + const internetGatewayId = deliveryAuthorization.getIssuerId()!; await this.keyStores.certificateStore.save( new CertificationPath(deliveryAuthorization, []), - publicGatewayPrivateAddress, + internetGatewayId, ); await this.keyStores.publicKeyStore.saveIdentityKey( - await publicGatewayIdentityCertificate.getPublicKey(), + await internetGatewayIdentityCertificate.getPublicKey(), ); await this.keyStores.publicKeyStore.saveSessionKey( - publicGatewaySessionPublicKey, - publicGatewayPrivateAddress, + internetGatewaySessionPublicKey, + internetGatewayId, new Date(), ); } - public async retrievePublicGatewayChannel( - publicGatewayPrivateAddress: string, - publicGatewayPublicAddress: string, - ): Promise { - const publicGatewayPublicKey = await this.keyStores.publicKeyStore.retrieveIdentityKey( - publicGatewayPrivateAddress, + public async retrieveInternetGatewayChannel( + internetGatewayId: string, + internetGatewayInternetAddress: string, + ): Promise { + const internetGatewayPublicKey = await this.keyStores.publicKeyStore.retrieveIdentityKey( + internetGatewayId, ); - if (!publicGatewayPublicKey) { + if (!internetGatewayPublicKey) { return null; } const privateGatewayDeliveryAuth = await this.keyStores.certificateStore.retrieveLatest( - this.privateAddress, - publicGatewayPrivateAddress, + this.id, + internetGatewayId, ); if (!privateGatewayDeliveryAuth) { return null; } - return new PrivatePublicGatewayChannel( + return new PrivateInternetGatewayChannel( this.identityPrivateKey, privateGatewayDeliveryAuth.leafCertificate, - publicGatewayPrivateAddress, - publicGatewayPublicKey, - publicGatewayPublicAddress, + internetGatewayId, + internetGatewayPublicKey, + internetGatewayInternetAddress, this.keyStores, this.cryptoOptions, ); diff --git a/src/lib/nodes/PublicNodeConnectionParams.spec.ts b/src/lib/nodes/PublicNodeConnectionParams.spec.ts index ee34ca02d..ca723fc2e 100644 --- a/src/lib/nodes/PublicNodeConnectionParams.spec.ts +++ b/src/lib/nodes/PublicNodeConnectionParams.spec.ts @@ -29,7 +29,7 @@ beforeAll(async () => { }); describe('serialize', () => { - test('Public address should be serialized', async () => { + test('Internet address should be serialized', async () => { const params = new PublicNodeConnectionParams(PUBLIC_ADDRESS, identityKey, sessionKey); const serialization = await params.serialize(); @@ -130,17 +130,17 @@ describe('deserialize', () => { ).rejects.toThrowWithMessage(InvalidPublicNodeConnectionParams, malformedErrorMessage); }); - test('Public address should be syntactically valid', async () => { - const invalidPublicAddress = 'not a public address'; + test('Internet address should be syntactically valid', async () => { + const invalidInternetAddress = 'not a domain name'; const invalidSerialization = makeImplicitlyTaggedSequence( - new VisibleString({ value: invalidPublicAddress }), + new VisibleString({ value: invalidInternetAddress }), new OctetString({ valueHex: identityKeySerialized }), sessionKeySequence, ).toBER(); await expect(PublicNodeConnectionParams.deserialize(invalidSerialization)).rejects.toThrow( new InvalidPublicNodeConnectionParams( - `Public address is syntactically invalid (${invalidPublicAddress})`, + `Internet address is syntactically invalid (${invalidInternetAddress})`, ), ); }); @@ -207,7 +207,7 @@ describe('deserialize', () => { const paramsDeserialized = await PublicNodeConnectionParams.deserialize(serialization); - expect(paramsDeserialized.publicAddress).toEqual(PUBLIC_ADDRESS); + expect(paramsDeserialized.internetAddress).toEqual(PUBLIC_ADDRESS); await expect(derSerializePublicKey(paramsDeserialized.identityKey)).resolves.toEqual( Buffer.from(identityKeySerialized), ); diff --git a/src/lib/nodes/PublicNodeConnectionParams.ts b/src/lib/nodes/PublicNodeConnectionParams.ts index 3bb842a54..d0e719224 100644 --- a/src/lib/nodes/PublicNodeConnectionParams.ts +++ b/src/lib/nodes/PublicNodeConnectionParams.ts @@ -24,10 +24,10 @@ export class PublicNodeConnectionParams { const paramsASN1 = (result.result as any).PublicNodeConnectionParams; const textDecoder = new TextDecoder(); - const publicAddress = textDecoder.decode(paramsASN1.publicAddress.valueBlock.valueHex); - if (!isValidDomain(publicAddress)) { + const internetAddress = textDecoder.decode(paramsASN1.internetAddress.valueBlock.valueHex); + if (!isValidDomain(internetAddress)) { throw new InvalidPublicNodeConnectionParams( - `Public address is syntactically invalid (${publicAddress})`, + `Internet address is syntactically invalid (${internetAddress})`, ); } @@ -59,26 +59,26 @@ export class PublicNodeConnectionParams { ); } - return new PublicNodeConnectionParams(publicAddress, identityKey, { + return new PublicNodeConnectionParams(internetAddress, identityKey, { keyId: Buffer.from(sessionKeyId), publicKey: sessionPublicKey, }); } private static readonly SCHEMA = makeHeterogeneousSequenceSchema('PublicNodeConnectionParams', [ - new Primitive({ name: 'publicAddress' }), + new Primitive({ name: 'internetAddress' }), new Primitive({ name: 'identityKey' }), new Constructed({ name: 'sessionKey', value: [ - new Primitive({ idBlock: { tagClass: 3, tagNumber: 0 } } as any), - new Primitive({ idBlock: { tagClass: 3, tagNumber: 1 } } as any), + new Primitive({ idBlock: { tagClass: 3, tagNumber: 0 } }), + new Primitive({ idBlock: { tagClass: 3, tagNumber: 1 } }), ], - } as any), + }), ]); constructor( - public readonly publicAddress: string, + public readonly internetAddress: string, public readonly identityKey: CryptoKey, public readonly sessionKey: SessionKey, ) {} @@ -93,7 +93,7 @@ export class PublicNodeConnectionParams { ); return makeImplicitlyTaggedSequence( - new VisibleString({ value: this.publicAddress }), + new VisibleString({ value: this.internetAddress }), new OctetString({ valueHex: bufferToArray(identityKeySerialized) }), sessionKeySequence, ).toBER(); diff --git a/src/lib/nodes/channels/Channel.spec.ts b/src/lib/nodes/channels/Channel.spec.ts index 866d9e0b0..707415a17 100644 --- a/src/lib/nodes/channels/Channel.spec.ts +++ b/src/lib/nodes/channels/Channel.spec.ts @@ -5,17 +5,18 @@ import { SessionEnvelopedData } from '../../crypto_wrappers/cms/envelopedData'; import { generateECDHKeyPair, generateRSAKeyPair, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, } from '../../crypto_wrappers/keys'; import Certificate from '../../crypto_wrappers/x509/Certificate'; 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 { NodeError } from '../errors'; import { Channel } from './Channel'; -let peerPrivateAddress: string; +let peerId: string; let peerPublicKey: CryptoKey; let nodePrivateKey: CryptoKey; let nodeCertificate: Certificate; @@ -23,7 +24,7 @@ beforeAll(async () => { const tomorrow = setMilliseconds(addDays(new Date(), 1), 0); const peerKeyPair = await generateRSAKeyPair(); - peerPrivateAddress = await getPrivateAddressFromIdentityKey(peerKeyPair.publicKey); + peerId = await getIdFromIdentityKey(peerKeyPair.publicKey); peerPublicKey = peerKeyPair.publicKey; const peerCertificate = reSerializeCertificate( await issueGatewayCertificate({ @@ -64,22 +65,22 @@ describe('wrapMessagePayload', () => { keyId: Buffer.from('key id'), publicKey: recipientSessionKeyPair.publicKey, }; - await KEY_STORES.publicKeyStore.saveSessionKey(peerSessionKey, peerPrivateAddress, new Date()); + await KEY_STORES.publicKeyStore.saveSessionKey(peerSessionKey, peerId, new Date()); }); test('There should be a session key for the recipient', async () => { - const unknownPeerPrivateAddress = `not-${peerPrivateAddress}`; + const unknownPeerId = `not-${peerId}`; const channel = new StubChannel( nodePrivateKey, nodeCertificate, - unknownPeerPrivateAddress, + unknownPeerId, peerPublicKey, KEY_STORES, ); await expect(channel.wrapMessagePayload(stubPayload)).rejects.toThrowWithMessage( NodeError, - `Could not find session key for peer ${unknownPeerPrivateAddress}`, + `Could not find session key for peer ${unknownPeerId}`, ); }); @@ -87,7 +88,7 @@ describe('wrapMessagePayload', () => { const channel = new StubChannel( nodePrivateKey, nodeCertificate, - peerPrivateAddress, + peerId, peerPublicKey, KEY_STORES, ); @@ -106,7 +107,7 @@ describe('wrapMessagePayload', () => { const channel = new StubChannel( nodePrivateKey, nodeCertificate, - peerPrivateAddress, + peerId, peerPublicKey, KEY_STORES, ); @@ -123,7 +124,7 @@ describe('wrapMessagePayload', () => { const channel = new StubChannel( nodePrivateKey, nodeCertificate, - peerPrivateAddress, + peerId, peerPublicKey, KEY_STORES, ); @@ -137,8 +138,8 @@ describe('wrapMessagePayload', () => { await expect( KEY_STORES.privateKeyStore.retrieveSessionKey( originatorSessionKey.keyId, - await nodeCertificate.calculateSubjectPrivateAddress(), - peerPrivateAddress, + await nodeCertificate.calculateSubjectId(), + peerId, ), ).resolves.toBeTruthy(); }); @@ -148,7 +149,7 @@ describe('wrapMessagePayload', () => { const channel = new StubChannel( nodePrivateKey, nodeCertificate, - peerPrivateAddress, + peerId, peerPublicKey, KEY_STORES, { encryption: { aesKeySize } }, @@ -165,7 +166,7 @@ describe('wrapMessagePayload', () => { }); class StubChannel extends Channel { - async getOutboundRAMFAddress(): Promise { + async getOutboundRAMFRecipient(): Promise { throw new Error('not implemented'); } } diff --git a/src/lib/nodes/channels/Channel.ts b/src/lib/nodes/channels/Channel.ts index 87347282f..6419f301d 100644 --- a/src/lib/nodes/channels/Channel.ts +++ b/src/lib/nodes/channels/Channel.ts @@ -1,8 +1,9 @@ import { SessionEnvelopedData } from '../../crypto_wrappers/cms/envelopedData'; -import { getPrivateAddressFromIdentityKey } from '../../crypto_wrappers/keys'; +import { getIdFromIdentityKey } from '../../crypto_wrappers/keys'; import Certificate from '../../crypto_wrappers/x509/Certificate'; import { KeyStoreSet } from '../../keyStores/KeyStoreSet'; import PayloadPlaintext from '../../messages/payloads/PayloadPlaintext'; +import { Recipient } from '../../messages/Recipient'; import { NodeError } from '../errors'; import { NodeCryptoOptions } from '../NodeCryptoOptions'; @@ -11,7 +12,7 @@ export abstract class Channel { constructor( protected readonly nodePrivateKey: CryptoKey, public readonly nodeDeliveryAuth: Certificate, - public readonly peerPrivateAddress: string, + public readonly peerId: string, public readonly peerPublicKey: CryptoKey, protected readonly keyStores: KeyStoreSet, public cryptoOptions: Partial = {}, @@ -26,10 +27,10 @@ export abstract class Channel { */ public async wrapMessagePayload(payload: PayloadPlaintext | ArrayBuffer): Promise { const recipientSessionKey = await this.keyStores.publicKeyStore.retrieveLastSessionKey( - this.peerPrivateAddress, + this.peerId, ); if (!recipientSessionKey) { - throw new NodeError(`Could not find session key for peer ${this.peerPrivateAddress}`); + throw new NodeError(`Could not find session key for peer ${this.peerId}`); } const { envelopedData, dhKeyId, dhPrivateKey } = await SessionEnvelopedData.encrypt( payload instanceof ArrayBuffer ? payload : payload.serialize(), @@ -39,15 +40,15 @@ export abstract class Channel { await this.keyStores.privateKeyStore.saveSessionKey( dhPrivateKey, Buffer.from(dhKeyId), - await this.getNodePrivateAddress(), - this.peerPrivateAddress, + await this.getNodeId(), + this.peerId, ); return envelopedData.serialize(); } - public abstract getOutboundRAMFAddress(): Promise; + public abstract getOutboundRAMFRecipient(): Promise; - protected async getNodePrivateAddress(): Promise { - return getPrivateAddressFromIdentityKey(this.nodePrivateKey); + protected async getNodeId(): Promise { + return getIdFromIdentityKey(this.nodePrivateKey); } } diff --git a/src/lib/nodes/channels/GatewayChannel.spec.ts b/src/lib/nodes/channels/GatewayChannel.spec.ts index 529055ed7..e392320ba 100644 --- a/src/lib/nodes/channels/GatewayChannel.spec.ts +++ b/src/lib/nodes/channels/GatewayChannel.spec.ts @@ -9,13 +9,14 @@ import { } from '../../_test_utils'; import { EnvelopedData, SessionEnvelopedData } from '../../crypto_wrappers/cms/envelopedData'; import { SignatureOptions } from '../../crypto_wrappers/cms/SignatureOptions'; -import { generateRSAKeyPair, getPrivateAddressFromIdentityKey } from '../../crypto_wrappers/keys'; +import { generateRSAKeyPair, getIdFromIdentityKey } from '../../crypto_wrappers/keys'; import Certificate from '../../crypto_wrappers/x509/Certificate'; import { MockKeyStoreSet } from '../../keyStores/testMocks'; import Cargo from '../../messages/Cargo'; import Parcel from '../../messages/Parcel'; import CargoMessageSet from '../../messages/payloads/CargoMessageSet'; import ServiceMessage from '../../messages/payloads/ServiceMessage'; +import { Recipient } from '../../messages/Recipient'; import { issueGatewayCertificate } from '../../pki/issuance'; import { RAMF_MAX_TTL } from '../../ramf/serialization'; import { SessionKey } from '../../SessionKey'; @@ -27,7 +28,7 @@ const MESSAGE = Buffer.from('This is a message to be included in a cargo'); const TOMORROW = setMilliseconds(addDays(new Date(), 1), 0); -let peerPrivateAddress: string; +let peerId: string; let peerPublicKey: CryptoKey; let nodePrivateKey: CryptoKey; let nodeCertificate: Certificate; @@ -35,7 +36,7 @@ beforeAll(async () => { const tomorrow = setMilliseconds(addDays(new Date(), 1), 0); const peerKeyPair = await generateRSAKeyPair(); - peerPrivateAddress = await getPrivateAddressFromIdentityKey(peerKeyPair.publicKey); + peerId = await getIdFromIdentityKey(peerKeyPair.publicKey); peerPublicKey = peerKeyPair.publicKey; const peerCertificate = reSerializeCertificate( await issueGatewayCertificate({ @@ -71,7 +72,7 @@ describe('generateCargoes', () => { beforeEach(async () => { await KEY_STORES.publicKeyStore.saveSessionKey( peerSessionKeyPair.sessionKey, - peerPrivateAddress, + peerId, new Date(), ); }); @@ -91,7 +92,7 @@ describe('generateCargoes', () => { ); const cargo = await Cargo.deserialize(bufferToArray(cargoesSerialized[0])); - expect(cargo.recipientAddress).toEqual(StubGatewayChannel.OUTBOUND_RAMF_ADDRESS); + expect(cargo.recipient.id).toEqual(StubGatewayChannel.OUTBOUND_RAMF_RECIPIENT_ID); }); test('Payload should be encrypted with session key', async () => { @@ -134,8 +135,8 @@ describe('generateCargoes', () => { await expect( KEY_STORES.privateKeyStore.retrieveSessionKey( originatorKey.keyId, - await nodeCertificate.calculateSubjectPrivateAddress(), - peerPrivateAddress, + await nodeCertificate.calculateSubjectId(), + peerId, ), ).toResolve(); }); @@ -269,7 +270,7 @@ describe('generateCargoes', () => { test('Messages should be encapsulated into as few cargoes as possible', async () => { const channel = new StubGatewayChannel(); const dummyParcel = await generateDummyParcel( - peerPrivateAddress, + peerId, peerSessionKeyPair.sessionKey, nodeCertificate, ); @@ -310,7 +311,7 @@ describe('generateCargoes', () => { }); async function generateDummyParcel( - recipientAddress: string, + recipientId: string, recipientSessionKey: SessionKey, finalSenderCertificate: Certificate, ): Promise { @@ -321,24 +322,17 @@ async function generateDummyParcel( recipientSessionKey, ); const payloadSerialized = Buffer.from(envelopedData.serialize()); - return new Parcel(recipientAddress, finalSenderCertificate, payloadSerialized); + return new Parcel({ id: recipientId }, finalSenderCertificate, payloadSerialized); } class StubGatewayChannel extends GatewayChannel { - public static readonly OUTBOUND_RAMF_ADDRESS = '0deadbeef'; + public static readonly OUTBOUND_RAMF_RECIPIENT_ID = '0deadbeef'; constructor(cryptoOptions: Partial = {}) { - super( - nodePrivateKey, - nodeCertificate, - peerPrivateAddress, - peerPublicKey, - KEY_STORES, - cryptoOptions, - ); + super(nodePrivateKey, nodeCertificate, peerId, peerPublicKey, KEY_STORES, cryptoOptions); } - async getOutboundRAMFAddress(): Promise { - return StubGatewayChannel.OUTBOUND_RAMF_ADDRESS; + async getOutboundRAMFRecipient(): Promise { + return { id: StubGatewayChannel.OUTBOUND_RAMF_RECIPIENT_ID }; } } diff --git a/src/lib/nodes/channels/GatewayChannel.ts b/src/lib/nodes/channels/GatewayChannel.ts index 54d175d7c..21454ffa7 100644 --- a/src/lib/nodes/channels/GatewayChannel.ts +++ b/src/lib/nodes/channels/GatewayChannel.ts @@ -14,12 +14,12 @@ export abstract class GatewayChannel extends Channel { public async *generateCargoes(messages: CargoMessageStream): AsyncIterable { const messagesAsArrayBuffers = convertBufferMessagesToArrayBuffer(messages); const cargoMessageSets = CargoMessageSet.batchMessagesSerialized(messagesAsArrayBuffers); - const recipientAddress = await this.getOutboundRAMFAddress(); + const recipient = await this.getOutboundRAMFRecipient(); for await (const { messageSerialized, expiryDate } of cargoMessageSets) { const creationDate = getCargoCreationTime(); const ttl = getSecondsBetweenDates(creationDate, expiryDate); const cargo = new Cargo( - recipientAddress, + recipient, this.nodeDeliveryAuth, await this.encryptPayload(messageSerialized), { creationDate, ttl: Math.min(ttl, RAMF_MAX_TTL) }, diff --git a/src/lib/nodes/channels/PrivateGatewayChannel.spec.ts b/src/lib/nodes/channels/PrivateGatewayChannel.spec.ts index 79c7503d0..ff88b0fd4 100644 --- a/src/lib/nodes/channels/PrivateGatewayChannel.spec.ts +++ b/src/lib/nodes/channels/PrivateGatewayChannel.spec.ts @@ -3,50 +3,50 @@ import { addDays, setMilliseconds, subMinutes, subSeconds } from 'date-fns'; import { derSerializePublicKey, generateRSAKeyPair, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, getRSAPublicKeyFromPrivate, } from '../../crypto_wrappers/keys'; import Certificate from '../../crypto_wrappers/x509/Certificate'; import { MockKeyStoreSet } from '../../keyStores/testMocks'; +import { Recipient } from '../../messages/Recipient'; import { CertificationPath } from '../../pki/CertificationPath'; import { issueGatewayCertificate } from '../../pki/issuance'; import { NodeCryptoOptions } from '../NodeCryptoOptions'; import { PrivateGatewayChannel } from './PrivateGatewayChannel'; -let publicGatewayPrivateAddress: string; -let publicGatewayPublicKey: CryptoKey; -let publicGatewayCertificate: Certificate; +let internetGatewayId: string; +let internetGatewayPublicKey: CryptoKey; +let internetGatewayCertificate: Certificate; beforeAll(async () => { const tomorrow = setMilliseconds(addDays(new Date(), 1), 0); - // Public gateway - const publicGatewayKeyPair = await generateRSAKeyPair(); - publicGatewayPublicKey = publicGatewayKeyPair.publicKey; - publicGatewayPrivateAddress = await getPrivateAddressFromIdentityKey(publicGatewayPublicKey); - publicGatewayCertificate = await issueGatewayCertificate({ - issuerPrivateKey: publicGatewayKeyPair.privateKey, - subjectPublicKey: publicGatewayPublicKey, + // Internet gateway + const internetGatewayKeyPair = await generateRSAKeyPair(); + internetGatewayPublicKey = internetGatewayKeyPair.publicKey; + internetGatewayId = await getIdFromIdentityKey(internetGatewayPublicKey); + internetGatewayCertificate = await issueGatewayCertificate({ + issuerPrivateKey: internetGatewayKeyPair.privateKey, + subjectPublicKey: internetGatewayPublicKey, validityEndDate: tomorrow, }); }); const KEY_STORES = new MockKeyStoreSet(); -let privateGatewayPrivateAddress: string; +let privateGatewayId: string; let privateGatewayPrivateKey: CryptoKey; let privateGatewayPDCCertificate: Certificate; beforeEach(async () => { - const { privateKey, publicKey, privateAddress } = - await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); + const { privateKey, publicKey, id } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); // Private gateway privateGatewayPrivateKey = privateKey; privateGatewayPDCCertificate = await issueGatewayCertificate({ - issuerCertificate: publicGatewayCertificate, + issuerCertificate: internetGatewayCertificate, issuerPrivateKey: privateKey, subjectPublicKey: publicKey, - validityEndDate: publicGatewayCertificate.expiryDate, + validityEndDate: internetGatewayCertificate.expiryDate, }); - privateGatewayPrivateAddress = privateAddress; + privateGatewayId = id; }); afterEach(() => { KEY_STORES.clear(); @@ -103,9 +103,7 @@ describe('getOrCreateCDAIssuer', () => { const issuer = await channel.getOrCreateCDAIssuer(); - await expect(issuer.calculateSubjectPrivateAddress()).resolves.toEqual( - privateGatewayPrivateAddress, - ); + await expect(issuer.calculateSubjectId()).resolves.toEqual(privateGatewayId); }); test('Certificate should be valid from 90 minutes in the past', async () => { @@ -130,8 +128,8 @@ describe('getOrCreateCDAIssuer', () => { async function retrieveCDAIssuer(): Promise { const issuerPath = await KEY_STORES.certificateStore.retrieveLatest( - privateGatewayPrivateAddress, - privateGatewayPrivateAddress, + privateGatewayId, + privateGatewayId, ); return issuerPath?.leafCertificate ?? null; } @@ -139,7 +137,7 @@ describe('getOrCreateCDAIssuer', () => { async function saveCDAIssuer(cdaIssuer: Certificate): Promise { await KEY_STORES.certificateStore.save( new CertificationPath(cdaIssuer, []), - await cdaIssuer.calculateSubjectPrivateAddress(), + await cdaIssuer.calculateSubjectId(), ); } }); @@ -161,7 +159,7 @@ describe('getCDAIssuers', () => { }); await KEY_STORES.certificateStore.save( new CertificationPath(differentSubjectCertificate, []), - privateGatewayPrivateAddress, + privateGatewayId, ); const channel = new StubPrivateGatewayChannel(); @@ -171,7 +169,7 @@ describe('getCDAIssuers', () => { test('Other issuers should be ignored', async () => { await KEY_STORES.certificateStore.save( new CertificationPath(privateGatewayPDCCertificate, []), - `not-${privateGatewayPrivateAddress}`, + `not-${privateGatewayId}`, ); const channel = new StubPrivateGatewayChannel(); @@ -194,14 +192,14 @@ class StubPrivateGatewayChannel extends PrivateGatewayChannel { super( privateGatewayPrivateKey, privateGatewayPDCCertificate, - publicGatewayPrivateAddress, - publicGatewayPublicKey, + internetGatewayId, + internetGatewayPublicKey, KEY_STORES, cryptoOptions, ); } - async getOutboundRAMFAddress(): Promise { + async getOutboundRAMFRecipient(): Promise { throw new Error('not implemented'); } } diff --git a/src/lib/nodes/channels/PrivateGatewayChannel.ts b/src/lib/nodes/channels/PrivateGatewayChannel.ts index d115fe9da..1d92a2d49 100644 --- a/src/lib/nodes/channels/PrivateGatewayChannel.ts +++ b/src/lib/nodes/channels/PrivateGatewayChannel.ts @@ -1,9 +1,6 @@ import { addDays, subMinutes } from 'date-fns'; -import { - getPrivateAddressFromIdentityKey, - getRSAPublicKeyFromPrivate, -} from '../../crypto_wrappers/keys'; +import { getIdFromIdentityKey, getRSAPublicKeyFromPrivate } from '../../crypto_wrappers/keys'; import Certificate from '../../crypto_wrappers/x509/Certificate'; import { CertificationPath } from '../../pki/CertificationPath'; import { issueGatewayCertificate } from '../../pki/issuance'; @@ -17,12 +14,9 @@ export abstract class PrivateGatewayChannel extends GatewayChannel { const now = new Date(); const publicKey = await getRSAPublicKeyFromPrivate(this.nodePrivateKey); - const privateAddress = await getPrivateAddressFromIdentityKey(publicKey); + const nodeId = await getIdFromIdentityKey(publicKey); - const existingIssuerPath = await this.keyStores.certificateStore.retrieveLatest( - privateAddress, - privateAddress, - ); + const existingIssuerPath = await this.keyStores.certificateStore.retrieveLatest(nodeId, nodeId); if (existingIssuerPath) { const minExpiryDate = addDays(now, 90); if (minExpiryDate <= existingIssuerPath.leafCertificate.expiryDate) { @@ -37,7 +31,7 @@ export abstract class PrivateGatewayChannel extends GatewayChannel { validityStartDate: subMinutes(now, 90), }); const path = new CertificationPath(issuer, []); - await this.keyStores.certificateStore.save(path, privateAddress); + await this.keyStores.certificateStore.save(path, nodeId); return issuer; } @@ -46,11 +40,8 @@ export abstract class PrivateGatewayChannel extends GatewayChannel { */ public async getCDAIssuers(): Promise { const publicKey = await getRSAPublicKeyFromPrivate(this.nodePrivateKey); - const privateAddress = await getPrivateAddressFromIdentityKey(publicKey); - const issuerPaths = await this.keyStores.certificateStore.retrieveAll( - privateAddress, - privateAddress, - ); + const nodeId = await getIdFromIdentityKey(publicKey); + const issuerPaths = await this.keyStores.certificateStore.retrieveAll(nodeId, nodeId); return issuerPaths.map((p) => p.leafCertificate); } } diff --git a/src/lib/nodes/channels/PrivatePublicGatewayChannel.ts b/src/lib/nodes/channels/PrivateInternetGatewayChannel.ts similarity index 83% rename from src/lib/nodes/channels/PrivatePublicGatewayChannel.ts rename to src/lib/nodes/channels/PrivateInternetGatewayChannel.ts index 2c7341411..9df7e2631 100644 --- a/src/lib/nodes/channels/PrivatePublicGatewayChannel.ts +++ b/src/lib/nodes/channels/PrivateInternetGatewayChannel.ts @@ -7,6 +7,7 @@ import Certificate from '../../crypto_wrappers/x509/Certificate'; import { KeyStoreSet } from '../../keyStores/KeyStoreSet'; import { CargoCollectionAuthorization } from '../../messages/CargoCollectionAuthorization'; import { CargoCollectionRequest } from '../../messages/payloads/CargoCollectionRequest'; +import { Recipient } from '../../messages/Recipient'; import { issueEndpointCertificate, issueGatewayCertificate } from '../../pki/issuance'; import { NodeCryptoOptions } from '../NodeCryptoOptions'; import { PrivateGatewayChannel } from './PrivateGatewayChannel'; @@ -15,33 +16,33 @@ const CLOCK_DRIFT_TOLERANCE_MINUTES = 90; const OUTBOUND_CARGO_TTL_DAYS = 14; /** - * Channel between a private gateway (the node) and its public gateway (the peer). + * Channel between a private gateway (the node) and its Internet gateway (the peer). */ -export class PrivatePublicGatewayChannel extends PrivateGatewayChannel { +export class PrivateInternetGatewayChannel extends PrivateGatewayChannel { /** * @internal */ constructor( privateGatewayPrivateKey: CryptoKey, privateGatewayDeliveryAuth: Certificate, - publicGatewayPrivateAddress: string, - publicGatewayPublicKey: CryptoKey, - public readonly publicGatewayPublicAddress: string, + internetGatewayId: string, + internetGatewayPublicKey: CryptoKey, + public readonly internetGatewayInternetAddress: string, keyStores: KeyStoreSet, cryptoOptions: Partial, ) { super( privateGatewayPrivateKey, privateGatewayDeliveryAuth, - publicGatewayPrivateAddress, - publicGatewayPublicKey, + internetGatewayId, + internetGatewayPublicKey, keyStores, cryptoOptions, ); } - async getOutboundRAMFAddress(): Promise { - return `https://${this.publicGatewayPublicAddress}`; + async getOutboundRAMFRecipient(): Promise { + return { id: this.peerId, internetAddress: this.internetGatewayInternetAddress }; } //region Private endpoint registration @@ -90,7 +91,11 @@ export class PrivatePublicGatewayChannel extends PrivateGatewayChannel { subjectPublicKey: endpointPublicKey, validityEndDate: addMonths(new Date(), 6), }); - const registration = new PrivateNodeRegistration(endpointCertificate, this.nodeDeliveryAuth); + const registration = new PrivateNodeRegistration( + endpointCertificate, + this.nodeDeliveryAuth, + this.internetGatewayInternetAddress, + ); return registration.serialize(); } @@ -111,7 +116,7 @@ export class PrivatePublicGatewayChannel extends PrivateGatewayChannel { const ccr = new CargoCollectionRequest(cargoDeliveryAuthorization); const ccaPayload = await this.wrapMessagePayload(ccr); const cca = new CargoCollectionAuthorization( - await this.getOutboundRAMFAddress(), + await this.getOutboundRAMFRecipient(), this.nodeDeliveryAuth, Buffer.from(ccaPayload), { creationDate: startDate, ttl: differenceInSeconds(endDate, startDate) }, diff --git a/src/lib/nodes/channels/PrivatePublicGatewayChannel.spec.ts b/src/lib/nodes/channels/PrivatePublicGatewayChannel.spec.ts index 6bacdf49e..2b1c70ffb 100644 --- a/src/lib/nodes/channels/PrivatePublicGatewayChannel.spec.ts +++ b/src/lib/nodes/channels/PrivatePublicGatewayChannel.spec.ts @@ -7,32 +7,33 @@ import { PrivateNodeRegistrationAuthorization } from '../../bindings/gsc/Private import { derSerializePublicKey, generateRSAKeyPair, - getPrivateAddressFromIdentityKey, + getIdFromIdentityKey, } from '../../crypto_wrappers/keys'; import Certificate from '../../crypto_wrappers/x509/Certificate'; import { MockKeyStoreSet } from '../../keyStores/testMocks'; import { CargoCollectionAuthorization } from '../../messages/CargoCollectionAuthorization'; import InvalidMessageError from '../../messages/InvalidMessageError'; +import { Recipient } from '../../messages/Recipient'; import { issueGatewayCertificate } from '../../pki/issuance'; import { SessionKeyPair } from '../../SessionKeyPair'; -import { PrivatePublicGatewayChannel } from './PrivatePublicGatewayChannel'; +import { PrivateInternetGatewayChannel } from './PrivateInternetGatewayChannel'; -let publicGatewayPrivateAddress: string; -let publicGatewayPublicKey: CryptoKey; -let publicGatewayCertificate: Certificate; -let privateGatewayPrivateAddress: string; +let internetGatewayId: string; +let internetGatewayPublicKey: CryptoKey; +let internetGatewayCertificate: Certificate; +let privateGatewayId: string; let privateGatewayKeyPair: CryptoKeyPair; let privateGatewayPDCCertificate: Certificate; beforeAll(async () => { const nextYear = setMilliseconds(addDays(new Date(), 360), 0); - // Public gateway - const publicGatewayKeyPair = await generateRSAKeyPair(); - publicGatewayPublicKey = publicGatewayKeyPair.publicKey; - publicGatewayPrivateAddress = await getPrivateAddressFromIdentityKey(publicGatewayPublicKey); - publicGatewayCertificate = await issueGatewayCertificate({ - issuerPrivateKey: publicGatewayKeyPair.privateKey, - subjectPublicKey: publicGatewayPublicKey, + // Internet gateway + const internetGatewayKeyPair = await generateRSAKeyPair(); + internetGatewayPublicKey = internetGatewayKeyPair.publicKey; + internetGatewayId = await getIdFromIdentityKey(internetGatewayPublicKey); + internetGatewayCertificate = await issueGatewayCertificate({ + issuerPrivateKey: internetGatewayKeyPair.privateKey, + subjectPublicKey: internetGatewayPublicKey, validityEndDate: nextYear, }); @@ -40,28 +41,26 @@ beforeAll(async () => { privateGatewayKeyPair = await generateRSAKeyPair(); privateGatewayPDCCertificate = reSerializeCertificate( await issueGatewayCertificate({ - issuerCertificate: publicGatewayCertificate, - issuerPrivateKey: publicGatewayKeyPair.privateKey, + issuerCertificate: internetGatewayCertificate, + issuerPrivateKey: internetGatewayKeyPair.privateKey, subjectPublicKey: privateGatewayKeyPair.publicKey, validityEndDate: nextYear, }), ); - privateGatewayPrivateAddress = await getPrivateAddressFromIdentityKey( - privateGatewayKeyPair.publicKey, - ); + privateGatewayId = await getIdFromIdentityKey(privateGatewayKeyPair.publicKey); }); -let publicGatewaySessionKeyPair: SessionKeyPair; +let internetGatewaySessionKeyPair: SessionKeyPair; beforeAll(async () => { - publicGatewaySessionKeyPair = await SessionKeyPair.generate(); + internetGatewaySessionKeyPair = await SessionKeyPair.generate(); }); const KEY_STORES = new MockKeyStoreSet(); beforeEach(async () => { - await KEY_STORES.publicKeyStore.saveIdentityKey(await publicGatewayCertificate.getPublicKey()); + await KEY_STORES.publicKeyStore.saveIdentityKey(await internetGatewayCertificate.getPublicKey()); await KEY_STORES.publicKeyStore.saveSessionKey( - publicGatewaySessionKeyPair.sessionKey, - publicGatewayPrivateAddress, + internetGatewaySessionKeyPair.sessionKey, + internetGatewayId, new Date(), ); }); @@ -69,25 +68,26 @@ afterEach(() => { KEY_STORES.clear(); }); -const PUBLIC_GATEWAY_PUBLIC_ADDRESS = 'example.com'; +const INTERNET_GATEWAY_INTERNET_ADDRESS = 'example.com'; -let channel: PrivatePublicGatewayChannel; +let channel: PrivateInternetGatewayChannel; beforeEach(() => { - channel = new PrivatePublicGatewayChannel( + channel = new PrivateInternetGatewayChannel( privateGatewayKeyPair.privateKey!, privateGatewayPDCCertificate, - publicGatewayPrivateAddress, - publicGatewayPublicKey, - PUBLIC_GATEWAY_PUBLIC_ADDRESS, + internetGatewayId, + internetGatewayPublicKey, + INTERNET_GATEWAY_INTERNET_ADDRESS, KEY_STORES, {}, ); }); -test('getOutboundRAMFAddress should return public address of public gateway', async () => { - await expect(channel.getOutboundRAMFAddress()).resolves.toEqual( - `https://${PUBLIC_GATEWAY_PUBLIC_ADDRESS}`, - ); +test('getOutboundRAMFAddress should return Internet address of Internet gateway', async () => { + await expect(channel.getOutboundRAMFRecipient()).resolves.toEqual({ + id: internetGatewayId, + internetAddress: INTERNET_GATEWAY_INTERNET_ADDRESS, + }); }); describe('Endpoint registration', () => { @@ -154,7 +154,7 @@ describe('Endpoint registration', () => { endpointPublicKey = endpointKeyPair.publicKey; }); - test('Endpoint certificate should be issued by public gateway', async () => { + test('Endpoint certificate should be issued by Internet gateway', async () => { const registrationSerialized = await channel.registerEndpoint(endpointPublicKey); const registration = await PrivateNodeRegistration.deserialize(registrationSerialized); @@ -204,6 +204,16 @@ describe('Endpoint registration', () => { expect(registration.gatewayCertificate.isEqual(privateGatewayPDCCertificate)).toBeTrue(); }); + test('Internet gateway Internet gateway should be included in registration', async () => { + const registrationSerialized = await channel.registerEndpoint(endpointPublicKey); + + const registration = await PrivateNodeRegistration.deserialize(registrationSerialized); + + expect(registration.internetGatewayInternetAddress).toEqual( + INTERNET_GATEWAY_INTERNET_ADDRESS, + ); + }); + test('Session key should be absent from registration', async () => { const registrationSerialized = await channel.registerEndpoint(endpointPublicKey); @@ -214,11 +224,14 @@ describe('Endpoint registration', () => { }); describe('generateCCA', () => { - test('Recipient should be public gateway', async () => { + test('Recipient should be Internet gateway', async () => { const ccaSerialized = await channel.generateCCA(); const cca = await CargoCollectionAuthorization.deserialize(ccaSerialized); - expect(cca.recipientAddress).toEqual(`https://${PUBLIC_GATEWAY_PUBLIC_ADDRESS}`); + expect(cca.recipient).toEqual({ + id: internetGatewayId, + internetAddress: INTERNET_GATEWAY_INTERNET_ADDRESS, + }); }); test('Creation date should be 90 minutes in the past to tolerate clock drift', async () => { @@ -255,15 +268,15 @@ describe('generateCCA', () => { }); describe('Cargo Delivery Authorization', () => { - test('Subject public key should be that of the public gateway', async () => { + test('Subject public key should be that of the Internet gateway', async () => { const ccaSerialized = await channel.generateCCA(); const cargoDeliveryAuthorization = await extractCDA(ccaSerialized); - expect(cargoDeliveryAuthorization.isEqual(publicGatewayCertificate)).toBeFalse(); + expect(cargoDeliveryAuthorization.isEqual(internetGatewayCertificate)).toBeFalse(); await expect( derSerializePublicKey(await cargoDeliveryAuthorization.getPublicKey()), ).resolves.toEqual( - await derSerializePublicKey(await publicGatewayCertificate.getPublicKey()), + await derSerializePublicKey(await internetGatewayCertificate.getPublicKey()), ); }); @@ -280,8 +293,8 @@ describe('generateCCA', () => { const cargoDeliveryAuthorization = await extractCDA(ccaSerialized); const cdaIssuer = await KEY_STORES.certificateStore.retrieveLatest( - privateGatewayPrivateAddress, - privateGatewayPrivateAddress, + privateGatewayId, + privateGatewayId, ); await expect( cargoDeliveryAuthorization.getCertificationPath([], [cdaIssuer!.leafCertificate]), @@ -290,7 +303,7 @@ describe('generateCCA', () => { async function extractCDA(ccaSerialized: ArrayBuffer): Promise { const cca = await CargoCollectionAuthorization.deserialize(ccaSerialized); - const { payload: ccr } = await cca.unwrapPayload(publicGatewaySessionKeyPair.privateKey); + const { payload: ccr } = await cca.unwrapPayload(internetGatewaySessionKeyPair.privateKey); return ccr.cargoDeliveryAuthorization; } }); diff --git a/src/lib/nodes/managers/EndpointManager.spec.ts b/src/lib/nodes/managers/EndpointManager.spec.ts index 82b612e60..8e46c3332 100644 --- a/src/lib/nodes/managers/EndpointManager.spec.ts +++ b/src/lib/nodes/managers/EndpointManager.spec.ts @@ -9,10 +9,10 @@ afterEach(() => { describe('get', () => { test('Endpoint instances should be returned', async () => { - const { privateAddress } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); + const { id } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); const manager = new EndpointManager(KEY_STORES); - const endpoint = await manager.get(privateAddress); + const endpoint = await manager.get(id); expect(endpoint).toBeInstanceOf(Endpoint); }); diff --git a/src/lib/nodes/managers/NodeConstructor.ts b/src/lib/nodes/managers/NodeConstructor.ts index ca9f48ccf..1d4a11dc3 100644 --- a/src/lib/nodes/managers/NodeConstructor.ts +++ b/src/lib/nodes/managers/NodeConstructor.ts @@ -3,7 +3,7 @@ import { NodeCryptoOptions } from '../NodeCryptoOptions'; import { Node } from '../Node'; export type NodeConstructor> = new ( - privateAddress: string, + id: string, identityPrivateKey: CryptoKey, keyStores: KeyStoreSet, cryptoOptions: Partial, diff --git a/src/lib/nodes/managers/NodeManager.spec.ts b/src/lib/nodes/managers/NodeManager.spec.ts index 0e631dd06..68b64bbf8 100644 --- a/src/lib/nodes/managers/NodeManager.spec.ts +++ b/src/lib/nodes/managers/NodeManager.spec.ts @@ -18,21 +18,20 @@ describe('get', () => { }); test('Node should be returned if private key exists', async () => { - const { privateKey, privateAddress } = - await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); + const { privateKey, id } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); const manager = new StubNodeManager(KEY_STORES); - const gateway = await manager.get(privateAddress); + const gateway = await manager.get(id); - expect(MOCK_NODE_CLASS).toBeCalledWith(privateAddress, privateKey, KEY_STORES, {}); + expect(MOCK_NODE_CLASS).toBeCalledWith(id, privateKey, KEY_STORES, {}); expect(gateway).toEqual(MOCK_NODE_CLASS.mock.instances[0]); }); test('Key stores should be passed on', async () => { - const { privateAddress } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); + const { id } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); const manager = new StubNodeManager(KEY_STORES); - await manager.get(privateAddress); + await manager.get(id); expect(MOCK_NODE_CLASS).toBeCalledWith( expect.anything(), @@ -43,11 +42,11 @@ describe('get', () => { }); test('Crypto options should be honoured if passed', async () => { - const { privateAddress } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); + const { id } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); const cryptoOptions = { encryption: { aesKeySize: 256 } }; const manager = new StubNodeManager(KEY_STORES, cryptoOptions); - await manager.get(privateAddress); + await manager.get(id); expect(MOCK_NODE_CLASS).toBeCalledWith( expect.anything(), @@ -61,17 +60,11 @@ describe('get', () => { const customPrivateGateway = {}; const customPrivateGatewayConstructor = jest.fn().mockReturnValue(customPrivateGateway); const manager = new StubNodeManager(KEY_STORES); - const { privateKey, privateAddress } = - await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); + const { privateKey, id } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); - const gateway = await manager.get(privateAddress, customPrivateGatewayConstructor); + const gateway = await manager.get(id, customPrivateGatewayConstructor); expect(gateway).toBe(customPrivateGateway); - expect(customPrivateGatewayConstructor).toBeCalledWith( - privateAddress, - privateKey, - KEY_STORES, - {}, - ); + expect(customPrivateGatewayConstructor).toBeCalledWith(id, privateKey, KEY_STORES, {}); }); }); diff --git a/src/lib/nodes/managers/NodeManager.ts b/src/lib/nodes/managers/NodeManager.ts index 307a1d9f8..0f66219af 100644 --- a/src/lib/nodes/managers/NodeManager.ts +++ b/src/lib/nodes/managers/NodeManager.ts @@ -12,30 +12,24 @@ export abstract class NodeManager> { ) {} /** - * Get node by `privateAddress`. + * Get node by `id`. * - * @param privateAddress + * @param id */ - public async get(privateAddress: string): Promise; + public async get(id: string): Promise; /** - * Get node by `privateAddress` but return instance of custom `customNodeClass`. + * Get node by `id` but return instance of custom `customNodeClass`. * - * @param privateAddress + * @param id * @param customNodeClass */ - public async get( - privateAddress: string, - customNodeClass: NodeConstructor, - ): Promise; - public async get( - privateAddress: string, - nodeConstructor?: NodeConstructor, - ): Promise { - const nodePrivateKey = await this.keyStores.privateKeyStore.retrieveIdentityKey(privateAddress); + public async get(id: string, customNodeClass: NodeConstructor): Promise; + public async get(id: string, nodeConstructor?: NodeConstructor): Promise { + const nodePrivateKey = await this.keyStores.privateKeyStore.retrieveIdentityKey(id); if (!nodePrivateKey) { return null; } const constructor = nodeConstructor ?? this.defaultNodeConstructor; - return new constructor(privateAddress, nodePrivateKey, this.keyStores, this.cryptoOptions); + return new constructor(id, nodePrivateKey, this.keyStores, this.cryptoOptions); } } diff --git a/src/lib/nodes/managers/PrivateGatewayManager.spec.ts b/src/lib/nodes/managers/PrivateGatewayManager.spec.ts index b8755bf0e..3ff51d0bb 100644 --- a/src/lib/nodes/managers/PrivateGatewayManager.spec.ts +++ b/src/lib/nodes/managers/PrivateGatewayManager.spec.ts @@ -9,10 +9,10 @@ afterEach(() => { describe('get', () => { test('PrivateGateway instances should be returned', async () => { - const { privateAddress } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); + const { id } = await KEY_STORES.privateKeyStore.generateIdentityKeyPair(); const manager = new PrivateGatewayManager(KEY_STORES); - const gateway = await manager.get(privateAddress); + const gateway = await manager.get(id); expect(gateway).toBeInstanceOf(PrivateGateway); }); diff --git a/src/lib/pki/issuance.spec.ts b/src/lib/pki/issuance.spec.ts index fbb9d3c47..99a6447ef 100644 --- a/src/lib/pki/issuance.spec.ts +++ b/src/lib/pki/issuance.spec.ts @@ -65,7 +65,7 @@ describe('issueGatewayCertificate', () => { expect(mockCertificateIssue.mock.calls[0][0]).toMatchObject(basicCertificateOptions); }); - test('Certificate should have its private address as its Common Name (CN)', async () => { + test('Certificate should have its id as its Common Name (CN)', async () => { await issueGatewayCertificate(minimalCertificateOptions); expect(mockCertificateIssue.mock.calls[0][0]).toHaveProperty( @@ -121,7 +121,7 @@ describe('issueEndpointCertificate', () => { expect(mockCertificateIssue.mock.calls[0][0]).toMatchObject(basicCertificateOptions); }); - test('Certificate should have its private address as its Common Name (CN)', async () => { + test('Certificate should have its id as its Common Name (CN)', async () => { await issueEndpointCertificate(minimalCertificateOptions); expect(mockCertificateIssue.mock.calls[0][0]).toHaveProperty( @@ -192,7 +192,7 @@ describe('issueDeliveryAuthorization', () => { expect(mockCertificateIssue.mock.calls[0][0]).toMatchObject(basicCertificateOptions); }); - test('Certificate should have its private address as its Common Name (CN)', async () => { + test('Certificate should have its id as its Common Name (CN)', async () => { await issueDeliveryAuthorization(minimalCertificateOptions); expect(mockCertificateIssue.mock.calls[0][0]).toHaveProperty( diff --git a/src/lib/ramf/serialization.spec.ts b/src/lib/ramf/serialization.spec.ts index 633bdfc25..b562353a2 100644 --- a/src/lib/ramf/serialization.spec.ts +++ b/src/lib/ramf/serialization.spec.ts @@ -1,5 +1,15 @@ -import * as asn1js from 'asn1js'; +import { + BaseBlock, + Constructed, + DateTime, + Integer, + Null, + OctetString, + Sequence, + VisibleString, +} from 'asn1js'; import bufferToArray from 'buffer-to-arraybuffer'; +import { addDays, setMilliseconds, subDays } from 'date-fns'; import * as jestDateMock from 'jest-date-mock'; import moment from 'moment'; import { SmartBuffer } from 'smart-buffer'; @@ -8,27 +18,28 @@ import { arrayBufferFrom, expectPkijsValuesToBeEqual, generateStubCert, - getAsn1SequenceItem, + getConstructedItemFromConstructed, + getPrimitiveItemFromConstructed, getPromiseRejection, } from '../_test_utils'; import { dateToASN1DateTimeInUTC, makeImplicitlyTaggedSequence } from '../asn1'; import { derDeserialize } from '../crypto_wrappers/_utils'; +import { HashingAlgorithm } from '../crypto_wrappers/algorithms'; import * as cmsSignedData from '../crypto_wrappers/cms/signedData'; import { generateRSAKeyPair } from '../crypto_wrappers/keys'; import Certificate from '../crypto_wrappers/x509/Certificate'; +import { Recipient } from '../messages/Recipient'; import { StubMessage } from './_test_utils'; import RAMFSyntaxError from './RAMFSyntaxError'; import RAMFValidationError from './RAMFValidationError'; import { deserialize, serialize } from './serialization'; -import { HashingAlgorithm } from '../crypto_wrappers/algorithms'; const PAYLOAD = Buffer.from('Hi'); const MAX_PAYLOAD_LENGTH = 2 ** 23 - 1; const MAX_TTL = 15552000; -const NOW = new Date(); // There should be tests covering rounding when there are milliseconds -NOW.setMilliseconds(0); +const NOW = setMilliseconds(new Date(), 0); const stubConcreteMessageTypeOctet = 0x44; const stubConcreteMessageVersionOctet = 0x2; @@ -47,21 +58,20 @@ afterEach(() => { }); describe('MessageSerializer', () => { - const RECIPIENT_ADDRESS = '0123456789'; + const RECIPIENT_ID = '0123456789'; + const RECIPIENT: Recipient = { id: RECIPIENT_ID }; + const INTERNET_ADDRESS = 'example.com'; - let SENDER_PRIVATE_KEY: CryptoKey; - let SENDER_CERTIFICATE: Certificate; + let senderPrivateKey: CryptoKey; + let senderCertificate: Certificate; beforeAll(async () => { - const yesterday = new Date(NOW); - yesterday.setDate(yesterday.getDate() - 1); - const tomorrow = new Date(NOW); - tomorrow.setDate(tomorrow.getDate() + 1); - const certificateAttributes = { validityStartDate: yesterday, validityEndDate: tomorrow }; - const senderKeyPair = await generateRSAKeyPair(); - SENDER_PRIVATE_KEY = senderKeyPair.privateKey; - SENDER_CERTIFICATE = await generateStubCert({ - attributes: certificateAttributes, + senderPrivateKey = senderKeyPair.privateKey; + senderCertificate = await generateStubCert({ + attributes: { + validityStartDate: subDays(NOW, 1), + validityEndDate: addDays(NOW, 1), + }, subjectPublicKey: senderKeyPair.publicKey, }); }); @@ -74,18 +84,18 @@ describe('MessageSerializer', () => { describe('Format signature', () => { let stubMessage: StubMessage; beforeAll(() => { - stubMessage = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + stubMessage = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); }); - test('The ASCII string "Relaynet" should be at the start', async () => { + test('The ASCII string "Awala" should be at the start', async () => { const messageSerialized = await serialize( stubMessage, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const formatSignature = parseFormatSignature(messageSerialized); - expect(formatSignature).toHaveProperty('magic', 'Relaynet'); + expect(formatSignature).toHaveProperty('magic', 'Awala'); }); test('The concrete message type should be represented with an octet', async () => { @@ -93,7 +103,7 @@ describe('MessageSerializer', () => { stubMessage, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const formatSignature = parseFormatSignature(messageSerialized); expect(formatSignature).toHaveProperty('concreteMessageType', stubConcreteMessageTypeOctet); @@ -104,7 +114,7 @@ describe('MessageSerializer', () => { stubMessage, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const formatSignature = parseFormatSignature(messageSerialized); expect(formatSignature).toHaveProperty( @@ -119,7 +129,7 @@ describe('MessageSerializer', () => { let cmsSignArgs: readonly any[]; beforeAll(async () => { senderCaCertificateChain = [await generateStubCert()]; - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD, { + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD, { senderCaCertificateChain, }); @@ -128,7 +138,7 @@ describe('MessageSerializer', () => { message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); expect(cmsSignedData.sign).toBeCalledTimes(1); // @ts-ignore @@ -138,13 +148,13 @@ describe('MessageSerializer', () => { test('The sender private key should be used to generate signature', () => { const actualSenderPrivateKey = cmsSignArgs[1]; - expect(actualSenderPrivateKey).toBe(SENDER_PRIVATE_KEY); + expect(actualSenderPrivateKey).toBe(senderPrivateKey); }); test('The sender certificate should be used to generate signature', () => { const actualSenderCertificate = cmsSignArgs[2]; - expect(actualSenderCertificate).toBe(SENDER_CERTIFICATE); + expect(actualSenderCertificate).toBe(senderCertificate); }); test('Sender certificate chain should be attached', () => { @@ -165,14 +175,14 @@ describe('MessageSerializer', () => { test.each(['SHA-384', 'SHA-512'] as readonly HashingAlgorithm[])( '%s should also be supported', async (hashingAlgorithmName) => { - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); jest.spyOn(cmsSignedData, 'sign'); await serialize( message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, { hashingAlgorithmName, }, @@ -187,65 +197,144 @@ describe('MessageSerializer', () => { describe('Fields', () => { test('Fields should be contained in SignedData value', async () => { - const stubMessage = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + const stubMessage = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); const messageSerialized = await serialize( stubMessage, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); await deserializeFields(messageSerialized); }); test('Fields should be serialized as a 5-item ASN.1 sequence', async () => { - const stubMessage = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + const stubMessage = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); const messageSerialized = await serialize( stubMessage, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const fields = await deserializeFields(messageSerialized); - expect(fields).toBeInstanceOf(asn1js.Sequence); + expect(fields).toBeInstanceOf(Sequence); expect(fields.valueBlock.value).toHaveLength(5); }); - describe('Recipient address', () => { - test('Address should be the first item', async () => { - const stubMessage = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + describe('Recipient', () => { + test('Recipient should be CONSTRUCTED', async () => { + const stubMessage = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); const messageSerialized = await serialize( stubMessage, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); + const fields = await deserializeFields(messageSerialized); - const addressDeserialized = getAsn1SequenceItem(fields, 0); - expect(addressDeserialized.valueBlock.valueHex).toEqual( - arrayBufferFrom(RECIPIENT_ADDRESS), - ); + const recipientASN1 = getConstructedItemFromConstructed(fields, 0); + expect(recipientASN1).toBeInstanceOf(Constructed); }); - test('Address should not span more than 1024 characters', async () => { - const invalidAddress = 'a'.repeat(1025); - const stubMessage = new StubMessage(invalidAddress, SENDER_CERTIFICATE, PAYLOAD); + describe('Id', () => { + test('Id should be first item in sub-sequence', async () => { + const stubMessage = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); - await expect( - serialize( + const messageSerialized = await serialize( stubMessage, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, - ), - ).rejects.toEqual( - new RAMFSyntaxError( - 'Recipient address should not span more than 1024 characters (got 1025)', - ), - ); + senderPrivateKey, + ); + + const fields = await deserializeFields(messageSerialized); + const recipientASN1 = getConstructedItemFromConstructed(fields, 0); + const idASN1 = getPrimitiveItemFromConstructed(recipientASN1, 0); + expect(Buffer.from(idASN1.valueBlock.valueHexView)).toEqual(Buffer.from(RECIPIENT_ID)); + }); + + test('Id should not span more than 1024 characters', async () => { + const invalidId = 'a'.repeat(1025); + const stubMessage = new StubMessage({ id: invalidId }, senderCertificate, PAYLOAD); + + await expect( + serialize( + stubMessage, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + senderPrivateKey, + ), + ).rejects.toEqual( + new RAMFSyntaxError( + 'Recipient id should not span more than 1024 characters (got 1025)', + ), + ); + }); + }); + + describe('Internet address', () => { + test('Internet address should be absent if unspecified', async () => { + expect(RECIPIENT.internetAddress).toBeUndefined(); + const stubMessage = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); + + const messageSerialized = await serialize( + stubMessage, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + senderPrivateKey, + ); + + const fields = await deserializeFields(messageSerialized); + const recipientASN1 = getConstructedItemFromConstructed(fields, 0); + expect(recipientASN1.valueBlock.value.length).toEqual(1); + }); + + test('Internet address should be second item in sub-sequence', async () => { + const stubMessage = new StubMessage( + { ...RECIPIENT, internetAddress: INTERNET_ADDRESS }, + senderCertificate, + PAYLOAD, + ); + + const messageSerialized = await serialize( + stubMessage, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + senderPrivateKey, + ); + + const fields = await deserializeFields(messageSerialized); + const recipientASN1 = getConstructedItemFromConstructed(fields, 0); + const idASN1 = getPrimitiveItemFromConstructed(recipientASN1, 1); + expect(Buffer.from(idASN1.valueBlock.valueHexView)).toEqual( + Buffer.from(INTERNET_ADDRESS), + ); + }); + + test('Internet address should not span more than 1024 characters', async () => { + const invalidInternetAddress = 'a'.repeat(1025); + const stubMessage = new StubMessage( + { ...RECIPIENT, internetAddress: invalidInternetAddress }, + senderCertificate, + PAYLOAD, + ); + + await expect( + serialize( + stubMessage, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + senderPrivateKey, + ), + ).rejects.toEqual( + new RAMFSyntaxError( + 'Recipient Internet address should not span more than 1024 characters (got 1025)', + ), + ); + }); }); }); @@ -253,7 +342,7 @@ describe('MessageSerializer', () => { test('Id should be the second item', async () => { const idLength = 64; const id = 'a'.repeat(idLength); - const stubMessage = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD, { + const stubMessage = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD, { id, }); @@ -261,16 +350,16 @@ describe('MessageSerializer', () => { stubMessage, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const fields = await deserializeFields(messageSerialized); - const idField = getAsn1SequenceItem(fields, 1); + const idField = getPrimitiveItemFromConstructed(fields, 1); expect(idField.valueBlock.valueHex).toEqual(arrayBufferFrom(stubMessage.id)); }); test('Ids longer than 64 characters should be refused', async () => { const id = 'a'.repeat(65); - const stubMessage = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD, { + const stubMessage = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD, { id, }); @@ -279,7 +368,7 @@ describe('MessageSerializer', () => { stubMessage, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ), ).rejects.toEqual( new RAMFSyntaxError('Id should not span more than 64 characters (got 65)'), @@ -290,7 +379,7 @@ describe('MessageSerializer', () => { describe('Date', () => { test('Date should be serialized with UTC and second-level precision', async () => { const nonUtcDate = new Date('01 Jan 2019 12:00:00 GMT+11:00'); - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD, { + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD, { creationDate: nonUtcDate, }); @@ -298,11 +387,11 @@ describe('MessageSerializer', () => { message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const fields = await deserializeFields(messageSerialized); - const datetimeBlock = getAsn1SequenceItem(fields, 2); + const datetimeBlock = getPrimitiveItemFromConstructed(fields, 2); expect(datetimeBlock.valueBlock.valueHex).toEqual( dateToASN1DateTimeInUTC(nonUtcDate).valueBlock.valueHex, ); @@ -311,24 +400,24 @@ describe('MessageSerializer', () => { describe('TTL', () => { test('TTL should be serialized as an integer', async () => { - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); const messageSerialized = await serialize( message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const fields = await deserializeFields(messageSerialized); - const ttlBlock = getAsn1SequenceItem(fields, 3); - const ttlIntegerBlock = new asn1js.Integer({ + const ttlBlock = getPrimitiveItemFromConstructed(fields, 3); + const ttlIntegerBlock = new Integer({ valueHex: ttlBlock.valueBlock.valueHexView, }); expect(Number(ttlIntegerBlock.toBigInt())).toEqual(message.ttl); }); test('TTL of zero should be accepted', async () => { - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD, { + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD, { ttl: 0, }); @@ -336,19 +425,19 @@ describe('MessageSerializer', () => { message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const fields = await deserializeFields(messageSerialized); - const ttlBlock = getAsn1SequenceItem(fields, 3); - const ttlIntegerBlock = new asn1js.Integer({ + const ttlBlock = getPrimitiveItemFromConstructed(fields, 3); + const ttlIntegerBlock = new Integer({ valueHex: ttlBlock.valueBlock.valueHex, } as any); expect(ttlIntegerBlock.valueBlock.valueDec).toEqual(0); }); test('TTL should not be negative', async () => { - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD, { + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD, { ttl: -1, }); await expect( @@ -356,13 +445,13 @@ describe('MessageSerializer', () => { message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ), ).rejects.toEqual(new RAMFSyntaxError('TTL cannot be negative')); }); test('TTL should not be more than 180 days', async () => { - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD, { + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD, { ttl: MAX_TTL + 1, }); await expect( @@ -370,7 +459,7 @@ describe('MessageSerializer', () => { message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ), ).rejects.toEqual( new RAMFSyntaxError(`TTL must be less than ${MAX_TTL} (got ${MAX_TTL + 1})`), @@ -380,47 +469,47 @@ describe('MessageSerializer', () => { describe('Payload', () => { test('Payload should be serialized as an OCTET STRING', async () => { - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); const messageSerialized = await serialize( message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const fields = await deserializeFields(messageSerialized); - const payloadBlock = getAsn1SequenceItem(fields, 4); + const payloadBlock = getPrimitiveItemFromConstructed(fields, 4); expect(Buffer.from(payloadBlock.valueBlock.valueHex)).toEqual(PAYLOAD); }); test('Payload can span up to 8 MiB', async () => { const largePayload = Buffer.from('a'.repeat(MAX_PAYLOAD_LENGTH)); - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, largePayload); + const message = new StubMessage(RECIPIENT, senderCertificate, largePayload); const messageSerialized = await serialize( message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const fields = await deserializeFields(messageSerialized); - const payloadBlock = getAsn1SequenceItem(fields, 4); - expect(Buffer.from((payloadBlock as asn1js.OctetString).valueBlock.valueHex)).toEqual( + const payloadBlock = getPrimitiveItemFromConstructed(fields, 4); + expect(Buffer.from((payloadBlock as OctetString).valueBlock.valueHex)).toEqual( largePayload, ); }); test('Payload size should not exceed 8 MiB', async () => { const largePayload = Buffer.from('a'.repeat(MAX_PAYLOAD_LENGTH + 1)); - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, largePayload); + const message = new StubMessage(RECIPIENT, senderCertificate, largePayload); await expect( serialize( message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ), ).rejects.toEqual( new RAMFSyntaxError( @@ -430,11 +519,11 @@ describe('MessageSerializer', () => { }); }); - async function deserializeFields(messageSerialized: ArrayBuffer): Promise { + async function deserializeFields(messageSerialized: ArrayBuffer): Promise { // Skip format signature - const cmsSignedDataSerialized = messageSerialized.slice(10); + const cmsSignedDataSerialized = messageSerialized.slice(7); const { plaintext } = await cmsSignedData.verifySignature(cmsSignedDataSerialized); - return derDeserialize(plaintext) as asn1js.Sequence; + return derDeserialize(plaintext) as Sequence; } }); }); @@ -455,7 +544,7 @@ describe('MessageSerializer', () => { ), ).rejects.toThrowWithMessage( RAMFSyntaxError, - 'RAMF format signature does not begin with "Relaynet"', + 'RAMF format signature does not begin with "Awala"', ); }); @@ -477,7 +566,7 @@ describe('MessageSerializer', () => { describe('Format signature', () => { test('Input should be long enough to contain format signature', async () => { - const serialization = bufferToArray(Buffer.from('a'.repeat(9))); + const serialization = bufferToArray(Buffer.from('a'.repeat(6))); await expect( deserialize( serialization, @@ -491,7 +580,7 @@ describe('MessageSerializer', () => { ); }); - test('Input should be refused if it does not start with "Relaynet"', async () => { + test('Input should be refused if it does not start with "Awala"', async () => { const serialization = bufferToArray(Buffer.from('Relaycorp00')); await expect( deserialize( @@ -502,17 +591,17 @@ describe('MessageSerializer', () => { ), ).rejects.toThrowWithMessage( RAMFSyntaxError, - 'RAMF format signature does not begin with "Relaynet"', + 'RAMF format signature does not begin with "Awala"', ); }); test('A non-matching concrete message type should be refused', async () => { - const altMessage = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + const altMessage = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); const serialization = await serialize( altMessage, stubConcreteMessageTypeOctet + 1, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); await expect( @@ -529,12 +618,12 @@ describe('MessageSerializer', () => { }); test('A non-matching concrete message version should be refused', async () => { - const altMessage = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + const altMessage = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); const serialization = await serialize( altMessage, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet + 1, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); await expect( @@ -577,12 +666,12 @@ describe('MessageSerializer', () => { }); test('Sender certificate should be extracted from signature', async () => { - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); const messageSerialized = await serialize( message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const messageDeserialized = await deserialize( @@ -594,20 +683,20 @@ describe('MessageSerializer', () => { expectPkijsValuesToBeEqual( messageDeserialized.senderCertificate.pkijsCertificate, - SENDER_CERTIFICATE.pkijsCertificate, + senderCertificate.pkijsCertificate, ); }); test('Sender certificate chain should be extracted from signature', async () => { const caCertificate = await generateStubCert(); - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD, { + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD, { senderCaCertificateChain: [caCertificate], }); const messageSerialized = await serialize( message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const { senderCaCertificateChain } = await deserialize( @@ -625,15 +714,15 @@ describe('MessageSerializer', () => { describe('Fields', () => { test('Fields should be DER-encoded', async () => { const serializer = new SmartBuffer(); - serializer.writeString('Relaynet'); + serializer.writeString('Awala'); serializer.writeUInt8(stubConcreteMessageTypeOctet); serializer.writeUInt8(stubConcreteMessageVersionOctet); serializer.writeBuffer( Buffer.from( await cmsSignedData.sign( bufferToArray(Buffer.from('Not a DER value')), - SENDER_PRIVATE_KEY, - SENDER_CERTIFICATE, + senderPrivateKey, + senderCertificate, ), ), ); @@ -651,14 +740,14 @@ describe('MessageSerializer', () => { test('Fields should be serialized as a sequence', async () => { const serializer = new SmartBuffer(); - serializer.writeString('Relaynet'); + serializer.writeString('Awala'); serializer.writeUInt8(stubConcreteMessageTypeOctet); serializer.writeUInt8(stubConcreteMessageVersionOctet); const signedData = await cmsSignedData.SignedData.sign( - new asn1js.Null().toBER(false), - SENDER_PRIVATE_KEY, - SENDER_CERTIFICATE, + new Null().toBER(false), + senderPrivateKey, + senderCertificate, ); serializer.writeBuffer(Buffer.from(signedData.serialize())); @@ -676,10 +765,10 @@ describe('MessageSerializer', () => { test('Fields sequence should not have fewer than 5 items', async () => { const serialization = await serializeRamfWithoutValidation([ - new asn1js.VisibleString({ value: 'address' }), - new asn1js.VisibleString({ value: 'the-id' }), + new VisibleString({ value: 'address' }), + new VisibleString({ value: 'the-id' }), dateToASN1DateTimeInUTC(NOW), - new asn1js.Integer({ value: 1_000 }), + new Integer({ value: 1_000 }), ]); await expect( @@ -692,121 +781,212 @@ describe('MessageSerializer', () => { ).rejects.toEqual(new RAMFSyntaxError('Invalid RAMF fields')); }); - describe('Recipient address', () => { - test('Address should be extracted', async () => { - const address = 'a'.repeat(1024); - const message = new StubMessage(address, SENDER_CERTIFICATE, PAYLOAD); - const serialization = await serialize( - message, - stubConcreteMessageTypeOctet, - stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, - ); + describe('Recipient', () => { + test('Recipient should be CONSTRUCTED', async () => { + const serialization = await serializeRamfWithoutValidation([ + new VisibleString({ value: 'address' }), + new VisibleString({ value: 'the-id' }), + dateToASN1DateTimeInUTC(NOW), + new Integer({ value: 1_000 }), + new OctetString({ valueHex: arrayBufferFrom('payload') }), + ]); - const deserialization = await deserialize( - serialization, - stubConcreteMessageTypeOctet, - stubConcreteMessageVersionOctet, - StubMessage, - ); - expect(deserialization.recipientAddress).toEqual(address); + await expect( + deserialize( + serialization, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + StubMessage, + ), + ).rejects.toEqual(new RAMFSyntaxError('Invalid RAMF fields')); }); - test('Address should not span more than 1024 octets', async () => { - const address = 'a'.repeat(1025); - const messageSerialized = await serializeRamfWithoutValidation([ - new asn1js.VisibleString({ value: address }), - new asn1js.VisibleString({ value: 'the-id' }), + test('Recipient CONSTRUCTED value should contain at least 1 value', async () => { + const serialization = await serializeRamfWithoutValidation([ + new Sequence({ value: [] }), + new VisibleString({ value: 'the-id' }), dateToASN1DateTimeInUTC(NOW), - new asn1js.Integer({ value: 1_000 }), - new asn1js.OctetString({ valueHex: new ArrayBuffer(0) }), + new Integer({ value: 1_000 }), + new OctetString({ valueHex: arrayBufferFrom('payload') }), ]); + await expect( deserialize( - messageSerialized, + serialization, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, StubMessage, ), ).rejects.toEqual( - new RAMFSyntaxError( - 'Recipient address should not span more than 1024 characters (got 1025)', - ), + new RAMFSyntaxError('Recipient SEQUENCE should at least contain the id'), ); }); - test('Private addresses should be accepted', async () => { - const address = '0deadbeef'; - const message = new StubMessage(address, SENDER_CERTIFICATE, PAYLOAD); - const serialization = await serialize( - message, - stubConcreteMessageTypeOctet, - stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, - ); + describe('Id', () => { + test('Id should be extracted', async () => { + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); + const serialization = await serialize( + message, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + senderPrivateKey, + ); - const deserialization = await deserialize( - serialization, - stubConcreteMessageTypeOctet, - stubConcreteMessageVersionOctet, - StubMessage, - ); - expect(deserialization.recipientAddress).toEqual(address); - }); + const deserialization = await deserialize( + serialization, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + StubMessage, + ); + expect(deserialization.recipient.id).toEqual(RECIPIENT.id); + }); - test('Public addresses should be accepted', async () => { - const address = 'https://example.com'; - const message = new StubMessage(address, SENDER_CERTIFICATE, PAYLOAD); - const serialization = await serialize( - message, - stubConcreteMessageTypeOctet, - stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, - ); + test('Id of up to 1024 octets should be accepted', async () => { + const id = 'a'.repeat(1024); + const message = new StubMessage({ id }, senderCertificate, PAYLOAD); + const serialization = await serialize( + message, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + senderPrivateKey, + ); - const deserialization = await deserialize( - serialization, - stubConcreteMessageTypeOctet, - stubConcreteMessageVersionOctet, - StubMessage, - ); - expect(deserialization.recipientAddress).toEqual(address); + const deserialization = await deserialize( + serialization, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + StubMessage, + ); + expect(deserialization.recipient.id).toEqual(id); + }); + + test('Id spanning more than 1024 octets should be refused', async () => { + const address = 'a'.repeat(1025); + const messageSerialized = await serializeRamfWithoutValidation([ + new Sequence({ value: [new VisibleString({ value: address })] }), + new VisibleString({ value: 'the-id' }), + dateToASN1DateTimeInUTC(NOW), + new Integer({ value: 1_000 }), + new OctetString({ valueHex: new ArrayBuffer(0) }), + ]); + await expect( + deserialize( + messageSerialized, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + StubMessage, + ), + ).rejects.toEqual( + new RAMFSyntaxError( + 'Recipient id should not span more than 1024 characters (got 1025)', + ), + ); + }); + + test('Malformed id should be refused', async () => { + const malformedId = 'not valid'; + const message = new StubMessage({ id: malformedId }, senderCertificate, PAYLOAD); + const serialization = await serialize( + message, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + senderPrivateKey, + ); + + await expect( + deserialize( + serialization, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + StubMessage, + ), + ).rejects.toThrowWithMessage( + RAMFSyntaxError, + `Recipient id is malformed ("${malformedId}")`, + ); + }); }); - test('Invalid addresses should be refused', async () => { - const invalidAddress = 'not valid'; - const message = new StubMessage(invalidAddress, SENDER_CERTIFICATE, PAYLOAD); - const serialization = await serialize( - message, - stubConcreteMessageTypeOctet, - stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, - ); + describe('Internet address', () => { + test('Address should be undefined if absent', async () => { + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); + const serialization = await serialize( + message, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + senderPrivateKey, + ); - await expect( - deserialize( + const deserialization = await deserialize( serialization, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, StubMessage, - ), - ).rejects.toEqual( - new RAMFSyntaxError( - `Recipient address should be a valid node address (got: "${invalidAddress}")`, - ), - ); + ); + expect(deserialization.recipient.internetAddress).toBeUndefined(); + }); + + test('Domain name should be accepted', async () => { + const message = new StubMessage( + { ...RECIPIENT, internetAddress: INTERNET_ADDRESS }, + senderCertificate, + PAYLOAD, + ); + const serialization = await serialize( + message, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + senderPrivateKey, + ); + + const deserialization = await deserialize( + serialization, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + StubMessage, + ); + expect(deserialization.recipient.internetAddress).toEqual(INTERNET_ADDRESS); + }); + + test('Malformed domain name should be refused', async () => { + const malformedDomainName = 'not valid'; + const message = new StubMessage( + { ...RECIPIENT, internetAddress: malformedDomainName }, + senderCertificate, + PAYLOAD, + ); + const serialization = await serialize( + message, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + senderPrivateKey, + ); + + await expect( + deserialize( + serialization, + stubConcreteMessageTypeOctet, + stubConcreteMessageVersionOctet, + StubMessage, + ), + ).rejects.toEqual( + new RAMFSyntaxError( + `Recipient Internet address is malformed ("${malformedDomainName}")`, + ), + ); + }); }); }); describe('Message id', () => { test('Id should be deserialized', async () => { const id = 'a'.repeat(64); - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD, { id }); + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD, { id }); const serialization = await serialize( message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const deserialization = await deserialize( serialization, @@ -820,11 +1000,11 @@ describe('MessageSerializer', () => { test('Id should not exceed 64 characters', async () => { const id = 'a'.repeat(65); const messageSerialized = await serializeRamfWithoutValidation([ - new asn1js.VisibleString({ value: RECIPIENT_ADDRESS }), - new asn1js.VisibleString({ value: id }), + new Sequence({ value: [new VisibleString({ value: RECIPIENT_ID })] }), + new VisibleString({ value: id }), dateToASN1DateTimeInUTC(NOW), - new asn1js.Integer({ value: 1_000 }), - new asn1js.OctetString({ valueHex: new ArrayBuffer(0) }), + new Integer({ value: 1_000 }), + new OctetString({ valueHex: new ArrayBuffer(0) }), ]); await expect( deserialize( @@ -843,11 +1023,11 @@ describe('MessageSerializer', () => { test('Valid date should be accepted', async () => { const date = moment.utc(NOW).format('YYYYMMDDHHmmss'); const messageSerialized = await serializeRamfWithoutValidation([ - new asn1js.VisibleString({ value: RECIPIENT_ADDRESS }), - new asn1js.VisibleString({ value: 'id' }), - new asn1js.DateTime({ value: date }), - new asn1js.Integer({ value: 1_000 }), - new asn1js.OctetString({ valueHex: new ArrayBuffer(0) }), + new Sequence({ value: [new VisibleString({ value: RECIPIENT_ID })] }), + new VisibleString({ value: 'id' }), + new DateTime({ value: date }), + new Integer({ value: 1_000 }), + new OctetString({ valueHex: new ArrayBuffer(0) }), ]); const message = await deserialize( @@ -862,11 +1042,11 @@ describe('MessageSerializer', () => { test('Date not serialized as an ASN.1 DATE-TIME should be refused', async () => { const messageSerialized = await serializeRamfWithoutValidation([ - new asn1js.VisibleString({ value: 'the-address' }), - new asn1js.VisibleString({ value: 'id' }), - new asn1js.DateTime({ value: '42' }), - new asn1js.Integer({ value: 1_000 }), - new asn1js.OctetString({ valueHex: new ArrayBuffer(0) }), + new Sequence({ value: [new VisibleString({ value: 'the-address' })] }), + new VisibleString({ value: 'id' }), + new DateTime({ value: '42' }), + new Integer({ value: 1_000 }), + new OctetString({ valueHex: new ArrayBuffer(0) }), ]); await expect( @@ -876,20 +1056,18 @@ describe('MessageSerializer', () => { stubConcreteMessageVersionOctet, StubMessage, ), - ).rejects.toMatchObject({ - message: /^Message date is invalid:/, - }); + ).rejects.toThrowWithMessage(RAMFValidationError, /^Message date is invalid:/); }); }); describe('TTL', () => { test('TTL of exactly 180 days should be accepted', async () => { const messageSerialized = await serializeRamfWithoutValidation([ - new asn1js.VisibleString({ value: RECIPIENT_ADDRESS }), - new asn1js.VisibleString({ value: 'the-id' }), + new Sequence({ value: [new VisibleString({ value: RECIPIENT_ID })] }), + new VisibleString({ value: 'the-id' }), dateToASN1DateTimeInUTC(NOW), - new asn1js.Integer({ value: MAX_TTL }), - new asn1js.OctetString({ valueHex: new ArrayBuffer(0) }), + new Integer({ value: MAX_TTL }), + new OctetString({ valueHex: new ArrayBuffer(0) }), ]); await expect( @@ -904,11 +1082,11 @@ describe('MessageSerializer', () => { test('TTL greater than 180 days should not be accepted', async () => { const messageSerialized = await serializeRamfWithoutValidation([ - new asn1js.VisibleString({ value: RECIPIENT_ADDRESS }), - new asn1js.VisibleString({ value: 'the-id' }), + new Sequence({ value: [new VisibleString({ value: RECIPIENT_ID })] }), + new VisibleString({ value: 'the-id' }), dateToASN1DateTimeInUTC(NOW), - new asn1js.Integer({ value: MAX_TTL + 1 }), - new asn1js.OctetString({ valueHex: new ArrayBuffer(0) }), + new Integer({ value: MAX_TTL + 1 }), + new OctetString({ valueHex: new ArrayBuffer(0) }), ]); await expect( deserialize( @@ -925,12 +1103,12 @@ describe('MessageSerializer', () => { describe('Payload', () => { test('Payload should be extracted', async () => { - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); const messageSerialized = await serialize( message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); const messageDeserialized = await deserialize( @@ -946,11 +1124,11 @@ describe('MessageSerializer', () => { test('Payload size should not exceed 8 MiB', async () => { const largePayload = Buffer.from('a'.repeat(MAX_PAYLOAD_LENGTH + 1)); const messageSerialized = await serializeRamfWithoutValidation([ - new asn1js.VisibleString({ value: RECIPIENT_ADDRESS }), - new asn1js.VisibleString({ value: 'the-id' }), + new Sequence({ value: [new VisibleString({ value: RECIPIENT_ID })] }), + new VisibleString({ value: 'the-id' }), dateToASN1DateTimeInUTC(NOW), - new asn1js.Integer({ value: 1_000 }), - new asn1js.OctetString({ valueHex: bufferToArray(largePayload) }), + new Integer({ value: 1_000 }), + new OctetString({ valueHex: bufferToArray(largePayload) }), ]); await expect( @@ -968,12 +1146,12 @@ describe('MessageSerializer', () => { }); test('Valid messages should be successfully deserialized', async () => { - const message = new StubMessage(RECIPIENT_ADDRESS, SENDER_CERTIFICATE, PAYLOAD); + const message = new StubMessage(RECIPIENT, senderCertificate, PAYLOAD); const messageSerialized = await serialize( message, stubConcreteMessageTypeOctet, stubConcreteMessageVersionOctet, - SENDER_PRIVATE_KEY, + senderPrivateKey, ); jest.spyOn(cmsSignedData, 'verifySignature'); @@ -984,24 +1162,24 @@ describe('MessageSerializer', () => { StubMessage, ); - expect(messageDeserialized.recipientAddress).toEqual(message.recipientAddress); + expect(messageDeserialized.recipient).toEqual(message.recipient); expect(messageDeserialized.senderCertificate.isEqual(message.senderCertificate)).toBeTrue(); expect(messageDeserialized.payloadSerialized).toEqual(message.payloadSerialized); }); async function serializeRamfWithoutValidation( - sequenceItems: ReadonlyArray>, - senderCertificate?: Certificate, + sequenceItems: ReadonlyArray>, + customSenderCertificate?: Certificate, ): Promise { const serializer = new SmartBuffer(); - serializer.writeString('Relaynet'); + serializer.writeString('Awala'); serializer.writeUInt8(stubConcreteMessageTypeOctet); serializer.writeUInt8(stubConcreteMessageVersionOctet); const signedData = await cmsSignedData.SignedData.sign( makeImplicitlyTaggedSequence(...sequenceItems).toBER(), - SENDER_PRIVATE_KEY, - senderCertificate ?? SENDER_CERTIFICATE, + senderPrivateKey, + customSenderCertificate ?? senderCertificate, ); serializer.writeBuffer(Buffer.from(signedData.serialize())); @@ -1019,8 +1197,8 @@ interface MessageFormatSignature { function parseFormatSignature(messageSerialized: ArrayBuffer): MessageFormatSignature { const buffer = Buffer.from(messageSerialized); return { - concreteMessageType: buffer.readUInt8(8), - concreteMessageVersion: buffer.readUInt8(9), - magic: buffer.slice(0, 8).toString(), + concreteMessageType: buffer.readUInt8(5), + concreteMessageVersion: buffer.readUInt8(6), + magic: buffer.slice(0, 5).toString(), }; } diff --git a/src/lib/ramf/serialization.ts b/src/lib/ramf/serialization.ts index 055835d68..4bbd431b1 100644 --- a/src/lib/ramf/serialization.ts +++ b/src/lib/ramf/serialization.ts @@ -1,8 +1,17 @@ -import * as asn1js from 'asn1js'; +import { + Constructed, + Integer, + OctetString, + Primitive, + Sequence, + verifySchema, + VisibleString, +} from 'asn1js'; import bufferToArray from 'buffer-to-arraybuffer'; +import isValidDomain from 'is-valid-domain'; import { TextDecoder } from 'util'; -import { SignatureOptions } from '../..'; +import { Recipient, SignatureOptions } from '../..'; import { asn1DateTimeToDate, dateToASN1DateTimeInUTC, @@ -27,14 +36,15 @@ const MAX_ID_LENGTH = 64; export const RAMF_MAX_TTL = 15552000; const MAX_PAYLOAD_LENGTH = 2 ** 23 - 1; // 8 MiB -const PRIVATE_ADDRESS_REGEX = /^[a-f0-9]+$/; +const NODE_ID_REGEX = /^[a-f\d]+$/; /** * Maximum length of any SDU to be encapsulated in a CMS EnvelopedData value, per the RAMF spec. */ export const MAX_SDU_PLAINTEXT_LENGTH = 8322048; -const FORMAT_SIGNATURE_CONSTANT = Buffer.from('Relaynet'); +const FORMAT_SIGNATURE_CONSTANT = Buffer.from('Awala'); +const FORMAT_SIGNATURE_LENGTH = FORMAT_SIGNATURE_CONSTANT.byteLength + 2; interface MessageFormatSignature { readonly concreteMessageType: number; @@ -42,7 +52,7 @@ interface MessageFormatSignature { } interface MessageFieldSet { - readonly recipientAddress: string; + readonly recipient: Recipient; readonly id: string; readonly date: Date; readonly ttl: number; @@ -50,11 +60,11 @@ interface MessageFieldSet { } const ASN1_SCHEMA = makeHeterogeneousSequenceSchema('RAMFMessage', [ - new asn1js.Primitive({ name: 'recipientAddress' }), - new asn1js.Primitive({ name: 'id' }), - new asn1js.Primitive({ name: 'date' }), - new asn1js.Primitive({ name: 'ttl' }), - new asn1js.Primitive({ name: 'payload' }), + new Constructed({ name: 'recipient' }), + new Primitive({ name: 'id' }), + new Primitive({ name: 'date' }), + new Primitive({ name: 'ttl' }), + new Primitive({ name: 'payload' }), ]); /** @@ -74,7 +84,6 @@ export async function serialize( signatureOptions?: Partial, ): Promise { //region Validation - validateRecipientAddressLength(message.recipientAddress); validateMessageIdLength(message.id); validateTtl(message.ttl); validatePayloadLength(message.payloadSerialized); @@ -86,11 +95,11 @@ export async function serialize( ); const fieldSetSerialized = makeImplicitlyTaggedSequence( - new asn1js.VisibleString({ value: message.recipientAddress }), - new asn1js.VisibleString({ value: message.id }), + encodeRecipientField(message.recipient), + new VisibleString({ value: message.id }), dateToASN1DateTimeInUTC(message.creationDate), - new asn1js.Integer({ value: message.ttl }), - new asn1js.OctetString({ valueHex: bufferToArray(message.payloadSerialized) }), + new Integer({ value: message.ttl }), + new OctetString({ valueHex: bufferToArray(message.payloadSerialized) }), ).toBER(); //region Signature @@ -114,6 +123,18 @@ export async function serialize( return serialization; } +function encodeRecipientField(recipient: Recipient): Sequence { + validateRecipientFieldsLength(recipient); + + const additionalItems = recipient.internetAddress + ? [new VisibleString({ value: recipient.internetAddress })] + : []; + return makeImplicitlyTaggedSequence( + new VisibleString({ value: recipient.id }), + ...additionalItems, + ); +} + function validateMessageLength(serialization: ArrayBuffer): void { if (MAX_RAMF_MESSAGE_LENGTH < serialization.byteLength) { throw new RAMFSyntaxError( @@ -129,24 +150,23 @@ export async function deserialize>( messageClass: new (...args: readonly any[]) => M, ): Promise { validateMessageLength(serialization); - const messageFormatSignature = parseMessageFormatSignature(serialization.slice(0, 10)); + const messageFormatSignature = parseMessageFormatSignature(serialization); validateFileFormatSignature( messageFormatSignature, concreteMessageTypeOctet, concreteMessageVersionOctet, ); - const signatureVerification = await verifySignature(serialization.slice(10)); + const signatureVerification = await verifySignature(serialization.slice(FORMAT_SIGNATURE_LENGTH)); const messageFields = parseMessageFields(signatureVerification.plaintext); - validateRecipientAddressLength(messageFields.recipientAddress); - validateRecipientAddress(messageFields.recipientAddress); + validateRecipient(messageFields.recipient); validateMessageIdLength(messageFields.id); validateTtl(messageFields.ttl); validatePayloadLength(messageFields.payload); return new messageClass( - messageFields.recipientAddress, + messageFields.recipient, signatureVerification.signerCertificate, messageFields.payload, { @@ -190,26 +210,34 @@ function validateFileFormatSignature( //endregion } -function validateRecipientAddress(recipientAddress: string): void { - try { - // tslint:disable-next-line:no-unused-expression - new URL(recipientAddress); - } catch (_) { - // The address isn't public. Check if it's private: - if (!recipientAddress.match(PRIVATE_ADDRESS_REGEX)) { - throw new RAMFValidationError( - `Recipient address should be a valid node address (got: "${recipientAddress}")`, - ); - } +function validateRecipient(recipient: Recipient): void { + validateRecipientFieldsLength(recipient); + + if (!recipient.id.match(NODE_ID_REGEX)) { + throw new RAMFSyntaxError(`Recipient id is malformed ("${recipient.id}")`); + } + + if (recipient.internetAddress && !isValidDomain(recipient.internetAddress)) { + throw new RAMFSyntaxError( + `Recipient Internet address is malformed ("${recipient.internetAddress}")`, + ); } } -function validateRecipientAddressLength(recipientAddress: string): void { - const length = recipientAddress.length; - if (MAX_RECIPIENT_ADDRESS_LENGTH < length) { +function validateRecipientFieldsLength(recipient: Recipient): void { + const idLength = recipient.id.length; + if (MAX_RECIPIENT_ADDRESS_LENGTH < idLength) { throw new RAMFSyntaxError( - `Recipient address should not span more than ${MAX_RECIPIENT_ADDRESS_LENGTH} characters ` + - `(got ${length})`, + `Recipient id should not span more than ${MAX_RECIPIENT_ADDRESS_LENGTH} characters ` + + `(got ${idLength})`, + ); + } + + const internetAddressLength = recipient.internetAddress?.length ?? 0; + if (MAX_RECIPIENT_ADDRESS_LENGTH < internetAddressLength) { + throw new RAMFSyntaxError( + `Recipient Internet address should not span more than ${MAX_RECIPIENT_ADDRESS_LENGTH} ` + + `characters (got ${internetAddressLength})`, ); } } @@ -244,34 +272,46 @@ function validatePayloadLength(payloadBuffer: ArrayBuffer): void { //region Deserialization validation function parseMessageFormatSignature(serialization: ArrayBuffer): MessageFormatSignature { - if (serialization.byteLength < 10) { + if (serialization.byteLength < FORMAT_SIGNATURE_LENGTH) { throw new RAMFSyntaxError('Serialization is too small to contain RAMF format signature'); } - const formatSignature = Buffer.from(serialization.slice(0, 10)); - if (!FORMAT_SIGNATURE_CONSTANT.equals(formatSignature.slice(0, 8))) { - throw new RAMFSyntaxError('RAMF format signature does not begin with "Relaynet"'); + const formatSignature = Buffer.from(serialization.slice(0, FORMAT_SIGNATURE_LENGTH)); + const formatSignatureConstant = formatSignature.slice(0, FORMAT_SIGNATURE_CONSTANT.byteLength); + if (!FORMAT_SIGNATURE_CONSTANT.equals(formatSignatureConstant)) { + throw new RAMFSyntaxError('RAMF format signature does not begin with "Awala"'); } - return { concreteMessageType: formatSignature[8], concreteMessageVersion: formatSignature[9] }; + return { concreteMessageType: formatSignature[5], concreteMessageVersion: formatSignature[6] }; } function parseMessageFields(serialization: ArrayBuffer): MessageFieldSet { - const result = asn1js.verifySchema(serialization, ASN1_SCHEMA); + const result = verifySchema(serialization, ASN1_SCHEMA); if (!result.verified) { throw new RAMFSyntaxError('Invalid RAMF fields'); } const messageBlock = result.result.RAMFMessage; const textDecoder = new TextDecoder(); const ttlBigInt = getIntegerFromPrimitiveBlock(messageBlock.ttl); + + const recipientSequence = messageBlock.recipient.valueBlock.value; + if (recipientSequence.length === 0) { + throw new RAMFSyntaxError('Recipient SEQUENCE should at least contain the id'); + } + const recipientId = textDecoder.decode(recipientSequence[0].valueBlock.valueHex); + const recipientInternetAddress = + 2 <= recipientSequence.length + ? textDecoder.decode(recipientSequence[1].valueBlock.valueHex) + : undefined; + return { date: getDateFromPrimitiveBlock(messageBlock.date), id: textDecoder.decode(messageBlock.id.valueBlock.valueHex), payload: Buffer.from(messageBlock.payload.valueBlock.valueHex), - recipientAddress: textDecoder.decode(messageBlock.recipientAddress.valueBlock.valueHex), + recipient: { id: recipientId, internetAddress: recipientInternetAddress }, ttl: Number(ttlBigInt), // Cannot exceed Number.MAX_SAFE_INTEGER anyway }; } -function getDateFromPrimitiveBlock(block: asn1js.Primitive): Date { +function getDateFromPrimitiveBlock(block: Primitive): Date { try { return asn1DateTimeToDate(block); } catch (exc) { @@ -279,8 +319,8 @@ function getDateFromPrimitiveBlock(block: asn1js.Primitive): Date { } } -function getIntegerFromPrimitiveBlock(block: asn1js.Primitive): bigint { - const integerBlock = new asn1js.Integer({ valueHex: block.valueBlock.valueHexView }); +function getIntegerFromPrimitiveBlock(block: Primitive): bigint { + const integerBlock = new Integer({ valueHex: block.valueBlock.valueHexView }); return integerBlock.toBigInt(); }