Skip to content

Commit

Permalink
Merge pull request #775 from MatrixAI/feature-network-segregation
Browse files Browse the repository at this point in the history
Network Segregation
  • Loading branch information
amydevs committed Sep 2, 2024
2 parents ea33fe1 + ad8134f commit 8490111
Show file tree
Hide file tree
Showing 15 changed files with 722 additions and 3 deletions.
5 changes: 5 additions & 0 deletions src/PolykeyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import * as workersUtils from './workers/utils';
import * as clientMiddleware from './client/middleware';
import clientServerManifest from './client/handlers';
import agentServerManifest from './nodes/agent/handlers';

/**
* Optional configuration for `PolykeyAgent`.
*/
Expand All @@ -61,6 +62,7 @@ type PolykeyAgentOptions = {
clientServicePort: number;
agentServiceHost: string;
agentServicePort: number;
network: string;
seedNodes: SeedNodes;
workers: number;
ipv6Only: boolean;
Expand Down Expand Up @@ -160,6 +162,7 @@ class PolykeyAgent {
agentServiceHost: config.defaultsUser.agentServiceHost,
agentServicePort: config.defaultsUser.agentServicePort,
seedNodes: config.defaultsUser.seedNodes,
network: config.defaultsUser.network,
workers: config.defaultsUser.workers,
ipv6Only: config.defaultsUser.ipv6Only,
keys: {
Expand Down Expand Up @@ -687,6 +690,7 @@ class PolykeyAgent {
groups: Array<string>;
port: number;
};
network: string;
seedNodes: SeedNodes;
}>;
workers?: number;
Expand All @@ -705,6 +709,7 @@ class PolykeyAgent {
groups: config.defaultsSystem.mdnsGroups,
port: config.defaultsSystem.mdnsPort,
},
network: config.defaultsUser.network,
seedNodes: config.defaultsUser.seedNodes,
});
// Register event handlers
Expand Down
2 changes: 1 addition & 1 deletion src/bootstrap/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import * as utils from '../utils';
import * as errors from '../errors';

/**
* Bootstraps the Node Path
* Bootstraps the Node Path`
*/
async function bootstrapState({
// Required parameters
Expand Down
93 changes: 93 additions & 0 deletions src/claims/payloads/claimNetworkAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { Claim, SignedClaim } from '../types';
import type { NodeIdEncoded } from '../../ids/types';
import type { SignedTokenEncoded } from '../../tokens/types';
import * as tokensSchema from '../../tokens/schemas';
import * as ids from '../../ids';
import * as claimsUtils from '../utils';
import * as tokensUtils from '../../tokens/utils';
import * as validationErrors from '../../validation/errors';
import * as utils from '../../utils';

/**
* Asserts that a node is apart of a network
*/
interface ClaimNetworkAccess extends Claim {
typ: 'ClaimNetworkAccess';
iss: NodeIdEncoded;
sub: NodeIdEncoded;
network: string;
signedClaimNetworkAuthorityEncoded?: SignedTokenEncoded;
}

function assertClaimNetworkAccess(
claimNetworkAccess: unknown,
): asserts claimNetworkAccess is ClaimNetworkAccess {
if (!utils.isObject(claimNetworkAccess)) {
throw new validationErrors.ErrorParse('must be POJO');
}
if (claimNetworkAccess['typ'] !== 'ClaimNetworkAccess') {
throw new validationErrors.ErrorParse(
'`typ` property must be `ClaimNetworkAccess`',
);
}
if (
claimNetworkAccess['iss'] == null ||
ids.decodeNodeId(claimNetworkAccess['iss']) == null
) {
throw new validationErrors.ErrorParse(
'`iss` property must be an encoded node ID',
);
}
if (
claimNetworkAccess['sub'] == null ||
ids.decodeNodeId(claimNetworkAccess['sub']) == null
) {
throw new validationErrors.ErrorParse(
'`sub` property must be an encoded node ID',
);
}
if (
claimNetworkAccess['network'] == null ||
typeof claimNetworkAccess['network'] !== 'string'
) {
throw new validationErrors.ErrorParse(
'`network` property must be a string',
);
}
if (
claimNetworkAccess['signedClaimNetworkAuthorityEncoded'] != null &&
!tokensSchema.validateSignedTokenEncoded(
claimNetworkAccess['signedClaimNetworkAuthorityEncoded'],
)
) {
throw new validationErrors.ErrorParse(
'`signedClaimNetworkAuthorityEncoded` property must be an encoded signed token',
);
}
}

function parseClaimNetworkAccess(
claimNetworkAccessEncoded: unknown,
): ClaimNetworkAccess {
const claimNetworkNode = claimsUtils.parseClaim(claimNetworkAccessEncoded);
assertClaimNetworkAccess(claimNetworkNode);
return claimNetworkNode;
}

function parseSignedClaimNetworkAccess(
signedClaimNetworkAccessEncoded: unknown,
): SignedClaim<ClaimNetworkAccess> {
const signedClaim = tokensUtils.parseSignedToken(
signedClaimNetworkAccessEncoded,
);
assertClaimNetworkAccess(signedClaim.payload);
return signedClaim as SignedClaim<ClaimNetworkAccess>;
}

export {
assertClaimNetworkAccess,
parseClaimNetworkAccess,
parseSignedClaimNetworkAccess,
};

export type { ClaimNetworkAccess };
71 changes: 71 additions & 0 deletions src/claims/payloads/claimNetworkAuthority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Claim, SignedClaim } from '../types';
import type { NodeIdEncoded } from '../../ids/types';
import * as ids from '../../ids';
import * as claimsUtils from '../utils';
import * as tokensUtils from '../../tokens/utils';
import * as validationErrors from '../../validation/errors';
import * as utils from '../../utils';

/**
* Asserts that a node is apart of a network
*/
interface ClaimNetworkAuthority extends Claim {
typ: 'ClaimNetworkAuthority';
iss: NodeIdEncoded;
sub: NodeIdEncoded;
}

function assertClaimNetworkAuthority(
claimNetworkAuthority: unknown,
): asserts claimNetworkAuthority is ClaimNetworkAuthority {
if (!utils.isObject(claimNetworkAuthority)) {
throw new validationErrors.ErrorParse('must be POJO');
}
if (claimNetworkAuthority['typ'] !== 'ClaimNetworkAuthority') {
throw new validationErrors.ErrorParse(
'`typ` property must be `ClaimNetworkAuthority`',
);
}
if (
claimNetworkAuthority['iss'] == null ||
ids.decodeNodeId(claimNetworkAuthority['iss']) == null
) {
throw new validationErrors.ErrorParse(
'`iss` property must be an encoded node ID',
);
}
if (
claimNetworkAuthority['sub'] == null ||
ids.decodeNodeId(claimNetworkAuthority['sub']) == null
) {
throw new validationErrors.ErrorParse(
'`sub` property must be an encoded node ID',
);
}
}

function parseClaimNetworkAuthority(
claimNetworkNodeEncoded: unknown,
): ClaimNetworkAuthority {
const claimNetworkNode = claimsUtils.parseClaim(claimNetworkNodeEncoded);
assertClaimNetworkAuthority(claimNetworkNode);
return claimNetworkNode;
}

function parseSignedClaimNetworkAuthority(
signedClaimNetworkNodeEncoded: unknown,
): SignedClaim<ClaimNetworkAuthority> {
const signedClaim = tokensUtils.parseSignedToken(
signedClaimNetworkNodeEncoded,
);
assertClaimNetworkAuthority(signedClaim.payload);
return signedClaim as SignedClaim<ClaimNetworkAuthority>;
}

export {
assertClaimNetworkAuthority,
parseClaimNetworkAuthority,
parseSignedClaimNetworkAuthority,
};

export type { ClaimNetworkAuthority };
1 change: 1 addition & 0 deletions src/claims/payloads/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './claimLinkIdentity';
export * from './claimLinkNode';
export * from './claimNetworkAccess';
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,12 @@ const config = {
*/
agentServiceHost: '::',
agentServicePort: 0,
/**
* Hostname of network to connect to.
*
* This is defaulted to 'mainnet.polykey.com'.
*/
network: 'mainnet.polykey.com',
/**
* Seed nodes.
*
Expand Down
135 changes: 133 additions & 2 deletions src/nodes/NodeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ import { MDNS, events as mdnsEvents, utils as mdnsUtils } from '@matrixai/mdns';
import * as nodesUtils from './utils';
import * as nodesEvents from './events';
import * as nodesErrors from './errors';
import * as agentErrors from './agent/errors';
import NodeConnectionQueue from './NodeConnectionQueue';
import { assertClaimNetworkAuthority } from '../claims/payloads/claimNetworkAuthority';
import { assertClaimNetworkAccess } from '../claims/payloads/claimNetworkAccess';
import Token from '../tokens/Token';
import * as keysUtils from '../keys/utils';
import * as tasksErrors from '../tasks/errors';
Expand Down Expand Up @@ -247,8 +250,8 @@ class NodeManager {
);
const successfulConnections = connectionResults.filter(
(r) => r.status === 'fulfilled',
).length;
if (successfulConnections === 0) {
) as Array<PromiseFulfilledResult<NodeConnection>>;
if (successfulConnections.length === 0) {
const failedConnectionErrors = connectionResults
.filter((r) => r.status === 'rejected')
.map((v) => {
Expand Down Expand Up @@ -1478,6 +1481,132 @@ class NodeManager {
});
}

public async handleClaimNetwork(
requestingNodeId: NodeId,
input: AgentRPCRequestParams<AgentClaimMessage>,
tran?: DBTransaction,
): Promise<AgentRPCResponseResult<AgentClaimMessage>> {
if (tran == null) {
return this.db.withTransactionF((tran) =>
this.handleClaimNetwork(requestingNodeId, input, tran),
);
}
const signedClaim = claimsUtils.parseSignedClaim(input.signedTokenEncoded);
const token = Token.fromSigned(signedClaim);
// Verify if the token is signed
if (
!token.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(requestingNodeId),
)
) {
throw new claimsErrors.ErrorSinglySignedClaimVerificationFailed();
}
// If verified, add your own signature to the received claim
token.signWithPrivateKey(this.keyRing.keyPair);
// Return the signed claim
const doublySignedClaim = token.toSigned();
const halfSignedClaimEncoded =
claimsUtils.generateSignedClaim(doublySignedClaim);
return {
signedTokenEncoded: halfSignedClaimEncoded,
};
}

public async handleVerifyClaimNetwork(
requestingNodeId: NodeId,
input: AgentRPCRequestParams<AgentClaimMessage>,
tran?: DBTransaction,
): Promise<AgentRPCResponseResult<{ success: true }>> {
if (tran == null) {
return this.db.withTransactionF((tran) =>
this.handleVerifyClaimNetwork(requestingNodeId, input, tran),
);
}
const signedClaim = claimsUtils.parseSignedClaim(input.signedTokenEncoded);
const token = Token.fromSigned(signedClaim);
assertClaimNetworkAccess(token.payload);
// Verify if the token is signed
if (
!token.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(requestingNodeId),
) ||
!token.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(
nodesUtils.decodeNodeId(token.payload.iss)!,
),
)
) {
throw new claimsErrors.ErrorDoublySignedClaimVerificationFailed();
}
if (
token.payload.network === 'testnet.polykey.com' ||
token.payload.network === 'mainnet.polykey.com'
) {
return { success: true };
}
if (token.payload.signedClaimNetworkAuthorityEncoded == null) {
throw new claimsErrors.ErrorDoublySignedClaimVerificationFailed();
}
const authorityToken = Token.fromEncoded(
token.payload.signedClaimNetworkAuthorityEncoded,
);
// Verify if the token is signed
if (
token.payload.iss !== authorityToken.payload.sub ||
!authorityToken.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(
nodesUtils.decodeNodeId(authorityToken.payload.sub)!,
),
) ||
!authorityToken.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(
nodesUtils.decodeNodeId(authorityToken.payload.iss)!,
),
)
) {
throw new claimsErrors.ErrorDoublySignedClaimVerificationFailed();
}

let success = false;
for await (const [_, claim] of this.sigchain.getSignedClaims({})) {
try {
assertClaimNetworkAccess(claim.payload);
} catch {
continue;
}
if (claim.payload.signedClaimNetworkAuthorityEncoded == null) {
throw new claimsErrors.ErrorDoublySignedClaimVerificationFailed();
}
const tokenNetworkAuthority = Token.fromEncoded(
claim.payload.signedClaimNetworkAuthorityEncoded,
);
try {
assertClaimNetworkAuthority(tokenNetworkAuthority.payload);
} catch {
continue;
}
// No need to check if local claims are correctly signed by an Network Authority.
if (
authorityToken.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(
nodesUtils.decodeNodeId(claim.payload.iss)!,
),
)
) {
success = true;
break;
}
}

if (!success) {
throw new agentErrors.ErrorNodesClaimNetworkVerificationFailed();
}

return {
success: true,
};
}

/**
* Adds a node to the node graph. This assumes that you have already authenticated the node
* Updates the node if the node already exists
Expand Down Expand Up @@ -1535,6 +1664,8 @@ class NodeManager {
);
}

// Need to await node connection verification, if fail, need to reject connection.

// When adding a node we need to handle 3 cases
// 1. The node already exists. We need to update it's last updated field
// 2. The node doesn't exist and bucket has room.
Expand Down
Loading

0 comments on commit 8490111

Please sign in to comment.