Skip to content

Commit

Permalink
fix(RAMF): Require private address and support public address of reci…
Browse files Browse the repository at this point in the history
…pient (#491)

Part of relaycorp/relayverse#19

- [x] Implement `RecipientAddressing` class.
- [x] Reimplement `RAMFMessage.recipient` as a `RecipientAddressing`
- [x] Remove `RAMFMessage.isRecipientAddressPrivate`
- [x] Remove `https://` prefix from Internet addresses.
  • Loading branch information
gnarea authored Jul 28, 2022
1 parent b648b36 commit 53e67ef
Show file tree
Hide file tree
Showing 63 changed files with 1,435 additions and 1,588 deletions.
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export {
getPublicKeyDigest,
getPublicKeyDigestHex,
getRSAPublicKeyFromPrivate,
getPrivateAddressFromIdentityKey,
getIdFromIdentityKey,
RSAKeyGenOptions,
} from './lib/crypto_wrappers/keys';
export { PrivateKey, RsaPssPrivateKey } from './lib/crypto_wrappers/PrivateKey';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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',
);
Expand All @@ -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 });
});
});
57 changes: 26 additions & 31 deletions src/integration_tests/pki.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { addDays, subSeconds } from 'date-fns';

import {
Certificate,
generateRSAKeyPair,
Expand All @@ -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,
}),
Expand All @@ -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,
Expand All @@ -51,6 +52,8 @@ beforeAll(async () => {
}),
);

peerEndpointId = await peerEndpointCert.calculateSubjectId();

const endpointKeyPair = await generateRSAKeyPair();
endpointPdaCert = reSerializeCertificate(
await issueDeliveryAuthorization({
Expand All @@ -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)),
]);
});

Expand All @@ -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'),
{
Expand All @@ -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',
);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/IdentityKeyPair.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export interface IdentityKeyPair extends CryptoKeyPair {
readonly privateAddress: string;
readonly id: string;
}
30 changes: 22 additions & 8 deletions src/lib/_test_utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -169,14 +169,28 @@ export async function asyncIterableToArray<T>(iterable: AsyncIterable<T>): Promi
return values;
}

export function getAsn1SequenceItem(
fields: asn1js.Sequence | asn1js.BaseBlock<any>,
export function getPrimitiveItemFromConstructed(
fields: BaseBlock<any>,
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<any>,
itemIndex: number,
): Constructed {
const itemBlock = getItemFromConstructed(fields, itemIndex);
expect(itemBlock).toBeInstanceOf(Constructed);
return itemBlock as Constructed;
}

function getItemFromConstructed(fields: BaseBlock<any>, itemIndex: number): BaseBlock<any> {
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;
}
39 changes: 15 additions & 24 deletions src/lib/bindings/gsc/ParcelCollection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { addDays } from 'date-fns';

import {
arrayBufferFrom,
expectArrayBuffersToEqual,
Expand All @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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],
Expand All @@ -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],
Expand Down
3 changes: 1 addition & 2 deletions src/lib/bindings/gsc/ParcelCollection.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -15,7 +14,7 @@ export class ParcelCollection {

public async deserializeAndValidateParcel(): Promise<Parcel> {
const parcel = await Parcel.deserialize(this.parcelSerialized);
await parcel.validate(RecipientAddressType.PRIVATE, this.trustedCertificates);
await parcel.validate(this.trustedCertificates);
return parcel;
}
}
Loading

0 comments on commit 53e67ef

Please sign in to comment.