Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/claims/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ class ErrorClaimsUndefinedClaimPayload<T> extends ErrorClaims<T> {
exitCode = sysexits.UNKNOWN;
}

class ErrorClaimsVerificationFailed<T> extends ErrorClaims<T> {
static description = 'Failed to verify claim';
exitCode = sysexits.SOFTWARE;
}

/**
* Exceptions arising in cross-signing process
*/
Expand Down Expand Up @@ -59,6 +64,7 @@ class ErrorNodesClaimType<T> extends ErrorSchemaValidate<T> {
export {
ErrorClaims,
ErrorClaimsUndefinedClaimPayload,
ErrorClaimsVerificationFailed,
ErrorEmptyStream,
ErrorUndefinedSinglySignedClaim,
ErrorUndefinedDoublySignedClaim,
Expand Down
91 changes: 88 additions & 3 deletions src/claims/payloads/claimNetworkAccess.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -84,10 +98,81 @@ function parseSignedClaimNetworkAccess(
return signedClaim as SignedClaim<ClaimNetworkAccess>;
}

function verifyClaimNetworkAccess(
networkNodeId: NodeId,
subjectNodeId: NodeId,
network: string,
tokenClaimNetworkAccess: Token<ClaimNetworkAccess>,
): 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 };
76 changes: 72 additions & 4 deletions src/claims/payloads/claimNetworkAuthority.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -55,17 +78,62 @@ function parseClaimNetworkAuthority(
function parseSignedClaimNetworkAuthority(
signedClaimNetworkNodeEncoded: unknown,
): SignedClaim<ClaimNetworkAuthority> {
const signedClaim = tokensUtils.parseSignedToken(
const signedClaim = claimsUtils.parseSignedClaim(
signedClaimNetworkNodeEncoded,
);
assertClaimNetworkAuthority(signedClaim.payload);
return signedClaim as SignedClaim<ClaimNetworkAuthority>;
}

function verifyClaimNetworkAuthority(
networkNodeId: NodeId,
targetNodeId: NodeId,
network: string,
token: Token<ClaimNetworkAuthority>,
): 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 };
2 changes: 1 addition & 1 deletion src/gestalts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;

Expand Down
10 changes: 7 additions & 3 deletions src/nodes/NodeConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<NodeConnection<Manifest>> {
if (this.keyRing.getNodeId().equals(targetNodeId)) {
Expand Down Expand Up @@ -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(
Expand Down
Loading