diff --git a/src/iden3comm/handlers/auth.ts b/src/iden3comm/handlers/auth.ts index a4092f9c..68c916cc 100644 --- a/src/iden3comm/handlers/auth.ts +++ b/src/iden3comm/handlers/auth.ts @@ -5,32 +5,21 @@ import { PROTOCOL_MESSAGE_TYPE } from '../constants'; import { AuthorizationRequestMessage, - AuthorizationRequestMessageBody, AuthorizationResponseMessage, IPackageManager, - PackerParams, + JWSPackerParams, ZeroKnowledgeProofRequest, ZeroKnowledgeProofResponse } from '../types'; import { DID } from '@iden3/js-iden3-core'; -import { proving } from '@iden3/js-jwz'; +import { ProvingMethodAlg, proving } from '@iden3/js-jwz'; import * as uuid from 'uuid'; import { ICredentialWallet } from '../../credentials'; -import { W3CCredential } from '../../verifiable'; +import { ProofQuery, W3CCredential } from '../../verifiable'; import { byteDecoder, byteEncoder } from '../../utils'; +import { IIdentityWallet, generateProfileDID } from '../../identity'; -/** - * ZKP request and credential that satisfies the zkp query conditions - * - * @export - * @interface ZKPRequestWithCredential - */ -export interface ZKPRequestWithCredential { - req: ZeroKnowledgeProofRequest; - credential: W3CCredential; - credentialSubjectProfileNonce: number; -} /** * Interface that allows the processing of the authorization request in the raw format for given identifier * @@ -39,29 +28,6 @@ export interface ZKPRequestWithCredential { * @interface IAuthHandler */ export interface IAuthHandler { - /** - * Handle authorization request protocol message - * - * @param {DID} id - identifier that will handle request - * @param {Uint8Array} request - request payload - * @returns `Promise<{ - * token: string; - * authRequest: AuthorizationRequestMessage; - * authResponse: AuthorizationResponseMessage; - * }>` - */ - handleAuthorizationRequestForGenesisDID(options: { - did: DID; - request: Uint8Array; - packer: { - mediaType: MediaType; - } & PackerParams; - }): Promise<{ - token: string; - authRequest: AuthorizationRequestMessage; - authResponse: AuthorizationResponseMessage; - }>; - /** * unpacks authorization request * @export @@ -72,30 +38,39 @@ export interface IAuthHandler { parseAuthorizationRequest(request: Uint8Array): Promise; /** - * Generates zero-knowledge proofs for given requests and credentials + * unpacks authorization request * @export * @beta - * @param {DID} userGenesisDID - user genesis identifier for which user holds key pair. - * @param {number} authProfileNonce - profile nonce that will be used for authorization. - * @param {AuthorizationRequestMessage} authRequest - authorization request, protocol message. - * @param {ZKPRequestWithCredential[]} zkpRequestsWithCreds - zero knowledge proof request with chosen credential to use. + * @param {did} did - sender DID + * @param {Uint8Array} request - raw byte message * @returns `Promise<{ - * token: string; - * authRequest: AuthorizationRequestMessage; - * authResponse: AuthorizationResponseMessage; - * }>}` + token: string; + authRequest: AuthorizationRequestMessage; + authResponse: AuthorizationResponseMessage; + }>` */ - generateAuthorizationResponse( - userGenesisDID: DID, - authProfileNonce: number, - authRequest: AuthorizationRequestMessage, - zkpRequestsWithCreds: ZKPRequestWithCredential[] + handleAuthorizationRequest( + did: DID, + request: Uint8Array, + opts?: AuthHandlerOptions ): Promise<{ token: string; authRequest: AuthorizationRequestMessage; authResponse: AuthorizationResponseMessage; }>; } + +interface AuthHandlerOptions { + mediaType: MediaType; + packerOptions: + | { + profileNonce: number; + provingMethodAlg: ProvingMethodAlg; + } + | JWSPackerParams; + credential?: W3CCredential; +} + /** * * Allows to process AuthorizationRequest protocol message and produce JWZ response. @@ -112,170 +87,161 @@ export class AuthHandler implements IAuthHandler { * @param {IPackageManager} _packerMgr - package manager to unpack message envelope * @param {IProofService} _proofService - proof service to verify zk proofs * @param {ICredentialWallet} _credentialWallet - wallet to search credentials + * @param {IIdentityWallet} _identityWallet - wallet to search profiles and identities + * * */ constructor( private readonly _packerMgr: IPackageManager, private readonly _proofService: IProofService, - private readonly _credentialWallet: ICredentialWallet + private readonly _credentialWallet: ICredentialWallet, + private readonly _identityWallet: IIdentityWallet ) {} /** - * Handles only messages with authorization/v1.0/request type - * Generates all requested proofs and wraps authorization response message to JWZ token - * works when profiles are not supported - * @param {DID} did - an identity that will process the request - * @param {Uint8Array} request - raw request - * @returns `Promise<{token: string; authRequest: AuthorizationRequestMessage; authResponse: AuthorizationResponseMessage;}>` JWZ token, parsed request and response + * unpacks authorization request + * @export + * @beta + * @param {Uint8Array} request - raw byte message + * @returns `Promise` */ - async handleAuthorizationRequestForGenesisDID(options: { - did: DID; - request: Uint8Array; - packer: { - mediaType: MediaType; - } & PackerParams; - }): Promise<{ + async parseAuthorizationRequest(request: Uint8Array): Promise { + 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'); + } + return authRequest; + } + + async handleAuthorizationRequest( + did: DID, + request: Uint8Array, + opts?: AuthHandlerOptions + ): Promise<{ token: string; authRequest: AuthorizationRequestMessage; authResponse: AuthorizationResponseMessage; }> { - const { - did, - request, - packer: { mediaType, ...packerParams } - } = options; + const authRequest = await this.parseAuthorizationRequest(request); - 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'); + if (authRequest.type !== PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE) { + throw new Error('Invalid message type for authorization request'); + } + + if (!opts) { + const zkpPackerOpts = { + profileNonce: 0, + provingMethodAlg: proving.provingMethodGroth16AuthV2Instance.methodAlg, + alg: '' + }; + + opts = { + packerOptions: zkpPackerOpts, + mediaType: MediaType.ZKPMessage + }; } - const authRequestBody = message.body as unknown as AuthorizationRequestMessageBody; const guid = uuid.v4(); + const senderDID = + opts!.mediaType === MediaType.SignedMessage + ? did.string() + : generateProfileDID(did, opts!.packerOptions!.profileNonce).string(); const authResponse: AuthorizationResponseMessage = { id: guid, - typ: mediaType, + typ: opts!.mediaType, type: PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE, - thid: message.thid ?? guid, + thid: authRequest.thid ?? guid, body: { - did_doc: undefined, // slipped for now, todo: get did doc for id - message: authRequestBody.message, + message: authRequest?.body?.message, scope: [] }, - from: options.did.string(), - to: message.from + from: senderDID, + to: authRequest.from }; - for (const proofReq of authRequestBody.scope) { + for (const proofReq of authRequest.body!.scope) { const zkpReq: ZeroKnowledgeProofRequest = { id: proofReq.id, circuitId: proofReq.circuitId as CircuitId, query: proofReq.query }; - const creds = await this._credentialWallet.findByQuery(proofReq.query); + const query = proofReq.query as unknown as ProofQuery; - const credsForGenesisDID = await this._credentialWallet.filterByCredentialSubject(creds, did); - if (credsForGenesisDID.length == 0) { - throw new Error(`no credential were issued on the given id ${did.string()}`); + if (opts!.credential) { + proofReq.query.claimId = opts!.credential.id; } - const { cred } = await this._credentialWallet.findNonRevokedCredential(credsForGenesisDID); + + const { credential, nonce } = await this.findCredentialForDID(did, query); const zkpRes: ZeroKnowledgeProofResponse = await this._proofService.generateProof( zkpReq, did, - cred + credential, + { + credentialSubjectProfileNonce: nonce, + skipRevocation: query.skipClaimRevocationCheck ?? false, + authProfileNonce: opts!.packerOptions!.profileNonce + } ); authResponse.body.scope.push(zkpRes); } + const msgBytes = byteEncoder.encode(JSON.stringify(authResponse)); const token = byteDecoder.decode( - await this._packerMgr.pack(mediaType, msgBytes, { + await this._packerMgr.pack(opts!.mediaType, msgBytes, { senderDID: did, - ...packerParams + ...opts!.packerOptions }) ); return { authRequest, authResponse, token }; } - - /** - * unpacks authorization request - * @export - * @beta - * @param {Uint8Array} request - raw byte message - * @returns `Promise` - */ - async parseAuthorizationRequest(request: Uint8Array): Promise { - 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'); + async findCredentialForDID( + did: DID, + query: ProofQuery + ): Promise<{ credential: W3CCredential; nonce: number }> { + const creds = await this._credentialWallet.findByQuery(query); + if (!creds.length) { + throw new Error(`no credential satisfied query`); } - return authRequest; - } - /** - * Generates zero-knowledge proofs for given requests and credentials - * @export - * @beta - * @param {DID} userGenesisDID - user genesis identifier for which user holds key pair. - * @param {number} authProfileNonce - profile nonce that will be used for authorization. - * @param {AuthorizationRequestMessage} authRequest - authorization request, protocol message. - * @param {ZKPRequestWithCredential[]} zkpRequestsWithCreds - zero knowledge proof request with chosen credential to use. - * @returns `Promise<{ - * token: string; - * authRequest: AuthorizationRequestMessage; - * authResponse: AuthorizationResponseMessage; - * }>}` - */ - async generateAuthorizationResponse( - userGenesisDID: DID, - authProfileNonce: number, - authRequest: AuthorizationRequestMessage, - zkpRequestsWithCreds?: ZKPRequestWithCredential[] - ): Promise<{ - token: string; - authRequest: AuthorizationRequestMessage; - authResponse: AuthorizationResponseMessage; - }> { - const guid = uuid.v4(); - const authResponse: AuthorizationResponseMessage = { - id: guid, - typ: MediaType.ZKPMessage, - type: PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE, - thid: authRequest.thid ?? guid, - body: { - message: authRequest?.body?.message, - scope: [] - }, - from: userGenesisDID.string(), - to: authRequest.from - }; + const profiles = await this._identityWallet.getProfilesByDID(did); - for (const r of zkpRequestsWithCreds || []) { - const zkpRes: ZeroKnowledgeProofResponse = await this._proofService.generateProof( - r.req, - userGenesisDID, - r.credential, - { - authProfileNonce: authProfileNonce, - credentialSubjectProfileNonce: r.credentialSubjectProfileNonce, - skipRevocation: false - } + const ownedCreds = creds.filter((cred) => { + const credentialSubjectId = cred.credentialSubject['id'] as string; // credential subject + return ( + credentialSubjectId == did.string() || + profiles.some((p) => { + return p.id === credentialSubjectId; + }) ); + }); - authResponse.body.scope.push(zkpRes); + if (!ownedCreds.length) { + throw new Error(`no credentials belong to did ot its profiles`); } - const msgBytes = byteEncoder.encode(JSON.stringify(authResponse)); - const token = byteDecoder.decode( - await this._packerMgr.pack(MediaType.ZKPMessage, msgBytes, { - senderDID: userGenesisDID, - profileNonce: authProfileNonce, - provingMethodAlg: proving.provingMethodGroth16AuthV2Instance.methodAlg - }) - ); - return { authRequest, authResponse, token }; + + // For EQ / IN / NIN / LT / GT operations selective if credential satisfies query - we can get any. + // TODO: choose credential for selective credentials + const cred = query.skipClaimRevocationCheck + ? ownedCreds[0] + : (await this._credentialWallet.findNonRevokedCredential(ownedCreds)).cred; + + // get profile nonce that was used as a part of subject in the credential + + let subjectDID = cred.credentialSubject['id']; + if (!subjectDID) { + subjectDID = cred.issuer; // self credential + } + const credentialSubjectProfileNonce = + subjectDID === did.string() + ? 0 + : profiles.find((p) => { + return p.id === subjectDID; + })!.nonce; + + return { credential: cred, nonce: credentialSubjectProfileNonce }; } } diff --git a/src/iden3comm/handlers/fetch.ts b/src/iden3comm/handlers/fetch.ts index a39fb19b..97612d76 100644 --- a/src/iden3comm/handlers/fetch.ts +++ b/src/iden3comm/handlers/fetch.ts @@ -4,6 +4,7 @@ import { PROTOCOL_MESSAGE_TYPE } from '../constants'; import { CredentialsOfferMessage, IPackageManager, + JWSPackerParams, MessageFetchRequestMessage, PackerParams } from '../types'; @@ -12,6 +13,23 @@ import { DID } from '@iden3/js-iden3-core'; import * as uuid from 'uuid'; import { W3CCredential } from '../../verifiable'; import { byteDecoder, byteEncoder } from '../../utils'; +import { ProvingMethodAlg, proving } from '@iden3/js-jwz'; +import { ICredentialWallet } from '../../credentials'; +import { IIdentityWallet } from '../../identity'; + +interface FetchHandlerOptions { + mediaType: MediaType; + packerOptions: + | { + profileNonce: number; + provingMethodAlg: ProvingMethodAlg; + } + | JWSPackerParams; + did?: DID; + headers?: { + [key: string]: string; + }; +} /** * Interface that allows the processing of the credential offer in the raw format for given identifier @@ -22,26 +40,18 @@ import { byteDecoder, byteEncoder } from '../../utils'; */ export interface IFetchHandler { /** - * Handle credential offer request protocol message - * - *@param {({ - * did: DID; identifier that will handle offer - * offer: Uint8Array; offer - raw offer message - * profileNonce?: number; nonce of the did to which credential has been offered - * packerOpts: { - * mediaType: MediaType; - * } & PackerParams; packer options how to pack message - * })} options how to fetch credential - * @returns `Promise` + * unpacks authorization request + * @export + * @beta + * @param {Uint8Array} offer - raw byte message + * @param {FetchHandlerOptions} opts - FetchHandlerOptions + * @returns `Promise<{ + token: string; + authRequest: AuthorizationRequestMessage; + authResponse: AuthorizationResponseMessage; + }>` */ - handleCredentialOffer(options: { - did: DID; - offer: Uint8Array; - profileNonce?: number; - packer: { - mediaType: MediaType; - } & PackerParams; - }): Promise; + handleCredentialOffer(offer: Uint8Array, opts?: FetchHandlerOptions): Promise; } /** * @@ -57,39 +67,41 @@ export class FetchHandler implements IFetchHandler { /** * Creates an instance of AuthHandler. * @param {IPackageManager} _packerMgr - package manager to unpack message envelope + * @param {ICredentialWallet} _credentialWallet - wallet to search credentials + * @param {IIdentityWallet} _identityWallet - wallet to search profiles and identities */ - constructor(private readonly _packerMgr: IPackageManager) {} + constructor( + private readonly _packerMgr: IPackageManager, + private readonly _identityWallet: IIdentityWallet, + private readonly _credentialWallet: ICredentialWallet + ) {} /** * Handles only messages with credentials/1.0/offer type * - * @param {({ - * did: DID; identifier that will handle offer + * @param { * offer: Uint8Array; offer - raw offer message - * profileNonce?: number; nonce of the did to which credential has been offered - * packer: { - * mediaType: MediaType; - * } & PackerParams; packer options how to pack message - * })} options how to fetch credential + * opts + * }) options how to fetch credential * @returns `Promise` */ - async handleCredentialOffer(options: { - did: DID; - offer: Uint8Array; - profileNonce?: number; - packer: { - mediaType: MediaType; - } & PackerParams; - headers?: { - [key: string]: string; - }; - }): Promise { - // each credential info in the offer we need to fetch - const { - did, - offer, - packer: { mediaType, ...packerParams } - } = options; + async handleCredentialOffer( + offer: Uint8Array, + opts?: FetchHandlerOptions + ): Promise { + if (!opts) { + const zkpPackerOpts = { + profileNonce: 0, + provingMethodAlg: proving.provingMethodGroth16AuthV2Instance.methodAlg, + alg: '' + }; + + opts = { + packerOptions: zkpPackerOpts, + mediaType: MediaType.ZKPMessage + }; + } + const { unpackedMessage: message } = await this._packerMgr.unpack(offer); const offerMessage = message as unknown as CredentialsOfferMessage; if (message.type !== PROTOCOL_MESSAGE_TYPE.CREDENTIAL_OFFER_MESSAGE_TYPE) { @@ -103,19 +115,29 @@ export class FetchHandler implements IFetchHandler { const guid = uuid.v4(); const fetchRequest: MessageFetchRequestMessage = { id: guid, - typ: mediaType, + typ: opts.mediaType, type: PROTOCOL_MESSAGE_TYPE.CREDENTIAL_FETCH_REQUEST_MESSAGE_TYPE, thid: offerMessage.thid ?? guid, body: { id: credentialInfo?.id || '' }, - from: did.string(), + from: opts.did ? opts.did.string() : offerMessage.to, to: offerMessage.from }; const msgBytes = byteEncoder.encode(JSON.stringify(fetchRequest)); + + // check if offer is for profile we need to find its nonce + // if it opts did is set, then + opts.packerOptions.profileNonce = opts.did + ? 0 + : this._identityWallet.getProfileNonce(DID.parse(offerMessage.to!)); + const token = byteDecoder.decode( - await this._packerMgr.pack(mediaType, msgBytes, { senderDID: did, ...packerParams }) + await this._packerMgr.pack(opts.mediaType, msgBytes, { + senderDID: offerMessage.to, + ...opts.packerOptions + }) ); let message: { body: { credential: W3CCredential } }; try { @@ -143,6 +165,7 @@ export class FetchHandler implements IFetchHandler { } } + this._credentialWallet.saveAll(credentials); return credentials; } } diff --git a/src/iden3comm/packers/jws.ts b/src/iden3comm/packers/jws.ts index 965ee266..9f6fdcc8 100644 --- a/src/iden3comm/packers/jws.ts +++ b/src/iden3comm/packers/jws.ts @@ -1,10 +1,10 @@ -import { BasicMessage, IPacker, PackerParams } from '../types'; +import { BasicMessage, IPacker, JWSPackerParams } from '../types'; import { MediaType, SUPPORTED_PUBLIC_KEY_TYPES } from '../constants'; import { extractPublicKeyBytes, resolveVerificationMethods } from '../utils/did'; import { keyPath, KMS } from '../../kms/'; -import { Signer, verifyJWS } from 'did-jwt'; -import { DIDDocument, Resolvable, VerificationMethod, parse } from 'did-resolver'; +import { verifyJWS } from 'did-jwt'; +import { DIDDocument, Resolvable, parse } from 'did-resolver'; import { byteDecoder, byteEncoder, @@ -12,7 +12,6 @@ import { decodeBase64url, encodeBase64url } from '../../utils'; -export type SignerFn = (vm: VerificationMethod, data: Uint8Array) => Signer; /** * Packer that can pack message to JWZ token, @@ -38,15 +37,7 @@ export class JWSPacker implements IPacker { * @param {PackerParams} params - sender id and proving alg are required * @returns `Promise` */ - async pack( - payload: Uint8Array, - params: PackerParams & { - alg: string; - kid?: string; - didDocument?: DIDDocument; - signer?: SignerFn; - } - ): Promise { + async pack(payload: Uint8Array, params: JWSPackerParams): Promise { if (!params.alg) { throw new Error('Missing algorithm'); } diff --git a/src/iden3comm/packers/zkp.ts b/src/iden3comm/packers/zkp.ts index e3cde6cc..6a9754bd 100644 --- a/src/iden3comm/packers/zkp.ts +++ b/src/iden3comm/packers/zkp.ts @@ -124,7 +124,7 @@ export class ZKPPacker implements IPacker { return provingParams?.dataPreparer?.prepare( hash, params.senderDID, - params.profileNonce, + params.profileNonce!, circuitId as CircuitId ); } diff --git a/src/iden3comm/types/packer.ts b/src/iden3comm/types/packer.ts index 6fac60c0..8cc67ce7 100644 --- a/src/iden3comm/types/packer.ts +++ b/src/iden3comm/types/packer.ts @@ -3,7 +3,8 @@ import { DataPrepareHandlerFunc, VerificationHandlerFunc } from '../packers'; import { ProvingMethodAlg } from '@iden3/js-jwz'; import { CircuitId } from '../../circuits'; import { MediaType } from '../constants'; - +import { DIDDocument, VerificationMethod } from 'did-resolver'; +import { Signer } from 'did-jwt'; /** * Protocol message type */ @@ -46,6 +47,21 @@ export type ZKPPackerParams = PackerParams & { provingMethodAlg: ProvingMethodAlg; }; +/** + * SignerFn Is function to sign data with a verification method + */ +export type SignerFn = (vm: VerificationMethod, data: Uint8Array) => Signer; + +/** + * JWSPackerParams are parameters for JWS packer + */ +export type JWSPackerParams = PackerParams & { + alg: string; + kid?: string; + didDocument?: DIDDocument; + signer?: SignerFn; +}; + /** * parameters for plain packer */ diff --git a/src/identity/common.ts b/src/identity/common.ts index 0cbc9f9b..5992585b 100644 --- a/src/identity/common.ts +++ b/src/identity/common.ts @@ -1,4 +1,4 @@ -import { IdPosition, MerklizedRootPosition } from '@iden3/js-iden3-core'; +import { DID, Id, IdPosition, MerklizedRootPosition } from '@iden3/js-iden3-core'; import { SchemaMetadata } from '../schema-processor'; import { SubjectPosition } from '../verifiable'; @@ -40,3 +40,16 @@ export const defineMerklizedRootPosition = ( return MerklizedRootPosition.Index; }; + +/** + * Returns profile DID based on did and profile nonce + * + * @param {DID} [did] - did from which profile will be derived + * @param {number} [profileNonce] - profile nonce + * @returns {DID} + */ +export const generateProfileDID = (did: DID, profileNonce?: number): DID => { + const id = DID.idFromDID(did); + const profile = Id.profileId(id, BigInt(profileNonce ?? 0)); + return DID.parseFromId(profile); +}; diff --git a/src/identity/identity-wallet.ts b/src/identity/identity-wallet.ts index fa5cb5ef..5a52a37b 100644 --- a/src/identity/identity-wallet.ts +++ b/src/identity/identity-wallet.ts @@ -15,7 +15,7 @@ import { import { Hex, poseidon, PublicKey, Signature } from '@iden3/js-crypto'; import { hashElems, ZERO_HASH } from '@iden3/js-merkletree'; -import { subjectPositionIndex } from './common'; +import { generateProfileDID, subjectPositionIndex } from './common'; import * as uuid from 'uuid'; import { JSONSchema, Parser, CoreClaimOptions } from '../schema-processor'; import { IDataStorage } from '../storage/interfaces/data-storage'; @@ -39,6 +39,7 @@ import { TreeState } from '../circuits'; import { byteEncoder } from '../utils'; import { Options, Path, getDocumentLoader } from '@iden3/js-jsonld-merklization'; import { sha256js } from 'cross-sha256'; +import { Profile } from '../storage'; /** * DID creation options @@ -232,6 +233,24 @@ export interface IIdentityWallet { * @returns `{Promise}` */ getCoreClaimFromCredential(credential: W3CCredential): Promise; + + /** + * + * gets profile identity by genesis identifiers + * + * @param {string} did - genesis identifier from which profile has been derived + * @returns `{Promise}` + */ + getProfilesByDID(did: DID): Promise; + + /** + * + * gets profile nonce by it's id. if profile is genesis identifier - 0 is returned + * + * @param {string} profile - profile has been derived + * @returns `{Promise}` + */ + getProfileNonce(profile: DID): Promise; } /** @@ -392,10 +411,21 @@ export class IdentityWallet implements IIdentityWallet { credential }; } + /** {@inheritDoc IIdentityWallet.getProfileNonce} */ + async getProfileNonce(profile: DID): Promise { + // check if it is a genesis identity + + const identity = await this._storage.identity.getIdentity(profile.string()); + + if (identity) { + return 0; + } + return (await this._storage.identity.getProfileById(profile.string())).nonce; + } /** {@inheritDoc IIdentityWallet.createProfile} */ async createProfile(did: DID, nonce: number, verifier: string): Promise { - const id = DID.idFromDID(did); + const profileDID = generateProfileDID(did, nonce); const identityProfiles = await this._storage.identity.getProfilesByGenesisIdentifier( did.string() @@ -408,9 +438,6 @@ export class IdentityWallet implements IIdentityWallet { throw new Error('profile with given nonce or verifier already exists'); } - const profile = Id.profileId(id, BigInt(nonce)); - const profileDID = DID.parseFromId(profile); - await this._storage.identity.saveProfile({ id: profileDID.string(), nonce, @@ -420,6 +447,16 @@ export class IdentityWallet implements IIdentityWallet { return profileDID; } + /** + * + * gets profile identity by genesis identifiers + * + * @param {string} genesisIdentifier - genesis identifier from which profile has been derived + * @returns `{Promise}` + */ + async getProfilesByDID(did: DID): Promise { + return this._storage.identity.getProfilesByGenesisIdentifier(did.string()); + } /** {@inheritDoc IIdentityWallet.generateKey} */ async generateKey(keyType: KmsKeyType): Promise { const key = await this._kms.createKeyFromSeed(keyType, getRandomBytes(32)); diff --git a/src/proof/proof-service.ts b/src/proof/proof-service.ts index 67fd1fed..732c6bb4 100644 --- a/src/proof/proof-service.ts +++ b/src/proof/proof-service.ts @@ -278,12 +278,11 @@ export class ProofService implements IProofService { } private async getPreparedCredential(credential: W3CCredential): Promise { - const { cred: nonRevokedCred, revStatus } = - await this._credentialWallet.findNonRevokedCredential([credential]); + const revStatus = await this._credentialWallet.getRevocationStatusFromCredential(credential); - const credCoreClaim = await this._identityWallet.getCoreClaimFromCredential(nonRevokedCred); + const credCoreClaim = await this._identityWallet.getCoreClaimFromCredential(credential); - return { credential: nonRevokedCred, revStatus, credentialCoreClaim: credCoreClaim }; + return { credential: credential, revStatus, credentialCoreClaim: credCoreClaim }; } private async prepareAuthBJJCredential( diff --git a/src/verifiable/presentation.ts b/src/verifiable/presentation.ts index e20e4106..2d46e7b5 100644 --- a/src/verifiable/presentation.ts +++ b/src/verifiable/presentation.ts @@ -83,7 +83,7 @@ export const verifiablePresentationFromCred = async ( }> => { const mz = await w3cCred.merklize(opts); - const request = requestObj as { [key: string]: unknown }; + const request = requestObj as unknown as { [key: string]: unknown }; const contextType = stringByPath(request, 'type'); diff --git a/src/verifiable/proof.ts b/src/verifiable/proof.ts index 2035c8fa..8583b058 100644 --- a/src/verifiable/proof.ts +++ b/src/verifiable/proof.ts @@ -121,6 +121,7 @@ export interface ProofQuery { credentialSubjectId?: string; context?: string; type?: string; + skipClaimRevocationCheck?: boolean; } /** diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts index 61bf5727..2aea7228 100644 --- a/tests/handlers/auth.test.ts +++ b/tests/handlers/auth.test.ts @@ -8,7 +8,6 @@ import { CredentialStorage, IAuthHandler, IdentityWallet, - ZKPRequestWithCredential, byteEncoder } from '../../src'; import { BjjProvider, KMS, KmsKeyType } from '../../src/kms'; @@ -184,7 +183,7 @@ describe('auth', () => { proofService.generateAuthV2Inputs.bind(proofService), proofService.verifyState.bind(proofService) ); - authHandler = new AuthHandler(packageMgr, proofService, credWallet); + authHandler = new AuthHandler(packageMgr, proofService, credWallet, idWallet); }); it('request-response flow genesis', async () => { @@ -271,15 +270,7 @@ describe('auth', () => { console.log(JSON.stringify(issuerCred)); const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); - const authRes = await authHandler.handleAuthorizationRequestForGenesisDID({ - did: userDID, - request: msgBytes, - packer: { - mediaType: MediaType.ZKPMessage, - profileNonce: 0, - provingMethodAlg: proving.provingMethodGroth16AuthV2Instance.methodAlg.toString() - } - }); + const authRes = await authHandler.handleAuthorizationRequest(userDID, msgBytes); const tokenStr = authRes.token; console.log(tokenStr); @@ -288,7 +279,7 @@ describe('auth', () => { expect(token).to.be.a('object'); }); - it('request-response flow profiles', async () => { + it('request-response flow profiles explicit credential', async () => { const { did: userDID } = await idWallet.createIdentity({ method: DidMethod.Iden3, blockchain: Blockchain.Polygon, @@ -370,9 +361,9 @@ describe('auth', () => { const authR = await authHandler.parseAuthorizationRequest(msgBytes); - // let's find cred for each request. - const reqCreds: ZKPRequestWithCredential[] = []; + let cred: W3CCredential; + // let's find cred for each request. for (let index = 0; index < authR.body!.scope.length; index++) { const zkpReq = authR.body!.scope[index]; @@ -398,28 +389,113 @@ describe('auth', () => { }); // you can show user credential that can be used for request - const chosenCredByUser = credsThatBelongToGenesisIdOrItsProfiles[0]; - - // get profile nonce that was used as a part of subject in the credential - const credentialSubjectProfileNonce = - chosenCredByUser.credentialSubject['id'] === userDID.string() - ? 0 - : profiles.find((p) => { - return p.id === chosenCredByUser.credentialSubject['id']; - })!.nonce; - reqCreds.push({ req: zkpReq, credential: chosenCredByUser, credentialSubjectProfileNonce }); + cred = credsThatBelongToGenesisIdOrItsProfiles[0]; } // you can create new profile here for auth or if you want to login with genesis set to 0. const authProfileNonce = 100; - const resp = await authHandler.generateAuthorizationResponse( - userDID, - authProfileNonce, - authR, - reqCreds - ); + const resp = await authHandler.handleAuthorizationRequest(userDID, msgBytes, { + mediaType: MediaType.ZKPMessage, + packerOptions: { + profileNonce: authProfileNonce, + provingMethodAlg: proving.provingMethodGroth16AuthV2Instance.methodAlg + }, + credential: cred! + }); + + console.log(resp); + }); + it('request-response flow profiles implicit credential', async () => { + const { did: userDID } = await idWallet.createIdentity({ + method: DidMethod.Iden3, + blockchain: Blockchain.Polygon, + networkId: NetworkId.Mumbai, + seed: seedPhrase, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: rhsUrl + } + }); + const profileDID = await idWallet.createProfile(userDID, 50, 'test verifier'); + + const { did: issuerDID } = await idWallet.createIdentity({ + method: DidMethod.Iden3, + blockchain: Blockchain.Polygon, + networkId: NetworkId.Mumbai, + seed: seedPhraseIssuer, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: rhsUrl + } + }); + + const claimReq: CredentialRequest = { + credentialSchema: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v2.json', + type: 'KYCAgeCredential', + credentialSubject: { + id: profileDID.string(), + birthday: 19960424, + documentType: 99 + }, + expiration: 1693526400, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: rhsUrl + } + }; + const issuerCred = await idWallet.issueCredential(issuerDID, claimReq); + + await credWallet.save(issuerCred); + + const proofReq: ZeroKnowledgeProofRequest = { + id: 1, + circuitId: CircuitId.AtomicQuerySigV2, + optional: false, + query: { + allowedIssuers: ['*'], + type: claimReq.type, + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v3.json-ld', + credentialSubject: { + documentType: { + $eq: 99 + } + } + } + }; + + const authReqBody: AuthorizationRequestMessageBody = { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'message', + did_doc: {}, + scope: [proofReq as ZeroKnowledgeProofRequest] + }; + + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: MediaType.PlainMessage, + type: PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: authReqBody, + from: DID.idFromDID(issuerDID).string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + + const authProfileNonce = 100; + + const resp = await authHandler.handleAuthorizationRequest(userDID, msgBytes, { + mediaType: MediaType.ZKPMessage, + packerOptions: { + profileNonce: authProfileNonce, + provingMethodAlg: proving.provingMethodGroth16AuthV2Instance.methodAlg + } + }); console.log(resp); }); diff --git a/tests/handlers/fetch.test.ts b/tests/handlers/fetch.test.ts index a3727be7..0fcaddab 100644 --- a/tests/handlers/fetch.test.ts +++ b/tests/handlers/fetch.test.ts @@ -33,7 +33,7 @@ import { import * as uuid from 'uuid'; import { byteEncoder } from '../../src/utils'; import { Blockchain, DidMethod, NetworkId } from '@iden3/js-iden3-core'; -import { assert, expect } from 'chai'; +import { assert, expect, use } from 'chai'; import fetchMock from '@gr2m/fetch-mock'; import { after } from 'mocha'; import { proving } from '@iden3/js-jwz'; @@ -47,6 +47,8 @@ describe('fetch', () => { let packageMgr: IPackageManager; const rhsUrl = process.env.RHS_URL as string; const agentUrl = 'https://testagent.com/'; + const agentUrlForProfile = 'https://testagentprofile.com/'; + const mockedToken = 'jwz token to fetch credential'; const mockStateStorage: IStateStorage = { @@ -145,6 +147,71 @@ describe('fetch', () => { "typ": "application/iden3comm-plain-json", "type": "https://iden3-communication.io/credentials/1.0/issuance-response" }`; + const mockerCredResponseForProfile = `{ + "body": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.iden3.io/core/jsonld/iden3proofs.jsonld", + "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v100.json-ld" + ], + "credentialSchema": { + "id": "https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v100.json", + "type": "JsonSchemaValidator2018" + }, + "credentialStatus": { + "id": "http://localhost:8001/api/v1/identities/did%3Aiden3%3AtSsTSJY6g9yUc54FFH6yhx2ymZNtsuTAD9p3avWCb/claims/revocation/status/3701011735", + "revocationNonce": 3701011735, + "type": "SparseMerkleTreeProof" + }, + "credentialSubject": { + "birthday": 19960424, + "documentType": 99, + "id": "did:polygon:mumbai:wwYSEgcJxHAwyjKbBmVbm7w1p33Xb3e1BbDCmHuxh", + "type": "KYCAgeCredential" + }, + "expirationDate": "2361-03-21T21:14:48+02:00", + "id": "http://localhost:8001/api/v1/identities/did:iden3:tSsTSJY6g9yUc54FFH6yhx2ymZNtsuTAD9p3avWCb/claims/d04fcbf8-b373-11ed-88d2-de17148ce1ce", + "issuanceDate": "2023-02-23T14:15:51.58546+02:00", + "issuer": "did:iden3:tSsTSJY6g9yUc54FFH6yhx2ymZNtsuTAD9p3avWCb", + "proof": [ + { + "coreClaim": "06ce4f021d1d9fe3b5dd115882f469ce2a000000000000000000000000000000021253a8d5867185af98ef6bb512c021fc5f099e3ac82f9b924b353128e20c005cff21a8096a19e3198035035c815a2c4d2384ea6b9b2c4a6cc3b9aebff2dc1d000000000000000000000000000000000000000000000000000000000000000017f598dc00000000281cdcdf0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "issuerData": { + "authCoreClaim": "cca3371a6cb1b715004407e325bd993c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004ccf39129a7759649ab1a70538602ca651f76abc1e9b7b7b84db2faa48037a0bd55ec8cdd16d2989f0a5c9824b578c914bd0fd10746bec83075773931d6cb1290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "credentialStatus": { + "id": "http://localhost:8001/api/v1/identities/did%3Aiden3%3AtSsTSJY6g9yUc54FFH6yhx2ymZNtsuTAD9p3avWCb/claims/revocation/status/0", + "revocationNonce": 0, + "type": "SparseMerkleTreeProof" + }, + "id": "did:iden3:tSsTSJY6g9yUc54FFH6yhx2ymZNtsuTAD9p3avWCb", + "mtp": { + "existence": true, + "siblings": [ + "307532286953684850322725598023921449547946667992416711686417617300288340824" + ] + }, + "state": { + "claimsTreeRoot": "188a0b752a0add6ab1b4639b2529999de16cffb42a251074b67551b838404330", + "value": "e74265e2e3c054db09bab873642d9675727538a7010f4dbd8250eed06ca54100" + } + }, + "signature": "62affc0893c76c1e9e279ea0a5b7a48d0fc186d6133303fcd924ae5d3cc0b39fd6791fe0903a80501707f272724ee889f134c64035e40a956fd0cf1c3e4baa02", + "type": "BJJSignature2021" + } + ], + "type": [ + "VerifiableCredential", + "KYCAgeCredential" + ] + } + }, + "from": "did:iden3:tSsTSJY6g9yUc54FFH6yhx2ymZNtsuTAD9p3avWCb", + "id": "30e37e90-2242-4a36-b475-799047d60481", + "to": "did:polygon:mumbai:wwYSEgcJxHAwyjKbBmVbm7w1p33Xb3e1BbDCmHuxh, + "typ": "application/iden3comm-plain-json", + "type": "https://iden3-communication.io/credentials/1.0/issuance-response" +}`; beforeEach(async () => { const memoryKeyStore = new InMemoryPrivateKeyStore(); @@ -178,15 +245,16 @@ describe('fetch', () => { return { unpackedMessage: msg, unpackedMediaType: PROTOCOL_CONSTANTS.MediaType.PlainMessage }; }; packageMgr.pack = async (): Promise => byteEncoder.encode(mockedToken); - fetchHandler = new FetchHandler(packageMgr); + fetchHandler = new FetchHandler(packageMgr, idWallet, credWallet); fetchMock.post(agentUrl, JSON.parse(mockedCredResponse)); + fetchMock.post(agentUrlForProfile, JSON.parse(mockedCredResponse)); }); after(() => { fetchMock.restore(); }); - it('fetch credential', async () => { + it('fetch credential issued to genesis did', async () => { const seedPhraseIssuer: Uint8Array = byteEncoder.encode('seedseedseedseedseedseedseedseed'); const seedPhrase: Uint8Array = byteEncoder.encode('seedseedseedseedseedseedseeduser'); @@ -225,16 +293,84 @@ describe('fetch', () => { url: agentUrl, credentials: [{ id: 'https://credentialId', description: 'kyc age credentials' }] } as CredentialsOfferMessageBody, - from: issuerDID.string() + from: issuerDID.string(), + to: userDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + + const res = await fetchHandler.handleCredentialOffer(msgBytes, { + did: userDID, + mediaType: PROTOCOL_CONSTANTS.MediaType.ZKPMessage, + packerOptions: { + profileNonce: 0, + provingMethodAlg: proving.provingMethodGroth16AuthV2Instance.methodAlg + } + }); + + await credWallet.saveAll(res); + + expect(res).to.be.a('array'); + expect(res).to.have.length(1); + assert.deepEqual( + res[0], + (JSON.parse(mockedCredResponse) as CredentialIssuanceMessage).body?.credential + ); + }); + + it('fetch credential issued to profile', async () => { + const seedPhraseIssuer: Uint8Array = byteEncoder.encode('seedseedseedseedseedseedseedseed'); + const seedPhrase: Uint8Array = byteEncoder.encode('seedseedseedseedseedseedseeduser'); + + const { did: userDID, credential: cred } = await idWallet.createIdentity({ + method: DidMethod.Iden3, + blockchain: Blockchain.Polygon, + networkId: NetworkId.Mumbai, + seed: seedPhrase, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: rhsUrl + } + }); + expect(cred).not.to.be.undefined; + const profileDID = await idWallet.createProfile(userDID, 50, 'test verifier'); + expect(profileDID).not.to.be.undefined; + console.log(profileDID); + + const { did: issuerDID, credential: issuerAuthCredential } = await idWallet.createIdentity({ + method: DidMethod.Iden3, + blockchain: Blockchain.Polygon, + networkId: NetworkId.Mumbai, + seed: seedPhraseIssuer, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: rhsUrl + } + }); + + expect(issuerAuthCredential).not.to.be.undefined; + + const id = uuid.v4(); + const authReq: CredentialsOfferMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.CREDENTIAL_OFFER_MESSAGE_TYPE, + thid: id, + body: { + url: agentUrlForProfile, + credentials: [{ id: 'https://credentialId', description: 'kyc age credentials' }] + } as CredentialsOfferMessageBody, + from: issuerDID.string(), + to: profileDID.string() }; const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); - const res = await fetchHandler.handleCredentialOffer({ + const res = await fetchHandler.handleCredentialOffer(msgBytes, { did: userDID, - offer: msgBytes, - packer: { - mediaType: PROTOCOL_CONSTANTS.MediaType.ZKPMessage, + mediaType: PROTOCOL_CONSTANTS.MediaType.ZKPMessage, + packerOptions: { + profileNonce: 0, provingMethodAlg: proving.provingMethodGroth16AuthV2Instance.methodAlg } });