Skip to content

Commit

Permalink
wip: token verification logic
Browse files Browse the repository at this point in the history
  • Loading branch information
amydevs committed Aug 22, 2024
1 parent 58d4f27 commit fad5e05
Show file tree
Hide file tree
Showing 13 changed files with 717 additions and 2 deletions.
80 changes: 80 additions & 0 deletions src/claims/payloads/claimNetworkAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Claim, SignedClaim } from '../types';
import type { NodeIdEncoded } from '../../ids/types';
import type { SignedTokenEncoded } from '../../tokens/types';
import * as ids from '../../ids';
import * as claimsUtils from '../utils';
import * as tokensUtils from '../../tokens/utils';
import * as validationErrors from '../../validation/errors';
import * as utils from '../../utils';

/**
* Asserts that a node is apart of a network
*/
interface ClaimNetworkAccess extends Claim {
typ: 'ClaimNetworkAccess';
iss: NodeIdEncoded;
sub: NodeIdEncoded;
signedClaimNetworkAuthorityEncoded: SignedTokenEncoded;
}

function assertClaimNetworkAccess(
claimNetworkAccess: unknown,
): asserts claimNetworkAccess is ClaimNetworkAccess {
if (!utils.isObject(claimNetworkAccess)) {
throw new validationErrors.ErrorParse('must be POJO');
}
if (claimNetworkAccess['typ'] !== 'ClaimNetworkAccess') {
throw new validationErrors.ErrorParse(
'`typ` property must be `ClaimNetworkAccess`',
);
}
if (
claimNetworkAccess['iss'] == null ||
ids.decodeNodeId(claimNetworkAccess['iss']) == null
) {
throw new validationErrors.ErrorParse(
'`iss` property must be an encoded node ID',
);
}
if (
claimNetworkAccess['sub'] == null ||
ids.decodeNodeId(claimNetworkAccess['sub']) == null
) {
throw new validationErrors.ErrorParse(
'`sub` property must be an encoded node ID',
);
}
if (
claimNetworkAccess['signedClaimNetworkAuthorityEncoded'] == null
) {
throw new validationErrors.ErrorParse(
'`signedClaimNetworkAuthorityEncoded` property must be an encoded signed token',
);
}
}

function parseClaimNetworkAccess(
claimNetworkAccessEncoded: unknown,
): ClaimNetworkAccess {
const claimNetworkNode = claimsUtils.parseClaim(claimNetworkAccessEncoded);
assertClaimNetworkAccess(claimNetworkNode);
return claimNetworkNode;
}

function parseSignedClaimNetworkAccess(
signedClaimNetworkAccessEncoded: unknown,
): SignedClaim<ClaimNetworkAccess> {
const signedClaim = tokensUtils.parseSignedToken(
signedClaimNetworkAccessEncoded,
);
assertClaimNetworkAccess(signedClaim.payload);
return signedClaim as SignedClaim<ClaimNetworkAccess>;
}

export {
assertClaimNetworkAccess,
parseClaimNetworkAccess,
parseSignedClaimNetworkAccess,
};

export type { ClaimNetworkAccess };
71 changes: 71 additions & 0 deletions src/claims/payloads/claimNetworkAuthority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Claim, SignedClaim } from '../types';
import type { NodeIdEncoded } from '../../ids/types';
import * as ids from '../../ids';
import * as claimsUtils from '../utils';
import * as tokensUtils from '../../tokens/utils';
import * as validationErrors from '../../validation/errors';
import * as utils from '../../utils';

/**
* Asserts that a node is apart of a network
*/
interface ClaimNetworkAuthority extends Claim {
typ: 'ClaimNetworkAuthority';
iss: NodeIdEncoded;
sub: NodeIdEncoded;
}

function assertClaimNetworkAuthority(
claimNetworkAuthority: unknown,
): asserts claimNetworkAuthority is ClaimNetworkAuthority {
if (!utils.isObject(claimNetworkAuthority)) {
throw new validationErrors.ErrorParse('must be POJO');
}
if (claimNetworkAuthority['typ'] !== 'ClaimNetworkAuthority') {
throw new validationErrors.ErrorParse(
'`typ` property must be `ClaimNetworkAuthority`',
);
}
if (
claimNetworkAuthority['iss'] == null ||
ids.decodeNodeId(claimNetworkAuthority['iss']) == null
) {
throw new validationErrors.ErrorParse(
'`iss` property must be an encoded node ID',
);
}
if (
claimNetworkAuthority['sub'] == null ||
ids.decodeNodeId(claimNetworkAuthority['sub']) == null
) {
throw new validationErrors.ErrorParse(
'`sub` property must be an encoded node ID',
);
}
}

function parseClaimNetworkAuthority(
claimNetworkNodeEncoded: unknown,
): ClaimNetworkAuthority {
const claimNetworkNode = claimsUtils.parseClaim(claimNetworkNodeEncoded);
assertClaimNetworkAuthority(claimNetworkNode);
return claimNetworkNode;
}

function parseSignedClaimNetworkAuthority(
signedClaimNetworkNodeEncoded: unknown,
): SignedClaim<ClaimNetworkAuthority> {
const signedClaim = tokensUtils.parseSignedToken(
signedClaimNetworkNodeEncoded,
);
assertClaimNetworkAuthority(signedClaim.payload);
return signedClaim as SignedClaim<ClaimNetworkAuthority>;
}

export {
assertClaimNetworkAuthority,
parseClaimNetworkAuthority,
parseSignedClaimNetworkAuthority,
};

export type { ClaimNetworkAuthority };
1 change: 1 addition & 0 deletions src/claims/payloads/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './claimLinkIdentity';
export * from './claimLinkNode';
export * from './claimNetworkNode';
1 change: 1 addition & 0 deletions src/nodes/NodeConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import * as nodesUtils from '../nodes/utils';
import { never } from '../utils';
import config from '../config';
import * as networkUtils from '../network/utils';
import * as keysUtils from '../keys/utils';

type AgentClientManifest = typeof agentClientManifest;

Expand Down
158 changes: 156 additions & 2 deletions src/nodes/NodeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { MDNS, events as mdnsEvents, utils as mdnsUtils } from '@matrixai/mdns';
import * as nodesUtils from './utils';
import * as nodesEvents from './events';
import * as nodesErrors from './errors';
import * as agentErrors from './agent/errors';
import NodeConnectionQueue from './NodeConnectionQueue';
import Token from '../tokens/Token';
import * as keysUtils from '../keys/utils';
Expand All @@ -57,6 +58,8 @@ import * as claimsErrors from '../claims/errors';
import * as utils from '../utils/utils';
import config from '../config';
import * as networkUtils from '../network/utils';
import { ClaimNetworkAccess, assertClaimNetworkAccess } from '../claims/payloads/claimNetworkAccess';
import { ClaimNetworkAuthority, assertClaimNetworkAuthority } from '@/claims/payloads/claimNetworkAuthority';

const abortEphemeralTaskReason = Symbol('abort ephemeral task reason');
const abortSingletonTaskReason = Symbol('abort singleton task reason');
Expand Down Expand Up @@ -247,8 +250,8 @@ class NodeManager {
);
const successfulConnections = connectionResults.filter(
(r) => r.status === 'fulfilled',
).length;
if (successfulConnections === 0) {
) as Array<PromiseFulfilledResult<NodeConnection>>;
if (successfulConnections.length === 0) {
const failedConnectionErrors = connectionResults
.filter((r) => r.status === 'rejected')
.map((v) => {
Expand All @@ -260,6 +263,43 @@ class NodeManager {
cause: new AggregateError(failedConnectionErrors),
},
);
} else {
// Wip: We should ideally take the fastest connection and use it here for node signing.
const conn = successfulConnections[0].value;
await this.sigchain.addClaim(
{
typ: 'ClaimNetworkNode',
iss: nodesUtils.encodeNodeId(conn.nodeId),
sub: nodesUtils.encodeNodeId(this.keyRing.getNodeId()),
},
undefined,
async (token) => {
const halfSignedClaim = token.toSigned();
const halfSignedClaimEncoded =
claimsUtils.generateSignedClaim(halfSignedClaim);
const receivedClaim =
await conn.rpcClient.methods.nodesClaimNetworkSign({
signedTokenEncoded: halfSignedClaimEncoded,
});
const signedClaim = claimsUtils.parseSignedClaim(
receivedClaim.signedTokenEncoded,
);
const fullySignedToken = Token.fromSigned(signedClaim);
// Check that the signatures are correct
const targetNodePublicKey = keysUtils.publicKeyFromNodeId(
conn.nodeId,
);
if (
!fullySignedToken.verifyWithPublicKey(
this.keyRing.keyPair.publicKey,
) ||
!fullySignedToken.verifyWithPublicKey(targetNodePublicKey)
) {
throw new claimsErrors.ErrorDoublySignedClaimVerificationFailed();
}
return fullySignedToken;
},
);
}
if (ctx.signal.aborted) return;

Expand Down Expand Up @@ -1478,6 +1518,118 @@ class NodeManager {
});
}

public async handleClaimNetwork(
requestingNodeId: NodeId,
input: AgentRPCRequestParams<AgentClaimMessage>,
tran?: DBTransaction,
): Promise<AgentRPCResponseResult<AgentClaimMessage>> {
if (tran == null) {
return this.db.withTransactionF((tran) =>
this.handleClaimNetwork(requestingNodeId, input, tran),
);
}
const signedClaim = claimsUtils.parseSignedClaim(input.signedTokenEncoded);
const token = Token.fromSigned(signedClaim);
// Verify if the token is signed
if (
!token.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(requestingNodeId),
)
) {
throw new claimsErrors.ErrorSinglySignedClaimVerificationFailed();
}
// If verified, add your own signature to the received claim
token.signWithPrivateKey(this.keyRing.keyPair);
// Return the signed claim
const doublySignedClaim = token.toSigned();
const halfSignedClaimEncoded =
claimsUtils.generateSignedClaim(doublySignedClaim);
return {
signedTokenEncoded: halfSignedClaimEncoded,
};
}

public async handleVerifyClaimNetwork(
requestingNodeId: NodeId,
input: AgentRPCRequestParams<AgentClaimMessage>,
tran?: DBTransaction,
): Promise<AgentRPCResponseResult<{ success: true }>> {
if (tran == null) {
return this.db.withTransactionF((tran) =>
this.handleVerifyClaimNetwork(requestingNodeId, input, tran),
);
}
const signedClaim = claimsUtils.parseSignedClaim(input.signedTokenEncoded);
const token = Token.fromSigned(signedClaim);
assertClaimNetworkAccess(token.payload);
// Verify if the token is signed
if (
!token.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(requestingNodeId),
) ||
!token.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(
nodesUtils.decodeNodeId(token.payload.iss)!,
),
)
) {
throw new claimsErrors.ErrorDoublySignedClaimVerificationFailed();
}
const authorityToken = Token.fromEncoded(token.payload.signedClaimNetworkAuthorityEncoded);
// Verify if the token is signed
if (
token.payload.iss !== authorityToken.payload.sub ||
!authorityToken.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(
nodesUtils.decodeNodeId(authorityToken.payload.sub)!,
),
) ||
!authorityToken.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(
nodesUtils.decodeNodeId(authorityToken.payload.iss)!,
),
)
) {
throw new claimsErrors.ErrorDoublySignedClaimVerificationFailed();
}

let success = false;
for await (const [_, claim] of this.sigchain.getSignedClaims({})) {
try {
assertClaimNetworkAccess(claim.payload);
}
catch {
continue;
}
const tokenNetworkAuthority = Token.fromEncoded(claim.payload.signedClaimNetworkAuthorityEncoded);
try {
assertClaimNetworkAuthority(tokenNetworkAuthority.payload);
}
catch {
continue;
}
// No need to check if local claims are correctly signed by an Network Authority.
if (
authorityToken.verifyWithPublicKey(
keysUtils.publicKeyFromNodeId(
nodesUtils.decodeNodeId(claim.payload.iss)!,
)
)
) {
success = true;
break;
}
}

if (!success) {
throw new agentErrors.ErrorNodesClaimNetworkVerificationFailed();
}

return {
success: true,
}
}

/**
* Adds a node to the node graph. This assumes that you have already authenticated the node
* Updates the node if the node already exists
Expand Down Expand Up @@ -1535,6 +1687,8 @@ class NodeManager {
);
}

// need to await node connection verification, if fail, need to reject connection.

// When adding a node we need to handle 3 cases
// 1. The node already exists. We need to update it's last updated field
// 2. The node doesn't exist and bucket has room.
Expand Down
6 changes: 6 additions & 0 deletions src/nodes/agent/callers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import nodesClosestLocalNodesGet from './nodesClosestLocalNodesGet';
import nodesConnectionSignalFinal from './nodesConnectionSignalFinal';
import nodesConnectionSignalInitial from './nodesConnectionSignalInitial';
import nodesCrossSignClaim from './nodesCrossSignClaim';
import nodesClaimNetworkSign from './nodesClaimNetworkSign';
import nodesClaimNetworkVerify from './nodesClaimNetworkVerify';
import notificationsSend from './notificationsSend';
import vaultsGitInfoGet from './vaultsGitInfoGet';
import vaultsGitPackGet from './vaultsGitPackGet';
Expand All @@ -19,6 +21,8 @@ const manifestClient = {
nodesConnectionSignalFinal,
nodesConnectionSignalInitial,
nodesCrossSignClaim,
nodesClaimNetworkSign,
nodesClaimNetworkVerify,
notificationsSend,
vaultsGitInfoGet,
vaultsGitPackGet,
Expand All @@ -36,6 +40,8 @@ export {
nodesConnectionSignalFinal,
nodesConnectionSignalInitial,
nodesCrossSignClaim,
nodesClaimNetworkSign,
nodesClaimNetworkVerify,
notificationsSend,
vaultsGitInfoGet,
vaultsGitPackGet,
Expand Down
Loading

0 comments on commit fad5e05

Please sign in to comment.