diff --git a/src/PolykeyAgent.ts b/src/PolykeyAgent.ts index 702887144..337434b82 100644 --- a/src/PolykeyAgent.ts +++ b/src/PolykeyAgent.ts @@ -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`. */ @@ -61,6 +62,7 @@ type PolykeyAgentOptions = { clientServicePort: number; agentServiceHost: string; agentServicePort: number; + network: string; seedNodes: SeedNodes; workers: number; ipv6Only: boolean; @@ -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: { @@ -687,6 +690,7 @@ class PolykeyAgent { groups: Array; port: number; }; + network: string; seedNodes: SeedNodes; }>; workers?: number; @@ -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 diff --git a/src/bootstrap/utils.ts b/src/bootstrap/utils.ts index dd4d2b4ed..d6cd2e66a 100644 --- a/src/bootstrap/utils.ts +++ b/src/bootstrap/utils.ts @@ -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 diff --git a/src/claims/payloads/claimNetworkAccess.ts b/src/claims/payloads/claimNetworkAccess.ts new file mode 100644 index 000000000..0be8a340e --- /dev/null +++ b/src/claims/payloads/claimNetworkAccess.ts @@ -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 { + const signedClaim = tokensUtils.parseSignedToken( + signedClaimNetworkAccessEncoded, + ); + assertClaimNetworkAccess(signedClaim.payload); + return signedClaim as SignedClaim; +} + +export { + assertClaimNetworkAccess, + parseClaimNetworkAccess, + parseSignedClaimNetworkAccess, +}; + +export type { ClaimNetworkAccess }; diff --git a/src/claims/payloads/claimNetworkAuthority.ts b/src/claims/payloads/claimNetworkAuthority.ts new file mode 100644 index 000000000..71a59fa6c --- /dev/null +++ b/src/claims/payloads/claimNetworkAuthority.ts @@ -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 { + const signedClaim = tokensUtils.parseSignedToken( + signedClaimNetworkNodeEncoded, + ); + assertClaimNetworkAuthority(signedClaim.payload); + return signedClaim as SignedClaim; +} + +export { + assertClaimNetworkAuthority, + parseClaimNetworkAuthority, + parseSignedClaimNetworkAuthority, +}; + +export type { ClaimNetworkAuthority }; diff --git a/src/claims/payloads/index.ts b/src/claims/payloads/index.ts index ba1f07f5f..52c955be6 100644 --- a/src/claims/payloads/index.ts +++ b/src/claims/payloads/index.ts @@ -1,2 +1,3 @@ export * from './claimLinkIdentity'; export * from './claimLinkNode'; +export * from './claimNetworkAccess'; diff --git a/src/config.ts b/src/config.ts index 393ce8117..b5fbac37d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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. * diff --git a/src/nodes/NodeManager.ts b/src/nodes/NodeManager.ts index fdb95fa44..4f0b3747f 100644 --- a/src/nodes/NodeManager.ts +++ b/src/nodes/NodeManager.ts @@ -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'; @@ -247,8 +250,8 @@ class NodeManager { ); const successfulConnections = connectionResults.filter( (r) => r.status === 'fulfilled', - ).length; - if (successfulConnections === 0) { + ) as Array>; + if (successfulConnections.length === 0) { const failedConnectionErrors = connectionResults .filter((r) => r.status === 'rejected') .map((v) => { @@ -1478,6 +1481,132 @@ class NodeManager { }); } + public async handleClaimNetwork( + requestingNodeId: NodeId, + input: AgentRPCRequestParams, + tran?: DBTransaction, + ): Promise> { + 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, + tran?: DBTransaction, + ): Promise> { + 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 @@ -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. diff --git a/src/nodes/agent/callers/index.ts b/src/nodes/agent/callers/index.ts index 12009b063..e213dbb4e 100644 --- a/src/nodes/agent/callers/index.ts +++ b/src/nodes/agent/callers/index.ts @@ -4,6 +4,8 @@ import nodesClosestLocalNodesGet from './nodesClosestLocalNodesGet'; import nodesConnectionSignalFinal from './nodesConnectionSignalFinal'; import nodesConnectionSignalInitial from './nodesConnectionSignalInitial'; import nodesCrossSignClaim from './nodesCrossSignClaim'; +import nodesClaimNetworkSign from './nodesClaimNetworkSign'; +import nodesClaimNetworkVerify from './nodesClaimNetworkVerify'; import notificationsSend from './notificationsSend'; import vaultsGitInfoGet from './vaultsGitInfoGet'; import vaultsGitPackGet from './vaultsGitPackGet'; @@ -19,6 +21,8 @@ const manifestClient = { nodesConnectionSignalFinal, nodesConnectionSignalInitial, nodesCrossSignClaim, + nodesClaimNetworkSign, + nodesClaimNetworkVerify, notificationsSend, vaultsGitInfoGet, vaultsGitPackGet, @@ -36,6 +40,8 @@ export { nodesConnectionSignalFinal, nodesConnectionSignalInitial, nodesCrossSignClaim, + nodesClaimNetworkSign, + nodesClaimNetworkVerify, notificationsSend, vaultsGitInfoGet, vaultsGitPackGet, diff --git a/src/nodes/agent/callers/nodesClaimNetworkSign.ts b/src/nodes/agent/callers/nodesClaimNetworkSign.ts new file mode 100644 index 000000000..afbf833b2 --- /dev/null +++ b/src/nodes/agent/callers/nodesClaimNetworkSign.ts @@ -0,0 +1,12 @@ +import type { HandlerTypes } from '@matrixai/rpc'; +import type NodesClaimNetworkSign from '../handlers/NodesClaimNetworkSign'; +import { UnaryCaller } from '@matrixai/rpc'; + +type CallerTypes = HandlerTypes; + +const nodesClaimNetworkSign = new UnaryCaller< + CallerTypes['input'], + CallerTypes['output'] +>(); + +export default nodesClaimNetworkSign; diff --git a/src/nodes/agent/callers/nodesClaimNetworkVerify.ts b/src/nodes/agent/callers/nodesClaimNetworkVerify.ts new file mode 100644 index 000000000..32b659beb --- /dev/null +++ b/src/nodes/agent/callers/nodesClaimNetworkVerify.ts @@ -0,0 +1,12 @@ +import type { HandlerTypes } from '@matrixai/rpc'; +import type NodesClaimNetworkVerify from '../handlers/NodesClaimNetworkVerify'; +import { UnaryCaller } from '@matrixai/rpc'; + +type CallerTypes = HandlerTypes; + +const nodesClaimNetworkVerify = new UnaryCaller< + CallerTypes['input'], + CallerTypes['output'] +>(); + +export default nodesClaimNetworkVerify; diff --git a/src/nodes/agent/errors.ts b/src/nodes/agent/errors.ts index 44bad9347..7ff9bba44 100644 --- a/src/nodes/agent/errors.ts +++ b/src/nodes/agent/errors.ts @@ -22,8 +22,14 @@ class ErrorNodesConnectionSignalRelayVerificationFailed< exitCode = sysexits.UNAVAILABLE; } +class ErrorNodesClaimNetworkVerificationFailed extends ErrorAgent { + static description = 'Failed to verify claim network message'; + exitCode = sysexits.UNAVAILABLE; +} + export { ErrorAgentNodeIdMissing, ErrorNodesConnectionSignalRequestVerificationFailed, ErrorNodesConnectionSignalRelayVerificationFailed, + ErrorNodesClaimNetworkVerificationFailed, }; diff --git a/src/nodes/agent/handlers/NodesClaimNetworkSign.ts b/src/nodes/agent/handlers/NodesClaimNetworkSign.ts new file mode 100644 index 000000000..aac7cfd30 --- /dev/null +++ b/src/nodes/agent/handlers/NodesClaimNetworkSign.ts @@ -0,0 +1,34 @@ +import type { + AgentRPCRequestParams, + AgentRPCResponseResult, + AgentClaimMessage, +} from '../types'; +import type NodeManager from '../../../nodes/NodeManager'; +import type { JSONValue } from '../../../types'; +import { UnaryHandler } from '@matrixai/rpc'; +import * as agentErrors from '../errors'; +import * as agentUtils from '../utils'; + +class NodesClaimNetworkSign extends UnaryHandler< + { + nodeManager: NodeManager; + }, + AgentRPCRequestParams, + AgentRPCResponseResult +> { + public handle = async ( + input: AgentRPCRequestParams, + _cancel, + meta: Record | undefined, + ): Promise> => { + const { nodeManager } = this.container; + // Connections should always be validated + const requestingNodeId = agentUtils.nodeIdFromMeta(meta); + if (requestingNodeId == null) { + throw new agentErrors.ErrorAgentNodeIdMissing(); + } + return nodeManager.handleClaimNetwork(requestingNodeId, input); + }; +} + +export default NodesClaimNetworkSign; diff --git a/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts b/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts new file mode 100644 index 000000000..f5eaa9886 --- /dev/null +++ b/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts @@ -0,0 +1,35 @@ +import type { + AgentClaimMessage, + AgentRPCRequestParams, + AgentRPCResponseResult, +} from '../types'; +import type NodeManager from '../../../nodes/NodeManager'; +import type { JSONValue } from '../../../types'; +import { UnaryHandler } from '@matrixai/rpc'; +import * as agentErrors from '../errors'; +import * as agentUtils from '../utils'; + +class NodesClaimNetworkVerify extends UnaryHandler< + { + nodeManager: NodeManager; + }, + AgentRPCRequestParams, + AgentRPCResponseResult<{ success: true }> +> { + public handle = async ( + input: AgentRPCRequestParams, + _cancel, + meta: Record | undefined, + ): Promise> => { + const requestingNodeId = agentUtils.nodeIdFromMeta(meta); + if (requestingNodeId == null) { + throw new agentErrors.ErrorAgentNodeIdMissing(); + } + return this.container.nodeManager.handleVerifyClaimNetwork( + requestingNodeId, + input, + ); + }; +} + +export default NodesClaimNetworkVerify; diff --git a/src/nodes/agent/handlers/index.ts b/src/nodes/agent/handlers/index.ts index a59564497..1a212c510 100644 --- a/src/nodes/agent/handlers/index.ts +++ b/src/nodes/agent/handlers/index.ts @@ -14,6 +14,7 @@ import NodesClosestLocalNodesGet from './NodesClosestLocalNodesGet'; import NodesConnectionSignalFinal from './NodesConnectionSignalFinal'; import NodesConnectionSignalInitial from './NodesConnectionSignalInitial'; import NodesCrossSignClaim from './NodesCrossSignClaim'; +import NodesClaimNetworkSign from './NodesClaimNetworkSign'; import NotificationsSend from './NotificationsSend'; import VaultsGitInfoGet from './VaultsGitInfoGet'; import VaultsGitPackGet from './VaultsGitPackGet'; @@ -43,6 +44,7 @@ const manifestServer = (container: { nodesConnectionSignalFinal: new NodesConnectionSignalFinal(container), nodesConnectionSignalInitial: new NodesConnectionSignalInitial(container), nodesCrossSignClaim: new NodesCrossSignClaim(container), + nodesClaimNetworkSign: new NodesClaimNetworkSign(container), notificationsSend: new NotificationsSend(container), vaultsGitInfoGet: new VaultsGitInfoGet(container), vaultsGitPackGet: new VaultsGitPackGet(container), @@ -61,6 +63,7 @@ export { NodesConnectionSignalFinal, NodesConnectionSignalInitial, NodesCrossSignClaim, + NodesClaimNetworkSign, NotificationsSend, VaultsGitInfoGet, VaultsGitPackGet, diff --git a/tests/nodes/agent/handlers/nodesClaimNetworkVerify.test.ts b/tests/nodes/agent/handlers/nodesClaimNetworkVerify.test.ts new file mode 100644 index 000000000..4859ed36f --- /dev/null +++ b/tests/nodes/agent/handlers/nodesClaimNetworkVerify.test.ts @@ -0,0 +1,304 @@ +import type NodeConnectionManager from '@/nodes/NodeConnectionManager'; +import type { NodeId } from '@/ids'; +import type { KeyPair } from '@/keys/types'; +import type { SignedTokenEncoded } from '@/tokens/types'; +import type { ClaimNetworkAuthority } from '@/claims/payloads/claimNetworkAuthority'; +import type { ClaimNetworkAccess } from '@/claims/payloads/claimNetworkAccess'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { QUICClient, QUICServer, events as quicEvents } from '@matrixai/quic'; +import { DB } from '@matrixai/db'; +import { RPCClient, RPCServer } from '@matrixai/rpc'; +import { nodesClaimNetworkVerify } from '@/nodes/agent/callers'; +import { Token } from '@/tokens'; +import Sigchain from '@/sigchain/Sigchain'; +import KeyRing from '@/keys/KeyRing'; +import NodeGraph from '@/nodes/NodeGraph'; +import NodesClaimNetworkVerify from '@/nodes/agent/handlers/NodesClaimNetworkVerify'; +import ACL from '@/acl/ACL'; +import NodeManager from '@/nodes/NodeManager'; +import GestaltGraph from '@/gestalts/GestaltGraph'; +import TaskManager from '@/tasks/TaskManager'; +import * as keysUtils from '@/keys/utils'; +import * as claimsUtils from '@/claims/utils'; +import * as nodesUtils from '@/nodes/utils'; +import * as networkUtils from '@/network/utils'; +import * as tlsTestsUtils from '../../../utils/tls'; + +describe('nodesClaimNetworkVerify', () => { + const logger = new Logger('nodesClaimNetworkVerify test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const password = 'password'; + const localHost = '127.0.0.1'; + + let dataDir: string; + + let keyRing: KeyRing; + let acl: ACL; + let remoteNodeId: NodeId; + let db: DB; + let sigchain: Sigchain; + let nodeGraph: NodeGraph; + let taskManager: TaskManager; + let rpcServer: RPCServer; + let quicServer: QUICServer; + + const clientManifest = { + nodesClaimNetworkVerify, + }; + type ClientManifest = typeof clientManifest; + let rpcClient: RPCClient; + let quicClient: QUICClient; + let authorityKeyPair: KeyPair; + let authorityNodeId: NodeId; + let seedKeyPair: KeyPair; + let seedNodeId: NodeId; + let clientKeyPair: KeyPair; + let localNodeId: NodeId; + let signedClaimNetworkAuthorityEncoded: SignedTokenEncoded; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + + // Handler dependencies + const keysPath = path.join(dataDir, 'keys'); + keyRing = await KeyRing.createKeyRing({ + keysPath, + password, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + logger, + }); + remoteNodeId = keyRing.getNodeId(); + const dbPath = path.join(dataDir, 'db'); + db = await DB.createDB({ + dbPath, + logger, + }); + nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyRing, + logger, + }); + + acl = await ACL.createACL({ + db, + logger, + }); + const gestaltGraph = await GestaltGraph.createGestaltGraph({ + db, + acl, + logger, + }); + taskManager = await TaskManager.createTaskManager({ + db, + logger, + }); + sigchain = await Sigchain.createSigchain({ + db, + keyRing, + logger, + }); + const nodeManager = new NodeManager({ + db, + keyRing, + gestaltGraph, + nodeConnectionManager: {} as NodeConnectionManager, + nodeGraph: {} as NodeGraph, + sigchain, + taskManager, + logger, + }); + await taskManager.startProcessing(); + + // Setting up server + const serverManifest = { + nodesClaimNetworkVerify: new NodesClaimNetworkVerify({ + nodeManager, + }), + }; + rpcServer = new RPCServer({ + fromError: networkUtils.fromError, + logger, + }); + await rpcServer.start({ manifest: serverManifest }); + const tlsConfigServer = await tlsTestsUtils.createTLSConfig( + keyRing.keyPair, + ); + quicServer = new QUICServer({ + config: { + key: tlsConfigServer.keyPrivatePem, + cert: tlsConfigServer.certChainPem, + verifyPeer: true, + verifyCallback: async () => { + return undefined; + }, + }, + crypto: nodesUtils.quicServerCrypto, + logger, + }); + const handleStream = async ( + event: quicEvents.EventQUICConnectionStream, + ) => { + // Streams are handled via the RPCServer. + const stream = event.detail; + logger.info('!!!!Handling new stream!!!!!'); + rpcServer.handleStream(stream); + }; + const handleConnection = async ( + event: quicEvents.EventQUICServerConnection, + ) => { + // Needs to setup stream handler + const conn = event.detail; + logger.info('!!!!Handling new Connection!!!!!'); + conn.addEventListener( + quicEvents.EventQUICConnectionStream.name, + handleStream, + ); + conn.addEventListener( + quicEvents.EventQUICConnectionStopped.name, + () => { + conn.removeEventListener( + quicEvents.EventQUICConnectionStream.name, + handleStream, + ); + }, + { once: true }, + ); + }; + quicServer.addEventListener( + quicEvents.EventQUICServerConnection.name, + handleConnection, + ); + quicServer.addEventListener( + quicEvents.EventQUICSocketStopped.name, + () => { + quicServer.removeEventListener( + quicEvents.EventQUICServerConnection.name, + handleConnection, + ); + }, + { once: true }, + ); + await quicServer.start({ + host: localHost, + }); + + // Setting up client + rpcClient = new RPCClient({ + manifest: clientManifest, + streamFactory: async () => { + return quicClient.connection.newStream(); + }, + toError: networkUtils.toError, + logger, + }); + + clientKeyPair = keysUtils.generateKeyPair(); + + localNodeId = keysUtils.publicKeyToNodeId(clientKeyPair.publicKey); + const tlsConfigClient = await tlsTestsUtils.createTLSConfig(clientKeyPair); + quicClient = await QUICClient.createQUICClient({ + crypto: nodesUtils.quicClientCrypto, + config: { + key: tlsConfigClient.keyPrivatePem, + cert: tlsConfigClient.certChainPem, + verifyPeer: true, + verifyCallback: async () => { + return undefined; + }, + }, + host: localHost, + port: quicServer.port, + localHost: localHost, + logger, + }); + + authorityKeyPair = keysUtils.generateKeyPair(); + authorityNodeId = keysUtils.publicKeyToNodeId(authorityKeyPair.publicKey); + seedKeyPair = keysUtils.generateKeyPair(); + seedNodeId = keysUtils.publicKeyToNodeId(seedKeyPair.publicKey); + const authorityClaimId = + claimsUtils.createClaimIdGenerator(authorityNodeId)(); + const authorityClaim: ClaimNetworkAuthority = { + typ: 'ClaimNetworkAuthority', + iss: nodesUtils.encodeNodeId(authorityNodeId), + sub: nodesUtils.encodeNodeId(seedNodeId), + jti: claimsUtils.encodeClaimId(authorityClaimId), + iat: 0, + nbf: 0, + seq: 0, + prevDigest: null, + prevClaimId: null, + }; + const authorityToken = Token.fromPayload(authorityClaim); + authorityToken.signWithPrivateKey(authorityKeyPair.privateKey); + authorityToken.signWithPrivateKey(seedKeyPair.privateKey); + signedClaimNetworkAuthorityEncoded = claimsUtils.generateSignedClaim( + authorityToken.toSigned(), + ); + await sigchain.addClaim( + { + typ: 'ClaimNetworkAccess', + iss: nodesUtils.encodeNodeId(seedNodeId), + sub: nodesUtils.encodeNodeId(remoteNodeId), + signedClaimNetworkAuthorityEncoded, + network: '', + }, + new Date(), + async (token) => { + token.signWithPrivateKey(seedKeyPair.privateKey); + return token; + }, + ); + }); + afterEach(async () => { + await taskManager.stop(); + await rpcServer.stop({ force: true }); + await quicServer.stop({ force: true }); + await nodeGraph.stop(); + await sigchain.stop(); + await db.stop(); + await keyRing.stop(); + + await quicServer.stop({ force: true }); + await quicClient.destroy({ force: true }); + }); + test('successfully verifies a claim', async () => { + // Adding into the ACL + await acl.setNodePerm(localNodeId, { + gestalt: { + claim: null, + scan: null, + }, + vaults: {}, + }); + const accessClaimId = claimsUtils.createClaimIdGenerator(authorityNodeId)(); + const accessClaim: ClaimNetworkAccess = { + typ: 'ClaimNetworkAccess', + iss: nodesUtils.encodeNodeId(seedNodeId), + sub: nodesUtils.encodeNodeId(localNodeId), + jti: claimsUtils.encodeClaimId(accessClaimId), + iat: 0, + nbf: 0, + seq: 0, + prevDigest: null, + prevClaimId: null, + signedClaimNetworkAuthorityEncoded, + network: '', + }; + const accessToken = Token.fromPayload(accessClaim); + accessToken.signWithPrivateKey(seedKeyPair.privateKey); + accessToken.signWithPrivateKey(clientKeyPair.privateKey); + const response = await rpcClient.methods.nodesClaimNetworkVerify({ + signedTokenEncoded: accessToken.toEncoded(), + }); + expect(response).toEqual({ success: true }); + }); +});