UNPKG

@0xpolygonid/js-sdk

Version:
1,500 lines (1,319 loc) 55.2 kB
import { IKeyProvider, KMS, KmsKeyId, KmsKeyType } from '../kms'; import { Blockchain, buildDIDType, BytesHelper, Claim, ClaimOptions, DID, DidMethod, getUnixTimestamp, Id, NetworkId, SchemaHash } from '@iden3/js-iden3-core'; import { poseidon, PublicKey, sha256, Signature, Hex, getRandomBytes, Poseidon } from '@iden3/js-crypto'; import { Hash, hashElems, ZERO_HASH } from '@iden3/js-merkletree'; import { generateProfileDID, subjectPositionIndex } from './common'; import * as uuid from 'uuid'; import { JSONSchema, JsonSchemaValidator, cacheLoader } from '../schema-processor'; import { IDataStorage, MerkleTreeType, Profile, UserStateTransitionInfo } from '../storage'; import { VerifiableConstants, BJJSignatureProof2021, MerklizedRootPosition, SubjectPosition, W3CCredential, MerkleTreeProofWithTreeState, Iden3SparseMerkleTreeProof, ProofType, CredentialStatusType, ProofQuery, CoreClaimCreationOptions } from '../verifiable'; import { CredentialRequest, getKMSIdByAuthCredential, getNodesRepresentation, ICredentialWallet, ProofNode, PublishMode, pushHashesToRHS, TreesModel } from '../credentials'; import { CircuitId, StateTransitionInputs, TreeState } from '../circuits'; import { buildDIDFromEthPubKey, byteEncoder, isEthereumIdentity } from '../utils'; import { Options } from '@iden3/js-jsonld-merklization'; import { Signer, TransactionReceipt } from 'ethers'; import { CredentialStatusPublisherRegistry, Iden3SmtRhsCredentialStatusPublisher } from '../credentials/status/credential-status-publisher'; import { InputGenerator, IZKProver } from '../proof'; import { ITransactionService, TransactionService } from '../blockchain'; import { DIDDocument, JsonDocumentObject } from '../iden3comm'; import { DIDDocumentBuilder, JWK2020_CONTEXT_V1, Jwk2020VerificationMethodBuilder } from '../iden3comm/utils'; /** * DID creation options * seed - seed to generate BJJ key pair * revocationOpts - * @type IdentityCreationOptions */ export type IdentityCreationOptions = { method?: string; blockchain?: string; networkId?: string; } & AuthBJJCredentialCreationOptions; /** * Options for creating profile * @type CreateProfileOptions */ export type CreateProfileOptions = { tags?: string[]; didDocument?: DIDDocument; encryptionKeyOps?: EncryptionKeyOps; metadata?: JsonDocumentObject; }; /** * Profile encryption creation options */ export type EncryptionKeyOps = { provider?: IKeyProvider; alias?: string; }; /** * Options for creating Auth BJJ credential * seed - seed to generate BJJ key pair * revocationOpts * nonce - explicit revocation nonce to use * onChain - onchain status related option * txCallback - defines how the TransactionReceipt is handled * publishMode - specifies the work of transaction polling type: sync / async / callback * genesisPublishingDisabled - genesis is publishing by default. Set `true` to prevent genesis publishing */ export type AuthBJJCredentialCreationOptions = { revocationOpts: { id: string; type: CredentialStatusType; nonce?: number; genesisPublishingDisabled?: boolean; onChain?: { txCallback?: (tx: TransactionReceipt) => Promise<void>; publishMode?: PublishMode; }; }; seed?: Uint8Array; }; /** * Options for creating Ethereum based identity */ export type EthereumBasedIdentityCreationOptions = IdentityCreationOptions & { ethSigner?: Signer; createBjjCredential?: boolean; }; /** * Options for RevocationInfoOptions. */ export type RevocationInfoOptions = { revokedNonces?: number[]; treeModel?: TreesModel; rhsUrl?: string; onChain?: { txCallback?: (tx: TransactionReceipt) => Promise<void>; publishMode?: PublishMode; }; }; /** * Proof creation result * * @public * @interface Iden3ProofCreationResult */ export interface Iden3ProofCreationResult { credentials: W3CCredential[]; oldTreeState: TreeState; newTreeState: TreeState; } /** * Interface for IdentityWallet * @public */ export interface IIdentityWallet { /** * Create Identity creates Auth BJJ credential, * Merkle trees for claims, revocations and root of roots, * adds auth BJJ credential to claims tree and generates mtp of inclusion * based on the resulting state it provides an identifier in DID form. * * @param {IdentityCreationOptions} opts - default is did:iden3:polygon:amoy** with generated key. * @returns `Promise<{ did: DID; credential: W3CCredential }>` - returns did and Auth BJJ credential * @public */ createIdentity(opts: IdentityCreationOptions): Promise<{ did: DID; credential: W3CCredential }>; /** * Credential wallet getter * * @returns {ICredentialWallet} * @memberof IIdentityWallet */ get credentialWallet(): ICredentialWallet; /** * Create Identity based in Ethereum address and it provides an identifier in DID form. * * @param {IdentityCreationOptions} opts - default is did:iden3:polygon:amoy** with generated key. * @returns `Promise<{ did: DID; credential: W3CCredential | undefined }>` - returns did and Auth BJJ credential * @public */ createEthereumBasedIdentity( opts: EthereumBasedIdentityCreationOptions ): Promise<{ did: DID; credential: W3CCredential | undefined }>; /** * Creates profile based on genesis identifier * * @param {DID} did - identity to derive profile from * @param {number |string} nonce - unique integer number to generate a profile * @param {string} verifier - verifier identity/alias in a string from * @param {string[]} tags - optional tag that can be assigned to profile by client * @returns `Promise<DID>` - profile did */ createProfile(did: DID, nonce: number | string, verifier: string, tags?: string[]): Promise<DID>; /** * Creates profile based on genesis identifier * * @param {DID} did - identity to derive profile from * @param {number |string} nonce - unique integer number to generate a profile * @param {string} verifier - verifier identity/alias in a string from * @param {CreateProfileOptions} options - optional parameters for profile creation * @returns `Promise<DID>` - profile did */ createProfile( did: DID, nonce: number | string, verifier: string, options?: CreateProfileOptions ): Promise<DID>; /** * Generates a new key * * @param {KmsKeyType} keyType - supported key type by KMS * @returns `Promise<KmsKeyId>` - creates a new key BJJ or ECDSA */ generateKey(keyType: KmsKeyType): Promise<KmsKeyId>; /** * Issues new credential from issuer according to the claim request * * @param {DID} issuerDID - issuer identity * @param {CredentialRequest} req - claim request * @returns `Promise<W3CCredential>` - returns created W3CCredential */ issueCredential(issuerDID: DID, req: CredentialRequest, opts?: Options): Promise<W3CCredential>; /** * Gets a tree model for given did that includes claims tree, revocation tree, the root of roots tree and calculated state hash * * @param {DID} did - did which trees info we need to receive * @returns `Promise<TreesModel>` * */ getDIDTreeModel(did: DID): Promise<TreesModel>; /** * Generates proof of credential inclusion / non-inclusion to the given claims tree * and its root or to the current root of the Claims tree in the given Merkle tree storage. * * @param {DID} did - issuer did * @param {W3CCredential} credential - credential to generate mtp * @param {TreeState} [treeState] - tree state when to generate a proof * @returns `Promise<MerkleTreeProofWithTreeState>` - MerkleTreeProof and TreeState on which proof has been generated */ generateCredentialMtp( did: DID, credential: W3CCredential, treeState?: TreeState ): Promise<MerkleTreeProofWithTreeState>; /** * Generates proof of core claim inclusion / non-inclusion to the given claims tree * and its root or to the current root of the Claims tree in the given Merkle tree storage. * * @param {DID} did - issuer did * @param {core.Claim} core - core claim to generate mtp * @param {TreeState} [treeState] - tree state when to generate a proof * @returns `Promise<MerkleTreeProofWithTreeState>` - MerkleTreeProof and TreeState on which proof has been generated */ generateCoreClaimMtp( did: DID, coreClaim: Claim, treeState?: TreeState ): Promise<MerkleTreeProofWithTreeState>; /** * Generates proof of credential revocation nonce (with credential as a param) inclusion / non-inclusion to the given revocation tree * and its root or to the current root of the Revocation tree in the given Merkle tree storage. * * @param {DID} did * @param {W3CCredential} credential * @param {TreeState} [treeState] * @returns `Promise<MerkleTreeProofWithTreeState>` - MerkleTreeProof and TreeState on which proof has been generated */ generateNonRevocationMtp( did: DID, credential: W3CCredential, treeState?: TreeState ): Promise<MerkleTreeProofWithTreeState>; /** * Generates proof of credential revocation nonce (with revNonce as a param) inclusion / non-inclusion to the given revocation tree * and its root or to the current root of the Revocation tree in the given Merkle tree storage. * * @param {DID} did * @param {bigint} revNonce * @param {TreeState} [treeState] * @returns `Promise<MerkleTreeProofWithTreeState>` - MerkleTreeProof and TreeState on which proof has been generated */ generateNonRevocationMtpWithNonce( did: DID, revNonce: bigint, treeState?: TreeState ): Promise<MerkleTreeProofWithTreeState>; /** * Signs a payload of arbitrary size with an Auth BJJ Credential that identifies a key for signing. * * @param {Uint8Array} payload * @param {W3CCredential} credential - Auth BJJ Credential * @returns `Promise<Signature>`- the signature object with R8 and S params */ sign(payload: Uint8Array, credential: W3CCredential): Promise<Signature>; /** * Signs a big integer with an Auth BJJ Credential that identifies a key for signing. * * * @param {bigint} payload - big number in Field * @param {W3CCredential} credential - Auth BJJ credential * @returns `Promise<Signature>` - the signature object with R8 and S params */ signChallenge(payload: bigint, credential: W3CCredential): Promise<Signature>; /** * * * @param {DID} issuerDID - identifier of the issuer * @param {W3CCredential} credential - credential to revoke * @returns `Promise<number>` a revocation nonce of credential */ revokeCredential(issuerDID: DID, credential: W3CCredential): Promise<number>; /** * Generate Iden3SparseMerkleTree proof of inclusion to issuer state of specific credentials * * @param {DID} issuerDID - issuer did * @param {W3CCredential[]} credentials - list of verifiable credentials to generate a proof * @param {string} txId - transaction hash in which state transition has been done * @param {number} [blockNumber] - block number in which state transition has been done * @param {number} [blockTimestamp] - block timestamp in which state transition has been done * @returns `Promise<W3CCredential[]>` credentials with an Iden3SparseMerkleTreeProof */ generateIden3SparseMerkleTreeProof( issuerDID: DID, credentials: W3CCredential[], txId: string, blockNumber?: number, blockTimestamp?: number ): Promise<W3CCredential[]>; /** * Adds verifiable credentials to issuer Claims Merkle tree * * @param {W3CCredential[]} credentials - credentials to include in the claims tree * @param {DID} issuerDID - issuer did * @returns `Promise<Iden3ProofCreationResult>`- old tree state and tree state with included credentials */ addCredentialsToMerkleTree( credentials: W3CCredential[], issuerDID: DID ): Promise<Iden3ProofCreationResult>; /** * Publishes issuer state to the reverse hash service by given URL * * @deprecated use publishRevocationInfoByCredentialStatusType instead with the same arguments in opts * @param {DID} issuerDID - issuer did * @param {string} rhsURL - reverse hash service URL * @param {number[]} [revokedNonces] - revoked nonces for the period from the last published * @returns `Promise<void>` */ publishStateToRHS( issuerDID: DID, rhsURL: string, revokedNonces?: number[], opts?: object ): Promise<void>; /** * Publishes specific state to the reverse hash service by given URL * @deprecated use publishRevocationInfoByCredentialStatusType instead with the same arguments in opts * @param {TreesModel} treeModel - trees model to publish * @param {string} rhsURL - reverse hash service URL * @param {number[]} [revokedNonces] - revoked nonces for the period from the last published * @returns `Promise<void>` */ publishSpecificStateToRHS( treeModel: TreesModel, rhsURL: string, revokedNonces?: number[], opts?: object ): Promise<void>; /** * Publishes revocation info by credential status predefined publishers * * @param {(RevocationInfoOptions)} opts * @returns {Promise<void>} * @memberof IIdentityWallet */ publishRevocationInfoByCredentialStatusType( issuerDID: DID, credentialStatusType: CredentialStatusType, opts?: RevocationInfoOptions ): Promise<void>; /** * Extracts core claim from signature or merkle tree proof. If both proof persists core claim must be the same * * @public * @param {W3CCredential} credential - credential to extract core claim * @returns `{Promise<Claim>}` */ getCoreClaimFromCredential(credential: W3CCredential): Promise<Claim>; /** * * gets profile identity by genesis identifiers * * @param {DID} did - genesis identifier from which profile has been derived * @returns `{Promise<Profile[]>}` */ getProfilesByDID(did: DID): Promise<Profile[]>; /** * * gets profile nonce by it's id. if profile is genesis identifier - 0 is returned * * @param {DID} did - profile that has been derived or genesis identity * @returns `{Promise<{nonce:number, genesisIdentifier: DID}>}` */ getGenesisDIDMetadata(did: DID): Promise<{ nonce: number | string; genesisDID: DID }>; /** * * find all credentials that belong to any profile or genesis identity for the given did * * @param {DID} did - profile that has been derived or genesis identity * @returns `{Promise<W3CCredential[]>}` */ findOwnedCredentialsByDID(did: DID, query: ProofQuery): Promise<W3CCredential[]>; /** * * gets profile identity by verifier * @deprecated The method should not be used. It returns only one profile per verifier, which can potentially restrict business use cases * @param {string} verifier - identifier of the verifier * @returns `{Promise<Profile>}` */ getProfileByVerifier(verifier: string): Promise<Profile | undefined>; /** * gets profile by verifiers * * @param {string} verifier - verifier to which profile has been shared * @param {string} tags - optional, tags to filter profile entry * @returns `{Promise<Profile[]>}` */ getProfilesByVerifier(verifier: string, tags?: string[]): Promise<Profile[]>; /** * * updates latest identity state in storage with given state or latest from the trees. * * @param {DID} issuerDID - identifier of the issuer * @param {boolean} published - if states is published onchain * @param {TreeState} treeState - contains state to upgrade * @returns `{Promise<void>}` */ updateIdentityState(issuerDID: DID, published: boolean, treeState?: TreeState): Promise<void>; /** * * gets actual auth credential with proofs for provided tree state or latest from the trees. * * @param {DID} issuerDID - identifier of the issuer * @param {TreeState} treeStateInfo - optional, state for retrieval * @returns `{Promise<{ authCredential: W3CCredential; incProof: MerkleTreeProofWithTreeState; nonRevProof: MerkleTreeProofWithTreeState; }>}` */ getActualAuthCredential( did: DID, treeStateInfo?: TreeState ): Promise<{ authCredential: W3CCredential; incProof: MerkleTreeProofWithTreeState; nonRevProof: MerkleTreeProofWithTreeState; }>; /** * Transit state for the identity with the given DID * * @param {DID} did - identifier of the user * @param {TreeState} oldTreeState - old state of the user * @param {boolean} isOldStateGenesis - if the old state is genesis * @param {IStateStorage} stateStorage - storage to save the new state * @param {Signer} ethSigner - signer to sign the transaction */ transitState( did: DID, oldTreeState: TreeState, isOldStateGenesis: boolean, ethSigner: Signer, prover?: IZKProver ): Promise<string>; /** * Add BJJ credential and transit state * * @param {DID} did - identifier of the user * @param {TreeState} oldTreeState - old tree state of the user * @param {boolean} isOldTreeState - if the old state is genesis * @param {Signer} ethSigner - signer to sign the transaction * @param {AuthBJJCredentialCreationOptions} opts - additional options */ addBJJAuthCredential( did: DID, oldTreeState: TreeState, isOldTreeStateGenesis: boolean, ethSigner: Signer, opts?: AuthBJJCredentialCreationOptions ): Promise<W3CCredential>; } /** * @public * Wallet instance to manage the digital identity based on iden3 protocol * allows to: create identity/profile, sign payloads (bigint / bytes), generate keys, * generate Merkle tree proofs of inclusion / non-inclusion to Merkle trees, issue credentials with a BJJSignature and Iden3SparseMerkleTree Proofs, * revoke credentials, add credentials to Merkle trees, push states to reverse hash service * * * @class IdentityWallet - class * @implements implements IIdentityWallet interface */ export class IdentityWallet implements IIdentityWallet { private readonly _credentialStatusPublisherRegistry: CredentialStatusPublisherRegistry; private readonly _inputsGenerator: InputGenerator; private readonly _transactionService: ITransactionService; /** * Constructs a new instance of the `IdentityWallet` class * * @param {KMS} _kms - Key Management System that allows signing data with BJJ key * @param {IDataStorage} _storage - data storage to access credential / identity / Merkle tree data * @param {ICredentialWallet} _credentialWallet - credential wallet instance to quickly access credential CRUD functionality * @public */ public constructor( private readonly _kms: KMS, private readonly _storage: IDataStorage, private readonly _credentialWallet: ICredentialWallet, private readonly _opts?: { credentialStatusPublisherRegistry?: CredentialStatusPublisherRegistry; } ) { this._credentialStatusPublisherRegistry = this.getCredentialStatusPublisherRegistry(_opts); this._inputsGenerator = new InputGenerator(this, _credentialWallet, _storage.states); this._transactionService = new TransactionService(_storage.states.getRpcProvider()); } get credentialWallet(): ICredentialWallet { return this._credentialWallet; } private getCredentialStatusPublisherRegistry( _opts: | { credentialStatusPublisherRegistry?: CredentialStatusPublisherRegistry | undefined } | undefined ): CredentialStatusPublisherRegistry { if (!_opts?.credentialStatusPublisherRegistry) { const registry = new CredentialStatusPublisherRegistry(); const emptyPublisher = { publish: () => Promise.resolve() }; registry.register( CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, new Iden3SmtRhsCredentialStatusPublisher() ); registry.register(CredentialStatusType.SparseMerkleTreeProof, emptyPublisher); registry.register(CredentialStatusType.Iden3commRevocationStatusV1, emptyPublisher); return registry; } else { return this._opts?.credentialStatusPublisherRegistry as CredentialStatusPublisherRegistry; } } private async createAuthCoreClaim( revNonce: number, seed: Uint8Array ): Promise<{ authClaim: Claim; pubKey: PublicKey }> { const keyId = await this._kms.createKeyFromSeed(KmsKeyType.BabyJubJub, seed); const pubKeyHex = await this._kms.publicKey(keyId); const pubKey = PublicKey.newFromHex(pubKeyHex); const schemaHash = SchemaHash.authSchemaHash; const authClaim = Claim.newClaim( schemaHash, ClaimOptions.withIndexDataInts(pubKey.p[0], pubKey.p[1]), ClaimOptions.withRevocationNonce(BigInt(0)) ); authClaim.setRevocationNonce(BigInt(revNonce)); return { authClaim, pubKey }; } private async createAuthBJJCredential( did: DID, pubKey: PublicKey, authClaim: Claim, currentState: Hash, revocationOpts: { id: string; type: CredentialStatusType } ): Promise<W3CCredential> { const authData = authClaim.getExpirationDate(); const expiration = authData ? getUnixTimestamp(authData) : 0; const request: CredentialRequest = { credentialSchema: VerifiableConstants.AUTH.AUTH_BJJ_CREDENTIAL_SCHEMA_JSON_URL, type: VerifiableConstants.AUTH.AUTH_BJJ_CREDENTIAL_TYPE, credentialSubject: { x: pubKey.p[0].toString(), y: pubKey.p[1].toString() }, subjectPosition: subjectPositionIndex(authClaim.getIdPosition()), version: 0, expiration, revocationOpts: { nonce: Number(authClaim.getRevocationNonce()), id: revocationOpts.id.replace(/\/$/, ''), type: revocationOpts.type, issuerState: currentState.hex() } }; // Check if has already an auth credential const authCredentials = await this._credentialWallet.getAllAuthBJJCredentials(did); let credential: W3CCredential = new W3CCredential(); if (authCredentials.length === 0) { const schema = JSON.parse(VerifiableConstants.AUTH.AUTH_BJJ_CREDENTIAL_SCHEMA_JSON); try { credential = this._credentialWallet.createCredential(did, request, schema); } catch (e) { throw new Error(`Error create w3c credential ${(e as Error).message}`); } } else { // credential with sigProof signed with previous auth bjj credential credential = await this.issueCredential(did, request); } return credential; } /** * {@inheritDoc IIdentityWallet.createIdentity} */ async createIdentity( opts: IdentityCreationOptions ): Promise<{ did: DID; credential: W3CCredential }> { const tmpIdentifier = opts.seed ? uuid.v5(Hex.encode(sha256(opts.seed)), uuid.NIL) : uuid.v4(); opts.seed = opts.seed ?? getRandomBytes(32); await this._storage.mt.createIdentityMerkleTrees(tmpIdentifier); const revNonce = opts.revocationOpts.nonce ?? 0; const { authClaim, pubKey } = await this.createAuthCoreClaim(revNonce, opts.seed); const { hi, hv } = authClaim.hiHv(); await this._storage.mt.addToMerkleTree(tmpIdentifier, MerkleTreeType.Claims, hi, hv); const claimsTree = await this._storage.mt.getMerkleTreeByIdentifierAndType( tmpIdentifier, MerkleTreeType.Claims ); const ctr = await claimsTree.root(); const currentState = hashElems([ctr.bigInt(), ZERO_HASH.bigInt(), ZERO_HASH.bigInt()]); const didType = buildDIDType( opts.method || DidMethod.Iden3, opts.blockchain || Blockchain.Polygon, opts.networkId || NetworkId.Amoy ); const identifier = Id.idGenesisFromIdenState(didType, currentState.bigInt()); const did = DID.parseFromId(identifier); await this._storage.mt.bindMerkleTreeToNewIdentifier(tmpIdentifier, did.string()); const oldTreeState = { revocationRoot: ZERO_HASH, claimsRoot: ctr, state: currentState, rootOfRoots: ZERO_HASH }; const identity = await this._storage.identity.getIdentity(did.string()); if (!identity) { await this._storage.identity.saveIdentity({ did: did.string(), state: currentState, isStatePublished: false, isStateGenesis: true }); } // check whether we have auth credential, if not - create a new one const credentials = await this._credentialWallet.findByQuery({ credentialSubject: { x: { $eq: pubKey.p[0].toString() }, y: { $eq: pubKey.p[1].toString() } }, allowedIssuers: [did.string()] }); // if credential exists with the same credential status type we return this credential if ( credentials.length === 1 && credentials[0].credentialStatus.type === opts.revocationOpts.type ) { return { did, credential: credentials[0] }; } // otherwise something is already wrong with storage as it has more than 1 credential in it or credential status type of existing credential is different from what user provides - We should remove everything and create new credential. // in this way credential status of auth credential can be upgraded for (let i = 0; i < credentials.length; i++) { await this._credentialWallet.remove(credentials[i].id); } // otherwise we create a new credential const credential = await this.createAuthBJJCredential( did, pubKey, authClaim, currentState, opts.revocationOpts ); const index = authClaim.hIndex(); const { proof } = await claimsTree.generateProof(index, ctr); const mtpProof: Iden3SparseMerkleTreeProof = new Iden3SparseMerkleTreeProof({ mtp: proof, issuerData: { id: did, state: { rootOfRoots: oldTreeState.rootOfRoots, revocationTreeRoot: oldTreeState.revocationRoot, claimsTreeRoot: ctr, value: currentState } }, coreClaim: authClaim }); credential.proof = [mtpProof]; // only if user specified that genesis state publishing is not needed we won't do this. if (!opts.revocationOpts.genesisPublishingDisabled) { await this.publishRevocationInfoByCredentialStatusType(did, opts.revocationOpts.type, { rhsUrl: opts.revocationOpts.id, onChain: opts.revocationOpts.onChain }); } await this._credentialWallet.save(credential); return { did, credential }; } /** * {@inheritDoc IIdentityWallet.createEthereumBasedIdentity} */ async createEthereumBasedIdentity( opts: EthereumBasedIdentityCreationOptions ): Promise<{ did: DID; credential: W3CCredential | undefined }> { opts.seed = opts.seed ?? getRandomBytes(32); opts.createBjjCredential = opts.createBjjCredential ?? true; let credential; const ethSigner = opts.ethSigner; if (opts.createBjjCredential && !ethSigner) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_SIGNER_IS_REQUIRED); } const currentState = ZERO_HASH; // In Ethereum identities we don't have an initial state with the auth credential const didType = buildDIDType( opts.method || DidMethod.Iden3, opts.blockchain || Blockchain.Polygon, opts.networkId || NetworkId.Amoy ); const keyIdEth = await this._kms.createKeyFromSeed(KmsKeyType.Secp256k1, opts.seed); const pubKeyHexEth = (await this._kms.publicKey(keyIdEth)).slice(2); // 04 + x + y (uncompressed key) const did = buildDIDFromEthPubKey(didType, pubKeyHexEth); await this._storage.mt.createIdentityMerkleTrees(did.string()); await this._storage.identity.saveIdentity({ did: did.string(), state: currentState, isStatePublished: false, isStateGenesis: true }); if (opts.createBjjCredential && ethSigner) { // Old tree state genesis state const oldTreeState: TreeState = { revocationRoot: ZERO_HASH, claimsRoot: ZERO_HASH, state: currentState, rootOfRoots: ZERO_HASH }; credential = await this.addBJJAuthCredential(did, oldTreeState, true, ethSigner, opts); } return { did, credential }; } /** {@inheritDoc IIdentityWallet.getGenesisDIDMetadata} */ async getGenesisDIDMetadata(did: DID): Promise<{ nonce: number | string; genesisDID: DID }> { // check if it is a genesis identity const identity = await this._storage.identity.getIdentity(did.string()); if (identity) { return { nonce: 0, genesisDID: DID.parse(identity.did) }; } const profile = await this._storage.identity.getProfileById(did.string()); if (!profile) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_PROFILE_OR_IDENTITY_NOT_FOUND); } return { nonce: profile.nonce, genesisDID: DID.parse(profile.genesisIdentifier) }; } /** {@inheritDoc IIdentityWallet.createProfile} */ async createProfile( did: DID, nonce: number | string, verifier: string, tagsOrOptions?: string[] | CreateProfileOptions ): Promise<DID> { const profileDID = generateProfileDID(did, nonce); const isArray = Array.isArray(tagsOrOptions); const tags = isArray ? tagsOrOptions : tagsOrOptions?.tags; const metadata = !isArray ? tagsOrOptions?.metadata : undefined; const { didDocument, encryptionKeyOps } = (isArray ? {} : tagsOrOptions ?? {}) as { didDocument?: DIDDocument; encryptionKeyOps?: EncryptionKeyOps; }; const identityProfiles = await this._storage.identity.getProfilesByGenesisIdentifier( did.string() ); const profilesForTagAndVerifier = await this._storage.identity.getProfilesByVerifier( verifier, tags ); if (profilesForTagAndVerifier.length) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_PROFILE_ALREADY_EXISTS_VERIFIER_TAGS); } const existingProfileWithNonce = identityProfiles.find((p) => p.nonce == nonce); if (existingProfileWithNonce) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_PROFILE_ALREADY_EXISTS); } let did_doc = didDocument; if (encryptionKeyOps?.provider) { const vmBuilder = new Jwk2020VerificationMethodBuilder(encryptionKeyOps.provider, { alias: encryptionKeyOps.alias }); if (!did_doc) { did_doc = ( await new DIDDocumentBuilder(profileDID.string()).addVerificationMethod( vmBuilder, JWK2020_CONTEXT_V1 ) ).build(); } else { const vm = await vmBuilder.build(profileDID.string()); const contextArr = [did_doc['@context']] .flat() .filter((c) => typeof c === 'string') as string[]; did_doc = { ...did_doc, verificationMethod: [...(did_doc.verificationMethod ?? []), vm], '@context': [...new Set([...contextArr, JWK2020_CONTEXT_V1])] }; } } await this._storage.identity.saveProfile({ id: profileDID.string(), nonce, genesisIdentifier: did.string(), verifier, tags, did_doc, metadata }); return profileDID; } /** * * gets profile identity by genesis identifiers * * @param {string} genesisIdentifier - genesis identifier from which profile has been derived * @returns `{Promise<Profile[]>}` */ async getProfilesByDID(did: DID): Promise<Profile[]> { return this._storage.identity.getProfilesByGenesisIdentifier(did.string()); } /** {@inheritDoc IIdentityWallet.generateKey} */ async generateKey(keyType: KmsKeyType): Promise<KmsKeyId> { const key = await this._kms.createKeyFromSeed(keyType, getRandomBytes(32)); return key; } /** * @deprecated The method should not be used. It returns only one profile per verifier, which can potentially restrict business use cases * {@inheritDoc IIdentityWallet.getProfileByVerifier} */ async getProfileByVerifier(verifier: string): Promise<Profile | undefined> { return this._storage.identity.getProfileByVerifier(verifier); } /** {@inheritDoc IIdentityWallet.getProfilesByVerifier} */ async getProfilesByVerifier(verifier: string, tags?: string[]): Promise<Profile[]> { return this._storage.identity.getProfilesByVerifier(verifier, tags); } /** {@inheritDoc IIdentityWallet.getDIDTreeModel} */ async getDIDTreeModel(did: DID): Promise<TreesModel> { const didStr = did.string(); const claimsTree = await this._storage.mt.getMerkleTreeByIdentifierAndType( didStr, MerkleTreeType.Claims ); const revocationTree = await this._storage.mt.getMerkleTreeByIdentifierAndType( didStr, MerkleTreeType.Revocations ); const rootsTree = await this._storage.mt.getMerkleTreeByIdentifierAndType( didStr, MerkleTreeType.Roots ); const state = hashElems([ (await claimsTree.root()).bigInt(), (await revocationTree.root()).bigInt(), (await rootsTree.root()).bigInt() ]); return { state, claimsTree, revocationTree, rootsTree }; } /** {@inheritDoc IIdentityWallet.generateClaimMtp} */ async generateCredentialMtp( did: DID, credential: W3CCredential, treeState?: TreeState ): Promise<MerkleTreeProofWithTreeState> { const coreClaim = await this.getCoreClaimFromCredential(credential); return this.generateCoreClaimMtp(did, coreClaim, treeState); } /** {@inheritDoc IIdentityWallet.generateClaimMtp} */ async generateCoreClaimMtp( did: DID, coreClaim: Claim, treeState?: TreeState ): Promise<MerkleTreeProofWithTreeState> { const treesModel = await this.getDIDTreeModel(did); const claimsTree = await this._storage.mt.getMerkleTreeByIdentifierAndType( did.string(), MerkleTreeType.Claims ); const claimsRoot = await treesModel.claimsTree.root(); const rootOfRoots = await treesModel.rootsTree.root(); const revocationRoot = await treesModel.revocationTree.root(); const { proof } = await claimsTree.generateProof( coreClaim.hIndex(), treeState ? treeState.claimsRoot : claimsRoot ); return { proof, treeState: treeState ?? { state: treesModel.state, claimsRoot, rootOfRoots, revocationRoot } }; } /** {@inheritDoc IIdentityWallet.generateNonRevocationMtp} */ async generateNonRevocationMtp( did: DID, credential: W3CCredential, treeState?: TreeState ): Promise<MerkleTreeProofWithTreeState> { const coreClaim = await this.getCoreClaimFromCredential(credential); const revNonce = coreClaim.getRevocationNonce(); return this.generateNonRevocationMtpWithNonce(did, revNonce, treeState); } /** {@inheritDoc IIdentityWallet.generateNonRevocationMtpWithNonce} */ async generateNonRevocationMtpWithNonce( did: DID, revNonce: bigint, treeState?: TreeState ): Promise<MerkleTreeProofWithTreeState> { const treesModel = await this.getDIDTreeModel(did); const revocationTree = await this._storage.mt.getMerkleTreeByIdentifierAndType( did.string(), MerkleTreeType.Revocations ); const claimsRoot = await treesModel.claimsTree.root(); const rootOfRoots = await treesModel.rootsTree.root(); const revocationRoot = await treesModel.revocationTree.root(); const { proof } = await revocationTree.generateProof( revNonce, treeState ? treeState.revocationRoot : revocationRoot ); return { proof, treeState: treeState ?? { state: treesModel.state, claimsRoot, rootOfRoots, revocationRoot } }; } /** {@inheritDoc IIdentityWallet.sign} */ async sign(message: Uint8Array, credential: W3CCredential): Promise<Signature> { const keyKMSId = getKMSIdByAuthCredential(credential); const payload = poseidon.hashBytes(message); const signature = await this._kms.sign(keyKMSId, BytesHelper.intToBytes(payload)); return Signature.newFromCompressed(signature); } /** {@inheritDoc IIdentityWallet.signChallenge} */ async signChallenge(challenge: bigint, credential: W3CCredential): Promise<Signature> { const keyKMSId = getKMSIdByAuthCredential(credential); const signature = await this._kms.sign(keyKMSId, BytesHelper.intToBytes(challenge)); return Signature.newFromCompressed(signature); } /** {@inheritDoc IIdentityWallet.issueCredential} */ async issueCredential( issuerDID: DID, req: CredentialRequest, opts?: Options ): Promise<W3CCredential> { req.revocationOpts.id = req.revocationOpts.id.replace(/\/$/, ''); let schema: object; const loader = opts?.documentLoader ?? cacheLoader(opts); try { schema = (await loader(req.credentialSchema)).document; } catch (e) { throw new Error(`can't load credential schema ${req.credentialSchema}`); } const jsonSchema = schema as JSONSchema; let credential: W3CCredential = new W3CCredential(); const issuerRoots = await this.getDIDTreeModel(issuerDID); req.revocationOpts.issuerState = issuerRoots.state.hex(); req.revocationOpts.nonce = typeof req.revocationOpts.nonce === 'number' ? req.revocationOpts.nonce : new DataView(getRandomBytes(16).buffer).getUint32(0, false); req.subjectPosition = req.subjectPosition ?? SubjectPosition.Index; try { credential = this._credentialWallet.createCredential(issuerDID, req, jsonSchema); const encodedCred = byteEncoder.encode(JSON.stringify(credential)); const encodedSchema = byteEncoder.encode(JSON.stringify(schema)); await new JsonSchemaValidator().validate(encodedCred, encodedSchema); } catch (e) { throw new Error(`Error create w3c credential ${(e as Error).message}`); } const { authCredential: issuerAuthBJJCredential } = await this.getActualAuthCredential( issuerDID ); const coreClaimOpts: CoreClaimCreationOptions = { revNonce: req.revocationOpts.nonce, subjectPosition: req.subjectPosition, merklizedRootPosition: req.merklizedRootPosition ?? MerklizedRootPosition.None, updatable: false, version: 0, merklizeOpts: { ...opts, documentLoader: loader } }; const coreClaim = await credential.toCoreClaim(coreClaimOpts); const { hi, hv } = coreClaim.hiHv(); const coreClaimHash = poseidon.hash([hi, hv]); const signature = await this.signChallenge(coreClaimHash, issuerAuthBJJCredential); if (!issuerAuthBJJCredential.proof) { throw new Error( VerifiableConstants.ERRORS.ID_WALLET_ISSUER_AUTH_BJJ_CRED_MUST_HAVE_ANY_PROOF ); } const mtpAuthBJJProof = issuerAuthBJJCredential.getIden3SparseMerkleTreeProof(); if (!mtpAuthBJJProof) { throw new Error( VerifiableConstants.ERRORS.ID_WALLET_ISSUER_AUTH_BJJ_CRED_MUST_HAVE_MTP_PROOF ); } const sigProof = new BJJSignatureProof2021({ issuerData: { id: issuerDID, state: mtpAuthBJJProof.issuerData.state, authCoreClaim: mtpAuthBJJProof.coreClaim, mtp: mtpAuthBJJProof.mtp, credentialStatus: issuerAuthBJJCredential.credentialStatus }, coreClaim, signature }); credential.proof = [sigProof]; return credential; } /** {@inheritDoc IIdentityWallet.getActualAuthCredential} */ async getActualAuthCredential( did: DID, treeStateInfo?: TreeState ): Promise<{ authCredential: W3CCredential; incProof: MerkleTreeProofWithTreeState; nonRevProof: MerkleTreeProofWithTreeState; }> { const authCredentials = await this._credentialWallet.getAllAuthBJJCredentials(did); for (let i = 0; i < authCredentials.length; i++) { const incProof = await this.generateCredentialMtp(did, authCredentials[i], treeStateInfo); if (!incProof.proof.existence) { continue; } const nonRevProof = await this.generateNonRevocationMtp( did, authCredentials[i], treeStateInfo ); if (!nonRevProof.proof.existence) { return { authCredential: authCredentials[i], incProof, nonRevProof }; } } throw new Error(VerifiableConstants.ERRORS.NO_AUTH_CRED_FOUND); } /** {@inheritDoc IIdentityWallet.revokeCredential} */ async revokeCredential(issuerDID: DID, credential: W3CCredential): Promise<number> { const issuerTree = await this.getDIDTreeModel(issuerDID); const coreClaim = await this.getCoreClaimFromCredential(credential); if (!coreClaim) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_CORE_CLAIM_REQUIRED_IN_ANY_PROOF); } const nonce = coreClaim.getRevocationNonce(); await issuerTree.revocationTree.add(nonce, BigInt(0)); return Number(BigInt.asUintN(64, nonce)); } /** {@inheritDoc IIdentityWallet.addCredentialsToMerkleTree} */ async addCredentialsToMerkleTree( credentials: W3CCredential[], issuerDID: DID ): Promise<Iden3ProofCreationResult> { const oldIssuerTree = await this.getDIDTreeModel(issuerDID); let claimsRoot = await oldIssuerTree.claimsTree.root(); let rootOfRoots = await oldIssuerTree.rootsTree.root(); let revocationRoot = await oldIssuerTree.revocationTree.root(); const oldTreeState: TreeState = { state: oldIssuerTree.state, claimsRoot, revocationRoot, rootOfRoots }; for (let index = 0; index < credentials.length; index++) { const credential = credentials[index]; // credential must have a bjj signature proof const coreClaim = credential.getCoreClaimFromProof(ProofType.BJJSignature); if (!coreClaim) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_CORE_CLAIM_REQUIRED_IN_SIG_PROOF); } await this._storage.mt.addToMerkleTree( issuerDID.string(), MerkleTreeType.Claims, coreClaim.hIndex(), coreClaim.hValue() ); } const newIssuerTreeState = await this.getDIDTreeModel(issuerDID); const claimTreeRoot = await newIssuerTreeState.claimsTree.root(); await this._storage.mt.addToMerkleTree( issuerDID.string(), MerkleTreeType.Roots, claimTreeRoot.bigInt(), BigInt(0) ); const newIssuerTreeStateWithROR = await this.getDIDTreeModel(issuerDID); claimsRoot = await newIssuerTreeStateWithROR.claimsTree.root(); rootOfRoots = await newIssuerTreeStateWithROR.rootsTree.root(); revocationRoot = await newIssuerTreeStateWithROR.revocationTree.root(); return { credentials, newTreeState: { state: newIssuerTreeStateWithROR.state, claimsRoot, rootOfRoots, revocationRoot }, oldTreeState: oldTreeState }; } /** {@inheritDoc IIdentityWallet.generateIden3SparseMerkleTreeProof} */ // treeState - optional, if it is not passed proof of claim inclusion will be generated on the latest state in the tree. async generateIden3SparseMerkleTreeProof( issuerDID: DID, credentials: W3CCredential[], txId: string, blockNumber?: number, blockTimestamp?: number, treeState?: TreeState, opts?: CoreClaimCreationOptions ): Promise<W3CCredential[]> { for (let index = 0; index < credentials.length; index++) { const credential = credentials[index]; // TODO: return coreClaim from generateCredentialMtp and use it below // credential must have a bjj signature proof const coreClaim = credential.getCoreClaimFromProof(ProofType.BJJSignature) || (await credential.toCoreClaim(opts)); if (!coreClaim) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_CORE_CLAIM_REQUIRED_IN_SIG_PROOF); } const mtpWithProof = await this.generateCoreClaimMtp(issuerDID, coreClaim, treeState); const mtpProof: Iden3SparseMerkleTreeProof = new Iden3SparseMerkleTreeProof({ mtp: mtpWithProof.proof, issuerData: { id: issuerDID, state: { claimsTreeRoot: mtpWithProof.treeState.claimsRoot, revocationTreeRoot: mtpWithProof.treeState.revocationRoot, rootOfRoots: mtpWithProof.treeState.rootOfRoots, value: mtpWithProof.treeState.state, txId, blockNumber, blockTimestamp } }, coreClaim }); if (Array.isArray(credentials[index].proof)) { (credentials[index].proof as unknown[]).push(mtpProof); } else { credentials[index].proof = credentials[index].proof ? [credentials[index].proof, mtpProof] : [mtpProof]; } } return credentials; } /** {@inheritDoc IIdentityWallet.publishSpecificStateToRHS} */ async publishSpecificStateToRHS( treeModel: TreesModel, rhsURL: string, revokedNonces?: number[] ): Promise<void> { await pushHashesToRHS(treeModel.state, treeModel, rhsURL, revokedNonces); } /** {@inheritDoc IIdentityWallet.publishStateToRHS} */ async publishStateToRHS(issuerDID: DID, rhsURL: string, revokedNonces?: number[]): Promise<void> { const treeState = await this.getDIDTreeModel(issuerDID); await pushHashesToRHS( treeState.state, { revocationTree: treeState.revocationTree, claimsTree: treeState.claimsTree, state: treeState.state, rootsTree: treeState.rootsTree }, rhsURL, revokedNonces ); } /** {@inheritDoc IIdentityWallet.publishRevocationInfoByCredentialStatusType} */ async publishRevocationInfoByCredentialStatusType( issuerDID: DID, credentialStatusType: CredentialStatusType, opts?: RevocationInfoOptions ): Promise<void> { const rhsPublishers = this._credentialStatusPublisherRegistry.get(credentialStatusType); if (!rhsPublishers) { throw new Error( `there is no registered publisher to save hash is not registered for ${credentialStatusType} is not registered` ); } let nodes: ProofNode[] = []; const tree = opts?.treeModel ?? (await this.getDIDTreeModel(issuerDID)); nodes = await getNodesRepresentation( opts?.revokedNonces ?? [], { revocationTree: tree.revocationTree, claimsTree: tree.claimsTree, state: tree.state, rootsTree: tree.rootsTree }, tree.state ); if (!nodes.length) { return; } const rhsPublishersTask = rhsPublishers.map((publisher) => publisher.publish({ nodes, ...opts, credentialStatusType, issuerDID }) ); await Promise.all(rhsPublishersTask); } public async getCoreClaimFromCredential(credential: W3CCredential): Promise<Claim> { const coreClaimFromSigProof = credential.getCoreClaimFromProof(ProofType.BJJSignature); const coreClaimFromMtpProof = credential.getCoreClaimFromProof( ProofType.Iden3SparseMerkleTreeProof ); if ( coreClaimFromMtpProof && coreClaimFromSigProof && coreClaimFromMtpProof.hex() !== coreClaimFromSigProof.hex() ) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_CORE_CLAIM_MISMATCH); } if (!coreClaimFromMtpProof && !coreClaimFromSigProof) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_CORE_CLAIM_IS_NOT_SET); } //eslint-disable-next-line @typescript-eslint/no-non-null-assertion const coreClaim = coreClaimFromMtpProof ?? coreClaimFromSigProof!; return coreClaim; } async findOwnedCredentialsByDID(did: DID, query: ProofQuery): Promise<W3CCredential[]> { const credentials = await this._credentialWallet.findByQuery(query); if (!credentials.length) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_NO_CREDENTIAL_SATISFIED_QUERY); } const { genesisDID } = await this.getGenesisDIDMetadata(did); const profiles = await this.getProfilesByDID(genesisDID); return credentials.filter((cred) => { const credentialSubjectId = cred.credentialSubject['id'] as string; // credential subject return ( credentialSubjectId == genesisDID.string() || profiles.some((p) => { return p.id === credentialSubjectId; }) ); }); } /** {@inheritDoc IIdentityWallet.updateIdentityState} */ async updateIdentityState( issuerDID: DID, published: boolean, treeState?: TreeState ): Promise<void> { const latestTreeState = await this.getDIDTreeModel(issuerDID); await this._storage.identity.saveIdentity({ did: issuerDID.string(), state: treeState?.state ?? latestTreeState.state, isStatePublished: published, isStateGenesis: false }); } /** {@inheritdoc IIdentityWallet.transitState} */ async transitState( did: DID, oldTreeState: TreeState, isOldStateGenesis: boolean, ethSigner: Signer, prover?: IZKProver ): Promise<string> { const newTreeModel = await this.getDIDTreeModel(did); const claimsRoot = await newTreeModel.claimsTree.root(); const rootOfRoots = await newTreeModel.rootsTree.root(); const revocationRoot = await newTreeModel.revocationTree.root(); const newTreeState: TreeState = { revocationRoot, claimsRoot, state: newTreeModel.state, rootOfRoots }; const userId = DID.idFromDID(did); let proof; const isEthIdentity = isEthereumIdentity(did); // don't generate proof for ethereum identities let txId; if (!isEthIdentity) { if (!prover) { throw new Error(VerifiableConstants.ERRORS.ID_WALLET_PROVER_IS_REQUIRED); } // generate the proof const authInfo = await this._inputsGenerator.prepareAuthBJJCredential(did, oldTreeState); const challenge = Poseidon.hash([oldTreeState.state.bigInt(), newTreeState.state.bigInt()]); const signature = await this.signChallenge(challenge, authInfo.cre