UNPKG

@0xpolygonid/js-sdk

Version:
569 lines (520 loc) 18 kB
import { MediaType } from '../constants'; import { IProofService } from '../../proof/proof-service'; import { PROTOCOL_MESSAGE_TYPE } from '../constants'; import { StateVerificationOpts, AuthorizationRequestMessage, AuthorizationResponseMessage, BasicMessage, IPackageManager, ZeroKnowledgeProofRequest, JSONObject, Attachment } from '../types'; import { DID, getUnixTimestamp } from '@iden3/js-iden3-core'; import { ProvingMethodAlg, proving } from '@iden3/js-jwz'; import * as uuid from 'uuid'; import { ProofQuery } from '../../verifiable'; import { byteDecoder, byteEncoder } from '../../utils'; import { HandlerPackerParams, initDefaultPackerOptions, processZeroKnowledgeProofRequests, verifyExpiresTime } from './common'; import { CircuitId } from '../../circuits'; import { AbstractMessageHandler, BasicHandlerOptions, IProtocolMessageHandler, defaultProvingMethodAlg } from './message-handler'; import { acceptHasProvingMethodAlg, buildAcceptFromProvingMethodAlg, parseAcceptProfile } from '../utils'; /** * Options to pass to createAuthorizationRequest function * @public */ export type AuthorizationRequestCreateOptions = { accept?: string[]; scope?: ZeroKnowledgeProofRequest[]; expires_time?: Date; attachments?: Attachment[]; }; /** * createAuthorizationRequest is a function to create protocol authorization request * @param {string} reason - reason to request proof * @param {string} sender - sender did * @param {string} callbackUrl - callback that user should use to send response * @param {AuthorizationRequestCreateOptions} opts - authorization request options * @returns `Promise<AuthorizationRequestMessage>` */ export function createAuthorizationRequest( reason: string, sender: string, callbackUrl: string, opts?: AuthorizationRequestCreateOptions ): AuthorizationRequestMessage { return createAuthorizationRequestWithMessage(reason, '', sender, callbackUrl, opts); } /** * createAuthorizationRequestWithMessage is a function to create protocol authorization request with explicit message to sign * @param {string} reason - reason to request proof * @param {string} message - message to sign in the response * @param {string} sender - sender did * @param {string} callbackUrl - callback that user should use to send response * @param {AuthorizationRequestCreateOptions} opts - authorization request options * @returns `Promise<AuthorizationRequestMessage>` */ export function createAuthorizationRequestWithMessage( reason: string, message: string, sender: string, callbackUrl: string, opts?: AuthorizationRequestCreateOptions ): AuthorizationRequestMessage { const uuidv4 = uuid.v4(); const request: AuthorizationRequestMessage = { id: uuidv4, thid: uuidv4, from: sender, typ: MediaType.PlainMessage, type: PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, body: { accept: opts?.accept, reason: reason, message: message, callbackUrl: callbackUrl, scope: opts?.scope ?? [] }, created_time: getUnixTimestamp(new Date()), expires_time: opts?.expires_time ? getUnixTimestamp(opts.expires_time) : undefined, attachments: opts?.attachments }; return request; } /** * * Options to pass to auth response handler * * @public */ export type AuthResponseHandlerOptions = StateVerificationOpts & BasicHandlerOptions & { // acceptedProofGenerationDelay is the period of time in milliseconds that a generated proof remains valid. acceptedProofGenerationDelay?: number; }; /** * Interface that allows the processing of the authorization request in the raw format for given identifier * * @public * @interface IAuthHandler */ export interface IAuthHandler { /** * unpacks authorization request * @public * @param {Uint8Array} request - raw byte message * @returns `Promise<AuthorizationRequestMessage>` */ parseAuthorizationRequest(request: Uint8Array): Promise<AuthorizationRequestMessage>; /** * unpacks authorization request * @public * @param {did} did - sender DID * @param {Uint8Array} request - raw byte message * @returns `Promise<{ token: string; authRequest: AuthorizationRequestMessage; authResponse: AuthorizationResponseMessage; }>` */ handleAuthorizationRequest( did: DID, request: Uint8Array, opts?: AuthHandlerOptions ): Promise<{ token: string; authRequest: AuthorizationRequestMessage; authResponse: AuthorizationResponseMessage; }>; /** * handle authorization response * @public * @param {AuthorizationResponseMessage} response - auth response * @param {AuthorizationRequestMessage} request - auth request * @param {AuthResponseHandlerOptions} opts - options * @returns `Promise<{ request: AuthorizationRequestMessage; response: AuthorizationResponseMessage; }>` */ handleAuthorizationResponse( response: AuthorizationResponseMessage, request: AuthorizationRequestMessage, opts?: AuthResponseHandlerOptions ): Promise<{ request: AuthorizationRequestMessage; response: AuthorizationResponseMessage; }>; } type AuthReqOptions = { senderDid: DID; mediaType?: MediaType; bypassProofsCache?: boolean; }; type AuthRespOptions = { request: AuthorizationRequestMessage; acceptedStateTransitionDelay?: number; acceptedProofGenerationDelay?: number; }; export type AuthMessageHandlerOptions = BasicHandlerOptions & (AuthReqOptions | AuthRespOptions); /** * * Options to pass to auth handler * * @public * @interface AuthHandlerOptions */ export type AuthHandlerOptions = BasicHandlerOptions & { mediaType: MediaType; packerOptions?: HandlerPackerParams; preferredAuthProvingMethod?: ProvingMethodAlg; bypassProofsCache?: boolean; }; /** * * Allows to process AuthorizationRequest protocol message and produce JWZ response. * * @public * @class AuthHandler * @implements implements IAuthHandler interface */ export class AuthHandler extends AbstractMessageHandler implements IAuthHandler, IProtocolMessageHandler { private readonly _supportedCircuits = [ CircuitId.AtomicQueryV3, CircuitId.AtomicQuerySigV2, CircuitId.AtomicQueryMTPV2, CircuitId.LinkedMultiQuery10 ]; /** * Creates an instance of AuthHandler. * @param {IPackageManager} _packerMgr - package manager to unpack message envelope * @param {IProofService} _proofService - proof service to verify zk proofs * */ constructor( private readonly _packerMgr: IPackageManager, private readonly _proofService: IProofService ) { super(); } handle(message: BasicMessage, ctx: AuthMessageHandlerOptions): Promise<BasicMessage | null> { switch (message.type) { case PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE: return this.handleAuthRequest( message as AuthorizationRequestMessage, ctx as AuthReqOptions ); case PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE: return this.handleAuthResponse( message as AuthorizationResponseMessage, ctx as AuthRespOptions ); default: return super.handle(message, ctx); } } /** * @inheritdoc IAuthHandler#parseAuthorizationRequest */ async parseAuthorizationRequest(request: Uint8Array): Promise<AuthorizationRequestMessage> { const { unpackedMessage: message } = await this._packerMgr.unpack(request); const authRequest = message as unknown as AuthorizationRequestMessage; if (message.type !== PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE) { throw new Error('Invalid media type'); } authRequest.body.scope = authRequest.body.scope || []; return authRequest; } private async handleAuthRequest( authRequest: AuthorizationRequestMessage, ctx: AuthReqOptions ): Promise<AuthorizationResponseMessage> { if (authRequest.type !== PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE) { throw new Error('Invalid message type for authorization request'); } // override sender did if it's explicitly specified in the auth request const to = authRequest.to ? DID.parse(authRequest.to) : ctx.senderDid; const guid = uuid.v4(); if (!authRequest.from) { throw new Error('auth request should contain from field'); } const responseType = PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE; const mediaType = this.getSupportedMediaTypeByProfile(ctx, authRequest.body.accept); const from = DID.parse(authRequest.from); const responseScope = await processZeroKnowledgeProofRequests( to, authRequest?.body.scope, from, this._proofService, { mediaType, supportedCircuits: this._supportedCircuits, bypassProofsCache: ctx.bypassProofsCache } ); return { id: guid, typ: mediaType, type: responseType, thid: authRequest.thid ?? guid, body: { message: authRequest?.body?.message, scope: responseScope }, created_time: getUnixTimestamp(new Date()), from: to.string(), to: authRequest.from }; } /** * @inheritdoc IAuthHandler#handleAuthorizationRequest */ async handleAuthorizationRequest( did: DID, request: Uint8Array, opts?: AuthHandlerOptions ): Promise<{ token: string; authRequest: AuthorizationRequestMessage; authResponse: AuthorizationResponseMessage; }> { const authRequest = await this.parseAuthorizationRequest(request); if (!opts?.allowExpiredMessages) { verifyExpiresTime(authRequest); } if (!opts) { opts = { mediaType: MediaType.ZKPMessage }; } const authResponse = await this.handleAuthRequest(authRequest, { senderDid: did, mediaType: opts.mediaType, bypassProofsCache: opts.bypassProofsCache }); const msgBytes = byteEncoder.encode(JSON.stringify(authResponse)); const packerOpts = initDefaultPackerOptions(opts.mediaType, opts.packerOptions, { provingMethodAlg: this.getDefaultProvingMethodAlg( opts.preferredAuthProvingMethod, authRequest.body.accept ), senderDID: did }); const token = byteDecoder.decode( await this._packerMgr.pack(opts.mediaType, msgBytes, packerOpts) ); return { authRequest, authResponse, token }; } private async handleAuthResponse( response: AuthorizationResponseMessage, ctx: AuthRespOptions ): Promise<BasicMessage | null> { const request = ctx.request; if (response.type !== PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE) { throw new Error('Invalid message type for authorization response'); } if ((request.body.message ?? '') !== (response.body.message ?? '')) { throw new Error('message for signing from request is not presented in response'); } if (request.from !== response.to) { throw new Error( `sender of the request is not a target of response - expected ${request.from}, given ${response.to}` ); } this.verifyAuthRequest(request); const requestScope = request.body.scope || []; const responseScope = response.body.scope || []; if (!response.from) { throw new Error(`proof response doesn't contain from field`); } const groupIdToLinkIdMap = new Map<number, { linkID: string; requestId: number | string }[]>(); // group requests by query group id for (const proofRequest of requestScope) { const groupId = proofRequest.query.groupId as number; const proofResp = responseScope.find( (resp) => resp.id.toString() === proofRequest.id.toString() ); if (!proofResp) { if (proofRequest.optional) { continue; } throw new Error(`proof is not given for requestId ${proofRequest.id}`); } const circuitId = proofResp.circuitId; if (circuitId !== proofRequest.circuitId) { throw new Error( `proof is not given for requested circuit expected: ${proofRequest.circuitId}, given ${circuitId}` ); } const params: JSONObject = proofRequest.params ?? {}; params.verifierDid = DID.parse(request.from); const opts = [ctx.acceptedProofGenerationDelay, ctx.acceptedStateTransitionDelay].some( (delay) => delay !== undefined ) ? { acceptedProofGenerationDelay: ctx.acceptedProofGenerationDelay, acceptedStateTransitionDelay: ctx.acceptedStateTransitionDelay } : undefined; const { linkID } = await this._proofService.verifyZKPResponse(proofResp, { query: proofRequest.query as unknown as ProofQuery, sender: response.from, params, opts }); // write linkId to the proof response // const pubSig = pubSignals as unknown as { linkID?: number }; if (linkID && groupId) { groupIdToLinkIdMap.set(groupId, [ ...(groupIdToLinkIdMap.get(groupId) ?? []), { linkID: linkID.toString(), requestId: proofResp.id } ]); } } // verify grouping links for (const [groupId, metas] of groupIdToLinkIdMap.entries()) { // check that all linkIds are the same if (metas.some((meta) => meta.linkID !== metas[0].linkID)) { throw new Error( `Link id validation failed for group ${groupId}, request linkID to requestIds info: ${JSON.stringify( metas )}` ); } } return response; } /** * @inheritdoc IAuthHandler#handleAuthorizationResponse */ async handleAuthorizationResponse( response: AuthorizationResponseMessage, request: AuthorizationRequestMessage, opts?: AuthResponseHandlerOptions | undefined ): Promise<{ request: AuthorizationRequestMessage; response: AuthorizationResponseMessage; }> { if (!opts?.allowExpiredMessages) { verifyExpiresTime(response); } const authResp = (await this.handleAuthResponse(response, { request, acceptedStateTransitionDelay: opts?.acceptedStateTransitionDelay, acceptedProofGenerationDelay: opts?.acceptedProofGenerationDelay })) as AuthorizationResponseMessage; return { request, response: authResp }; } private verifyAuthRequest(request: AuthorizationRequestMessage) { const groupIdValidationMap: { [k: string]: ZeroKnowledgeProofRequest[] } = {}; const requestScope = request.body.scope || []; for (const proofRequest of requestScope) { const groupId = proofRequest.query.groupId as number; if (groupId) { const existingRequests = groupIdValidationMap[groupId] ?? []; //validate that all requests in the group have the same schema, issuer and circuit for (const existingRequest of existingRequests) { if (existingRequest.query.type !== proofRequest.query.type) { throw new Error(`all requests in the group should have the same type`); } if (existingRequest.query.context !== proofRequest.query.context) { throw new Error(`all requests in the group should have the same context`); } const allowedIssuers = proofRequest.query.allowedIssuers as string[]; const existingRequestAllowedIssuers = existingRequest.query.allowedIssuers as string[]; if ( !( allowedIssuers.includes('*') || allowedIssuers.every((issuer) => existingRequestAllowedIssuers.includes(issuer)) ) ) { throw new Error(`all requests in the group should have the same issuer`); } } groupIdValidationMap[groupId] = [...(groupIdValidationMap[groupId] ?? []), proofRequest]; } } } private getSupportedMediaTypeByProfile( ctx: AuthReqOptions, profile?: string[] | undefined ): MediaType { let mediaType: MediaType; if (!profile?.length) { return ctx.mediaType || MediaType.ZKPMessage; } const supportedMediaTypes: MediaType[] = []; for (const acceptProfile of profile) { const { env } = parseAcceptProfile(acceptProfile); if (this._packerMgr.isProfileSupported(env, acceptProfile)) { supportedMediaTypes.push(env); } } if (!supportedMediaTypes.length) { throw new Error('no packer with profile which meets `accept` header requirements'); } mediaType = supportedMediaTypes.includes(MediaType.ZKPMessage) ? MediaType.ZKPMessage : supportedMediaTypes[0]; if (ctx.mediaType && supportedMediaTypes.includes(ctx.mediaType)) { mediaType = ctx.mediaType; } return mediaType; } private getDefaultProvingMethodAlg( preferredAuthProvingMethod?: ProvingMethodAlg, accept?: string[] ): ProvingMethodAlg { if ( preferredAuthProvingMethod && this._packerMgr.isProfileSupported( MediaType.ZKPMessage, buildAcceptFromProvingMethodAlg(preferredAuthProvingMethod) ) && (!accept?.length || acceptHasProvingMethodAlg(accept, preferredAuthProvingMethod)) ) { return preferredAuthProvingMethod; } if (accept?.length) { const authV3_8_32 = proving.provingMethodGroth16AuthV3_8_32Instance.methodAlg; if ( acceptHasProvingMethodAlg(accept, authV3_8_32) && this._packerMgr.isProfileSupported( MediaType.ZKPMessage, buildAcceptFromProvingMethodAlg(authV3_8_32) ) ) { return authV3_8_32; } const authV3 = proving.provingMethodGroth16AuthV3Instance.methodAlg; if ( acceptHasProvingMethodAlg(accept, authV3) && this._packerMgr.isProfileSupported( MediaType.ZKPMessage, buildAcceptFromProvingMethodAlg(authV3) ) ) { return authV3; } } return defaultProvingMethodAlg; } }