Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Network Segregation #775

Merged
merged 1 commit into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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