diff --git a/src/claims/payloads/claimNetworkAccess.ts b/src/claims/payloads/claimNetworkAccess.ts new file mode 100644 index 000000000..2af7f1400 --- /dev/null +++ b/src/claims/payloads/claimNetworkAccess.ts @@ -0,0 +1,80 @@ +import type { Claim, SignedClaim } from '../types'; +import type { NodeIdEncoded } from '../../ids/types'; +import type { SignedTokenEncoded } from '../../tokens/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 ClaimNetworkAccess extends Claim { + typ: 'ClaimNetworkAccess'; + iss: NodeIdEncoded; + sub: NodeIdEncoded; + 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['signedClaimNetworkAuthorityEncoded'] == null + ) { + 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..39097b48d 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 './claimNetworkNode'; diff --git a/src/nodes/NodeConnection.ts b/src/nodes/NodeConnection.ts index 095379c06..8b059f511 100644 --- a/src/nodes/NodeConnection.ts +++ b/src/nodes/NodeConnection.ts @@ -25,6 +25,7 @@ import * as nodesUtils from '../nodes/utils'; import { never } from '../utils'; import config from '../config'; import * as networkUtils from '../network/utils'; +import * as keysUtils from '../keys/utils'; type AgentClientManifest = typeof agentClientManifest; diff --git a/src/nodes/NodeManager.ts b/src/nodes/NodeManager.ts index fdb95fa44..06d41bce3 100644 --- a/src/nodes/NodeManager.ts +++ b/src/nodes/NodeManager.ts @@ -48,6 +48,7 @@ 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 Token from '../tokens/Token'; import * as keysUtils from '../keys/utils'; @@ -57,6 +58,8 @@ import * as claimsErrors from '../claims/errors'; import * as utils from '../utils/utils'; import config from '../config'; import * as networkUtils from '../network/utils'; +import { ClaimNetworkAccess, assertClaimNetworkAccess } from '../claims/payloads/claimNetworkAccess'; +import { ClaimNetworkAuthority, assertClaimNetworkAuthority } from '@/claims/payloads/claimNetworkAuthority'; const abortEphemeralTaskReason = Symbol('abort ephemeral task reason'); const abortSingletonTaskReason = Symbol('abort singleton task reason'); @@ -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) => { @@ -260,6 +263,43 @@ class NodeManager { cause: new AggregateError(failedConnectionErrors), }, ); + } else { + // Wip: We should ideally take the fastest connection and use it here for node signing. + const conn = successfulConnections[0].value; + await this.sigchain.addClaim( + { + typ: 'ClaimNetworkNode', + iss: nodesUtils.encodeNodeId(conn.nodeId), + sub: nodesUtils.encodeNodeId(this.keyRing.getNodeId()), + }, + undefined, + async (token) => { + const halfSignedClaim = token.toSigned(); + const halfSignedClaimEncoded = + claimsUtils.generateSignedClaim(halfSignedClaim); + const receivedClaim = + await conn.rpcClient.methods.nodesClaimNetworkSign({ + signedTokenEncoded: halfSignedClaimEncoded, + }); + const signedClaim = claimsUtils.parseSignedClaim( + receivedClaim.signedTokenEncoded, + ); + const fullySignedToken = Token.fromSigned(signedClaim); + // Check that the signatures are correct + const targetNodePublicKey = keysUtils.publicKeyFromNodeId( + conn.nodeId, + ); + if ( + !fullySignedToken.verifyWithPublicKey( + this.keyRing.keyPair.publicKey, + ) || + !fullySignedToken.verifyWithPublicKey(targetNodePublicKey) + ) { + throw new claimsErrors.ErrorDoublySignedClaimVerificationFailed(); + } + return fullySignedToken; + }, + ); } if (ctx.signal.aborted) return; @@ -1478,6 +1518,118 @@ 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(); + } + 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; + } + 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 +1687,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..9a97f8fbb 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..d73bc5ef8 --- /dev/null +++ b/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts @@ -0,0 +1,37 @@ +import type { + AgentClaimMessage, + AgentRPCRequestParams, + AgentRPCResponseResult, +} from '../types'; +import type NodeManager from '../../../nodes/NodeManager'; +import type { Host, Port } from '../../../network/types'; +import type { JSONValue } from '../../../types'; +import { UnaryHandler } from '@matrixai/rpc'; +import * as x509 from '@peculiar/x509'; +import * as agentErrors from '../errors'; +import * as agentUtils from '../utils'; +import { never } from '../../../utils'; +import * as keysUtils from '../../../keys/utils'; +import * as ids from '../../../ids'; + +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..a7b14ba24 --- /dev/null +++ b/tests/nodes/agent/handlers/nodesClaimNetworkVerify.test.ts @@ -0,0 +1,298 @@ +import type NodeConnectionManager from '@/nodes/NodeConnectionManager'; +import type { AgentClaimMessage } from '@/nodes/agent/types'; +import type { NodeId } from '@/ids'; +import type { ClaimLinkNode } from '@/claims/payloads'; +import type { KeyPair } from '@/keys/types'; +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 Sigchain from '@/sigchain/Sigchain'; +import KeyRing from '@/keys/KeyRing'; +import NodeGraph from '@/nodes/NodeGraph'; +import { nodesClaimNetworkVerify } from '@/nodes/agent/callers'; +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 { Token } from '@/tokens'; +import * as nodesUtils from '@/nodes/utils'; +import { generateKeyPair } from '@/keys/utils/generate'; +import * as networkUtils from '@/network/utils'; +import * as tlsTestsUtils from '../../../utils/tls'; +import { SignedTokenEncoded } from '@/tokens/types'; +import { ClaimNetworkAuthority } from '@/claims/payloads/claimNetworkAuthority'; +import { ClaimNetworkAccess } from '@/claims/payloads/claimNetworkAccess'; + +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 = 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 = generateKeyPair(); + authorityNodeId = keysUtils.publicKeyToNodeId(authorityKeyPair.publicKey); + seedKeyPair = 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, + }, 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, + } + 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 }); + }); +});