diff --git a/.gitignore b/.gitignore index 78a1b31bb..c827a4749 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ dist # editor .vscode/ .idea/ +.prettierrc diff --git a/src/client/callers/authSignToken.ts b/src/client/callers/authSignToken.ts new file mode 100644 index 000000000..34861e645 --- /dev/null +++ b/src/client/callers/authSignToken.ts @@ -0,0 +1,12 @@ +import type { HandlerTypes } from '@matrixai/rpc'; +import type AuthSignToken from '../handlers/AuthSignToken.js'; +import { UnaryCaller } from '@matrixai/rpc'; + +type CallerTypes = HandlerTypes; + +const authSignToken = new UnaryCaller< + CallerTypes['input'], + CallerTypes['output'] +>(); + +export default authSignToken; diff --git a/src/client/callers/index.ts b/src/client/callers/index.ts index 560b3a4eb..d3aa78c8e 100644 --- a/src/client/callers/index.ts +++ b/src/client/callers/index.ts @@ -4,6 +4,7 @@ import agentStop from './agentStop.js'; import agentUnlock from './agentUnlock.js'; import auditEventsGet from './auditEventsGet.js'; import auditMetricGet from './auditMetricGet.js'; +import authSignToken from './authSignToken.js'; import gestaltsActionsGetByIdentity from './gestaltsActionsGetByIdentity.js'; import gestaltsActionsGetByNode from './gestaltsActionsGetByNode.js'; import gestaltsActionsSetByIdentity from './gestaltsActionsSetByIdentity.js'; @@ -85,6 +86,7 @@ const clientManifest = { agentUnlock, auditEventsGet, auditMetricGet, + authSignToken, gestaltsActionsGetByIdentity, gestaltsActionsGetByNode, gestaltsActionsSetByIdentity, @@ -165,6 +167,7 @@ export { agentStop, agentUnlock, auditEventsGet, + authSignToken, gestaltsActionsGetByIdentity, gestaltsActionsGetByNode, gestaltsActionsSetByIdentity, diff --git a/src/client/errors.ts b/src/client/errors.ts index 24cee22b7..476b7a2e6 100644 --- a/src/client/errors.ts +++ b/src/client/errors.ts @@ -50,6 +50,13 @@ class ErrorClientVerificationFailed extends ErrorClientService { exitCode = sysexits.USAGE; } +class ErrorAuthentication extends ErrorPolykey {} + +class ErrorAuthenticationInvalidToken extends ErrorAuthentication { + static description = 'Token is invalid'; + exitCode = sysexits.PROTOCOL; +} + export { ErrorClient, ErrorClientAuthMissing, @@ -62,4 +69,6 @@ export { ErrorClientServiceNotRunning, ErrorClientServiceDestroyed, ErrorClientVerificationFailed, + ErrorAuthentication, + ErrorAuthenticationInvalidToken, }; diff --git a/src/client/handlers/AuthSignToken.ts b/src/client/handlers/AuthSignToken.ts new file mode 100644 index 000000000..04910b722 --- /dev/null +++ b/src/client/handlers/AuthSignToken.ts @@ -0,0 +1,59 @@ +import type { + ClientRPCRequestParams, + ClientRPCResponseResult, + IdentityRequestData, + IdentityResponseData, + TokenIdentityRequest, + TokenIdentityResponse, +} from '../types.js'; +import type KeyRing from '../../keys/KeyRing.js'; +import type { PublicKey } from '../../keys/types.js'; +import { UnaryHandler } from '@matrixai/rpc'; +import Token from '../../tokens/Token.js'; +import * as clientErrors from '../errors.js'; +import * as nodesUtils from '../../nodes/utils.js'; + +class AuthSignToken extends UnaryHandler< + { + keyRing: KeyRing; + }, + ClientRPCRequestParams, + ClientRPCResponseResult +> { + public handle = async ( + input: ClientRPCRequestParams, + ): Promise => { + const { keyRing }: { keyRing: KeyRing } = this.container; + + // Get and verify incoming node + const inputToken = { payload: input.payload, signatures: input.signatures }; + const incomingToken = Token.fromEncoded(inputToken); + if (!('publicKey' in incomingToken.payload)) { + throw new clientErrors.ErrorAuthenticationInvalidToken( + 'Input token does not contain public key', + ); + } + const incomingPublicKey = Buffer.from( + incomingToken.payload.publicKey, + 'base64url', + ) as PublicKey; + if (!incomingToken.verifyWithPublicKey(incomingPublicKey)) { + throw new clientErrors.ErrorAuthenticationInvalidToken( + 'Incoming token does not match its signature', + ); + } + + // Create the outgoing token with the incoming token integrated into the + // payload. + const outgoingTokenPayload: IdentityResponseData = { + requestToken: inputToken, + nodeId: nodesUtils.encodeNodeId(keyRing.getNodeId()), + }; + const outgoingToken = + Token.fromPayload(outgoingTokenPayload); + outgoingToken.signWithPrivateKey(keyRing.keyPair); + return outgoingToken.toEncoded(); + }; +} + +export default AuthSignToken; diff --git a/src/client/handlers/index.ts b/src/client/handlers/index.ts index 037eb0318..d846ed093 100644 --- a/src/client/handlers/index.ts +++ b/src/client/handlers/index.ts @@ -22,6 +22,7 @@ import AgentStop from './AgentStop.js'; import AgentUnlock from './AgentUnlock.js'; import AuditEventsGet from './AuditEventsGet.js'; import AuditMetricGet from './AuditMetricGet.js'; +import AuthSignToken from './AuthSignToken.js'; import GestaltsActionsGetByIdentity from './GestaltsActionsGetByIdentity.js'; import GestaltsActionsGetByNode from './GestaltsActionsGetByNode.js'; import GestaltsActionsSetByIdentity from './GestaltsActionsSetByIdentity.js'; @@ -122,6 +123,7 @@ const serverManifest = (container: { agentUnlock: new AgentUnlock(container), auditEventsGet: new AuditEventsGet(container), auditMetricGet: new AuditMetricGet(container), + authSignToken: new AuthSignToken(container), gestaltsActionsGetByIdentity: new GestaltsActionsGetByIdentity(container), gestaltsActionsGetByNode: new GestaltsActionsGetByNode(container), gestaltsActionsSetByIdentity: new GestaltsActionsSetByIdentity(container), @@ -208,6 +210,7 @@ export { AgentUnlock, AuditEventsGet, AuditMetricGet, + AuthSignToken, GestaltsActionsGetByIdentity, GestaltsActionsGetByNode, GestaltsActionsSetByIdentity, diff --git a/src/client/types.ts b/src/client/types.ts index 8e08254fe..e9900e89d 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -21,6 +21,7 @@ import type { } from '../keys/types.js'; import type { Notification } from '../notifications/types.js'; import type { ProviderToken } from '../identities/types.js'; +import type { TokenPayload, SignedTokenEncoded } from '../tokens/types.js'; import type { AuditMetricGetTypeOverride } from './callers/auditMetricGet.js'; import type { NodeContact, @@ -107,6 +108,22 @@ type TokenMessage = { token: ProviderToken; }; +// Return URL must be present on the token, otherwise token contents is decided +// by the client. +type IdentityRequestData = TokenPayload & { + returnURL: string; + publicKey: string; +}; + +type TokenIdentityRequest = SignedTokenEncoded; + +type IdentityResponseData = TokenPayload & { + requestToken: TokenIdentityRequest; + nodeId: NodeIdEncoded; +}; + +type TokenIdentityResponse = SignedTokenEncoded; + // Nodes messages type NodeIdMessage = { @@ -405,6 +422,10 @@ export type { ClaimIdMessage, ClaimNodeMessage, TokenMessage, + IdentityRequestData, + IdentityResponseData, + TokenIdentityRequest, + TokenIdentityResponse, NodeIdMessage, AddressMessage, NodeAddressMessage, diff --git a/src/network/utils.ts b/src/network/utils.ts index 253d2bcb5..c6e252a97 100644 --- a/src/network/utils.ts +++ b/src/network/utils.ts @@ -477,7 +477,10 @@ function fromError(error: any) { // serialising only the error type, message and its stack. const wrappedError = new errors.ErrorPolykeyUnexpected( `Unexpected error occurred: ${error.name}`, - { cause: error }, + { + cause: error, + data: { message: 'message' in error ? error.message : undefined }, + }, ); return wrappedError.toJSON(); }