From 37f8ed15887991460718605e50eebaf16cba85f6 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Wed, 11 Jun 2025 16:38:36 +1000 Subject: [PATCH] feat: adding creation logic for `ClaimNetworkAuthority` and `ClaimNetworkAccess` along with verification and authentication logic using them --- src/claims/errors.ts | 6 + src/claims/payloads/claimNetworkAccess.ts | 91 +- src/claims/payloads/claimNetworkAuthority.ts | 76 +- src/gestalts/types.ts | 2 +- src/nodes/NodeConnectionManager.ts | 10 +- src/nodes/NodeManager.ts | 684 ++++++++++++--- src/nodes/agent/callers/index.ts | 7 +- .../callers/nodesClaimNetworkAuthorityGet.ts | 12 + .../agent/callers/nodesClaimNetworkSign.ts | 4 +- .../agent/callers/nodesClaimNetworkVerify.ts | 12 - .../handlers/NodesClaimNetworkAuthorityGet.ts | 32 + .../agent/handlers/NodesClaimNetworkSign.ts | 47 +- .../agent/handlers/NodesClaimNetworkVerify.ts | 35 - src/nodes/agent/handlers/index.ts | 3 + src/nodes/agent/types.ts | 9 + src/nodes/errors.ts | 9 + src/nodes/types.ts | 1 + src/nodes/utils.ts | 74 +- .../payloads/claimNetworkAccess.test.ts | 56 ++ .../payloads/claimNetworkAuthority.test.ts | 56 ++ tests/claims/payloads/utils.ts | 79 ++ tests/nodes/NodeConnectionManager.test.ts | 45 + tests/nodes/NodeManager.test.ts | 785 +++++++++++++++++- .../handlers/nodesClaimNetworkVerify.test.ts | 305 ------- 24 files changed, 1952 insertions(+), 488 deletions(-) create mode 100644 src/nodes/agent/callers/nodesClaimNetworkAuthorityGet.ts delete mode 100644 src/nodes/agent/callers/nodesClaimNetworkVerify.ts create mode 100644 src/nodes/agent/handlers/NodesClaimNetworkAuthorityGet.ts delete mode 100644 src/nodes/agent/handlers/NodesClaimNetworkVerify.ts create mode 100644 tests/claims/payloads/claimNetworkAccess.test.ts create mode 100644 tests/claims/payloads/claimNetworkAuthority.test.ts delete mode 100644 tests/nodes/agent/handlers/nodesClaimNetworkVerify.test.ts diff --git a/src/claims/errors.ts b/src/claims/errors.ts index 6d8934813..5503fe650 100644 --- a/src/claims/errors.ts +++ b/src/claims/errors.ts @@ -8,6 +8,11 @@ class ErrorClaimsUndefinedClaimPayload extends ErrorClaims { exitCode = sysexits.UNKNOWN; } +class ErrorClaimsVerificationFailed extends ErrorClaims { + static description = 'Failed to verify claim'; + exitCode = sysexits.SOFTWARE; +} + /** * Exceptions arising in cross-signing process */ @@ -59,6 +64,7 @@ class ErrorNodesClaimType extends ErrorSchemaValidate { export { ErrorClaims, ErrorClaimsUndefinedClaimPayload, + ErrorClaimsVerificationFailed, ErrorEmptyStream, ErrorUndefinedSinglySignedClaim, ErrorUndefinedDoublySignedClaim, diff --git a/src/claims/payloads/claimNetworkAccess.ts b/src/claims/payloads/claimNetworkAccess.ts index 8b7eb34e4..fd869d192 100644 --- a/src/claims/payloads/claimNetworkAccess.ts +++ b/src/claims/payloads/claimNetworkAccess.ts @@ -1,22 +1,28 @@ import type { Claim, SignedClaim } from '../types.js'; -import type { NodeIdEncoded } from '../../ids/types.js'; +import type { NodeId, NodeIdEncoded } from '../../ids/types.js'; import type { SignedTokenEncoded } from '../../tokens/types.js'; +import * as claimNetworkAuthorityUtils from './claimNetworkAuthority.js'; +import Token from '../../tokens/Token.js'; import * as tokensSchema from '../../tokens/schemas/index.js'; import * as ids from '../../ids/index.js'; import * as claimsUtils from '../utils.js'; +import * as claimsErrors from '../errors.js'; import * as tokensUtils from '../../tokens/utils.js'; import * as validationErrors from '../../validation/errors.js'; import * as utils from '../../utils/index.js'; +import * as nodesUtils from '../../nodes/utils.js'; +import * as keysUtils from '../../keys/utils/index.js'; /** - * Asserts that a node is apart of a network + * Asserts that a node is a part of a network */ interface ClaimNetworkAccess extends Claim { typ: 'ClaimNetworkAccess'; iss: NodeIdEncoded; sub: NodeIdEncoded; network: string; - signedClaimNetworkAuthorityEncoded?: SignedTokenEncoded; + signedClaimNetworkAuthorityEncoded: SignedTokenEncoded; + isPrivate: boolean; } function assertClaimNetworkAccess( @@ -64,6 +70,14 @@ function assertClaimNetworkAccess( '`signedClaimNetworkAuthorityEncoded` property must be an encoded signed token', ); } + if ( + claimNetworkAccess['isPrivate'] == null || + typeof claimNetworkAccess['isPrivate'] !== 'boolean' + ) { + throw new validationErrors.ErrorParse( + '`isPrivate` property must be a boolean', + ); + } } function parseClaimNetworkAccess( @@ -84,10 +98,81 @@ function parseSignedClaimNetworkAccess( return signedClaim as SignedClaim; } +function verifyClaimNetworkAccess( + networkNodeId: NodeId, + subjectNodeId: NodeId, + network: string, + tokenClaimNetworkAccess: Token, +): void { + const signedClaim = + claimNetworkAuthorityUtils.parseSignedClaimNetworkAuthority( + tokenClaimNetworkAccess.payload.signedClaimNetworkAuthorityEncoded, + ); + const claimNetworkAuthority = Token.fromSigned(signedClaim); + const issuerNodeId = nodesUtils.decodeNodeId( + tokenClaimNetworkAccess.payload.iss, + ); + if (issuerNodeId == null) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'failed to decode issuer nodeId', + ); + } + claimNetworkAuthorityUtils.verifyClaimNetworkAuthority( + networkNodeId, + issuerNodeId, + network, + claimNetworkAuthority, + ); + // For the access claim + // 1. issuer is current node + // 2. subject is target node + // 3. is signed by both the target and issuer + + // Issuer should be the subject of the ClaimNetworkAuthority and signed by it + const claimNetworkAuthoritySub = claimNetworkAuthority.payload.sub; + const nodeIdIss = tokenClaimNetworkAccess.payload.iss; + if (nodeIdIss !== claimNetworkAuthoritySub) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Issuer NodeIdEncoded does not match the expected network id', + ); + } + const networkPublicKey = keysUtils.publicKeyFromNodeId(issuerNodeId); + if (!tokenClaimNetworkAccess.verifyWithPublicKey(networkPublicKey)) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Token was not signed by the issuer node', + ); + } + + // Subject should be the target node and signed by it + const targetNodeIdEncoded = nodesUtils.encodeNodeId(subjectNodeId); + const nodeIdSub = tokenClaimNetworkAccess.payload.sub; + if (nodeIdSub !== targetNodeIdEncoded) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Subject NodeIdEncoded does not match the expected subject node', + ); + } + const targetPublicKey = keysUtils.publicKeyFromNodeId(subjectNodeId); + + if (!tokenClaimNetworkAccess.verifyWithPublicKey(targetPublicKey)) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Token was not signed by the subject node', + ); + } + + // Checking if the network name matches + const networkName = tokenClaimNetworkAccess.payload.network; + if (networkName !== network) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Network name does not match the expected network', + ); + } +} + export { assertClaimNetworkAccess, parseClaimNetworkAccess, parseSignedClaimNetworkAccess, + verifyClaimNetworkAccess, }; export type { ClaimNetworkAccess }; diff --git a/src/claims/payloads/claimNetworkAuthority.ts b/src/claims/payloads/claimNetworkAuthority.ts index 82baaae4f..459eb1365 100644 --- a/src/claims/payloads/claimNetworkAuthority.ts +++ b/src/claims/payloads/claimNetworkAuthority.ts @@ -1,18 +1,25 @@ import type { Claim, SignedClaim } from '../types.js'; -import type { NodeIdEncoded } from '../../ids/types.js'; +import type { NodeId, NodeIdEncoded } from '../../ids/types.js'; +import type Token from '../../tokens/Token.js'; import * as ids from '../../ids/index.js'; import * as claimsUtils from '../utils.js'; -import * as tokensUtils from '../../tokens/utils.js'; +import * as claimsErrors from '../errors.js'; import * as validationErrors from '../../validation/errors.js'; import * as utils from '../../utils/index.js'; +import * as nodesUtils from '../../nodes/utils.js'; +import * as keysUtils from '../../keys/utils/index.js'; /** - * Asserts that a node is apart of a network + * Asserts that a node has the authority of a network. + * The issuing nodeId has to be the root keypair for the whole network. */ + interface ClaimNetworkAuthority extends Claim { typ: 'ClaimNetworkAuthority'; iss: NodeIdEncoded; sub: NodeIdEncoded; + network: string; + isPrivate: boolean; } function assertClaimNetworkAuthority( @@ -42,6 +49,22 @@ function assertClaimNetworkAuthority( '`sub` property must be an encoded node ID', ); } + if ( + claimNetworkAuthority['network'] == null || + typeof claimNetworkAuthority['network'] !== 'string' + ) { + throw new validationErrors.ErrorParse( + '`network` property must be a network name string', + ); + } + if ( + claimNetworkAuthority['isPrivate'] == null || + typeof claimNetworkAuthority['isPrivate'] !== 'boolean' + ) { + throw new validationErrors.ErrorParse( + '`isPrivate` property must be a boolean', + ); + } } function parseClaimNetworkAuthority( @@ -55,17 +78,62 @@ function parseClaimNetworkAuthority( function parseSignedClaimNetworkAuthority( signedClaimNetworkNodeEncoded: unknown, ): SignedClaim { - const signedClaim = tokensUtils.parseSignedToken( + const signedClaim = claimsUtils.parseSignedClaim( signedClaimNetworkNodeEncoded, ); assertClaimNetworkAuthority(signedClaim.payload); return signedClaim as SignedClaim; } +function verifyClaimNetworkAuthority( + networkNodeId: NodeId, + targetNodeId: NodeId, + network: string, + token: Token, +): void { + // Should be signed by the network authority as the issuer + const nodeIdIss = token.payload.iss; + const networkNodeIdEncoded = nodesUtils.encodeNodeId(networkNodeId); + if (nodeIdIss !== networkNodeIdEncoded) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Issuer NodeIdEncoded does not match the expected network id', + ); + } + const networkPublicKey = keysUtils.publicKeyFromNodeId(networkNodeId); + if (!token.verifyWithPublicKey(networkPublicKey)) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Token was not signed by the network authority', + ); + } + // Now we check if the claim applies to the target node + const targetNodeIdEncoded = nodesUtils.encodeNodeId(targetNodeId); + const nodeIdSub = token.payload.sub; + if (nodeIdSub !== targetNodeIdEncoded) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Subject NodeIdEncoded does not match the expected target Node', + ); + } + // Checking if the claim was signed by the subject + const targetPublicKey = keysUtils.publicKeyFromNodeId(targetNodeId); + if (!token.verifyWithPublicKey(targetPublicKey)) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Token was not signed by the network authority', + ); + } + // Checking if the network name matches + const networkName = token.payload.network; + if (networkName !== network) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Network name does not match the expected network', + ); + } +} + export { assertClaimNetworkAuthority, parseClaimNetworkAuthority, parseSignedClaimNetworkAuthority, + verifyClaimNetworkAuthority, }; export type { ClaimNetworkAuthority }; diff --git a/src/gestalts/types.ts b/src/gestalts/types.ts index c7f302986..e219a72e5 100644 --- a/src/gestalts/types.ts +++ b/src/gestalts/types.ts @@ -14,7 +14,7 @@ import type { } from '../claims/payloads/index.js'; import type { ProviderPaginationToken } from '../identities/types.js'; -const gestaltActions = ['notify', 'scan', 'claim'] as const; +const gestaltActions = ['notify', 'scan', 'claim', 'join'] as const; type GestaltKey = Opaque<'GestaltKey', Buffer>; diff --git a/src/nodes/NodeConnectionManager.ts b/src/nodes/NodeConnectionManager.ts index 11649a4ea..989636be5 100644 --- a/src/nodes/NodeConnectionManager.ts +++ b/src/nodes/NodeConnectionManager.ts @@ -111,7 +111,11 @@ const activeForwardAuthenticateCancellationReason = Symbol( 'active forward authenticate cancellation reason', ); -const rpcMethodsWhitelist = ['nodesAuthenticateConnection']; +const rpcMethodsWhitelist = [ + 'nodesAuthenticateConnection', + 'nodesClaimNetworkSign', + 'nodesClaimNetworkAuthorityGet', +]; /** * NodeConnectionManager is a server that manages all node connections. @@ -711,7 +715,7 @@ class NodeConnectionManager< * @param targetNodeId Id of target node to communicate with * @returns ResourceAcquire Resource API for use in with contexts */ - protected acquireConnectionInternal( + public acquireConnectionInternal( targetNodeId: NodeId, ): ResourceAcquire> { if (this.keyRing.getNodeId().equals(targetNodeId)) { @@ -1837,7 +1841,7 @@ class NodeConnectionManager< } try { // Should resolve without issue if authentication succeeds. - await this.authenticateNetworkReverseCallback(message, ctx); + await this.authenticateNetworkReverseCallback(message, nodeId, ctx); connectionsEntry.authenticatedReverse = AuthenticatingState.SUCCESS; } catch (e) { const err = new nodesErrors.ErrorNodeManagerAuthenticationFailedReverse( diff --git a/src/nodes/NodeManager.ts b/src/nodes/NodeManager.ts index e85de4dec..232297309 100644 --- a/src/nodes/NodeManager.ts +++ b/src/nodes/NodeManager.ts @@ -1,4 +1,4 @@ -import type { DB, DBTransaction } from '@matrixai/db'; +import type { DB, DBTransaction, LevelPath } from '@matrixai/db'; import type { ContextTimed, ContextTimedInput } from '@matrixai/contexts'; import type { PromiseCancellable } from '@matrixai/async-cancellable'; import type { ResourceAcquire } from '@matrixai/resources'; @@ -14,8 +14,16 @@ import type { } from '../tasks/types.js'; import type { SignedTokenEncoded } from '../tokens/types.js'; import type { Host, Port } from '../network/types.js'; -import type { Claim, ClaimId, SignedClaim } from '../claims/types.js'; -import type { ClaimLinkNode } from '../claims/payloads/index.js'; +import type { + Claim, + ClaimId, + ClaimIdEncoded, + SignedClaim, +} from '../claims/types.js'; +import type { + ClaimLinkNode, + ClaimNetworkAccess, +} from '../claims/payloads/index.js'; import type NodeConnection from '../nodes/NodeConnection.js'; import type { AgentClaimMessage, @@ -34,6 +42,7 @@ import type NodeConnectionManager from './NodeConnectionManager.js'; import type NodeGraph from './NodeGraph.js'; import type { ServicePOJO } from '@matrixai/mdns'; import type { AgentClientManifestNodeManager } from './agent/callers/index.js'; +import type { ClaimNetworkAuthority } from '../claims/payloads/claimNetworkAuthority.js'; import { withF } from '@matrixai/resources'; import { events as mdnsEvents, MDNS, utils as mdnsUtils } from '@matrixai/mdns'; import Logger from '@matrixai/logger'; @@ -46,8 +55,8 @@ import * as nodesEvents from './events.js'; import * as nodesErrors from './errors.js'; import NodeConnectionQueue from './NodeConnectionQueue.js'; import config from '../config.js'; -import { assertClaimNetworkAuthority } from '../claims/payloads/claimNetworkAuthority.js'; -import { assertClaimNetworkAccess } from '../claims/payloads/claimNetworkAccess.js'; +import * as claimNetworkAuthorityUtils from '../claims/payloads/claimNetworkAuthority.js'; +import * as claimNetworkAccessUtils from '../claims/payloads/claimNetworkAccess.js'; import Token from '../tokens/Token.js'; import * as keysUtils from '../keys/utils/index.js'; import * as tasksErrors from '../tasks/errors.js'; @@ -124,6 +133,33 @@ class NodeManager { */ protected connectionLockBox: LockBox = new LockBox(); + /** + * If this node is acting as a network authority then the claim is stored here and used as needed. + * If the node is not acting as an authority then the lack of claim here should indicate that. + * If the claim is missing then any request that requires it should reject with an error. + */ + protected claimNetworkAuthority: Token | undefined = + undefined; + /** + * If a node has joined a network then it's `ClaimNetworkAccess` is tracked here + */ + protected claimNetworkAccess: Token | undefined = + undefined; + + /** + * These are the level paths for mapping the ClaimNetworkAccess and ClaimNetworkAuthority claims for each network it has joined. + * Used to look up and switch between networks as needed. + */ + protected nodeManagerDbPath: LevelPath = [this.constructor.name]; + protected nodeManagerClaimNetworkAuthorityPath: LevelPath = [ + ...this.nodeManagerDbPath, + 'claimNetworkAuthority', + ]; + protected nodeManagerClaimNetworkAccessPath: LevelPath = [ + ...this.nodeManagerDbPath, + 'claimNetworkAccess', + ]; + protected refreshBucketHandler: TaskHandler = async ( ctx, _taskInfo, @@ -1506,19 +1542,443 @@ class NodeManager { }); } - public async handleClaimNetwork( + /** + * Creates a claim on the sigchain granting this node authority over a network to create `ClaimNetworkAccess` claims. + * + * @param networkNodeId - The public key NodeId for the root authority for the network + * @param network - The network URL. + * @param isPrivate - Indicates if the network is private or not. + * @param signingHook - A callback used to sign the claim with the network's private key. + * @param tran + */ + public async createClaimNetworkAuthority( + networkNodeId: NodeId, + network: string, + isPrivate: boolean, + signingHook: (token: Token) => Promise>, + tran?: DBTransaction, + ): Promise<[ClaimId, Token]> { + if (tran == null) { + return await this.db.withTransactionF((tran) => + this.createClaimNetworkAuthority( + networkNodeId, + network, + isPrivate, + signingHook, + tran, + ), + ); + } + + const [claimId, signedClaim] = await this.sigchain.addClaim( + { + typ: 'ClaimNetworkAuthority', + sub: nodesUtils.encodeNodeId(this.keyRing.getNodeId()), + iss: nodesUtils.encodeNodeId(networkNodeId), + network, + isPrivate, + }, + undefined, + signingHook, + ); + const token = Token.fromSigned( + signedClaim as SignedClaim, + ); + this.claimNetworkAuthority = token; + await this.setClaimNetworkAuthority(token); + await this.switchNetwork(network); + return [claimId, token]; + } + + public async createSelfSignedClaimNetworkAccess( + claimNetworkAuthority: Token, + ): Promise<[ClaimId, Token]> { + const thisNodeId = this.keyRing.getNodeId(); + const encodedNetworkAuthority = claimsUtils.generateSignedClaim( + claimNetworkAuthority.toSigned(), + ); + if ( + claimNetworkAuthority.payload.sub !== nodesUtils.encodeNodeId(thisNodeId) + ) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'ClaimNetworkAuthority does not grant authority to this node', + ); + } + const network = claimNetworkAuthority.payload.network; + const isPrivate = claimNetworkAuthority.payload.isPrivate; + const [claimId, signedClaim] = await this.sigchain.addClaim({ + typ: 'ClaimNetworkAccess', + iss: nodesUtils.encodeNodeId(thisNodeId), + sub: nodesUtils.encodeNodeId(thisNodeId), + network, + isPrivate, + signedClaimNetworkAuthorityEncoded: encodedNetworkAuthority, + }); + const token = Token.fromSigned( + signedClaim as SignedClaim, + ); + this.claimNetworkAccess = token; + await this.setClaimNetworkAccess(token); + await this.switchNetwork(network); + return [claimId, token]; + } + + /** + * This takes a `ClaimNetworkAuthority` and tracks it in the database under the network name. + */ + protected async setClaimNetworkAuthority( + claimNetworkAuthority: Token, + tran?: DBTransaction, + ): Promise { + if (tran == null) { + return await this.db.withTransactionF((tran) => + this.setClaimNetworkAuthority(claimNetworkAuthority, tran), + ); + } + + const network = claimNetworkAuthority.payload.network; + await tran.put( + [...this.nodeManagerClaimNetworkAuthorityPath, network], + claimNetworkAuthority.payload.jti, + false, + ); + } + + /** + * This returns the `ClaimNetworkAuthority` for the given network. + */ + protected async getClaimNetworkAuthority( + network: string, + tran?: DBTransaction, + ): Promise | undefined> { + if (tran == null) { + return await this.db.withTransactionF((tran) => + this.getClaimNetworkAuthority(network, tran), + ); + } + + const jti = await tran.get( + [...this.nodeManagerClaimNetworkAuthorityPath, network], + false, + ); + if (jti == null) return; + const claim = await this.sigchain.getSignedClaim( + claimsUtils.decodeClaimId(jti)!, + tran, + ); + if (claim == null) return; + const token = Token.fromSigned(claim); + claimNetworkAuthorityUtils.assertClaimNetworkAuthority(token.payload); + return token as Token; + } + + /** + * This takes a `ClaimNetworkAccess` and tracks it in the database under the network name. + */ + protected async setClaimNetworkAccess( + claimNetworkAccess: Token, + tran?: DBTransaction, + ): Promise { + if (tran == null) { + return await this.db.withTransactionF((tran) => + this.setClaimNetworkAccess(claimNetworkAccess, tran), + ); + } + + const network = claimNetworkAccess.payload.network; + await tran.put( + [...this.nodeManagerClaimNetworkAccessPath, network], + claimNetworkAccess.payload.jti, + false, + ); + } + + /** + * This returns the `ClaimNetworkAccess` for the given network. + */ + protected async getClaimNetworkAccess( + network: string, + tran?: DBTransaction, + ): Promise | undefined> { + if (tran == null) { + return await this.db.withTransactionF((tran) => + this.getClaimNetworkAccess(network, tran), + ); + } + + const jti = await tran.get( + [...this.nodeManagerClaimNetworkAccessPath, network], + false, + ); + if (jti == null) return; + const claim = await this.sigchain.getSignedClaim( + claimsUtils.decodeClaimId(jti)!, + tran, + ); + if (claim == null) return; + const token = Token.fromSigned(claim); + claimNetworkAccessUtils.assertClaimNetworkAccess(token.payload); + return token as Token; + } + + /** + * This switches out the active `ClaimNetworkAuthority` and `ClaimNetworkAccess` for the desired network. + * If no claims exist for the network or no network is provided, then it switches to using no network. + * In doing so this also updates the `NodeConnectionManager`'s authentication callbacks to use the selected + * network for authentication. + * @param network - The Network URL for the desired network to switch to. + * @param tran + */ + public async switchNetwork( + network?: string, + tran?: DBTransaction, + ): Promise { + if (tran == null) { + return await this.db.withTransactionF((tran) => + this.switchNetwork(network, tran), + ); + } + + if (network == null) { + this.claimNetworkAuthority = undefined; + this.claimNetworkAccess = undefined; + // Use the basic no network behavior + this.nodeConnectionManager.setAuthenticateNetworkForwardCallback( + nodesUtils.nodesAuthenticateConnectionForwardDefault, + ); + this.nodeConnectionManager.setAuthenticateNetworkReverseCallback( + nodesUtils.nodesAuthenticateConnectionReverseDeny, + ); + return; + } + + this.claimNetworkAuthority = await this.getClaimNetworkAuthority( + network, + tran, + ); + this.claimNetworkAccess = await this.getClaimNetworkAccess(network, tran); + if (this.claimNetworkAccess != null) { + // Use the claim to verify connections + this.nodeConnectionManager.setAuthenticateNetworkForwardCallback( + nodesUtils.nodesAuthenticationConnectionForwardPrivateFactory( + this.claimNetworkAccess, + ), + ); + this.nodeConnectionManager.setAuthenticateNetworkReverseCallback( + nodesUtils.nodesAuthenticationConnectionReversePrivateFactory( + this.claimNetworkAccess, + ), + ); + } else { + // Use the basic no network behavior + this.nodeConnectionManager.setAuthenticateNetworkForwardCallback( + nodesUtils.nodesAuthenticateConnectionForwardDefault, + ); + this.nodeConnectionManager.setAuthenticateNetworkReverseCallback( + nodesUtils.nodesAuthenticateConnectionReverseDeny, + ); + } + } + + /** + * Quick hand utility for checking if the active `ClaimNetworkAuthority` is a private network + */ + public isClaimNetworkAuthorityPrivate(): boolean | undefined { + // Return undefined if we're not acting as a network authority + if (this.claimNetworkAuthority == null) return; + return this.claimNetworkAuthority.payload.isPrivate; + } + + /** + * This creates a cross-signed `ClaimNetworkAccess` on the sigchain. The resulting `ClaimNetworkAccess` is used to + * authenticate connections between nodes within the network. + * + * @param targetNodeId - This is a node with an active `ClaimNetworkAuthority` for the network you wish to join. + * This usually is a seed node for that network. + * @param network - The URL of the network you wish to join. + * @param tran + * @param ctx + */ + public async claimNetwork( + targetNodeId: NodeId, + network: string, + tran?: DBTransaction, + ctx?: ContextTimedInput, + ): Promise<[ClaimId, Token]>; + @startStop.ready(new nodesErrors.ErrorNodeManagerNotRunning()) + public async claimNetwork( + targetNodeId: NodeId, + network: string, + tran: DBTransaction | undefined, + @decorators.context ctx: ContextTimed, + ): Promise<[ClaimId, Token]> { + if (tran == null) { + return await this.db.withTransactionF((tran) => + this.claimNetwork(targetNodeId, network, tran, ctx), + ); + } + + // Validating that the ClaimNetworkAuthority is correct. + const claimNetworkAuthority = await this.remoteClaimNetworkAuthorityGet( + network, + targetNodeId, + ); + const encodedNetworkAuthority = claimsUtils.generateSignedClaim( + claimNetworkAuthority.toSigned(), + ); + const networkNodeId = nodesUtils.decodeNodeId( + claimNetworkAuthority.payload.iss, + ); + if (networkNodeId == null) utils.never('failed to decode networkNodeId'); + + const subjectNodeId = this.keyRing.getNodeId(); + const isPrivate = claimNetworkAuthority.payload.isPrivate; + const [claimId, signedClaim] = await this.sigchain.addClaim( + { + typ: 'ClaimNetworkAccess', + iss: nodesUtils.encodeNodeId(targetNodeId), + sub: nodesUtils.encodeNodeId(subjectNodeId), + network, + isPrivate, + signedClaimNetworkAuthorityEncoded: encodedNetworkAuthority, + }, + undefined, + async (token) => { + // Using the nodeConnection.withConnF so we can use the connection without being authenticated + return await withF( + [this.nodeConnectionManager.acquireConnectionInternal(targetNodeId)], + async ([conn]) => { + // 2. create the agentClaim message to send + const halfSignedClaim = token.toSigned(); + const halfSignedClaimEncoded = + claimsUtils.generateSignedClaim(halfSignedClaim); + const client = conn.getClient(); + const stream = await client.methods.nodesClaimNetworkSign(); + const writer = stream.writable.getWriter(); + const reader = stream.readable.getReader(); + let fullySignedToken: Token; + try { + await writer.write({ + signedTokenEncoded: halfSignedClaimEncoded, + }); + // 3. We expect to receive the doubly signed claim + const readStatus = await reader.read(); + if (readStatus.done) { + throw new claimsErrors.ErrorEmptyStream( + 'nodesClaimNetworkSign stream ended too soon, likely due to lack of permission', + ); + } + const receivedClaim = readStatus.value; + // We need to re-construct the token from the message + const receivedClaimNetworkAccess = + claimNetworkAccessUtils.parseSignedClaimNetworkAccess( + receivedClaim.signedTokenEncoded, + ); + fullySignedToken = Token.fromSigned(receivedClaimNetworkAccess); + claimNetworkAccessUtils.verifyClaimNetworkAccess( + networkNodeId, + subjectNodeId, + network, + fullySignedToken, + ); + + // Next stage is to process the claim for the other node + const readStatus2 = await reader.read(); + if (readStatus2.done) { + throw new claimsErrors.ErrorEmptyStream(); + } + const receivedClaimRemote = readStatus2.value; + + // We need to re-construct the token from the message + const signedClaimRemote = + claimNetworkAccessUtils.parseSignedClaimNetworkAccess( + receivedClaimRemote.signedTokenEncoded, + ); + // This is a singly signed claim, + // we want to verify it before signing and sending back + const signedTokenRemote = Token.fromSigned(signedClaimRemote); + signedTokenRemote.signWithPrivateKey(this.keyRing.keyPair); + // Verify everything is correct + claimNetworkAccessUtils.verifyClaimNetworkAccess( + networkNodeId, + subjectNodeId, + network, + signedTokenRemote, + ); + // 4. X <- responds with double signing the X signed claim <- Y + const agentClaimedMessageRemote = claimsUtils.generateSignedClaim( + signedTokenRemote.toSigned(), + ); + await writer.write({ + signedTokenEncoded: agentClaimedMessageRemote, + }); + + // Check the stream is closed (should be closed by other side) + const finalResponse = await reader.read(); + if (finalResponse.done != null) { + await writer.close(); + } + } catch (e) { + await writer.abort(e); + throw e; + } + return fullySignedToken; + }, + ); + }, + tran, + ); + const token = Token.fromSigned( + signedClaim as SignedClaim, + ); + this.claimNetworkAccess = token; + await this.setClaimNetworkAccess(token, tran); + await this.switchNetwork(network, tran); + return [claimId, token]; + } + + /** + * This provides the handler side of the ClaimNetwork logic. + * @param requestingNodeId - The nodeId of the node making the request. This should be taken from the connections + * certificate to confirm that `ClaimNetworkAccess` is being created for the node requesting it. + * @param input - The input stream for the RPC handler. + * @param tran + */ + public async *handleClaimNetwork( requestingNodeId: NodeId, - input: AgentRPCRequestParams, + input: AsyncIterableIterator>, tran?: DBTransaction, - ): Promise> { + ): AsyncGenerator> { if (tran == null) { - return await this.db.withTransactionF( - async (tran) => - await this.handleClaimNetwork(requestingNodeId, input, tran), + return yield* this.db.withTransactionG((tran) => + this.handleClaimNetwork(requestingNodeId, input, tran), + ); + } + if (this.claimNetworkAuthority == null) { + throw new nodesErrors.ErrorNodeManagerClaimNetworkAuthorityMissing( + 'Node is not acting as a network authority and can not create a claimNetworkAccess', ); } - const signedClaim = claimsUtils.parseSignedClaim(input.signedTokenEncoded); + + const readStatus = await input.next(); + // If nothing to read, end and destroy + if (readStatus.done) { + throw new claimsErrors.ErrorEmptyStream(); + } + const receivedMessage = readStatus.value; + const signedClaim = claimsUtils.parseSignedClaim( + receivedMessage.signedTokenEncoded, + ); const token = Token.fromSigned(signedClaim); + // Verify if token is for our network + if ( + token.payload.network == null || + token.payload.network !== this.claimNetworkAuthority.payload.network + ) { + throw new claimsErrors.ErrorClaimsVerificationFailed( + 'Claim does not match expected network', + ); + } // Verify if the token is signed if ( !token.verifyWithPublicKey( @@ -1533,105 +1993,123 @@ class NodeManager { const doublySignedClaim = token.toSigned(); const halfSignedClaimEncoded = claimsUtils.generateSignedClaim(doublySignedClaim); - return { + yield { signedTokenEncoded: halfSignedClaimEncoded, }; - } - public async handleVerifyClaimNetwork( - requestingNodeId: NodeId, - input: AgentRPCRequestParams, - tran?: DBTransaction, - ): Promise> { - if (tran == null) { - return await this.db.withTransactionF( - async (tran) => - await 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)!, + // Now we want to send our own claim signed + const { p: halfSignedClaimP, resolveP: halfSignedClaimResolveP } = + utils.promise(); + const claimP = this.sigchain.addClaim( + { + typ: 'ClaimNetworkAccess', + iss: nodesUtils.encodeNodeId(this.keyRing.getNodeId()), + sub: nodesUtils.encodeNodeId(requestingNodeId), + network: this.claimNetworkAuthority.payload.network, + isPrivate: this.claimNetworkAuthority.payload.isPrivate, + signedClaimNetworkAuthorityEncoded: claimsUtils.generateSignedClaim( + this.claimNetworkAuthority.toSigned(), ), - ) - ) { - 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, + }, + undefined, + async (token) => { + const halfSignedClaim = token.toSigned(); + const halfSignedClaimEncoded = + claimsUtils.generateSignedClaim(halfSignedClaim); + halfSignedClaimResolveP(halfSignedClaimEncoded); + const readStatus = await input.next(); + if (readStatus.done) { + throw new claimsErrors.ErrorEmptyStream(); + } + const receivedClaim = readStatus.value; + // We need to re-construct the token from the message + const signedClaim = + claimNetworkAccessUtils.parseSignedClaimNetworkAccess( + receivedClaim.signedTokenEncoded, + ); + const fullySignedToken = Token.fromSigned(signedClaim); + // Check that the signatures are correct + const networkNodeId = nodesUtils.decodeNodeId( + this.claimNetworkAuthority!.payload.iss, + ); + if (networkNodeId == null) { + utils.never('failed to decode networkNodeId'); + } + + claimNetworkAccessUtils.verifyClaimNetworkAccess( + networkNodeId, + requestingNodeId, + this.claimNetworkAuthority!.payload.network, + fullySignedToken, + ); + // Ending the stream + return fullySignedToken; + }, ); - // 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(); - } + // Prevent async promise handling leak + void claimP.catch(() => {}); + yield { + signedTokenEncoded: await halfSignedClaimP, + }; + const [, claim] = await claimP; + // With the claim created we want to add it to the gestalt graph + const issNodeInfo = { + nodeId: requestingNodeId, + }; + const subNodeInfo = { + nodeId: this.keyRing.getNodeId(), + }; + await this.gestaltGraph.linkNodeAndNode(issNodeInfo, subNodeInfo, { + claim: claim as SignedClaim, + meta: {}, + }); + } - 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 a Network Authority. - if ( - authorityToken.verifyWithPublicKey( - keysUtils.publicKeyFromNodeId( - nodesUtils.decodeNodeId(claim.payload.iss)!, - ), - ) - ) { - success = true; - break; - } - } + /** + * Gets the `ClaimNetworkAuthority` from the target node. It also verifies its valid and for the expected network. + * @param network + * @param targetNodeId + */ + public async remoteClaimNetworkAuthorityGet( + network: string, + targetNodeId: NodeId, + ): Promise> { + return await withF( + [this.nodeConnectionManager.acquireConnectionInternal(targetNodeId)], + async ([conn]) => { + const client = conn.getClient(); + const receivedClaim = + await client.methods.nodesClaimNetworkAuthorityGet({}); + const signedClaim = + claimNetworkAuthorityUtils.parseSignedClaimNetworkAuthority( + receivedClaim, + ); + const token = Token.fromSigned(signedClaim); + const networkNodeId = nodesUtils.decodeNodeId(token.payload.iss); + if (networkNodeId == null) { + utils.never('failed to decode networkNodeId'); + } + claimNetworkAuthorityUtils.verifyClaimNetworkAuthority( + networkNodeId, + targetNodeId, + network, + token, + ); + return token; + }, + ); + } - if (!success) { - throw new nodesErrors.ErrorNodeClaimNetworkVerificationFailed(); + /** + * The handler side logic for `remoteClaimNetworkAuthorityGet`. + */ + public async handleClaimNetworkAuthorityGet(): Promise { + if (this.claimNetworkAuthority == null) { + throw new nodesErrors.ErrorNodeManagerClaimNetworkAuthorityMissing(); } - - return { - success: true, - }; + return claimsUtils.generateSignedClaim( + this.claimNetworkAuthority.toSigned(), + ); } /** diff --git a/src/nodes/agent/callers/index.ts b/src/nodes/agent/callers/index.ts index e3e4640ef..2f3ce13ee 100644 --- a/src/nodes/agent/callers/index.ts +++ b/src/nodes/agent/callers/index.ts @@ -1,5 +1,6 @@ import type { ClientManifest } from '@matrixai/rpc'; import nodesAuthenticateConnection from './nodesAuthenticateConnection.js'; +import nodesClaimNetworkAuthorityGet from './nodesClaimNetworkAuthorityGet.js'; import nodesClaimsGet from './nodesClaimsGet.js'; import nodesClosestActiveConnectionsGet from './nodesClosestActiveConnectionsGet.js'; import nodesClosestLocalNodesGet from './nodesClosestLocalNodesGet.js'; @@ -7,7 +8,6 @@ import nodesConnectionSignalFinal from './nodesConnectionSignalFinal.js'; import nodesConnectionSignalInitial from './nodesConnectionSignalInitial.js'; import nodesCrossSignClaim from './nodesCrossSignClaim.js'; import nodesClaimNetworkSign from './nodesClaimNetworkSign.js'; -import nodesClaimNetworkVerify from './nodesClaimNetworkVerify.js'; import notificationsSend from './notificationsSend.js'; import vaultsGitInfoGet from './vaultsGitInfoGet.js'; import vaultsGitPackGet from './vaultsGitPackGet.js'; @@ -23,7 +23,9 @@ type AgentClientManifestNodeConnectionManager = typeof manifestClientNodeConnectionManager & ClientManifest; const manifestClientNodeManager = { + nodesClaimNetworkAuthorityGet, nodesClaimsGet, + nodesClaimNetworkSign, nodesClosestActiveConnectionsGet, nodesClosestLocalNodesGet, nodesCrossSignClaim, @@ -39,8 +41,6 @@ type AgentClientManifestNodeManager = typeof manifestClientNodeManager & const manifestClient = { ...manifestClientNodeConnectionManager, ...manifestClientNodeManager, - nodesClaimNetworkSign, - nodesClaimNetworkVerify, notificationsSend, vaultsGitInfoGet, vaultsGitPackGet, @@ -62,7 +62,6 @@ export { nodesConnectionSignalInitial, nodesCrossSignClaim, nodesClaimNetworkSign, - nodesClaimNetworkVerify, notificationsSend, vaultsGitInfoGet, vaultsGitPackGet, diff --git a/src/nodes/agent/callers/nodesClaimNetworkAuthorityGet.ts b/src/nodes/agent/callers/nodesClaimNetworkAuthorityGet.ts new file mode 100644 index 000000000..d414c4068 --- /dev/null +++ b/src/nodes/agent/callers/nodesClaimNetworkAuthorityGet.ts @@ -0,0 +1,12 @@ +import type { HandlerTypes } from '@matrixai/rpc'; +import type NodesClaimNetworkAuthorityGet from '../handlers/NodesClaimNetworkAuthorityGet.js'; +import { UnaryCaller } from '@matrixai/rpc'; + +type CallerTypes = HandlerTypes; + +const nodesClaimNetworkAuthorityGet = new UnaryCaller< + CallerTypes['input'], + CallerTypes['output'] +>(); + +export default nodesClaimNetworkAuthorityGet; diff --git a/src/nodes/agent/callers/nodesClaimNetworkSign.ts b/src/nodes/agent/callers/nodesClaimNetworkSign.ts index 07d877b79..b834881d8 100644 --- a/src/nodes/agent/callers/nodesClaimNetworkSign.ts +++ b/src/nodes/agent/callers/nodesClaimNetworkSign.ts @@ -1,10 +1,10 @@ import type { HandlerTypes } from '@matrixai/rpc'; import type NodesClaimNetworkSign from '../handlers/NodesClaimNetworkSign.js'; -import { UnaryCaller } from '@matrixai/rpc'; +import { DuplexCaller } from '@matrixai/rpc'; type CallerTypes = HandlerTypes; -const nodesClaimNetworkSign = new UnaryCaller< +const nodesClaimNetworkSign = new DuplexCaller< CallerTypes['input'], CallerTypes['output'] >(); diff --git a/src/nodes/agent/callers/nodesClaimNetworkVerify.ts b/src/nodes/agent/callers/nodesClaimNetworkVerify.ts deleted file mode 100644 index 2b8c553fa..000000000 --- a/src/nodes/agent/callers/nodesClaimNetworkVerify.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { HandlerTypes } from '@matrixai/rpc'; -import type NodesClaimNetworkVerify from '../handlers/NodesClaimNetworkVerify.js'; -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/handlers/NodesClaimNetworkAuthorityGet.ts b/src/nodes/agent/handlers/NodesClaimNetworkAuthorityGet.ts new file mode 100644 index 000000000..3431d08d8 --- /dev/null +++ b/src/nodes/agent/handlers/NodesClaimNetworkAuthorityGet.ts @@ -0,0 +1,32 @@ +import type { + AgentRPCRequestParams, + AgentRPCResponseResult, +} from '../types.js'; +import type { AgentClientManifest } from '../callers/index.js'; +import type NodeManager from '../../NodeManager.js'; +import type { SignedTokenEncoded } from '../../../tokens/types.js'; +import { UnaryHandler } from '@matrixai/rpc'; + +/** + * Sends a notification to a node + */ +class NodesClaimNetworkAuthorityGet extends UnaryHandler< + { + nodeManager: NodeManager; + }, + AgentRPCRequestParams, + AgentRPCResponseResult +> { + public handle = async ( + _input: AgentRPCRequestParams, + ): Promise> => { + const { + nodeManager, + }: { + nodeManager: NodeManager; + } = this.container; + return await nodeManager.handleClaimNetworkAuthorityGet(); + }; +} + +export default NodesClaimNetworkAuthorityGet; diff --git a/src/nodes/agent/handlers/NodesClaimNetworkSign.ts b/src/nodes/agent/handlers/NodesClaimNetworkSign.ts index a68fa9289..85bfa995f 100644 --- a/src/nodes/agent/handlers/NodesClaimNetworkSign.ts +++ b/src/nodes/agent/handlers/NodesClaimNetworkSign.ts @@ -1,36 +1,57 @@ +import type { JSONValue } from '@matrixai/rpc'; import type { AgentRPCRequestParams, AgentRPCResponseResult, AgentClaimMessage, } from '../types.js'; -import type NodeManager from '../../../nodes/NodeManager.js'; -import type { JSONValue } from '../../../types.js'; +import type NodeManager from '../../NodeManager.js'; import type { AgentClientManifest } from '../callers/index.js'; -import { UnaryHandler } from '@matrixai/rpc'; +import type ACL from '../../../acl/ACL.js'; +import { DuplexHandler } from '@matrixai/rpc'; import * as agentUtils from '../utils.js'; import * as nodesErrors from '../../errors.js'; -class NodesClaimNetworkSign extends UnaryHandler< +/** + * Claims a node + */ +class NodesCrossSignClaim extends DuplexHandler< { nodeManager: NodeManager; + acl: ACL; }, AgentRPCRequestParams, AgentRPCResponseResult > { - public handle = async ( - input: AgentRPCRequestParams, + public handle = async function* ( + input: AsyncIterableIterator>, _cancel: (reason?: any) => void, - meta: Record | undefined, - ): Promise> => { - const { nodeManager }: { nodeManager: NodeManager } = - this.container; - // Connections should always be validated + meta: Record, + ): AsyncGenerator> { + const { + nodeManager, + acl, + }: { + nodeManager: NodeManager; + acl: ACL; + } = this.container; const requestingNodeId = agentUtils.nodeIdFromMeta(meta); if (requestingNodeId == null) { throw new nodesErrors.ErrorNodeConnectionInvalidIdentity(); } - return nodeManager.handleClaimNetwork(requestingNodeId, input); + // Get the current NetworkAccessPermission + const isPrivate = nodeManager.isClaimNetworkAuthorityPrivate(); + // Check the ACL for permissions + const permissions = await acl.getNodePerm(requestingNodeId); + // Permissions only apply if isPrivate is true + if (isPrivate != null && isPrivate && permissions?.gestalt.join !== null) { + // Throw new nodesErrors.ErrorNodePermissionDenied(); + // Throwing seems to be broken right now. We're going to return early to force a protocol error + return; + } + + // Handle claiming the node + yield* nodeManager.handleClaimNetwork(requestingNodeId, input); }; } -export default NodesClaimNetworkSign; +export default NodesCrossSignClaim; diff --git a/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts b/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts deleted file mode 100644 index 362ec58cd..000000000 --- a/src/nodes/agent/handlers/NodesClaimNetworkVerify.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { - AgentClaimMessage, - AgentRPCRequestParams, - AgentRPCResponseResult, -} from '../types.js'; -import type NodeManager from '../../../nodes/NodeManager.js'; -import type { JSONValue } from '../../../types.js'; -import type { AgentClientManifest } from '../callers/index.js'; -import { UnaryHandler } from '@matrixai/rpc'; -import * as agentUtils from '../utils.js'; -import * as nodesErrors from '../../errors.js'; - -class NodesClaimNetworkVerify extends UnaryHandler< - { - nodeManager: NodeManager; - }, - AgentRPCRequestParams, - AgentRPCResponseResult<{ success: true }> -> { - public handle = async ( - input: AgentRPCRequestParams, - _cancel: (reason?: any) => void, - meta: Record | undefined, - ): Promise> => { - const { nodeManager }: { nodeManager: NodeManager } = - this.container; - const requestingNodeId = agentUtils.nodeIdFromMeta(meta); - if (requestingNodeId == null) { - throw new nodesErrors.ErrorNodeConnectionInvalidIdentity(); - } - return nodeManager.handleVerifyClaimNetwork(requestingNodeId, input); - }; -} - -export default NodesClaimNetworkVerify; diff --git a/src/nodes/agent/handlers/index.ts b/src/nodes/agent/handlers/index.ts index 087bbbac9..07ebcec3a 100644 --- a/src/nodes/agent/handlers/index.ts +++ b/src/nodes/agent/handlers/index.ts @@ -9,6 +9,7 @@ import type NodeConnectionManager from '../../../nodes/NodeConnectionManager.js' import type NotificationsManager from '../../../notifications/NotificationsManager.js'; import type VaultManager from '../../../vaults/VaultManager.js'; import type { AgentClientManifest } from '../callers/index.js'; +import NodesClaimNetworkAuthorityGet from './NodesClaimNetworkAuthorityGet.js'; import NodesAuthenticateConnection from './NodesAuthenticateConnection.js'; import NodesClaimsGet from './NodesClaimsGet.js'; import NodesClosestActiveConnectionsGet from './NodesClosestActiveConnectionsGet.js'; @@ -38,6 +39,7 @@ const manifestServer = (container: { vaultManager: VaultManager; }) => { return { + nodesClaimNetworkAuthorityGet: new NodesClaimNetworkAuthorityGet(container), nodesAuthenticateConnection: new NodesAuthenticateConnection(container), nodesClaimsGet: new NodesClaimsGet(container), nodesClosestActiveConnectionsGet: new NodesClosestActiveConnectionsGet( @@ -60,6 +62,7 @@ type AgentServerManifest = ReturnType; export default manifestServer; export { + NodesClaimNetworkAuthorityGet, NodesAuthenticateConnection, NodesClaimsGet, NodesClosestActiveConnectionsGet, diff --git a/src/nodes/agent/types.ts b/src/nodes/agent/types.ts index 0d3ca1764..7d9a307fd 100644 --- a/src/nodes/agent/types.ts +++ b/src/nodes/agent/types.ts @@ -15,6 +15,7 @@ import type { SignedNotification } from '../../notifications/types.js'; import type { Host, Hostname, Port } from '../../network/types.js'; import type { NetworkId, NodeContact } from '../../nodes/types.js'; import type { AuditEvent } from '../../audit/types.js'; +import type { SignedClaimEncoded } from '../../claims/types.js'; type AgentRPCRequestParams = JSONRPCRequestParams; @@ -106,13 +107,20 @@ type SuccessMessage = { }; type NodesAuthenticateConnectionMessage = + | NodesAuthenticateConnectionMessagePrivate | NodesAuthenticateConnectionMessageBasicPublic | NodesAuthenticateConnectionMessageNone; +type NodesAuthenticateConnectionMessagePrivate = { + type: 'NodesAuthenticateConnectionMessagePrivate'; + claimNetworkAccessEncoded: SignedClaimEncoded; +}; + type NodesAuthenticateConnectionMessageBasicPublic = { type: 'NodesAuthenticateConnectionMessageBasicPublic'; networkId: NetworkId; }; + type NodesAuthenticateConnectionMessageNone = { type: 'NodesAuthenticateConnectionMessageNone'; }; @@ -136,6 +144,7 @@ export type { VaultsScanMessage, SuccessMessage, NodesAuthenticateConnectionMessage, + NodesAuthenticateConnectionMessagePrivate, NodesAuthenticateConnectionMessageBasicPublic, NodesAuthenticateConnectionMessageNone, }; diff --git a/src/nodes/errors.ts b/src/nodes/errors.ts index d24de831d..3180eb4de 100644 --- a/src/nodes/errors.ts +++ b/src/nodes/errors.ts @@ -67,6 +67,14 @@ class ErrorNodeManagerAuthenticationTimedOut extends ErrorNodeManager { exitCode = sysexits.USAGE; } +class ErrorNodeManagerClaimNetworkAuthorityMissing< + T, +> extends ErrorNodeManager { + static description = + 'Missing ClaimNetworkAuthority required to create ClaimNetworkAccess'; + exitCode = sysexits.USAGE; +} + class ErrorNodeGraph extends ErrorNodes {} class ErrorNodeGraphRunning extends ErrorNodeGraph { @@ -302,6 +310,7 @@ export { ErrorNodeManagerAuthenticationFailedForward, ErrorNodeManagerAuthenticationFailedReverse, ErrorNodeManagerAuthenticationTimedOut, + ErrorNodeManagerClaimNetworkAuthorityMissing, ErrorNodeGraph, ErrorNodeGraphRunning, ErrorNodeGraphNotRunning, diff --git a/src/nodes/types.ts b/src/nodes/types.ts index dfd200e4e..a2e1b7523 100644 --- a/src/nodes/types.ts +++ b/src/nodes/types.ts @@ -84,6 +84,7 @@ type AuthenticateNetworkForwardCallback = ( */ type AuthenticateNetworkReverseCallback = ( message: NodesAuthenticateConnectionMessage, + requestingNodeId: NodeId, ctx: ContextTimed, ) => Promise; diff --git a/src/nodes/utils.ts b/src/nodes/utils.ts index 8d780aa66..5557fa01f 100644 --- a/src/nodes/utils.ts +++ b/src/nodes/utils.ts @@ -13,12 +13,16 @@ import type { NodeBucketIndex, NodeId, SeedNodes, + AuthenticateNetworkForwardCallback, + AuthenticateNetworkReverseCallback, } from './types.js'; import type { NodesAuthenticateConnectionMessage, NodesAuthenticateConnectionMessageBasicPublic, NodesAuthenticateConnectionMessageNone, + NodesAuthenticateConnectionMessagePrivate, } from './agent/types.js'; +import type { ClaimNetworkAccess } from '../claims/payloads/index.js'; import dns from 'dns'; import { utils as dbUtils } from '@matrixai/db'; import { IdInternal } from '@matrixai/id'; @@ -33,6 +37,11 @@ import * as networkUtils from '../network/utils.js'; import * as validationErrors from '../validation/errors.js'; import config from '../config.js'; import * as utils from '../utils/index.js'; +import * as claimsUtils from '../claims/utils.js'; +import Token from '../tokens/Token.js'; +import * as claimNetworkAuthorityUtils from '../claims/payloads/claimNetworkAuthority.js'; +import * as nodesUtils from '../nodes/utils.js'; +import * as claimNetworkAccessUtils from '../claims/payloads/claimNetworkAccess.js'; const sepBuffer = dbUtils.sep; @@ -808,7 +817,7 @@ async function nodesAuthenticateConnectionReverseDefault(): Promise { function nodesAuthenticateConnectionForwardBasicPublicFactory( networkId: string, -) { +): AuthenticateNetworkForwardCallback { return async (): Promise => { return { type: 'NodesAuthenticateConnectionMessageBasicPublic', @@ -819,7 +828,7 @@ function nodesAuthenticateConnectionForwardBasicPublicFactory( function nodesAuthenticateConnectionReverseBasicPublicFactory( networkId: string, -) { +): AuthenticateNetworkReverseCallback { return async (message: NodesAuthenticateConnectionMessage): Promise => { if (message.type !== 'NodesAuthenticateConnectionMessageBasicPublic') { throw new nodesErrors.ErrorNodeAuthenticationFailed( @@ -840,6 +849,65 @@ async function nodesAuthenticateConnectionReverseDeny() { ); } +function nodesAuthenticationConnectionForwardPrivateFactory( + networkAccessClaim: Token, +): AuthenticateNetworkForwardCallback { + const claimNetworkAccessEncoded = claimsUtils.generateSignedClaim( + networkAccessClaim.toSigned(), + ); + return async (): Promise => { + return { + type: 'NodesAuthenticateConnectionMessagePrivate', + claimNetworkAccessEncoded, + }; + }; +} + +function nodesAuthenticationConnectionReversePrivateFactory( + networkAccessClaim: Token, +): AuthenticateNetworkReverseCallback { + const expectedNetwork = networkAccessClaim.payload.network; + const claimNetworkAuthority = + claimNetworkAuthorityUtils.parseSignedClaimNetworkAuthority( + networkAccessClaim.payload.signedClaimNetworkAuthorityEncoded, + ); + const tokenClaimNetworkAuthority = Token.fromSigned(claimNetworkAuthority); + const expectedNetworkNodeId = nodesUtils.decodeNodeId( + tokenClaimNetworkAuthority.payload.iss, + ); + if (expectedNetworkNodeId == null) { + utils.never('expectedNetworkNodeId should be defined'); + } + + return async ( + message: NodesAuthenticateConnectionMessage, + requestingNodeId: NodeId, + ): Promise => { + if (message.type !== 'NodesAuthenticateConnectionMessagePrivate') { + throw new nodesErrors.ErrorNodeAuthenticationFailed( + 'message type must be "NodesAuthenticateConnectionMessagePrivate"', + ); + } + const claimNetworkAccess = + claimNetworkAccessUtils.parseSignedClaimNetworkAccess( + message.claimNetworkAccessEncoded, + ); + const tokenClaimNetworkAccess = Token.fromSigned(claimNetworkAccess); + try { + claimNetworkAccessUtils.verifyClaimNetworkAccess( + expectedNetworkNodeId, + requestingNodeId, + expectedNetwork, + tokenClaimNetworkAccess, + ); + } catch (e) { + throw new nodesErrors.ErrorNodeAuthenticationFailed( + `authentication failed with ${e.name}:${e.message}`, + ); + } + }; +} + export { sepBuffer, nodeContactAddress, @@ -879,6 +947,8 @@ export { nodesAuthenticateConnectionForwardBasicPublicFactory, nodesAuthenticateConnectionReverseBasicPublicFactory, nodesAuthenticateConnectionReverseDeny, + nodesAuthenticationConnectionForwardPrivateFactory, + nodesAuthenticationConnectionReversePrivateFactory, }; export { encodeNodeId, decodeNodeId } from '../ids/index.js'; diff --git a/tests/claims/payloads/claimNetworkAccess.test.ts b/tests/claims/payloads/claimNetworkAccess.test.ts new file mode 100644 index 000000000..fdfd3dd12 --- /dev/null +++ b/tests/claims/payloads/claimNetworkAccess.test.ts @@ -0,0 +1,56 @@ +import { test, fc } from '@fast-check/jest'; +import * as testsClaimsPayloadsUtils from './utils.js'; +import * as claimsPayloadsClaimNetworkAccess from '#claims/payloads/claimNetworkAccess.js'; + +describe('claims/payloads/claimNetworkAccess', () => { + test.prop([ + testsClaimsPayloadsUtils.claimNetworkAccessEncodedArb(), + fc.noShrink(fc.string()), + ])( + 'parse claim network access', + ( + claimNetworkAccessEncodedCorrect, + signedClaimNetworkAccessEncodedIncorrect, + ) => { + expect(() => { + claimsPayloadsClaimNetworkAccess.parseClaimNetworkAccess( + claimNetworkAccessEncodedCorrect, + ); + }).not.toThrow(); + expect(() => { + claimsPayloadsClaimNetworkAccess.parseClaimNetworkAccess( + signedClaimNetworkAccessEncodedIncorrect, + ); + }); + }, + ); + test.prop([ + testsClaimsPayloadsUtils.signedClaimEncodedArb( + testsClaimsPayloadsUtils.claimNetworkAccessArb(), + ), + fc.record( + { + payload: fc.string(), + signatures: fc.array(fc.string()), + }, + { noNullPrototype: true }, + ), + ])( + 'parse signed claim network access', + ( + signedClaimLinkIdentityEncodedCorrect, + signedClaimLinkIdentityEncodedIncorrect, + ) => { + expect(() => { + claimsPayloadsClaimNetworkAccess.parseSignedClaimNetworkAccess( + signedClaimLinkIdentityEncodedCorrect, + ); + }).not.toThrow(); + expect(() => { + claimsPayloadsClaimNetworkAccess.parseSignedClaimNetworkAccess( + signedClaimLinkIdentityEncodedIncorrect, + ); + }).toThrow(); + }, + ); +}); diff --git a/tests/claims/payloads/claimNetworkAuthority.test.ts b/tests/claims/payloads/claimNetworkAuthority.test.ts new file mode 100644 index 000000000..76e1e9be5 --- /dev/null +++ b/tests/claims/payloads/claimNetworkAuthority.test.ts @@ -0,0 +1,56 @@ +import { test, fc } from '@fast-check/jest'; +import * as testsClaimsPayloadsUtils from './utils.js'; +import * as claimsPayloadsClaimNetworkAuthority from '#claims/payloads/claimNetworkAuthority.js'; + +describe('claims/payloads/claimNetworkAccess', () => { + test.prop([ + testsClaimsPayloadsUtils.claimNetworkAuthorityEncodedArb(), + fc.noShrink(fc.string()), + ])( + 'parse claim network access', + ( + claimNetworkAccessEncodedCorrect, + signedClaimNetworkAccessEncodedIncorrect, + ) => { + expect(() => { + claimsPayloadsClaimNetworkAuthority.parseClaimNetworkAuthority( + claimNetworkAccessEncodedCorrect, + ); + }).not.toThrow(); + expect(() => { + claimsPayloadsClaimNetworkAuthority.parseClaimNetworkAuthority( + signedClaimNetworkAccessEncodedIncorrect, + ); + }); + }, + ); + test.prop([ + testsClaimsPayloadsUtils.signedClaimEncodedArb( + testsClaimsPayloadsUtils.claimNetworkAuthorityArb(), + ), + fc.record( + { + payload: fc.string(), + signatures: fc.array(fc.string()), + }, + { noNullPrototype: true }, + ), + ])( + 'parse signed claim network access', + ( + signedClaimLinkIdentityEncodedCorrect, + signedClaimLinkIdentityEncodedIncorrect, + ) => { + expect(() => { + claimsPayloadsClaimNetworkAuthority.parseSignedClaimNetworkAuthority( + signedClaimLinkIdentityEncodedCorrect, + ); + }).not.toThrow(); + expect(() => { + claimsPayloadsClaimNetworkAuthority.parseSignedClaimNetworkAuthority( + signedClaimLinkIdentityEncodedIncorrect, + ); + }).toThrow(); + }, + ); +}); diff --git a/tests/claims/payloads/utils.ts b/tests/claims/payloads/utils.ts index 1b1678b35..9bfd66ba8 100644 --- a/tests/claims/payloads/utils.ts +++ b/tests/claims/payloads/utils.ts @@ -2,7 +2,11 @@ import type { Claim, SignedClaim } from '#claims/types.js'; import type { ClaimLinkNode, ClaimLinkIdentity, + ClaimNetworkAccess, } from '#claims/payloads/index.js'; +import type { SignedTokenEncoded } from '#tokens/types.js'; +import type { ClaimNetworkAuthority } from '#claims/payloads/claimNetworkAuthority.js'; +import type { NodeIdEncoded } from '#ids/index.js'; import fc from 'fast-check'; import * as testsClaimsUtils from '../utils.js'; import * as testsTokensUtils from '../../tokens/utils.js'; @@ -66,6 +70,77 @@ const signedClaimArb =

( const signedClaimEncodedArb = (payloadArb: fc.Arbitrary) => signedClaimArb(payloadArb).map(claimsUtils.generateSignedClaim); +const claimNetworkAuthorityArb = ( + iss: fc.Arbitrary = testsIdsUtils.nodeIdEncodedArb, + sub: fc.Arbitrary = testsIdsUtils.nodeIdEncodedArb, +) => + fc.noShrink( + testsClaimsUtils.claimArb.chain((claim) => { + return fc + .record( + { + iss, + sub, + network: fc.webUrl(), + isPrivate: fc.boolean(), + }, + { noNullPrototype: true }, + ) + .chain((value) => { + return fc.constant({ + typ: 'ClaimNetworkAuthority', + ...claim, + ...value, + }); + }); + }) as fc.Arbitrary, + ); + +const claimNetworkAuthorityEncodedArb = ( + iss: fc.Arbitrary = testsIdsUtils.nodeIdEncodedArb, + sub: fc.Arbitrary = testsIdsUtils.nodeIdEncodedArb, +) => claimNetworkAuthorityArb(iss, sub).map(claimsUtils.generateClaim); + +const claimNetworkAccessArb = ( + iss: fc.Arbitrary = testsIdsUtils.nodeIdEncodedArb, + sub: fc.Arbitrary = testsIdsUtils.nodeIdEncodedArb, + network: fc.Arbitrary = fc.string(), + signedClaimNetworkAuthorityEncoded: fc.Arbitrary = signedClaimEncodedArb( + claimNetworkAuthorityArb( + testsIdsUtils.nodeIdEncodedArb, + testsIdsUtils.nodeIdEncodedArb, + ), + ), +) => + fc.noShrink( + testsClaimsUtils.claimArb.chain((claim) => { + return fc + .record( + { + iss, + sub, + network, + signedClaimNetworkAuthorityEncoded, + isPrivate: fc.boolean(), + }, + { noNullPrototype: true }, + ) + .chain((value) => { + return fc.constant({ + typ: 'ClaimNetworkAccess', + ...claim, + ...value, + }); + }); + }) as fc.Arbitrary, + ); + +const claimNetworkAccessEncodedArb = ( + iss: fc.Arbitrary = testsIdsUtils.nodeIdEncodedArb, + sub: fc.Arbitrary = testsIdsUtils.nodeIdEncodedArb, + network: fc.Arbitrary = fc.string(), +) => claimNetworkAccessArb(iss, sub, network).map(claimsUtils.generateClaim); + export { claimLinkIdentityArb, claimLinkIdentityEncodedArb, @@ -73,4 +148,8 @@ export { claimLinkNodeEncodedArb, signedClaimArb, signedClaimEncodedArb, + claimNetworkAccessArb, + claimNetworkAccessEncodedArb, + claimNetworkAuthorityArb, + claimNetworkAuthorityEncodedArb, }; diff --git a/tests/nodes/NodeConnectionManager.test.ts b/tests/nodes/NodeConnectionManager.test.ts index d1e986c47..c5c2aea4a 100644 --- a/tests/nodes/NodeConnectionManager.test.ts +++ b/tests/nodes/NodeConnectionManager.test.ts @@ -1175,6 +1175,51 @@ describe(`${NodeConnectionManager.name}`, () => { }, ); }); + + test('can authenticate a connection to a private network', async () => { + ncmLocal.nodeConnectionManager.setAuthenticateNetworkForwardCallback( + nodesUtils.nodesAuthenticateConnectionForwardBasicPublicFactory( + 'someNetwork', + ), + ); + ncmPeer1.nodeConnectionManager.setAuthenticateNetworkForwardCallback( + nodesUtils.nodesAuthenticateConnectionForwardBasicPublicFactory( + 'someNetwork', + ), + ); + ncmLocal.nodeConnectionManager.setAuthenticateNetworkReverseCallback( + nodesUtils.nodesAuthenticateConnectionReverseBasicPublicFactory( + 'someNetwork', + ), + ); + ncmPeer1.nodeConnectionManager.setAuthenticateNetworkReverseCallback( + nodesUtils.nodesAuthenticateConnectionReverseBasicPublicFactory( + 'someNetwork', + ), + ); + + // Creating connection + await ncmLocal.nodeConnectionManager.createConnection( + [ncmPeer1.nodeId], + localHost, + ncmPeer1.port, + ); + // Checking authentication result + await ncmLocal.nodeConnectionManager.withConnF( + ncmPeer1.nodeId, + undefined, + async () => { + // Do nothing + }, + ); + await ncmPeer1.nodeConnectionManager.withConnF( + ncmLocal.nodeId, + undefined, + async () => { + // Do nothing + }, + ); + }); }); describe('with 2 peers', () => { let ncmLocal: NCMState; diff --git a/tests/nodes/NodeManager.test.ts b/tests/nodes/NodeManager.test.ts index 777b14a2b..846f799c4 100644 --- a/tests/nodes/NodeManager.test.ts +++ b/tests/nodes/NodeManager.test.ts @@ -2,7 +2,11 @@ import type { Host, Port } from '#network/types.js'; import type { AgentServerManifest } from '#nodes/agent/handlers/index.js'; import type nodeGraph from '#nodes/NodeGraph.js'; import type { NCMState } from './utils.js'; -import type { NodeAddress, NodeContactAddressData } from '#nodes/types.js'; +import type { + NodeAddress, + NodeContactAddressData, + NodeId, +} from '#nodes/types.js'; import type { AgentRPCRequestParams, AgentRPCResponseResult, @@ -48,6 +52,11 @@ import { KeyRing } from '#keys/index.js'; import NodeConnectionQueue from '#nodes/NodeConnectionQueue.js'; import * as utils from '#utils/index.js'; import rpcClientManifest from '#nodes/agent/callers/index.js'; +import * as claimNetworkAuthorityUtils from '#claims/payloads/claimNetworkAuthority.js'; +import * as claimNetworkAccessUtils from '#claims/payloads/claimNetworkAccess.js'; +import NodesClaimNetworkSign from '#nodes/agent/handlers/NodesClaimNetworkSign.js'; +import NodesClaimNetworkAuthorityGet from '#nodes/agent/handlers/NodesClaimNetworkAuthorityGet.js'; +import * as claimsErrors from '#claims/errors.js'; class DummyNodesAuthenticateConnection extends UnaryHandler< ObjectEmpty, @@ -67,6 +76,16 @@ class DummyNodesAuthenticateConnection extends UnaryHandler< }; } +async function allowNodeToJoin( + gestaltGraph: GestaltGraph, + nodeId: NodeId, +): Promise { + await gestaltGraph.setNode({ + nodeId: nodeId, + }); + await gestaltGraph.setGestaltAction(['node', nodeId], 'join'); +} + describe(`${NodeManager.name}`, () => { const logger = new Logger(`${NodeManager.name} test`, LogLevel.WARN, [ new StreamHandler( @@ -425,6 +444,80 @@ describe(`${NodeManager.name}`, () => { await nodeManager.setNode(nodeId, nodeAddress, nodeContactAddressData); waitResolveP(); }); + test('can create a claimNetworkAuthority and verify using the network public ID', async () => { + const networkKeyPair = keysUtils.generateKeyPair(); + const networkNodeId = keysUtils.publicKeyToNodeId( + networkKeyPair.publicKey, + ); + const network = 'test.network.com'; + const result = await nodeManager.createClaimNetworkAuthority( + networkNodeId, + network, + true, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair.privateKey); + return claim; + }, + ); + + const [, token] = result; + const targetNodeId = keyRing.getNodeId(); + // The generated claim is valid + claimNetworkAuthorityUtils.verifyClaimNetworkAuthority( + networkNodeId, + targetNodeId, + network, + token, + ); + + // Will throw because the subject node isn't the network authority or vice versa + expect(() => + claimNetworkAuthorityUtils.verifyClaimNetworkAuthority( + targetNodeId, + networkNodeId, + network, + token, + ), + ).toThrow(); + // Will throw if network doesn't match + expect(() => + claimNetworkAuthorityUtils.verifyClaimNetworkAuthority( + networkNodeId, + targetNodeId, + 'some.other.network.com', + token, + ), + ).toThrow(); + }); + test('can create a self signed claimNetworkAccess and verify it', async () => { + const networkKeyPair = keysUtils.generateKeyPair(); + const networkNodeId = keysUtils.publicKeyToNodeId( + networkKeyPair.publicKey, + ); + const network = 'test.network.com'; + const result = await nodeManager.createClaimNetworkAuthority( + networkNodeId, + network, + true, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair.privateKey); + return claim; + }, + ); + + const [, token] = result; + const targetNodeId = keyRing.getNodeId(); + + // Creating the self signed access claim + const [, selfSignedClaimNetworkAccessToken] = + await nodeManager.createSelfSignedClaimNetworkAccess(token); + claimNetworkAccessUtils.verifyClaimNetworkAccess( + networkNodeId, + targetNodeId, + network, + selfSignedClaimNetworkAccessToken, + ); + }); }); describe('with 1 peer', () => { let basePath: string; @@ -600,6 +693,13 @@ describe(`${NodeManager.name}`, () => { nodesAuthenticateConnection: new NodesAuthenticateConnection({ nodeConnectionManager: nodeConnectionManagerPeer, }), + nodesClaimNetworkSign: new NodesClaimNetworkSign({ + nodeManager: nodeManagerPeer, + acl: aclPeer, + }), + nodesClaimNetworkAuthorityGet: new NodesClaimNetworkAuthorityGet({ + nodeManager: nodeManagerPeer, + }), } as AgentServerManifest, host: localHost, }); @@ -940,6 +1040,51 @@ describe(`${NodeManager.name}`, () => { expect(await nodeGraph.nodesTotal()).toBe(0); expect(await nodeGraphPeer.nodesTotal()).toBe(0); }); + + // TODO tests + // 1. claiming a network should fail if issuer doesn't have a valid claimNetworkAuthority + // 2. Claming a network should fail if issuer doesn't allow the requesting node access. + test('creating a claimNetworkAccess token and verifying it', async () => { + const nodeIdTarget = keyRingPeer.getNodeId(); + // Adding permission + await aclPeer.setNodePerm(keyRing.getNodeId(), { + gestalt: { + claim: null, + }, + vaults: {}, + }); + + const networkKeyPair = keysUtils.generateKeyPair(); + const networkNodeId = keysUtils.publicKeyToNodeId( + networkKeyPair.publicKey, + ); + const network = 'test.network.com'; + + await nodeManagerPeer.createClaimNetworkAuthority( + networkNodeId, + network, + true, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair.privateKey); + return claim; + }, + ); + // Start the connection + await nodeConnectionManager.createConnection( + [nodeIdTarget], + localHost, + nodeConnectionManagerPeer.port, + ); + await allowNodeToJoin(gestaltGraphPeer, keyRing.getNodeId()); + const result = await nodeManager.claimNetwork(nodeIdTarget, network); + const [, token] = result; + claimNetworkAccessUtils.verifyClaimNetworkAccess( + networkNodeId, + keyRing.getNodeId(), + network, + token, + ); + }); }); describe('with 1 peer and mdns', () => { let basePath: string; @@ -2181,4 +2326,642 @@ describe(`${NodeManager.name}`, () => { expect(await nodeGraph.nodesTotal()).toBe(5); }); }); + describe('simulating a private network', () => { + let basePath: string; + + // Will create 6 peers forming a simple network + const ncmPeers: Array<{ + db: DB; + keyRing: KeyRing; + acl: ACL; + sigchain: Sigchain; + gestaltGraph: GestaltGraph; + nodeGraph: NodeGraph; + nodeConnectionManager: NodeConnectionManager; + taskManager: TaskManager; + nodeManager: NodeManager; + }> = []; + + const createPeerNode = async (): Promise<{ + db: DB; + keyRing: KeyRing; + acl: ACL; + sigchain: Sigchain; + gestaltGraph: GestaltGraph; + nodeGraph: NodeGraph; + nodeConnectionManager: NodeConnectionManager; + taskManager: TaskManager; + nodeManager: NodeManager; + }> => { + const newId = ncmPeers.length; + const db = await DB.createDB({ + dbPath: path.join(basePath, `db-${newId}`), + logger, + }); + const keyRing = await KeyRing.createKeyRing({ + keysPath: path.join(basePath, `key-${newId}`), + password, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + logger, + }); + const acl = await ACL.createACL({ + db, + logger: logger.getChild(ACL.name), + }); + const sigchain = await Sigchain.createSigchain({ + db, + keyRing, + logger: logger.getChild(Sigchain.name), + }); + const gestaltGraph = await GestaltGraph.createGestaltGraph({ + db, + acl, + logger: logger.getChild(GestaltGraph.name), + }); + const nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyRing, + logger: logger.getChild(NodeGraph.name), + }); + const nodeConnectionManager = new NodeConnectionManager({ + keyRing, + tlsConfig: await testsUtils.createTLSConfig(keyRing.keyPair), + rpcClientManifest: rpcClientManifest, + authenticateNetworkForwardCallback: + nodesUtils.nodesAuthenticateConnectionForwardDefault, + authenticateNetworkReverseCallback: + nodesUtils.nodesAuthenticateConnectionReverseDeny, + logger: logger.getChild(NodeConnectionManager.name), + connectionConnectTimeoutTime: timeoutTime, + }); + const taskManager = await TaskManager.createTaskManager({ + db, + logger: logger.getChild(TaskManager.name), + }); + const nodeManager = new NodeManager({ + db, + keyRing, + gestaltGraph, + nodeGraph, + nodeConnectionManager, + sigchain, + taskManager, + logger: logger.getChild(NodeManager.name), + }); + await nodeConnectionManager.start({ + agentService: { + nodesAuthenticateConnection: new NodesAuthenticateConnection({ + nodeConnectionManager: nodeConnectionManager, + }), + nodesClaimNetworkSign: new NodesClaimNetworkSign({ + nodeManager, + acl, + }), + nodesClaimNetworkAuthorityGet: new NodesClaimNetworkAuthorityGet({ + nodeManager, + }), + }, + host: localHost, + }); + await nodeManager.start(); + + const peer = { + db, + keyRing, + acl, + sigchain, + gestaltGraph, + nodeGraph, + nodeConnectionManager, + taskManager, + nodeManager, + }; + ncmPeers[newId] = peer; + return peer; + }; + + beforeEach(async () => { + basePath = path.join(dataDir, 'local'); + await fs.promises.mkdir(basePath); + }); + afterEach(async () => { + for (const ncmPeer of ncmPeers) { + await ncmPeer.nodeManager.stop(); + await ncmPeer.taskManager.stop(); + await ncmPeer.nodeConnectionManager.stop(); + await ncmPeer.nodeGraph.stop(); + await ncmPeer.gestaltGraph.stop(); + await ncmPeer.sigchain.stop(); + await ncmPeer.acl.stop(); + await ncmPeer.db.stop(); + await ncmPeer.keyRing.stop(); + } + }); + + test('one seed node and one joining node', async () => { + // Creating network credentials + const networkKeyPair = keysUtils.generateKeyPair(); + const networkNodeId = keysUtils.publicKeyToNodeId( + networkKeyPair.publicKey, + ); + const network = 'test.network.com'; + + // Setting up seed nodes claims + const seedNode = await createPeerNode(); + const [, seedNodeClaimNetworkAuthority] = + await seedNode.nodeManager.createClaimNetworkAuthority( + networkNodeId, + network, + true, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair.privateKey); + return claim; + }, + ); + await seedNode.nodeManager.createSelfSignedClaimNetworkAccess( + seedNodeClaimNetworkAuthority, + ); + const seedNodeId = seedNode.keyRing.getNodeId(); + + // Setting up the new node entering the network + const node1 = await createPeerNode(); + // Connect to the seed node + await node1.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + const node1Id = node1.keyRing.getNodeId(); + await allowNodeToJoin(seedNode.gestaltGraph, node1Id); + const [, peerClaimNetworkAccess] = await node1.nodeManager.claimNetwork( + seedNodeId, + network, + ); + claimNetworkAccessUtils.verifyClaimNetworkAccess( + networkNodeId, + node1Id, + network, + peerClaimNetworkAccess, + ); + + // We have now proved that a node can request access to the network from a node with network authority. + // Now We should be able to connect while authenticated to the seed node. + + // Re-initiate authentication + await seedNode.nodeConnectionManager.destroyConnection(node1Id, true); + await node1.nodeConnectionManager.destroyConnection(seedNodeId, true); + await node1.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + await node1.nodeManager.withConnF(seedNodeId, undefined, async () => { + // Do nothing + }); + }); + test('joining node can restart and stay apart of the network', async () => { + // Creating network credentials + const networkKeyPair = keysUtils.generateKeyPair(); + const networkNodeId = keysUtils.publicKeyToNodeId( + networkKeyPair.publicKey, + ); + const network = 'test.network.com'; + + // Setting up seed nodes claims + const seedNode = await createPeerNode(); + const [, seedNodeClaimNetworkAuthority] = + await seedNode.nodeManager.createClaimNetworkAuthority( + networkNodeId, + network, + true, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair.privateKey); + return claim; + }, + ); + await seedNode.nodeManager.createSelfSignedClaimNetworkAccess( + seedNodeClaimNetworkAuthority, + ); + const seedNodeId = seedNode.keyRing.getNodeId(); + + // Setting up the new node entering the network + const node1 = await createPeerNode(); + // Connect to the seed node + await node1.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + await allowNodeToJoin(seedNode.gestaltGraph, node1.keyRing.getNodeId()); + const [, peerClaimNetworkAccess] = await node1.nodeManager.claimNetwork( + seedNodeId, + network, + ); + const node1Id = node1.keyRing.getNodeId(); + claimNetworkAccessUtils.verifyClaimNetworkAccess( + networkNodeId, + node1Id, + network, + peerClaimNetworkAccess, + ); + + // We have now proved that a node can request access to the network from a node with network authority. + // Now We should be able to connect while authenticated to the seed node. + + // Re-initiate authentication + await seedNode.nodeConnectionManager.destroyConnection(node1Id, true); + + await node1.nodeManager.stop(); + await node1.nodeConnectionManager.stop(); + await node1.nodeConnectionManager.start({ + agentService: { + nodesAuthenticateConnection: new NodesAuthenticateConnection({ + nodeConnectionManager: node1.nodeConnectionManager, + }), + nodesClaimNetworkSign: new NodesClaimNetworkSign({ + nodeManager: node1.nodeManager, + acl: node1.acl, + }), + } as AgentServerManifest, + host: localHost, + }); + await node1.nodeManager.start(); + await node1.nodeManager.switchNetwork(network); + + await node1.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + await node1.nodeManager.withConnF(seedNodeId, undefined, async () => { + // Do nothing + }); + }); + test('two nodes can join a network and connect to each other', async () => { + // Creating network credentials + const networkKeyPair = keysUtils.generateKeyPair(); + const networkNodeId = keysUtils.publicKeyToNodeId( + networkKeyPair.publicKey, + ); + const network = 'test.network.com'; + + // Setting up seed nodes claims + const seedNode = await createPeerNode(); + const [, seedNodeClaimNetworkAuthority] = + await seedNode.nodeManager.createClaimNetworkAuthority( + networkNodeId, + network, + true, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair.privateKey); + return claim; + }, + ); + await seedNode.nodeManager.createSelfSignedClaimNetworkAccess( + seedNodeClaimNetworkAuthority, + ); + const seedNodeId = seedNode.keyRing.getNodeId(); + + // Setting up the new nodes entering the network + const node1 = await createPeerNode(); + const node2 = await createPeerNode(); + const node1Id = node1.keyRing.getNodeId(); + const node2Id = node2.keyRing.getNodeId(); + + await node1.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + await node2.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + await allowNodeToJoin(seedNode.gestaltGraph, node1Id); + await node1.nodeManager.claimNetwork(seedNodeId, network); + await allowNodeToJoin(seedNode.gestaltGraph, node2Id); + await node2.nodeManager.claimNetwork(seedNodeId, network); + + // The two nodes should allow connections to each other + await node1.nodeConnectionManager.createConnection( + [node2Id], + localHost, + node2.nodeConnectionManager.port, + ); + + await node1.nodeManager.withConnF(node2Id, undefined, async () => {}); + }); + test('two nodes can not communicate if they do not share the network', async () => { + // Creating network credentials + const networkKeyPair = keysUtils.generateKeyPair(); + const networkNodeId = keysUtils.publicKeyToNodeId( + networkKeyPair.publicKey, + ); + const network = 'test.network.com'; + + // Setting up seed nodes claims + const seedNode = await createPeerNode(); + const [, seedNodeClaimNetworkAuthority] = + await seedNode.nodeManager.createClaimNetworkAuthority( + networkNodeId, + network, + true, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair.privateKey); + return claim; + }, + ); + await seedNode.nodeManager.createSelfSignedClaimNetworkAccess( + seedNodeClaimNetworkAuthority, + ); + const seedNodeId = seedNode.keyRing.getNodeId(); + + // Setting up the new nodes entering the network + const node1 = await createPeerNode(); + const node2 = await createPeerNode(); + const node1Id = node1.keyRing.getNodeId(); + const node2Id = node2.keyRing.getNodeId(); + + await node1.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + await node2.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + await allowNodeToJoin(seedNode.gestaltGraph, node1Id); + await node1.nodeManager.claimNetwork(seedNodeId, network); + // We intentionally don't have node2 join the network here + + // The two nodes should allow connections to each other + await node1.nodeConnectionManager.createConnection( + [node2Id], + localHost, + node2.nodeConnectionManager.port, + ); + + await expect( + node1.nodeManager.withConnF(node2Id, undefined, async () => { + // Do nothing + }), + ).rejects.toThrow(nodesErrors.ErrorNodeManagerAuthenticationFailed); + }); + test('two networks can operate side by side without crosstalk', async () => { + // Creating network credentials + const networkKeyPair1 = keysUtils.generateKeyPair(); + const networkNodeId1 = keysUtils.publicKeyToNodeId( + networkKeyPair1.publicKey, + ); + const network1 = 'test1.network.com'; + + // Setting up seed nodes claims + const seedNode1 = await createPeerNode(); + const [, seedNodeClaimNetworkAuthority1] = + await seedNode1.nodeManager.createClaimNetworkAuthority( + networkNodeId1, + network1, + true, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair1.privateKey); + return claim; + }, + ); + await seedNode1.nodeManager.createSelfSignedClaimNetworkAccess( + seedNodeClaimNetworkAuthority1, + ); + const seedNodeId1 = seedNode1.keyRing.getNodeId(); + + // Setting up 2nd seed node + // Creating network credentials + const networkKeyPair2 = keysUtils.generateKeyPair(); + const networkNodeId2 = keysUtils.publicKeyToNodeId( + networkKeyPair2.publicKey, + ); + const network2 = 'test2.network.com'; + + // Setting up seed nodes claims + const seedNode2 = await createPeerNode(); + const [, seedNodeClaimNetworkAuthority2] = + await seedNode2.nodeManager.createClaimNetworkAuthority( + networkNodeId2, + network2, + true, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair2.privateKey); + return claim; + }, + ); + await seedNode2.nodeManager.createSelfSignedClaimNetworkAccess( + seedNodeClaimNetworkAuthority2, + ); + const seedNodeId2 = seedNode2.keyRing.getNodeId(); + + // Setting up the new nodes entering the network + const node1 = await createPeerNode(); + const node1Id = node1.keyRing.getNodeId(); + await node1.nodeConnectionManager.createConnection( + [seedNodeId1], + localHost, + seedNode1.nodeConnectionManager.port, + ); + await allowNodeToJoin(seedNode1.gestaltGraph, node1Id); + await node1.nodeManager.claimNetwork(seedNodeId1, network1); + + const node2 = await createPeerNode(); + const node2Id = node2.keyRing.getNodeId(); + await node2.nodeConnectionManager.createConnection( + [seedNodeId2], + localHost, + seedNode2.nodeConnectionManager.port, + ); + await allowNodeToJoin(seedNode2.gestaltGraph, node2Id); + await node2.nodeManager.claimNetwork(seedNodeId2, network2); + + // The two nodes should allow connections to each other + await node1.nodeConnectionManager.createConnection( + [node2Id], + localHost, + node2.nodeConnectionManager.port, + ); + await node1.nodeConnectionManager.createConnection( + [seedNodeId2], + localHost, + seedNode2.nodeConnectionManager.port, + ); + await node2.nodeConnectionManager.createConnection( + [node1Id], + localHost, + node1.nodeConnectionManager.port, + ); + await node2.nodeConnectionManager.createConnection( + [seedNodeId1], + localHost, + seedNode1.nodeConnectionManager.port, + ); + await seedNode1.nodeConnectionManager.createConnection( + [seedNodeId2], + localHost, + seedNode2.nodeConnectionManager.port, + ); + + // Two nodes can't talk + await expect( + node1.nodeManager.withConnF(node2Id, undefined, async () => { + // Do nothing + }), + ).rejects.toThrow(nodesErrors.ErrorNodeManagerAuthenticationFailed); + await expect( + node2.nodeManager.withConnF(node1Id, undefined, async () => { + // Do nothing + }), + ).rejects.toThrow(nodesErrors.ErrorNodeManagerAuthenticationFailed); + + // Two seed nodes can't talk + await expect( + seedNode1.nodeManager.withConnF(seedNodeId2, undefined, async () => { + // Do nothing + }), + ).rejects.toThrow(nodesErrors.ErrorNodeManagerAuthenticationFailed); + await expect( + seedNode2.nodeManager.withConnF(seedNodeId1, undefined, async () => { + // Do nothing + }), + ).rejects.toThrow(nodesErrors.ErrorNodeManagerAuthenticationFailed); + + await seedNode1.nodeConnectionManager.destroyConnection(node1Id, true); + await node1.nodeConnectionManager.destroyConnection(seedNodeId1, true); + await node1.nodeConnectionManager.createConnection( + [seedNodeId1], + localHost, + seedNode1.nodeConnectionManager.port, + ); + // Test1 network can talk + await node1.nodeManager.withConnF(seedNodeId1, undefined, async () => { + // Do nothing + }); + await seedNode1.nodeManager.withConnF(node1Id, undefined, async () => { + // Do nothing + }); + + await seedNode2.nodeConnectionManager.destroyConnection(node2Id, true); + await node2.nodeConnectionManager.destroyConnection(seedNodeId2, true); + await node2.nodeConnectionManager.createConnection( + [seedNodeId2], + localHost, + seedNode2.nodeConnectionManager.port, + ); + // Test2 network can talk + await node2.nodeManager.withConnF(seedNodeId2, undefined, async () => { + // Do nothing + }); + await seedNode2.nodeManager.withConnF(node2Id, undefined, async () => { + // Do nothing + }); + }); + test('a node can join a public network without permissions', async () => { + // Creating network credentials + const networkKeyPair = keysUtils.generateKeyPair(); + const networkNodeId = keysUtils.publicKeyToNodeId( + networkKeyPair.publicKey, + ); + const network = 'public.network.com'; + + // Setting up seed nodes claims + const seedNode = await createPeerNode(); + const [, seedNodeClaimNetworkAuthority] = + await seedNode.nodeManager.createClaimNetworkAuthority( + networkNodeId, + network, + false, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair.privateKey); + return claim; + }, + ); + await seedNode.nodeManager.createSelfSignedClaimNetworkAccess( + seedNodeClaimNetworkAuthority, + ); + const seedNodeId = seedNode.keyRing.getNodeId(); + + // Setting up the new node entering the network + const node1 = await createPeerNode(); + // Connect to the seed node + await node1.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + const node1Id = node1.keyRing.getNodeId(); + // We intentionally do not add permission for the joining node to the seed node + const [, peerClaimNetworkAccess] = await node1.nodeManager.claimNetwork( + seedNodeId, + network, + ); + claimNetworkAccessUtils.verifyClaimNetworkAccess( + networkNodeId, + node1Id, + network, + peerClaimNetworkAccess, + ); + + // We have now proved that a node can request access to the network from a node with network authority. + // Now We should be able to connect while authenticated to the seed node. + + // Re-initiate authentication + await seedNode.nodeConnectionManager.destroyConnection(node1Id, true); + await node1.nodeConnectionManager.destroyConnection(seedNodeId, true); + await node1.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + await node1.nodeManager.withConnF(seedNodeId, undefined, async () => { + // Do nothing + }); + }); + test('node can not join network without permission', async () => { + // Creating network credentials + const networkKeyPair = keysUtils.generateKeyPair(); + const networkNodeId = keysUtils.publicKeyToNodeId( + networkKeyPair.publicKey, + ); + const network = 'test.network.com'; + + // Setting up seed nodes claims + const seedNode = await createPeerNode(); + const [, seedNodeClaimNetworkAuthority] = + await seedNode.nodeManager.createClaimNetworkAuthority( + networkNodeId, + network, + true, + async (claim) => { + claim.signWithPrivateKey(networkKeyPair.privateKey); + return claim; + }, + ); + await seedNode.nodeManager.createSelfSignedClaimNetworkAccess( + seedNodeClaimNetworkAuthority, + ); + const seedNodeId = seedNode.keyRing.getNodeId(); + + // Setting up the new node entering the network + const node1 = await createPeerNode(); + // Connect to the seed node + await node1.nodeConnectionManager.createConnection( + [seedNodeId], + localHost, + seedNode.nodeConnectionManager.port, + ); + // We intentionally don't provide permission here + await expect( + node1.nodeManager.claimNetwork(seedNodeId, network), + ).rejects.toThrow(claimsErrors.ErrorEmptyStream); + }); + }); }); diff --git a/tests/nodes/agent/handlers/nodesClaimNetworkVerify.test.ts b/tests/nodes/agent/handlers/nodesClaimNetworkVerify.test.ts deleted file mode 100644 index f73415ca6..000000000 --- a/tests/nodes/agent/handlers/nodesClaimNetworkVerify.test.ts +++ /dev/null @@ -1,305 +0,0 @@ -import type NodeConnectionManager from '#nodes/NodeConnectionManager.js'; -import type { NodeId } from '#ids/index.js'; -import type { KeyPair } from '#keys/types.js'; -import type { SignedTokenEncoded } from '#tokens/types.js'; -import type { ClaimNetworkAuthority } from '#claims/payloads/claimNetworkAuthority.js'; -import type { ClaimNetworkAccess } from '#claims/payloads/claimNetworkAccess.js'; -import type { AgentClientManifest } from '#nodes/agent/callers/index.js'; -import fs from 'node:fs'; -import path from 'node:path'; -import os from 'node: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 * as tlsTestsUtils from '../../../utils/tls.js'; -import { nodesClaimNetworkVerify } from '#nodes/agent/callers/index.js'; -import { Token } from '#tokens/index.js'; -import Sigchain from '#sigchain/Sigchain.js'; -import KeyRing from '#keys/KeyRing.js'; -import NodeGraph from '#nodes/NodeGraph.js'; -import NodesClaimNetworkVerify from '#nodes/agent/handlers/NodesClaimNetworkVerify.js'; -import ACL from '#acl/ACL.js'; -import NodeManager from '#nodes/NodeManager.js'; -import GestaltGraph from '#gestalts/GestaltGraph.js'; -import TaskManager from '#tasks/TaskManager.js'; -import * as keysUtils from '#keys/utils/index.js'; -import * as claimsUtils from '#claims/utils.js'; -import * as nodesUtils from '#nodes/utils.js'; -import * as networkUtils from '#network/utils.js'; - -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 }); - }); -});