diff --git a/index.html b/index.html index e0dcafd5..7f1a9150 100644 --- a/index.html +++ b/index.html @@ -74,6 +74,9 @@ publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getGISTProof: () => { return Promise.resolve({ root: 0n, diff --git a/src/credentials/status/reverse-sparse-merkle-tree.ts b/src/credentials/status/reverse-sparse-merkle-tree.ts index 25474129..c03137a8 100644 --- a/src/credentials/status/reverse-sparse-merkle-tree.ts +++ b/src/credentials/status/reverse-sparse-merkle-tree.ts @@ -5,7 +5,7 @@ import { IStateStorage } from '../../storage'; import { CredentialStatusResolver, CredentialStatusResolveOptions } from './resolver'; import { CredentialStatus, RevocationStatus, State } from '../../verifiable'; import { VerifiableConstants, CredentialStatusType } from '../../verifiable/constants'; -import { isGenesisState } from '../../utils'; +import { isEthereumIdentity, isGenesisState } from '../../utils'; import { IssuerResolver } from './sparse-merkle-tree'; /** @@ -173,11 +173,19 @@ export class RHSResolver implements CredentialStatusResolver { return this.getRevocationStatusFromIssuerData(issuerDID, issuerData, genesisState); } const currentStateBigInt = Hash.fromHex(stateHex).bigInt(); - if (!isGenesisState(issuerDID, currentStateBigInt)) { + + const isEthIdentity = isEthereumIdentity(issuerDID); + + if (!isEthIdentity && !isGenesisState(issuerDID, currentStateBigInt)) { throw new Error( `latest state not found and state parameter ${stateHex} is not genesis state` ); } + + if (isEthIdentity) { + throw new Error(`State must be published for Ethereum based identity`); + } + latestState = currentStateBigInt; } diff --git a/src/identity/identity-wallet.ts b/src/identity/identity-wallet.ts index 831b7444..5580916f 100644 --- a/src/identity/identity-wallet.ts +++ b/src/identity/identity-wallet.ts @@ -12,13 +12,20 @@ import { NetworkId, SchemaHash } from '@iden3/js-iden3-core'; -import { poseidon, PublicKey, sha256, Signature, Hex, getRandomBytes } from '@iden3/js-crypto'; -import { hashElems, ZERO_HASH } from '@iden3/js-merkletree'; - +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 } from '../storage'; +import { IDataStorage, MerkleTreeType, Profile, UserStateTransitionInfo } from '../storage'; import { VerifiableConstants, BJJSignatureProof2021, @@ -42,14 +49,15 @@ import { pushHashesToRHS, TreesModel } from '../credentials'; -import { TreeState } from '../circuits'; -import { byteEncoder } from '../utils'; +import { CircuitId, StateTransitionInputs, TreeState } from '../circuits'; +import { buildDIDFromEthPubKey, byteEncoder, isEthereumIdentity } from '../utils'; import { Options } from '@iden3/js-jsonld-merklization'; -import { TransactionReceipt } from 'ethers'; +import { Signer, TransactionReceipt } from 'ethers'; import { CredentialStatusPublisherRegistry, Iden3SmtRhsCredentialStatusPublisher } from '../credentials/status/credential-status-publisher'; +import { InputGenerator, IZKProver } from '../proof'; /** * DID creation options @@ -62,6 +70,14 @@ export type IdentityCreationOptions = { method?: string; blockchain?: string; networkId?: string; +} & AuthBJJCredentialCreationOptions; + +/** + * Options for creating Auth BJJ credential + * seed - seed to generate BJJ key pair + * revocationOpts - + */ +export type AuthBJJCredentialCreationOptions = { revocationOpts: { id: string; type: CredentialStatusType; @@ -74,6 +90,14 @@ export type IdentityCreationOptions = { seed?: Uint8Array; }; +/** + * Options for creating Ethereum based identity + */ +export type EthereumBasedIdentityCreationOptions = IdentityCreationOptions & { + ethSigner?: Signer; + createBjjCredential?: boolean; +}; + /** * Options for RevocationInfoOptions. */ @@ -109,13 +133,23 @@ export interface IIdentityWallet { * 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:mumbai** with generated key. + * @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 }>; + /** + * 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 * @@ -166,6 +200,21 @@ export interface IIdentityWallet { treeState?: TreeState ): Promise; + /** + * 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` - MerkleTreeProof and TreeState on which proof has been generated + */ + generateCoreClaimMtp( + did: DID, + coreClaim: Claim, + treeState?: TreeState + ): Promise; + /** * 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. @@ -311,7 +360,7 @@ export interface IIdentityWallet { * * gets profile identity by genesis identifiers * - * @param {string} did - genesis identifier from which profile has been derived + * @param {DID} did - genesis identifier from which profile has been derived * @returns `{Promise}` */ getProfilesByDID(did: DID): Promise; @@ -320,7 +369,7 @@ export interface IIdentityWallet { * * gets profile nonce by it's id. if profile is genesis identifier - 0 is returned * - * @param {string} did - profile that has been derived or genesis identity + * @param {DID} did - profile that has been derived or genesis identity * @returns `{Promise<{nonce:number, genesisIdentifier: DID}>}` */ getGenesisDIDMetadata(did: DID): Promise<{ nonce: number; genesisDID: DID }>; @@ -329,7 +378,7 @@ export interface IIdentityWallet { * * find all credentials that belong to any profile or genesis identity for the given did * - * @param {string} did - profile that has been derived or genesis identity + * @param {DID} did - profile that has been derived or genesis identity * @returns `{Promise}` */ findOwnedCredentialsByDID(did: DID, query: ProofQuery): Promise; @@ -373,6 +422,40 @@ export interface IIdentityWallet { 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; + + /** + * 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; } /** @@ -388,6 +471,7 @@ export interface IIdentityWallet { */ export class IdentityWallet implements IIdentityWallet { private readonly _credentialStatusPublisherRegistry: CredentialStatusPublisherRegistry; + private readonly _inputsGenerator: InputGenerator; /** * Constructs a new instance of the `IdentityWallet` class @@ -406,6 +490,7 @@ export class IdentityWallet implements IIdentityWallet { } ) { this._credentialStatusPublisherRegistry = this.getCredentialStatusPublisherRegistry(_opts); + this._inputsGenerator = new InputGenerator(this, _credentialWallet, _storage.states); } private getCredentialStatusPublisherRegistry( @@ -428,26 +513,12 @@ export class IdentityWallet implements IIdentityWallet { } } - /** - * {@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.method = opts.method ?? DidMethod.Iden3; - opts.blockchain = opts.blockchain ?? Blockchain.Polygon; - opts.networkId = opts.networkId ?? NetworkId.Amoy; - - await this._storage.mt.createIdentityMerkleTrees(tmpIdentifier); - - opts.seed = opts.seed ?? getRandomBytes(32); - - const keyId = await this._kms.createKeyFromSeed(KmsKeyType.BabyJubJub, opts.seed); - + 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; @@ -457,35 +528,18 @@ export class IdentityWallet implements IIdentityWallet { ClaimOptions.withIndexDataInts(pubKey.p[0], pubKey.p[1]), ClaimOptions.withRevocationNonce(BigInt(0)) ); - const revNonce = opts.revocationOpts.nonce ?? 0; authClaim.setRevocationNonce(BigInt(revNonce)); - await this._storage.mt.addToMerkleTree( - tmpIdentifier, - MerkleTreeType.Claims, - authClaim.hiHv().hi, - authClaim.hiHv().hv - ); - - const claimsTree = await this._storage.mt.getMerkleTreeByIdentifierAndType( - tmpIdentifier, - MerkleTreeType.Claims - ); - - const currentState = hashElems([ - (await claimsTree.root()).bigInt(), - ZERO_HASH.bigInt(), - ZERO_HASH.bigInt() - ]); - - const didType = buildDIDType(opts.method, opts.blockchain, opts.networkId); - const identifier = Id.idGenesisFromIdenState(didType, currentState.bigInt()); - const did = DID.parseFromId(identifier); - - await this._storage.mt.bindMerkleTreeToNewIdentifier(tmpIdentifier, did.string()); - - const schema = JSON.parse(VerifiableConstants.AUTH.AUTH_BJJ_CREDENTIAL_SCHEMA_JSON); + return { authClaim, pubKey }; + } + private async createAuthBJJCredential( + did: DID, + pubKey: PublicKey, + authClaim: Claim, + currentState: Hash, + revocationOpts: { id: string; type: CredentialStatusType } + ): Promise { const authData = authClaim.getExpirationDate(); const expiration = authData ? getUnixTimestamp(authData) : 0; @@ -500,23 +554,84 @@ export class IdentityWallet implements IIdentityWallet { version: 0, expiration, revocationOpts: { - nonce: revNonce, - id: opts.revocationOpts.id.replace(/\/$/, ''), - type: opts.revocationOpts.type, + 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(); - try { - credential = this._credentialWallet.createCredential(did, request, schema); - } catch (e) { - throw new Error(`Error create w3c credential ${(e as Error).message}`); + 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 index = authClaim.hIndex(); 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 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({ @@ -524,8 +639,8 @@ export class IdentityWallet implements IIdentityWallet { issuerData: { id: did, state: { - rootOfRoots: ZERO_HASH, - revocationTreeRoot: ZERO_HASH, + rootOfRoots: oldTreeState.rootOfRoots, + revocationTreeRoot: oldTreeState.revocationRoot, claimsTreeRoot: ctr, value: currentState } @@ -555,6 +670,63 @@ export class IdentityWallet implements IIdentityWallet { }; } + /** + * {@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 (!ethSigner) { + throw new Error( + 'Ethereum signer is required to create Ethereum identities in order to transit state' + ); + } + + 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) { + // 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; genesisDID: DID }> { // check if it is a genesis identity @@ -650,8 +822,15 @@ export class IdentityWallet implements IIdentityWallet { treeState?: TreeState ): Promise { const coreClaim = await this.getCoreClaimFromCredential(credential); - // todo: Parser.parseClaim + return this.generateCoreClaimMtp(did, coreClaim, treeState); + } + /** {@inheritDoc IIdentityWallet.generateClaimMtp} */ + async generateCoreClaimMtp( + did: DID, + coreClaim: Claim, + treeState?: TreeState + ): Promise { const treesModel = await this.getDIDTreeModel(did); const claimsTree = await this._storage.mt.getMerkleTreeByIdentifierAndType( @@ -867,10 +1046,10 @@ export class IdentityWallet implements IIdentityWallet { async revokeCredential(issuerDID: DID, credential: W3CCredential): Promise { const issuerTree = await this.getDIDTreeModel(issuerDID); - const coreClaim = credential.getCoreClaimFromProof(ProofType.BJJSignature); + const coreClaim = await this.getCoreClaimFromCredential(credential); if (!coreClaim) { - throw new Error('credential must have coreClaim representation in the signature proof'); + throw new Error('credential must have coreClaim representation in proofs'); } const nonce = coreClaim.getRevocationNonce(); @@ -946,20 +1125,23 @@ export class IdentityWallet implements IIdentityWallet { txId: string, blockNumber?: number, blockTimestamp?: number, - treeState?: TreeState + treeState?: TreeState, + opts?: CoreClaimCreationOptions ): Promise { for (let index = 0; index < credentials.length; index++) { const credential = credentials[index]; - const mtpWithProof = await this.generateCredentialMtp(issuerDID, credential, treeState); - // TODO: return coreClaim from generateCredentialMtp and use it below // credential must have a bjj signature proof - const coreClaim = credential.getCoreClaimFromProof(ProofType.BJJSignature); + + const coreClaim = + credential.getCoreClaimFromProof(ProofType.BJJSignature) || + (await credential.toCoreClaim(opts)); if (!coreClaim) { throw new Error('credential must have coreClaim representation in the signature proof'); } + const mtpWithProof = await this.generateCoreClaimMtp(issuerDID, coreClaim, treeState); const mtpProof: Iden3SparseMerkleTreeProof = new Iden3SparseMerkleTreeProof({ mtp: mtpWithProof.proof, @@ -981,7 +1163,9 @@ export class IdentityWallet implements IIdentityWallet { if (Array.isArray(credentials[index].proof)) { (credentials[index].proof as unknown[]).push(mtpProof); } else { - credentials[index].proof = [credentials[index].proof, mtpProof]; + credentials[index].proof = credentials[index].proof + ? [credentials[index].proof, mtpProof] + : [mtpProof]; } } return credentials; @@ -1122,4 +1306,162 @@ export class IdentityWallet implements IIdentityWallet { isStateGenesis: false }); } + + /** {@inheritdoc IIdentityWallet.transitState} */ + async transitState( + did: DID, + oldTreeState: TreeState, + isOldStateGenesis: boolean, + ethSigner: Signer, + prover?: IZKProver + ): Promise { + 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('prover is required to generate proofs for non ethereum identities'); + } + // 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.credential); + + const circuitInputs = new StateTransitionInputs(); + circuitInputs.id = userId; + + circuitInputs.signature = signature; + circuitInputs.isOldStateGenesis = isOldStateGenesis; + + const authClaimIncProofNewState = await this.generateCredentialMtp( + did, + authInfo.credential, + newTreeState + ); + + circuitInputs.newTreeState = authClaimIncProofNewState.treeState; + circuitInputs.authClaimNewStateIncProof = authClaimIncProofNewState.proof; + + circuitInputs.oldTreeState = oldTreeState; + circuitInputs.authClaim = { + claim: authInfo.coreClaim, + incProof: authInfo.incProof, + nonRevProof: authInfo.nonRevProof + }; + + const inputs = circuitInputs.inputsMarshal(); + + proof = await prover.generate(inputs, CircuitId.StateTransition); + + txId = await this._storage.states.publishState(proof, ethSigner); + } else { + const oldUserState = oldTreeState.state; + const newUserState = newTreeState.state; + const userStateTransitionInfo: UserStateTransitionInfo = { + userId, + oldUserState, + newUserState, + isOldStateGenesis, + methodId: BigInt(1), + methodParams: '0x' + } as UserStateTransitionInfo; + txId = await this._storage.states.publishStateGeneric(ethSigner, userStateTransitionInfo); + } + await this.updateIdentityState(did, true, newTreeState); + + return txId; + } + + /** {@inheritdoc IIdentityWallet.addBJJAuthCredential} */ + async addBJJAuthCredential( + did: DID, + oldTreeState: TreeState, + isOldStateGenesis: boolean, + ethSigner: Signer, + opts: AuthBJJCredentialCreationOptions, + prover?: IZKProver // it will be needed in case of non ethereum identities + ): Promise { + opts.seed = opts.seed ?? getRandomBytes(32); + opts.revocationOpts.nonce = + opts.revocationOpts.nonce ?? + (isOldStateGenesis + ? 0 + : opts.revocationOpts.nonce ?? new DataView(getRandomBytes(12).buffer).getUint32(0)); + + const { authClaim, pubKey } = await this.createAuthCoreClaim( + opts.revocationOpts.nonce ?? 0, + opts.seed + ); + + const { hi, hv } = authClaim.hiHv(); + await this._storage.mt.addToMerkleTree(did.string(), MerkleTreeType.Claims, hi, hv); + + // Calculate current state after adding credential to merkle tree + const claimsTree = await this._storage.mt.getMerkleTreeByIdentifierAndType( + did.string(), + MerkleTreeType.Claims + ); + const currentState = hashElems([ + (await claimsTree.root()).bigInt(), + oldTreeState.revocationRoot.bigInt(), + oldTreeState.rootOfRoots.bigInt() + ]); + + let credential = await this.createAuthBJJCredential( + did, + pubKey, + authClaim, + currentState, + opts.revocationOpts + ); + + const txId = await this.transitState(did, oldTreeState, isOldStateGenesis, ethSigner, prover); + // TODO: update to get blockNumber and blockTimestamp from function instead of passing 0s + const credsWithIden3MTPProof = await this.generateIden3SparseMerkleTreeProof( + did, + [credential], + txId, + 0, + 0, + undefined, + { + revNonce: Number(authClaim.getRevocationNonce()), + subjectPosition: SubjectPosition.None, + merklizedRootPosition: MerklizedRootPosition.None, + updatable: false, + version: 0, + merklizeOpts: { documentLoader: cacheLoader() } + } + ); + await this._credentialWallet.saveAll(credsWithIden3MTPProof); + + const credRefreshed = await this._credentialWallet.findById(credential.id); + if (!credRefreshed) { + throw new Error('Credential not found in credential wallet'); + } + credential = credRefreshed; + + await this.publishRevocationInfoByCredentialStatusType(did, opts.revocationOpts.type, { + rhsUrl: opts.revocationOpts.id, + onChain: opts.revocationOpts.onChain + }); + + return credential; + } } diff --git a/src/kms/key-providers/sec256k1-provider.ts b/src/kms/key-providers/sec256k1-provider.ts index 3cf002be..a7693a8d 100644 --- a/src/kms/key-providers/sec256k1-provider.ts +++ b/src/kms/key-providers/sec256k1-provider.ts @@ -44,7 +44,10 @@ export class Sec256k1Provider implements IKeyProvider { type: this.keyType, id: providerHelpers.keyPath(this.keyType, keyPair.getPublic().encode('hex', false)) }; - await this._keyStore.importKey({ alias: kmsId.id, key: keyPair.getPrivate().toString('hex') }); + await this._keyStore.importKey({ + alias: kmsId.id, + key: keyPair.getPrivate().toString('hex').padStart(64, '0') + }); return kmsId; } @@ -56,7 +59,7 @@ export class Sec256k1Provider implements IKeyProvider { */ async publicKey(keyId: KmsKeyId): Promise { const privateKeyHex = await this.privateKey(keyId); - return this._ec.keyFromPrivate(privateKeyHex, 'hex').getPublic().encode('hex', false); + return this._ec.keyFromPrivate(privateKeyHex, 'hex').getPublic().encode('hex', false); // 04 + x + y (uncompressed key) } /** diff --git a/src/proof/proof-service.ts b/src/proof/proof-service.ts index 618feeb1..2f3770a5 100644 --- a/src/proof/proof-service.ts +++ b/src/proof/proof-service.ts @@ -1,15 +1,6 @@ -import { Poseidon } from '@iden3/js-crypto'; import { BytesHelper, DID, MerklizedRootPosition } from '@iden3/js-iden3-core'; import { Hash } from '@iden3/js-merkletree'; -import { - AuthV2Inputs, - CircuitId, - Operators, - Query, - StateTransitionInputs, - TreeState, - ValueProof -} from '../circuits'; +import { AuthV2Inputs, CircuitId, Operators, Query, TreeState, ValueProof } from '../circuits'; import { ICredentialWallet } from '../credentials'; import { IIdentityWallet } from '../identity'; import { @@ -137,7 +128,6 @@ export interface IProofService { * @param {DID} did - identity that will transit state * @param {TreeState} oldTreeState - previous tree state * @param {boolean} isOldStateGenesis - is a transition state is done from genesis state - * @param {IStateStorage} stateStorage - storage of identity states (only eth based storage currently) * @param {Signer} ethSigner - signer for transaction * @returns `{Promise}` - transaction hash is returned */ @@ -357,57 +347,16 @@ export class ProofService implements IProofService { did: DID, oldTreeState: TreeState, isOldStateGenesis: boolean, - stateStorage: IStateStorage, + stateStorage: IStateStorage, // for compatibility with previous versions we leave this parameter ethSigner: Signer ): Promise { - const authInfo = await this._inputsGenerator.prepareAuthBJJCredential(did, oldTreeState); - - const newTreeModel = await this._identityWallet.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 challenge = Poseidon.hash([oldTreeState.state.bigInt(), newTreeState.state.bigInt()]); - - const signature = await this._identityWallet.signChallenge(challenge, authInfo.credential); - - const circuitInputs = new StateTransitionInputs(); - circuitInputs.id = DID.idFromDID(did); - - circuitInputs.signature = signature; - circuitInputs.isOldStateGenesis = isOldStateGenesis; - - const authClaimIncProofNewState = await this._identityWallet.generateCredentialMtp( + return this._identityWallet.transitState( did, - authInfo.credential, - newTreeState + oldTreeState, + isOldStateGenesis, + ethSigner, + this._prover ); - - circuitInputs.newTreeState = authClaimIncProofNewState.treeState; - circuitInputs.authClaimNewStateIncProof = authClaimIncProofNewState.proof; - - circuitInputs.oldTreeState = oldTreeState; - circuitInputs.authClaim = { - claim: authInfo.coreClaim, - incProof: authInfo.incProof, - nonRevProof: authInfo.nonRevProof - }; - - const inputs = circuitInputs.inputsMarshal(); - - const proof = await this._prover.generate(inputs, CircuitId.StateTransition); - - const txId = await stateStorage.publishState(proof, ethSigner); - - await this._identityWallet.updateIdentityState(did, true, newTreeState); - - return txId; } private async generateInputs( diff --git a/src/proof/provers/inputs-generator.ts b/src/proof/provers/inputs-generator.ts index 5a2cba8e..37bbf9f3 100644 --- a/src/proof/provers/inputs-generator.ts +++ b/src/proof/provers/inputs-generator.ts @@ -1,4 +1,4 @@ -import { DID, Id, getUnixTimestamp } from '@iden3/js-iden3-core'; +import { DID, getUnixTimestamp } from '@iden3/js-iden3-core'; import { Iden3SparseMerkleTreeProof, ProofType, @@ -37,6 +37,7 @@ import { ICredentialWallet, getUserDIDFromCredential } from '../../credentials'; +import { isEthereumIdentity } from '../../utils'; export type DIDProfileMetadata = { authProfileNonce: number; @@ -537,12 +538,7 @@ export class InputGenerator { ? BigInt(proofReq.params?.nullifierSessionId?.toString()) : BigInt(0); - let isEthIdentity = true; - try { - Id.ethAddressFromId(circuitInputs.id); - } catch { - isEthIdentity = false; - } + const isEthIdentity = isEthereumIdentity(identifier); circuitInputs.isBJJAuthEnabled = isEthIdentity ? 0 : 1; circuitInputs.challenge = BigInt(params.challenge ?? 0); diff --git a/src/storage/blockchain/state.ts b/src/storage/blockchain/state.ts index f10641c2..5e55c39b 100644 --- a/src/storage/blockchain/state.ts +++ b/src/storage/blockchain/state.ts @@ -1,6 +1,6 @@ import { RootInfo, StateProof } from './../entities/state'; import { ZKProof } from '@iden3/js-jwz'; -import { IStateStorage } from '../interfaces/state'; +import { IStateStorage, UserStateTransitionInfo } from '../interfaces/state'; import { Contract, JsonRpcProvider, Signer, TransactionRequest } from 'ethers'; import { StateInfo } from '../entities/state'; import { StateTransitionPubSignals } from '../../circuits'; @@ -144,18 +144,50 @@ export class EthStateStorage implements IStateStorage { maxFeePerGas, maxPriorityFeePerGas }; - const tx = await signer.sendTransaction(request); - const txnReceipt = await tx.wait(); - if (!txnReceipt) { - throw new Error(`transaction: ${tx.hash} failed to mined`); - } - const status: number | null = txnReceipt.status; - const txnHash: string = txnReceipt.hash; + const txnHash: string = await this.sendTransactionRequest(signer, request); - if (!status) { - throw new Error(`transaction: ${txnHash} failed to mined`); - } + return txnHash; + } + + /** {@inheritdoc IStateStorage.publishStateGeneric} */ + async publishStateGeneric( + signer: Signer, + userStateTransitionInfo: UserStateTransitionInfo + ): Promise { + const { userId, oldUserState, newUserState, isOldStateGenesis, methodId, methodParams } = + userStateTransitionInfo; + const { stateContract, provider } = this.getStateContractAndProviderForId(userId.bigInt()); + const contract = stateContract.connect(signer) as Contract; + const feeData = await provider.getFeeData(); + + const maxFeePerGas = defaultEthConnectionConfig.maxFeePerGas + ? BigInt(defaultEthConnectionConfig.maxFeePerGas) + : feeData.maxFeePerGas; + const maxPriorityFeePerGas = defaultEthConnectionConfig.maxPriorityFeePerGas + ? BigInt(defaultEthConnectionConfig.maxPriorityFeePerGas) + : feeData.maxPriorityFeePerGas; + + const payload = [ + userId.bigInt().toString(), + oldUserState.bigInt().toString(), + newUserState.bigInt().toString(), + isOldStateGenesis, + methodId, //BigInt(1), + methodParams //'0x' + ]; + const gasLimit = await contract.transitStateGeneric.estimateGas(...payload); + const txData = await contract.transitStateGeneric.populateTransaction(...payload); + + const request: TransactionRequest = { + to: txData.to, + data: txData.data, + gasLimit, + maxFeePerGas, + maxPriorityFeePerGas + }; + + const txnHash: string = await this.sendTransactionRequest(signer, request); return txnHash; } @@ -227,4 +259,23 @@ export class EthStateStorage implements IStateStorage { return this.ethConfig as EthConnectionConfig; } + + private async sendTransactionRequest( + signer: Signer, + request: TransactionRequest + ): Promise { + const tx = await signer.sendTransaction(request); + const txnReceipt = await tx.wait(); + if (!txnReceipt) { + throw new Error(`transaction: ${tx.hash} failed to mined`); + } + const status: number | null = txnReceipt.status; + const txnHash: string = txnReceipt.hash; + + if (!status) { + throw new Error(`transaction: ${txnHash} failed to mined`); + } + + return txnHash; + } } diff --git a/src/storage/interfaces/state.ts b/src/storage/interfaces/state.ts index 4996408b..d5cb44b1 100644 --- a/src/storage/interfaces/state.ts +++ b/src/storage/interfaces/state.ts @@ -1,6 +1,17 @@ import { ZKProof } from '@iden3/js-jwz'; import { Signer } from 'ethers'; import { RootInfo, StateInfo, StateProof } from '../entities/state'; +import { Id } from '@iden3/js-iden3-core'; +import { Hash } from '@iden3/js-merkletree'; + +export interface UserStateTransitionInfo { + userId: Id; // Identity id + oldUserState: Hash; // Previous identity state + newUserState: Hash; // New identity state + isOldStateGenesis: boolean; // Is the previous state genesis? + methodId: bigint; // State transition method id + methodParams: string; // State transition method-specific params +} /** * Interface that defines methods for state storage @@ -32,7 +43,18 @@ export interface IStateStorage { * @param {Signer} signer - signer of transaction * @returns `Promise` - transaction identifier */ - publishState(proof: ZKProof, signer: Signer): Promise; + publishState(proof: ZKProof | undefined, signer: Signer): Promise; + /** + * method to publish state onchain + * + * @param {Signer} signer - signer of transaction + * @param {UserStateTransitionInfo} userStateTransitionInfo - user state transition information + * @returns `Promise` - transaction identifier + */ + publishStateGeneric( + signer: Signer, + userStateTransitionInfo?: UserStateTransitionInfo + ): Promise; /** * generates proof of inclusion / non-inclusion to global identity state for given identity * diff --git a/src/utils/did-helper.ts b/src/utils/did-helper.ts index c77b0242..2bb3eb9f 100644 --- a/src/utils/did-helper.ts +++ b/src/utils/did-helper.ts @@ -2,11 +2,13 @@ import { Hex } from '@iden3/js-crypto'; import { Id, buildDIDType, genesisFromEthAddress, DID } from '@iden3/js-iden3-core'; import { Hash } from '@iden3/js-merkletree'; import { DIDResolutionResult, VerificationMethod } from 'did-resolver'; +import { keccak256 } from 'js-sha3'; +import { hexToBytes } from './encoding'; /** * Checks if state is genesis state * - * @param {string} did - did + * @param {DID} did - did * @param {bigint|string} state - hash on bigInt or hex string format * @returns boolean */ @@ -22,6 +24,24 @@ export function isGenesisState(did: DID, state: bigint | string): boolean { return id.bigInt().toString() === idFromState.bigInt().toString(); } +/** + * Checks if DID is an ethereum identity + * + * @param {DID} did - did + * @returns boolean + */ +export function isEthereumIdentity(did: DID): boolean { + const issuerId = DID.idFromDID(did); + try { + Id.ethAddressFromId(issuerId); + // is an ethereum identity + return true; + } catch { + // not an ethereum identity (BabyJubJub or other) + return false; + } +} + export const buildVerifierId = ( address: string, info: { method: string; blockchain: string; networkId: string } @@ -65,3 +85,16 @@ export const resolveDIDDocumentAuth = async ( (i) => i.type === 'Iden3StateInfo2023' ); }; + +export const buildDIDFromEthPubKey = (didType: Uint8Array, pubKeyEth: string): DID => { + // Use Keccak-256 hash function to get public key hash + const hashOfPublicKey = keccak256(hexToBytes(pubKeyEth)); + // Convert hash to buffer + const ethAddressBuffer = hexToBytes(hashOfPublicKey); + // Ethereum Address is '0x' concatenated with last 20 bytes + // of the public key hash + const ethAddr = ethAddressBuffer.slice(-20); + const genesis = genesisFromEthAddress(ethAddr); + const identifier = new Id(didType, genesis); + return DID.parseFromId(identifier); +}; diff --git a/tests/credentials/credential-statuses/on-chain-revocation.test.ts b/tests/credentials/credential-statuses/on-chain-revocation.test.ts index b0ffa923..736a61f3 100644 --- a/tests/credentials/credential-statuses/on-chain-revocation.test.ts +++ b/tests/credentials/credential-statuses/on-chain-revocation.test.ts @@ -29,7 +29,7 @@ import { import { createIdentity, - registerBJJIntoInMemoryKMS, + registerKeyProvidersInMemoryKMS, STATE_CONTRACT, RPC_URL, WALLET_KEY, @@ -238,7 +238,7 @@ describe('onchain revocation checks', () => { new Iden3OnchainSmtCredentialStatusPublisher(storage) ); - idWallet = new IdentityWallet(registerBJJIntoInMemoryKMS(), dataStorage, credWallet, { + idWallet = new IdentityWallet(registerKeyProvidersInMemoryKMS(), dataStorage, credWallet, { credentialStatusPublisherRegistry }); proofService = new ProofService(idWallet, credWallet, circuitStorage, ethStorage); diff --git a/tests/credentials/credential-statuses/rhs.test.ts b/tests/credentials/credential-statuses/rhs.test.ts index 1e692aa4..9b2bed0e 100644 --- a/tests/credentials/credential-statuses/rhs.test.ts +++ b/tests/credentials/credential-statuses/rhs.test.ts @@ -37,6 +37,9 @@ describe('rhs', () => { publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getGISTProof: (): Promise => { return Promise.resolve({ root: 0n, @@ -79,6 +82,9 @@ describe('rhs', () => { publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getGISTProof: (): Promise => { return Promise.resolve({ root: 0n, @@ -120,6 +126,9 @@ describe('rhs', () => { publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getGISTProof: (): Promise => { return Promise.resolve({ root: 0n, diff --git a/tests/credentials/credential-statuses/sparse-merkle-tree-proof.test.ts b/tests/credentials/credential-statuses/sparse-merkle-tree-proof.test.ts index 56fdc9d4..d8300b10 100644 --- a/tests/credentials/credential-statuses/sparse-merkle-tree-proof.test.ts +++ b/tests/credentials/credential-statuses/sparse-merkle-tree-proof.test.ts @@ -8,7 +8,7 @@ import { SEED_USER, createIdentity, getInMemoryDataStorage, - registerBJJIntoInMemoryKMS + registerKeyProvidersInMemoryKMS } from '../../helpers'; import { DID } from '@iden3/js-iden3-core'; import fetchMock from '@gr2m/fetch-mock'; @@ -26,7 +26,7 @@ describe('SparseMerkleTreeProof', () => { const issuerWalletUrl = 'https://issuer.com'; beforeEach(async () => { - const kms = registerBJJIntoInMemoryKMS(); + const kms = registerKeyProvidersInMemoryKMS(); dataStorage = getInMemoryDataStorage(MOCK_STATE_STORAGE); credWallet = new CredentialWallet(dataStorage); diff --git a/tests/credentials/credential-validation.test.ts b/tests/credentials/credential-validation.test.ts index 9367ef15..57d3ae4b 100644 --- a/tests/credentials/credential-validation.test.ts +++ b/tests/credentials/credential-validation.test.ts @@ -50,6 +50,9 @@ const mockStateStorage: IStateStorage = { publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getGISTProof: (): Promise => { return Promise.resolve({ root: 0n, diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts index 3b8639d7..f96984ea 100644 --- a/tests/handlers/auth.test.ts +++ b/tests/handlers/auth.test.ts @@ -24,23 +24,42 @@ import { createAuthorizationRequestWithMessage, AuthorizationResponseMessage, ZeroKnowledgeProofResponse, - ProofType + ProofType, + KmsKeyType, + defaultEthConnectionConfig, + InMemoryPrivateKeyStore, + BjjProvider, + KMS, + CredentialStorage, + InMemoryDataSource, + IdentityStorage, + Identity, + Profile, + InMemoryMerkleTreeStorage, + W3CCredential, + Sec256k1Provider, + StateInfo, + hexToBytes, + NativeProver } from '../../src'; import { Token } from '@iden3/js-jwz'; -import { DID } from '@iden3/js-iden3-core'; +import { Blockchain, DID, DidMethod, NetworkId } from '@iden3/js-iden3-core'; import { expect } from 'chai'; import { ethers } from 'ethers'; import * as uuid from 'uuid'; import { MOCK_STATE_STORAGE, getInMemoryDataStorage, - registerBJJIntoInMemoryKMS, + registerKeyProvidersInMemoryKMS, IPFS_URL, getPackageMgr, createIdentity, SEED_USER, RHS_URL, - WALLET_KEY + WALLET_KEY, + STATE_CONTRACT, + RPC_URL, + SEED_ISSUER } from '../helpers'; import { testOpts } from './mock'; @@ -57,7 +76,7 @@ describe('auth', () => { let issuerDID: DID; beforeEach(async () => { - const kms = registerBJJIntoInMemoryKMS(); + const kms = registerKeyProvidersInMemoryKMS(); dataStorage = getInMemoryDataStorage(MOCK_STATE_STORAGE); const circuitStorage = new FSCircuitStorage({ dirname: path.join(__dirname, '../proofs/testdata') @@ -389,6 +408,422 @@ describe('auth', () => { expect(token).to.be.a('object'); }); + it('auth flow identity (profile) with ethereum identity issuer with circuits V3', async () => { + const ethSigner = new ethers.Wallet( + WALLET_KEY, + (dataStorage.states as EthStateStorage).provider + ); + + const { did: didIssuer, credential: issuerAuthCredential } = + await idWallet.createEthereumBasedIdentity({ + method: DidMethod.PolygonId, + blockchain: Blockchain.Polygon, + networkId: NetworkId.Mumbai, + seed: SEED_ISSUER, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + }, + ethSigner + }); + expect(issuerAuthCredential).not.to.be.undefined; + + const profileDID = await idWallet.createProfile(userDID, 777, didIssuer.string()); + + const claimReq: CredentialRequest = { + credentialSchema: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/kyc-nonmerklized.json', + type: 'KYCAgeCredential', + credentialSubject: { + id: userDID.string(), + birthday: 19960424, + documentType: 99 + }, + expiration: 2793526400, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + const issuerCred = await idWallet.issueCredential(didIssuer, claimReq); + const employeeCredRequest: CredentialRequest = { + credentialSchema: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCEmployee-v101.json', + type: 'KYCEmployee', + credentialSubject: { + id: profileDID.string(), + ZKPexperiance: true, + hireDate: '2023-12-11', + position: 'boss', + salary: 200, + documentType: 1 + }, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + const employeeCred = await idWallet.issueCredential(didIssuer, employeeCredRequest); + + await credWallet.saveAll([employeeCred, issuerCred]); + + const res = await idWallet.addCredentialsToMerkleTree([employeeCred], didIssuer); + await idWallet.publishStateToRHS(didIssuer, RHS_URL); + + const txId = await proofService.transitState( + didIssuer, + res.oldTreeState, + true, + dataStorage.states, + ethSigner + ); + + const credsWithIden3MTPProof = await idWallet.generateIden3SparseMerkleTreeProof( + didIssuer, + res.credentials, + txId + ); + + await credWallet.saveAll(credsWithIden3MTPProof); + + const proofReqs: ZeroKnowledgeProofRequest[] = [ + { + id: 1, + circuitId: CircuitId.AtomicQueryV3, + optional: false, + query: { + proofType: ProofType.BJJSignature, + allowedIssuers: ['*'], + type: 'KYCAgeCredential', + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-nonmerklized.jsonld', + credentialSubject: { + documentType: { + $eq: 99 + } + } + } + }, + { + id: 2, + circuitId: CircuitId.LinkedMultiQuery10, + optional: false, + query: { + groupId: 1, + proofType: ProofType.Iden3SparseMerkleTreeProof, + allowedIssuers: ['*'], + type: 'KYCEmployee', + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v101.json-ld', + credentialSubject: { + documentType: { + $eq: 1 + }, + position: { + $eq: 'boss', + $ne: 'employee' + } + } + } + }, + { + id: 3, + circuitId: CircuitId.AtomicQueryV3, + optional: false, + query: { + groupId: 1, + proofType: ProofType.BJJSignature, + allowedIssuers: ['*'], + type: 'KYCEmployee', + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v101.json-ld', + credentialSubject: { + hireDate: { + $eq: '2023-12-11' + } + } + }, + params: { + nullifierSessionId: '12345' + } + } + ]; + + const authReqBody: AuthorizationRequestMessageBody = { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'mesage', + did_doc: {}, + scope: proofReqs + }; + + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: authReqBody, + from: didIssuer.string() + }; + + // Ethereum identities should have a previous state in state storage. We mock it here. + const previousGetLatestStateById = MOCK_STATE_STORAGE.getLatestStateById; + MOCK_STATE_STORAGE.getLatestStateById = async (id: bigint): Promise => { + return { + id, + state: res.oldTreeState.state.bigInt(), + replacedByState: 0n, + createdAtTimestamp: 1712062738n, + replacedAtTimestamp: 0n, + createdAtBlock: 5384981n, + replacedAtBlock: 0n + }; + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + const authRes = await authHandler.handleAuthorizationRequest(userDID, msgBytes); + + // Restore the mock state storage + MOCK_STATE_STORAGE.getLatestStateById = previousGetLatestStateById; + + // console.log(JSON.stringify(authRes.authResponse)); + const tokenStr = authRes.token; + // console.log(tokenStr); + expect(tokenStr).to.be.a('string'); + const token = await Token.parse(tokenStr); + expect(token).to.be.a('object'); + }); + + // SKIPPED : ethereum identity integration test + it.skip('auth flow identity (profile) with ethereum identity issuer with circuits V3 (integration)', async () => { + const stateEthConfig = defaultEthConnectionConfig; + stateEthConfig.url = RPC_URL; + stateEthConfig.contractAddress = STATE_CONTRACT; + stateEthConfig.chainId = 80002; // Amoy + + const memoryKeyStore = new InMemoryPrivateKeyStore(); + const bjjProvider = new BjjProvider(KmsKeyType.BabyJubJub, memoryKeyStore); + const sec256k1Provider = new Sec256k1Provider(KmsKeyType.Secp256k1, memoryKeyStore); + const kms = new KMS(); + kms.registerKeyProvider(KmsKeyType.BabyJubJub, bjjProvider); + kms.registerKeyProvider(KmsKeyType.Secp256k1, sec256k1Provider); + + dataStorage = { + credential: new CredentialStorage(new InMemoryDataSource()), + identity: new IdentityStorage( + new InMemoryDataSource(), + new InMemoryDataSource() + ), + mt: new InMemoryMerkleTreeStorage(40), + states: new EthStateStorage(stateEthConfig) + }; + const circuitStorage = new FSCircuitStorage({ + dirname: path.join(__dirname, '../proofs/testdata') + }); + + const resolvers = new CredentialStatusResolverRegistry(); + resolvers.register( + CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + new RHSResolver(dataStorage.states) + ); + credWallet = new CredentialWallet(dataStorage, resolvers); + + idWallet = new IdentityWallet(kms, dataStorage, credWallet); + + proofService = new ProofService(idWallet, credWallet, circuitStorage, dataStorage.states, { + ipfsNodeURL: IPFS_URL + }); + + packageMgr = await getPackageMgr( + await circuitStorage.loadCircuitData(CircuitId.AuthV2), + proofService.generateAuthV2Inputs.bind(proofService), + proofService.verifyState.bind(proofService) + ); + + authHandler = new AuthHandler(packageMgr, proofService); + + const { did: didUser, credential: userAuthCredential } = await idWallet.createIdentity({ + method: DidMethod.PolygonId, + blockchain: Blockchain.Polygon, + networkId: NetworkId.Amoy, + seed: SEED_USER, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }); + + expect(userAuthCredential).not.to.be.undefined; + + const ethSigner = new ethers.Wallet( + WALLET_KEY, + (dataStorage.states as EthStateStorage).provider + ); + + const { did: didIssuer, credential: issuerAuthCredential } = + await idWallet.createEthereumBasedIdentity({ + method: DidMethod.PolygonId, + blockchain: Blockchain.Polygon, + networkId: NetworkId.Amoy, + seed: hexToBytes(WALLET_KEY), + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + }, + ethSigner + }); + expect(issuerAuthCredential).not.to.be.undefined; + + const profileDID = await idWallet.createProfile(didUser, 777, didIssuer.string()); + + const claimReq: CredentialRequest = { + credentialSchema: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/kyc-nonmerklized.json', + type: 'KYCAgeCredential', + credentialSubject: { + id: didUser.string(), + birthday: 19960424, + documentType: 99 + }, + expiration: 2793526400, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + const issuerCred = await idWallet.issueCredential(didIssuer, claimReq); + const employeeCredRequest: CredentialRequest = { + credentialSchema: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCEmployee-v101.json', + type: 'KYCEmployee', + credentialSubject: { + id: profileDID.string(), + ZKPexperiance: true, + hireDate: '2023-12-11', + position: 'boss', + salary: 200, + documentType: 1 + }, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + const employeeCred = await idWallet.issueCredential(didIssuer, employeeCredRequest); + + await credWallet.saveAll([employeeCred, issuerCred]); + + const res = await idWallet.addCredentialsToMerkleTree([employeeCred], didIssuer); + await idWallet.publishStateToRHS(didIssuer, RHS_URL); + + const txId = await proofService.transitState( + didIssuer, + res.oldTreeState, + false, + dataStorage.states, + ethSigner + ); + + const credsWithIden3MTPProof = await idWallet.generateIden3SparseMerkleTreeProof( + didIssuer, + res.credentials, + txId + ); + + await credWallet.saveAll(credsWithIden3MTPProof); + + const proofReqs: ZeroKnowledgeProofRequest[] = [ + { + id: 1, + circuitId: CircuitId.AtomicQueryV3, + optional: false, + query: { + proofType: ProofType.BJJSignature, + allowedIssuers: ['*'], + type: 'KYCAgeCredential', + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-nonmerklized.jsonld', + credentialSubject: { + documentType: { + $eq: 99 + } + } + } + }, + { + id: 2, + circuitId: CircuitId.LinkedMultiQuery10, + optional: false, + query: { + groupId: 1, + proofType: ProofType.Iden3SparseMerkleTreeProof, + allowedIssuers: ['*'], + type: 'KYCEmployee', + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v101.json-ld', + credentialSubject: { + documentType: { + $eq: 1 + }, + position: { + $eq: 'boss', + $ne: 'employee' + } + } + } + }, + { + id: 3, + circuitId: CircuitId.AtomicQueryV3, + optional: false, + query: { + groupId: 1, + proofType: ProofType.BJJSignature, + allowedIssuers: ['*'], + type: 'KYCEmployee', + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v101.json-ld', + credentialSubject: { + hireDate: { + $eq: '2023-12-11' + } + } + }, + params: { + nullifierSessionId: '12345' + } + } + ]; + + const authReqBody: AuthorizationRequestMessageBody = { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'mesage', + did_doc: {}, + scope: proofReqs + }; + + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: authReqBody, + from: didIssuer.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + const authRes = await authHandler.handleAuthorizationRequest(didUser, msgBytes); + // console.log('authRes', JSON.stringify(authRes.authResponse)); + const tokenStr = authRes.token; + // console.log(tokenStr); + expect(tokenStr).to.be.a('string'); + const token = await Token.parse(tokenStr); + expect(token).to.be.a('object'); + }); + it('auth response: TestVerifyMessageWithoutProof', async () => { const sender = '1125GJqgw6YEsKFwj63GY87MMxPL9kwDKxPUiwMLNZ'; const userId = '119tqceWdRd2F6WnAyVuFQRFjK3WUXq2LorSPyG9LJ'; @@ -1473,4 +1908,361 @@ describe('auth', () => { await authHandler.handleAuthorizationResponse(response, request, testOpts); }); + + it('auth flow identity (profile) with circuits V3', async () => { + const profileDID = await idWallet.createProfile(userDID, 777, issuerDID.string()); + + const claimReq: CredentialRequest = { + credentialSchema: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/kyc-nonmerklized.json', + type: 'KYCAgeCredential', + credentialSubject: { + id: userDID.string(), + birthday: 19960424, + documentType: 99 + }, + expiration: 2793526400, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + const issuerCred = await idWallet.issueCredential(issuerDID, claimReq); + const employeeCredRequest: CredentialRequest = { + credentialSchema: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCEmployee-v101.json', + type: 'KYCEmployee', + credentialSubject: { + id: profileDID.string(), + ZKPexperiance: true, + hireDate: '2023-12-11', + position: 'boss', + salary: 200, + documentType: 1 + }, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + const employeeCred = await idWallet.issueCredential(issuerDID, employeeCredRequest); + + await credWallet.saveAll([employeeCred, issuerCred]); + + const res = await idWallet.addCredentialsToMerkleTree([employeeCred], issuerDID); + await idWallet.publishStateToRHS(issuerDID, RHS_URL); + + const ethSigner = new ethers.Wallet( + WALLET_KEY, + (dataStorage.states as EthStateStorage).provider + ); + + const txId = await proofService.transitState( + issuerDID, + res.oldTreeState, + true, + dataStorage.states, + ethSigner + ); + + const credsWithIden3MTPProof = await idWallet.generateIden3SparseMerkleTreeProof( + issuerDID, + res.credentials, + txId + ); + + await credWallet.saveAll(credsWithIden3MTPProof); + + const proofReqs: ZeroKnowledgeProofRequest[] = [ + { + id: 1, + circuitId: CircuitId.AtomicQueryV3, + optional: false, + query: { + proofType: ProofType.BJJSignature, + allowedIssuers: ['*'], + type: 'KYCAgeCredential', + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-nonmerklized.jsonld', + credentialSubject: { + documentType: { + $eq: 99 + } + } + } + }, + { + id: 2, + circuitId: CircuitId.LinkedMultiQuery10, + optional: false, + query: { + groupId: 1, + proofType: ProofType.Iden3SparseMerkleTreeProof, + allowedIssuers: ['*'], + type: 'KYCEmployee', + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v101.json-ld', + credentialSubject: { + documentType: { + $eq: 1 + }, + position: { + $eq: 'boss', + $ne: 'employee' + } + } + } + }, + { + id: 3, + circuitId: CircuitId.AtomicQueryV3, + optional: false, + query: { + groupId: 1, + proofType: ProofType.BJJSignature, + allowedIssuers: ['*'], + type: 'KYCEmployee', + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v101.json-ld', + credentialSubject: { + hireDate: { + $eq: '2023-12-11' + } + } + }, + params: { + nullifierSessionId: '12345' + } + } + ]; + + const authReqBody: AuthorizationRequestMessageBody = { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'mesage', + did_doc: {}, + scope: proofReqs + }; + + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: authReqBody, + from: issuerDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + const authRes = await authHandler.handleAuthorizationRequest(userDID, msgBytes); + // console.log(JSON.stringify(authRes.authResponse)); + const tokenStr = authRes.token; + // console.log(tokenStr); + expect(tokenStr).to.be.a('string'); + const token = await Token.parse(tokenStr); + expect(token).to.be.a('object'); + }); + + it('key rotation use case', async () => { + const claimReq: CredentialRequest = { + credentialSchema: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v4.json', + type: 'KYCAgeCredential', + credentialSubject: { + id: userDID.string(), + birthday: 19960424, + documentType: 99 + }, + expiration: 2793526400, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + const issuerCred = await idWallet.issueCredential(issuerDID, claimReq); + await credWallet.save(issuerCred); + const res = await idWallet.addCredentialsToMerkleTree([issuerCred], issuerDID); + await idWallet.publishStateToRHS(issuerDID, RHS_URL); + + const proofReq: ZeroKnowledgeProofRequest = { + id: 1, + circuitId: CircuitId.AtomicQueryV3, + optional: false, + query: { + allowedIssuers: ['*'], + type: claimReq.type, + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v4.jsonld', + credentialSubject: { + documentType: { + $eq: 99 + } + }, + proofType: ProofType.BJJSignature + } + }; + + const authReqBody: AuthorizationRequestMessageBody = { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'mesage', + did_doc: {}, + scope: [proofReq as ZeroKnowledgeProofRequest] + }; + + const handleAuthorizationRequest = async ( + userDID: DID, + authReqBody: AuthorizationRequestMessageBody + ) => { + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: authReqBody, + from: issuerDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + const authRes = await authHandler.handleAuthorizationRequest(userDID, msgBytes); + expect(authRes.token).to.be.a('string'); + const token = await Token.parse(authRes.token); + expect(token).to.be.a('object'); + }; + + await handleAuthorizationRequest(userDID, authReqBody); + + // add second Bjj auth credential + const circuitStorage = new FSCircuitStorage({ + dirname: path.join(__dirname, '../proofs/testdata') + }); + const prover = new NativeProver(circuitStorage); + + const ethSigner = new ethers.Wallet( + WALLET_KEY, + (dataStorage.states as EthStateStorage).provider + ); + const opts = { + seed: SEED_USER, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + + const treesModel = await idWallet.getDIDTreeModel(issuerDID); + const [ctrHex, rtrHex, rorTrHex] = await Promise.all([ + treesModel.claimsTree.root(), + treesModel.revocationTree.root(), + treesModel.rootsTree.root() + ]); + + const oldTreeState = { + state: treesModel.state, + claimsRoot: ctrHex, + revocationRoot: rtrHex, + rootOfRoots: rorTrHex + }; + + // add k2 auth credential (we have k1 already) + const credential2 = await idWallet.addBJJAuthCredential( + issuerDID, + oldTreeState, + false, + ethSigner, + opts, + prover + ); + + expect(credential2?.proof).not.to.be.undefined; + + // get actual auth credential (k1) + const { authCredential: issuerAuthCredential } = await idWallet.getActualAuthCredential( + issuerDID + ); + + // revoke k1 auth credential + const nonce = await idWallet.revokeCredential(issuerDID, issuerAuthCredential); + await idWallet.publishStateToRHS(issuerDID, RHS_URL, [nonce]); + await proofService.transitState( + issuerDID, + res.oldTreeState, + false, + dataStorage.states, + ethSigner + ); + + await handleAuthorizationRequest(userDID, authReqBody); + + // get actual auth credential (k2) + const { authCredential: issuerAuthCredential2 } = await idWallet.getActualAuthCredential( + issuerDID + ); + expect(issuerAuthCredential2).to.be.deep.equal(credential2); + + // check we can issue new credential with k2 + const issuerCred2 = await idWallet.issueCredential(issuerDID, claimReq); + expect(issuerCred2).to.be.not.undefined; + + const treesModel2 = await idWallet.getDIDTreeModel(issuerDID); + const [ctrHex2, rtrHex2, rorTrHex2] = await Promise.all([ + treesModel2.claimsTree.root(), + treesModel2.revocationTree.root(), + treesModel2.rootsTree.root() + ]); + + const oldTreeState2 = { + state: treesModel2.state, + claimsRoot: ctrHex2, + revocationRoot: rtrHex2, + rootOfRoots: rorTrHex2 + }; + + // revoke k2 auth credential + const nonce2 = await idWallet.revokeCredential(issuerDID, issuerAuthCredential2); + await idWallet.publishStateToRHS(issuerDID, RHS_URL, [nonce2]); + await proofService.transitState(issuerDID, oldTreeState2, false, dataStorage.states, ethSigner); + + // check that we don't have auth credentials now + await expect(idWallet.getActualAuthCredential(issuerDID)).to.rejectedWith( + 'no auth credentials found' + ); + + // check that we can't issue new credential + await expect(idWallet.issueCredential(issuerDID, claimReq)).to.rejectedWith( + 'no auth credentials found' + ); + + // this should this work because we haven't revoked user keys + await handleAuthorizationRequest(userDID, authReqBody); + + // get actual auth credential for user + const { authCredential: userAuthCredential } = await idWallet.getActualAuthCredential(userDID); + + const treesModel3 = await idWallet.getDIDTreeModel(userDID); + const [ctrHex3, rtrHex3, rorTrHex3] = await Promise.all([ + treesModel3.claimsTree.root(), + treesModel3.revocationTree.root(), + treesModel3.rootsTree.root() + ]); + + const oldTreeState3 = { + state: treesModel3.state, + claimsRoot: ctrHex3, + revocationRoot: rtrHex3, + rootOfRoots: rorTrHex3 + }; + + // revoke user keys + const nonce3 = await idWallet.revokeCredential(userDID, userAuthCredential); + await idWallet.publishStateToRHS(userDID, RHS_URL, [nonce3]); + await proofService.transitState(userDID, oldTreeState3, true, dataStorage.states, ethSigner); + + // this should not work because we revoked user keys + await expect(handleAuthorizationRequest(userDID, authReqBody)).to.rejectedWith( + 'no auth credentials found' + ); + }); }); diff --git a/tests/handlers/contract-request.test.ts b/tests/handlers/contract-request.test.ts index 089b0f12..aef5df57 100644 --- a/tests/handlers/contract-request.test.ts +++ b/tests/handlers/contract-request.test.ts @@ -82,6 +82,9 @@ describe('contract-request', () => { publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getGISTProof: (): Promise => { return Promise.resolve({ root: 0n, diff --git a/tests/handlers/fetch.test.ts b/tests/handlers/fetch.test.ts index 3cd2d63a..3285075d 100644 --- a/tests/handlers/fetch.test.ts +++ b/tests/handlers/fetch.test.ts @@ -28,7 +28,7 @@ import { createIdentity, getInMemoryDataStorage, getPackageMgr, - registerBJJIntoInMemoryKMS + registerKeyProvidersInMemoryKMS } from '../helpers'; import * as uuid from 'uuid'; @@ -112,7 +112,7 @@ describe('fetch', () => { }`; beforeEach(async () => { - const kms = registerBJJIntoInMemoryKMS(); + const kms = registerKeyProvidersInMemoryKMS(); dataStorage = getInMemoryDataStorage(MOCK_STATE_STORAGE); const circuitStorage = new FSCircuitStorage({ dirname: path.join(__dirname, '../proofs/testdata') diff --git a/tests/handlers/revocation-status.test.ts b/tests/handlers/revocation-status.test.ts index e80b7d24..e538d696 100644 --- a/tests/handlers/revocation-status.test.ts +++ b/tests/handlers/revocation-status.test.ts @@ -19,7 +19,7 @@ import { MOCK_STATE_STORAGE, getInMemoryDataStorage, getPackageMgr, - registerBJJIntoInMemoryKMS, + registerKeyProvidersInMemoryKMS, createIdentity, SEED_USER, SEED_ISSUER @@ -35,7 +35,7 @@ describe('revocation status', () => { let idWallet: IdentityWallet; beforeEach(async () => { - const kms = registerBJJIntoInMemoryKMS(); + const kms = registerKeyProvidersInMemoryKMS(); const dataStorage = getInMemoryDataStorage(MOCK_STATE_STORAGE); const circuitStorage = new FSCircuitStorage({ dirname: path.join(__dirname, '../proofs/testdata') diff --git a/tests/helpers.ts b/tests/helpers.ts index 21f823d3..8a053415 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -6,6 +6,7 @@ import { CredentialStatusType, CredentialStorage, DataPrepareHandlerFunc, + EthereumBasedIdentityCreationOptions, IIdentityWallet, IPackageManager, IStateStorage, @@ -22,6 +23,7 @@ import { Profile, ProvingParams, RootInfo, + Sec256k1Provider, StateProof, StateVerificationFunc, VerifiableConstants, @@ -59,6 +61,23 @@ export const createIdentity = async ( }); }; +export const createEthereumBasedIdentity = async ( + wallet: IIdentityWallet, + opts?: Partial +) => { + return await wallet.createEthereumBasedIdentity({ + method: DidMethod.Iden3, + blockchain: Blockchain.Polygon, + networkId: NetworkId.Amoy, + seed: SEED_ISSUER, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + }, + ...opts + }); +}; + export const MOCK_STATE_STORAGE: IStateStorage = { getLatestStateById: async () => { throw new Error(VerifiableConstants.ERRORS.IDENTITY_DOES_NOT_EXIST); @@ -80,6 +99,9 @@ export const MOCK_STATE_STORAGE: IStateStorage = { publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getGISTProof: (): Promise => { return Promise.resolve({ root: 0n, @@ -104,11 +126,13 @@ export const MOCK_STATE_STORAGE: IStateStorage = { } }; -export const registerBJJIntoInMemoryKMS = (): KMS => { +export const registerKeyProvidersInMemoryKMS = (): KMS => { const memoryKeyStore = new InMemoryPrivateKeyStore(); const bjjProvider = new BjjProvider(KmsKeyType.BabyJubJub, memoryKeyStore); const kms = new KMS(); kms.registerKeyProvider(KmsKeyType.BabyJubJub, bjjProvider); + const sec256k1Provider = new Sec256k1Provider(KmsKeyType.Secp256k1, memoryKeyStore); + kms.registerKeyProvider(KmsKeyType.Secp256k1, sec256k1Provider); return kms; }; diff --git a/tests/identity/id.test.ts b/tests/identity/id.test.ts index 2dc9304f..32cf9c61 100644 --- a/tests/identity/id.test.ts +++ b/tests/identity/id.test.ts @@ -1,4 +1,5 @@ /* eslint-disable no-console */ +import path from 'path'; import { IdentityWallet, byteEncoder, @@ -9,7 +10,12 @@ import { CredentialWallet, CredentialStatusResolverRegistry, RHSResolver, - CredentialStatusType + CredentialStatusType, + EthStateStorage, + FSCircuitStorage, + NativeProver, + Iden3SparseMerkleTreeProof, + BJJSignatureProof2021 } from '../../src'; import { MOCK_STATE_STORAGE, @@ -17,9 +23,12 @@ import { createIdentity, RHS_URL, getInMemoryDataStorage, - registerBJJIntoInMemoryKMS + registerKeyProvidersInMemoryKMS, + WALLET_KEY, + createEthereumBasedIdentity } from '../helpers'; import { expect } from 'chai'; +import { Wallet } from 'ethers'; describe('identity', () => { let credWallet: ICredentialWallet; @@ -58,8 +67,9 @@ describe('identity', () => { new RHSResolver(dataStorage.states) ); credWallet = new CredentialWallet(dataStorage, resolvers); - idWallet = new IdentityWallet(registerBJJIntoInMemoryKMS(), dataStorage, credWallet); + idWallet = new IdentityWallet(registerKeyProvidersInMemoryKMS(), dataStorage, credWallet); }); + it('createIdentity', async () => { const { did, credential } = await createIdentity(idWallet); @@ -126,15 +136,6 @@ describe('identity', () => { expect(proof.proof.existence).to.equal(false); }); - it('generateNonRevProof', async () => { - const { did, credential } = await createIdentity(idWallet); - expect(did.string()).to.equal(expectedDID); - - const proof = await idWallet.generateNonRevocationMtp(did, credential); - - expect(proof.proof.existence).to.equal(false); - }); - it('issueCredential', async () => { const { did: issuerDID, credential: issuerAuthCredential } = await createIdentity(idWallet); @@ -167,4 +168,155 @@ describe('identity', () => { await credWallet.getRevocationStatusFromCredential(issuerCred); }); + + it('createIdentity Secp256k1', async () => { + const ethSigner = new Wallet(WALLET_KEY, (dataStorage.states as EthStateStorage).provider); + + const { did, credential } = await createEthereumBasedIdentity(idWallet, { + ethSigner + }); + + expect(did.string()).to.equal( + 'did:iden3:polygon:amoy:x6x5sor7zpxsu478u36QvEgaRUfPjmzqFo5PHHzbM' + ); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const dbCred = await dataStorage.credential.findCredentialById(credential!.id); + expect(credential).to.deep.equal(dbCred); + + const claimsTree = await dataStorage.mt.getMerkleTreeByIdentifierAndType( + did.string(), + MerkleTreeType.Claims + ); + + expect((await claimsTree.root()).bigInt()).not.to.equal(0); + }); + + it('add auth bjj credential', async () => { + const { did, credential } = await createIdentity(idWallet); + expect(did.string()).to.equal(expectedDID); + + const proof = await idWallet.generateCredentialMtp(did, credential); + expect(proof.proof.existence).to.equal(true); + + const circuitStorage = new FSCircuitStorage({ + dirname: path.join(__dirname, '../proofs/testdata') + }); + const prover = new NativeProver(circuitStorage); + + const ethSigner = new Wallet(WALLET_KEY, (dataStorage.states as EthStateStorage).provider); + const opts = { + seed: SEED_USER, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + + const treesModel = await idWallet.getDIDTreeModel(did); + const [ctrHex, rtrHex, rorTrHex] = await Promise.all([ + treesModel.claimsTree.root(), + treesModel.revocationTree.root(), + treesModel.rootsTree.root() + ]); + + const oldTreeState = { + state: treesModel.state, + claimsRoot: ctrHex, + revocationRoot: rtrHex, + rootOfRoots: rorTrHex + }; + + expect(credential?.proof).not.to.be.undefined; + expect((credential?.proof as unknown[])[0]).to.instanceOf(Iden3SparseMerkleTreeProof); + expect((credential?.proof as unknown[]).length).to.equal(1); + + const credential2 = await idWallet.addBJJAuthCredential( + did, + oldTreeState, + false, + ethSigner, + opts, + prover + ); + expect(credential2?.proof).not.to.be.undefined; + expect((credential2?.proof as unknown[]).length).to.equal(2); + expect((credential2?.proof as unknown[])[0]).to.instanceOf(BJJSignatureProof2021); + expect((credential2?.proof as unknown[])[1]).to.instanceOf(Iden3SparseMerkleTreeProof); + + const proof2 = await idWallet.generateCredentialMtp(did, credential2); + expect(proof2.proof.existence).to.equal(true); + }); + + it('rotate identity keys', async () => { + const { did, credential } = await createIdentity(idWallet); + expect(did.string()).to.equal(expectedDID); + + const proof = await idWallet.generateCredentialMtp(did, credential); + expect(proof.proof.existence).to.equal(true); + + const circuitStorage = new FSCircuitStorage({ + dirname: path.join(__dirname, '../proofs/testdata') + }); + const prover = new NativeProver(circuitStorage); + + const ethSigner = new Wallet(WALLET_KEY, (dataStorage.states as EthStateStorage).provider); + const opts = { + seed: SEED_USER, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + + const treesModel = await idWallet.getDIDTreeModel(did); + const [ctrHex, rtrHex, rorTrHex] = await Promise.all([ + treesModel.claimsTree.root(), + treesModel.revocationTree.root(), + treesModel.rootsTree.root() + ]); + + const oldTreeState = { + state: treesModel.state, + claimsRoot: ctrHex, + revocationRoot: rtrHex, + rootOfRoots: rorTrHex + }; + + expect(credential?.proof).not.to.be.undefined; + expect((credential?.proof as unknown[])[0]).to.instanceOf(Iden3SparseMerkleTreeProof); + expect((credential?.proof as unknown[]).length).to.equal(1); + + const credential2 = await idWallet.addBJJAuthCredential( + did, + oldTreeState, + false, + ethSigner, + opts, + prover + ); + expect(credential2?.proof).not.to.be.undefined; + expect((credential2?.proof as unknown[]).length).to.equal(2); + expect((credential2?.proof as unknown[])[0]).to.instanceOf(BJJSignatureProof2021); + expect((credential2?.proof as unknown[])[1]).to.instanceOf(Iden3SparseMerkleTreeProof); + + const proof2 = await idWallet.generateCredentialMtp(did, credential2); + expect(proof2.proof.existence).to.equal(true); + + const proofNRcredential = await idWallet.generateNonRevocationMtp(did, credential); + expect(proofNRcredential.proof.existence).to.equal(false); + + const proofNRcredential2 = await idWallet.generateNonRevocationMtp(did, credential2); + expect(proofNRcredential2.proof.existence).to.equal(false); + + const nonce = await idWallet.revokeCredential(did, credential); + + await idWallet.publishStateToRHS(did, RHS_URL, [nonce]); + + const afterRevokeProofNRcredential = await idWallet.generateNonRevocationMtp(did, credential); + expect(afterRevokeProofNRcredential.proof.existence).to.equal(true); + + const afterRevokeProofNRcredential2 = await idWallet.generateNonRevocationMtp(did, credential2); + expect(afterRevokeProofNRcredential2.proof.existence).to.equal(false); + }); }); diff --git a/tests/proofs/mtp-onchain.test.ts b/tests/proofs/mtp-onchain.test.ts index a8310636..28c61bf1 100644 --- a/tests/proofs/mtp-onchain.test.ts +++ b/tests/proofs/mtp-onchain.test.ts @@ -58,6 +58,9 @@ describe('mtp onchain proofs', () => { publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getGISTProof: (): Promise => { return Promise.resolve({ root: 0n, diff --git a/tests/proofs/mtp.test.ts b/tests/proofs/mtp.test.ts index 23a3a107..990afbdd 100644 --- a/tests/proofs/mtp.test.ts +++ b/tests/proofs/mtp.test.ts @@ -54,6 +54,9 @@ describe('mtp proofs', () => { publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getGISTProof: (): Promise => { return Promise.resolve({ root: 0n, @@ -113,6 +116,7 @@ describe('mtp proofs', () => { new RHSResolver(dataStorage.states) ); credWallet = new CredentialWallet(dataStorage, resolvers); + idWallet = new IdentityWallet(kms, dataStorage, credWallet); proofService = new ProofService(idWallet, credWallet, circuitStorage, mockStateStorage); diff --git a/tests/proofs/sig-onchain.test.ts b/tests/proofs/sig-onchain.test.ts index 897463e2..b0229c74 100644 --- a/tests/proofs/sig-onchain.test.ts +++ b/tests/proofs/sig-onchain.test.ts @@ -45,6 +45,9 @@ describe('sig onchain proofs', () => { publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getGISTProof: (): Promise => { return Promise.resolve({ root: 0n, diff --git a/tests/proofs/sig.test.ts b/tests/proofs/sig.test.ts index b5fe7a47..18534dbb 100644 --- a/tests/proofs/sig.test.ts +++ b/tests/proofs/sig.test.ts @@ -44,6 +44,9 @@ describe('sig proofs', () => { publishState: async () => { return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; }, + publishStateGeneric: async () => { + return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; + }, getStateInfoByIdAndState: async () => { throw new Error(VerifiableConstants.ERRORS.IDENTITY_DOES_NOT_EXIST); }, diff --git a/tests/utils/utils.test.ts b/tests/utils/utils.test.ts index 5c0a8c75..9c25419e 100644 --- a/tests/utils/utils.test.ts +++ b/tests/utils/utils.test.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; -import { JSONObject, mergeObjects } from '../../src'; +import { buildDIDFromEthPubKey, JSONObject, mergeObjects } from '../../src'; +import { Blockchain, buildDIDType, DidMethod, NetworkId } from '@iden3/js-iden3-core'; describe('merge credential subjects to create query', () => { it('should merge two valid JSONObjects correctly', () => { @@ -149,3 +150,16 @@ describe('merge credential subjects to create query', () => { } }); }); + +describe('build did from ethereum public key', () => { + it('should build did from ethereum public key correctly', async () => { + const pubKeyHexEth = + '8318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5'; + const didType = buildDIDType(DidMethod.Iden3, Blockchain.Polygon, NetworkId.Amoy); + const did = buildDIDFromEthPubKey(didType, pubKeyHexEth); + + expect(did.string()).to.equal( + 'did:iden3:polygon:amoy:x6x5sor7zpycB7z7Q9348dXJxZ9s5b9AgmPeSccZz' + ); + }); +});