Skip to content

Commit 6a2225e

Browse files
committed
feat: adding creation logic for ClaimNetworkAuthority and ClaimNetworkAccess along with verification and authentication logic using them
1 parent 171d473 commit 6a2225e

24 files changed

+1887
-485
lines changed

src/claims/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ class ErrorClaimsUndefinedClaimPayload<T> extends ErrorClaims<T> {
88
exitCode = sysexits.UNKNOWN;
99
}
1010

11+
class ErrorClaimsVerificationFailed<T> extends ErrorClaims<T> {
12+
static description = 'Failed to verify claim';
13+
exitCode = sysexits.SOFTWARE;
14+
}
15+
1116
/**
1217
* Exceptions arising in cross-signing process
1318
*/
@@ -59,6 +64,7 @@ class ErrorNodesClaimType<T> extends ErrorSchemaValidate<T> {
5964
export {
6065
ErrorClaims,
6166
ErrorClaimsUndefinedClaimPayload,
67+
ErrorClaimsVerificationFailed,
6268
ErrorEmptyStream,
6369
ErrorUndefinedSinglySignedClaim,
6470
ErrorUndefinedDoublySignedClaim,

src/claims/payloads/claimNetworkAccess.ts

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import type { Claim, SignedClaim } from '../types.js';
2-
import type { NodeIdEncoded } from '../../ids/types.js';
2+
import type { NodeId, NodeIdEncoded } from '../../ids/types.js';
33
import type { SignedTokenEncoded } from '../../tokens/types.js';
4+
import * as claimNetworkAuthorityUtils from './claimNetworkAuthority.js';
5+
import Token from '../../tokens/Token.js';
46
import * as tokensSchema from '../../tokens/schemas/index.js';
57
import * as ids from '../../ids/index.js';
68
import * as claimsUtils from '../utils.js';
9+
import * as claimsErrors from '../errors.js';
710
import * as tokensUtils from '../../tokens/utils.js';
811
import * as validationErrors from '../../validation/errors.js';
912
import * as utils from '../../utils/index.js';
13+
import * as nodesUtils from '../../nodes/utils.js';
14+
import * as keysUtils from '../../keys/utils/index.js';
1015

1116
/**
12-
* Asserts that a node is apart of a network
17+
* Asserts that a node is a part of a network
1318
*/
1419
interface ClaimNetworkAccess extends Claim {
1520
typ: 'ClaimNetworkAccess';
1621
iss: NodeIdEncoded;
1722
sub: NodeIdEncoded;
1823
network: string;
19-
signedClaimNetworkAuthorityEncoded?: SignedTokenEncoded;
24+
signedClaimNetworkAuthorityEncoded: SignedTokenEncoded;
25+
isPrivate: boolean;
2026
}
2127

2228
function assertClaimNetworkAccess(
@@ -64,6 +70,14 @@ function assertClaimNetworkAccess(
6470
'`signedClaimNetworkAuthorityEncoded` property must be an encoded signed token',
6571
);
6672
}
73+
if (
74+
claimNetworkAccess['isPrivate'] == null ||
75+
typeof claimNetworkAccess['isPrivate'] !== 'boolean'
76+
) {
77+
throw new validationErrors.ErrorParse(
78+
'`isPrivate` property must be a boolean',
79+
);
80+
}
6781
}
6882

6983
function parseClaimNetworkAccess(
@@ -84,10 +98,81 @@ function parseSignedClaimNetworkAccess(
8498
return signedClaim as SignedClaim<ClaimNetworkAccess>;
8599
}
86100

101+
function verifyClaimNetworkAccess(
102+
networkNodeId: NodeId,
103+
subjectNodeId: NodeId,
104+
network: string,
105+
tokenClaimNetworkAccess: Token<ClaimNetworkAccess>,
106+
): void {
107+
const signedClaim =
108+
claimNetworkAuthorityUtils.parseSignedClaimNetworkAuthority(
109+
tokenClaimNetworkAccess.payload.signedClaimNetworkAuthorityEncoded,
110+
);
111+
const claimNetworkAuthority = Token.fromSigned(signedClaim);
112+
const issuerNodeId = nodesUtils.decodeNodeId(
113+
tokenClaimNetworkAccess.payload.iss,
114+
);
115+
if (issuerNodeId == null) {
116+
throw new claimsErrors.ErrorClaimsVerificationFailed(
117+
'failed to decode issuer nodeId',
118+
);
119+
}
120+
claimNetworkAuthorityUtils.verifyClaimNetworkAuthority(
121+
networkNodeId,
122+
issuerNodeId,
123+
network,
124+
claimNetworkAuthority,
125+
);
126+
// For the access claim
127+
// 1. issuer is current node
128+
// 2. subject is target node
129+
// 3. is signed by both the target and issuer
130+
131+
// Issuer should be the subject of the ClaimNetworkAuthority and signed by it
132+
const claimNetworkAuthoritySub = claimNetworkAuthority.payload.sub;
133+
const nodeIdIss = tokenClaimNetworkAccess.payload.iss;
134+
if (nodeIdIss !== claimNetworkAuthoritySub) {
135+
throw new claimsErrors.ErrorClaimsVerificationFailed(
136+
'Issuer NodeIdEncoded does not match the expected network id',
137+
);
138+
}
139+
const networkPublicKey = keysUtils.publicKeyFromNodeId(issuerNodeId);
140+
if (!tokenClaimNetworkAccess.verifyWithPublicKey(networkPublicKey)) {
141+
throw new claimsErrors.ErrorClaimsVerificationFailed(
142+
'Token was not signed by the issuer node',
143+
);
144+
}
145+
146+
// Subject should be the target node and signed by it
147+
const targetNodeIdEncoded = nodesUtils.encodeNodeId(subjectNodeId);
148+
const nodeIdSub = tokenClaimNetworkAccess.payload.sub;
149+
if (nodeIdSub !== targetNodeIdEncoded) {
150+
throw new claimsErrors.ErrorClaimsVerificationFailed(
151+
'Subject NodeIdEncoded does not match the expected subject node',
152+
);
153+
}
154+
const targetPublicKey = keysUtils.publicKeyFromNodeId(subjectNodeId);
155+
156+
if (!tokenClaimNetworkAccess.verifyWithPublicKey(targetPublicKey)) {
157+
throw new claimsErrors.ErrorClaimsVerificationFailed(
158+
'Token was not signed by the subject node',
159+
);
160+
}
161+
162+
// Checking if the network name matches
163+
const networkName = tokenClaimNetworkAccess.payload.network;
164+
if (networkName !== network) {
165+
throw new claimsErrors.ErrorClaimsVerificationFailed(
166+
'Network name does not match the expected network',
167+
);
168+
}
169+
}
170+
87171
export {
88172
assertClaimNetworkAccess,
89173
parseClaimNetworkAccess,
90174
parseSignedClaimNetworkAccess,
175+
verifyClaimNetworkAccess,
91176
};
92177

93178
export type { ClaimNetworkAccess };

src/claims/payloads/claimNetworkAuthority.ts

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import type { Claim, SignedClaim } from '../types.js';
2-
import type { NodeIdEncoded } from '../../ids/types.js';
2+
import type { NodeId, NodeIdEncoded } from '../../ids/types.js';
3+
import type Token from '../../tokens/Token.js';
34
import * as ids from '../../ids/index.js';
45
import * as claimsUtils from '../utils.js';
5-
import * as tokensUtils from '../../tokens/utils.js';
6+
import * as claimsErrors from '../errors.js';
67
import * as validationErrors from '../../validation/errors.js';
78
import * as utils from '../../utils/index.js';
9+
import * as nodesUtils from '../../nodes/utils.js';
10+
import * as keysUtils from '../../keys/utils/index.js';
811

912
/**
10-
* Asserts that a node is apart of a network
13+
* Asserts that a node has the authority of a network.
14+
* The issuing nodeId has to be the root keypair for the whole network.
1115
*/
16+
1217
interface ClaimNetworkAuthority extends Claim {
1318
typ: 'ClaimNetworkAuthority';
1419
iss: NodeIdEncoded;
1520
sub: NodeIdEncoded;
21+
network: string;
22+
isPrivate: boolean;
1623
}
1724

1825
function assertClaimNetworkAuthority(
@@ -42,6 +49,22 @@ function assertClaimNetworkAuthority(
4249
'`sub` property must be an encoded node ID',
4350
);
4451
}
52+
if (
53+
claimNetworkAuthority['network'] == null ||
54+
typeof claimNetworkAuthority['network'] !== 'string'
55+
) {
56+
throw new validationErrors.ErrorParse(
57+
'`network` property must be a network name string',
58+
);
59+
}
60+
if (
61+
claimNetworkAuthority['isPrivate'] == null ||
62+
typeof claimNetworkAuthority['isPrivate'] !== 'boolean'
63+
) {
64+
throw new validationErrors.ErrorParse(
65+
'`isPrivate` property must be a boolean',
66+
);
67+
}
4568
}
4669

4770
function parseClaimNetworkAuthority(
@@ -55,17 +78,62 @@ function parseClaimNetworkAuthority(
5578
function parseSignedClaimNetworkAuthority(
5679
signedClaimNetworkNodeEncoded: unknown,
5780
): SignedClaim<ClaimNetworkAuthority> {
58-
const signedClaim = tokensUtils.parseSignedToken(
81+
const signedClaim = claimsUtils.parseSignedClaim(
5982
signedClaimNetworkNodeEncoded,
6083
);
6184
assertClaimNetworkAuthority(signedClaim.payload);
6285
return signedClaim as SignedClaim<ClaimNetworkAuthority>;
6386
}
6487

88+
function verifyClaimNetworkAuthority(
89+
networkNodeId: NodeId,
90+
targetNodeId: NodeId,
91+
network: string,
92+
token: Token<ClaimNetworkAuthority>,
93+
): void {
94+
// Should be signed by the network authority as the issuer
95+
const nodeIdIss = token.payload.iss;
96+
const networkNodeIdEncoded = nodesUtils.encodeNodeId(networkNodeId);
97+
if (nodeIdIss !== networkNodeIdEncoded) {
98+
throw new claimsErrors.ErrorClaimsVerificationFailed(
99+
'Issuer NodeIdEncoded does not match the expected network id',
100+
);
101+
}
102+
const networkPublicKey = keysUtils.publicKeyFromNodeId(networkNodeId);
103+
if (!token.verifyWithPublicKey(networkPublicKey)) {
104+
throw new claimsErrors.ErrorClaimsVerificationFailed(
105+
'Token was not signed by the network authority',
106+
);
107+
}
108+
// Now we check if the claim applies to the target node
109+
const targetNodeIdEncoded = nodesUtils.encodeNodeId(targetNodeId);
110+
const nodeIdSub = token.payload.sub;
111+
if (nodeIdSub !== targetNodeIdEncoded) {
112+
throw new claimsErrors.ErrorClaimsVerificationFailed(
113+
'Subject NodeIdEncoded does not match the expected target Node',
114+
);
115+
}
116+
// Checking if the claim was signed by the subject
117+
const targetPublicKey = keysUtils.publicKeyFromNodeId(targetNodeId);
118+
if (!token.verifyWithPublicKey(targetPublicKey)) {
119+
throw new claimsErrors.ErrorClaimsVerificationFailed(
120+
'Token was not signed by the network authority',
121+
);
122+
}
123+
// Checking if the network name matches
124+
const networkName = token.payload.network;
125+
if (networkName !== network) {
126+
throw new claimsErrors.ErrorClaimsVerificationFailed(
127+
'Network name does not match the expected network',
128+
);
129+
}
130+
}
131+
65132
export {
66133
assertClaimNetworkAuthority,
67134
parseClaimNetworkAuthority,
68135
parseSignedClaimNetworkAuthority,
136+
verifyClaimNetworkAuthority,
69137
};
70138

71139
export type { ClaimNetworkAuthority };

src/gestalts/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
} from '../claims/payloads/index.js';
1515
import type { ProviderPaginationToken } from '../identities/types.js';
1616

17-
const gestaltActions = ['notify', 'scan', 'claim'] as const;
17+
const gestaltActions = ['notify', 'scan', 'claim', 'join'] as const;
1818

1919
type GestaltKey = Opaque<'GestaltKey', Buffer>;
2020

src/nodes/NodeConnectionManager.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ const activeForwardAuthenticateCancellationReason = Symbol(
111111
'active forward authenticate cancellation reason',
112112
);
113113

114-
const rpcMethodsWhitelist = ['nodesAuthenticateConnection'];
114+
const rpcMethodsWhitelist = [
115+
'nodesAuthenticateConnection',
116+
'nodesClaimNetworkSign',
117+
'nodesClaimNetworkAuthorityGet',
118+
];
115119

116120
/**
117121
* NodeConnectionManager is a server that manages all node connections.
@@ -711,7 +715,7 @@ class NodeConnectionManager<
711715
* @param targetNodeId Id of target node to communicate with
712716
* @returns ResourceAcquire Resource API for use in with contexts
713717
*/
714-
protected acquireConnectionInternal(
718+
public acquireConnectionInternal(
715719
targetNodeId: NodeId,
716720
): ResourceAcquire<NodeConnection<Manifest>> {
717721
if (this.keyRing.getNodeId().equals(targetNodeId)) {
@@ -1837,7 +1841,7 @@ class NodeConnectionManager<
18371841
}
18381842
try {
18391843
// Should resolve without issue if authentication succeeds.
1840-
await this.authenticateNetworkReverseCallback(message, ctx);
1844+
await this.authenticateNetworkReverseCallback(message, nodeId, ctx);
18411845
connectionsEntry.authenticatedReverse = AuthenticatingState.SUCCESS;
18421846
} catch (e) {
18431847
const err = new nodesErrors.ErrorNodeManagerAuthenticationFailedReverse(

0 commit comments

Comments
 (0)