Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anoncreds): use legacy prover did #1374

Merged
4 changes: 2 additions & 2 deletions packages/anoncreds-rs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
"dependencies": {
"@aries-framework/core": "0.3.3",
"@aries-framework/anoncreds": "0.3.3",
"@hyperledger/anoncreds-shared": "^0.1.0-dev.6",
"@hyperledger/anoncreds-shared": "^0.1.0-dev.9",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"rxjs": "^7.2.0",
"tsyringe": "^4.7.0"
},
"devDependencies": {
"@hyperledger/anoncreds-nodejs": "^0.1.0-dev.6",
"@hyperledger/anoncreds-nodejs": "^0.1.0-dev.9",
"rimraf": "^4.0.7",
"typescript": "~4.9.4"
}
Expand Down
210 changes: 119 additions & 91 deletions packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,34 @@ import type {
AnonCredsCredential,
AnonCredsRequestedAttributeMatch,
AnonCredsRequestedPredicateMatch,
AnonCredsCredentialRequest,
AnonCredsCredentialRequestMetadata,
} from '@aries-framework/anoncreds'
import type { AgentContext, Query, SimpleQuery } from '@aries-framework/core'
import type { CredentialEntry, CredentialProve } from '@hyperledger/anoncreds-shared'
import type {
CredentialEntry,
CredentialProve,
CredentialRequestMetadata,
JsonObject,
} from '@hyperledger/anoncreds-shared'

import {
AnonCredsCredentialRecord,
AnonCredsLinkSecretRepository,
AnonCredsCredentialRepository,
generateLegacyProverDidLikeString,
legacyIndyCredentialDefinitionIdRegex,
} from '@aries-framework/anoncreds'
import { utils, injectable } from '@aries-framework/core'
import { AriesFrameworkError, utils, injectable } from '@aries-framework/core'
import {
CredentialRequestMetadata,
anoncreds,
Credential,
CredentialDefinition,
CredentialOffer,
CredentialRequest,
CredentialRevocationState,
MasterSecret,
Presentation,
PresentationRequest,
RevocationRegistryDefinition,
RevocationStatusList,
Schema,
} from '@hyperledger/anoncreds-shared'

import { AnonCredsRsError } from '../errors/AnonCredsRsError'
Expand All @@ -48,31 +53,35 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService {
agentContext: AgentContext,
options?: CreateLinkSecretOptions
): Promise<CreateLinkSecretReturn> {
let masterSecret: MasterSecret | undefined
try {
masterSecret = MasterSecret.create()

// FIXME: This is a very specific format of anoncreds-rs. I think it should be simply a string
const linkSecretJson = masterSecret.toJson() as { value: { ms: string } }

return {
linkSecretId: options?.linkSecretId ?? utils.uuid(),
linkSecretValue: JSON.parse(MasterSecret.create().toJson()).value.ms,
linkSecretValue: linkSecretJson.value.ms,
}
} catch (error) {
agentContext.config.logger.error(`Error creating Link Secret`, {
error,
})
throw new AnonCredsRsError('Error creating Link Secret', { cause: error })
} finally {
masterSecret?.handle.clear()
}
}

public async createProof(agentContext: AgentContext, options: CreateProofOptions): Promise<AnonCredsProof> {
const { credentialDefinitions, proofRequest, selectedCredentials, schemas } = options

let presentation: Presentation | undefined
try {
const rsCredentialDefinitions: Record<string, CredentialDefinition> = {}
const rsCredentialDefinitions: Record<string, JsonObject> = {}
for (const credDefId in credentialDefinitions) {
rsCredentialDefinitions[credDefId] = CredentialDefinition.load(JSON.stringify(credentialDefinitions[credDefId]))
rsCredentialDefinitions[credDefId] = credentialDefinitions[credDefId] as unknown as JsonObject
}

const rsSchemas: Record<string, Schema> = {}
const rsSchemas: Record<string, JsonObject> = {}
for (const schemaId in schemas) {
rsSchemas[schemaId] = Schema.load(JSON.stringify(schemas[schemaId]))
rsSchemas[schemaId] = schemas[schemaId] as unknown as JsonObject
}

const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository)
Expand All @@ -89,45 +98,50 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService {
retrievedCredentials.set(attribute.credentialId, credentialRecord)
}

const credential = Credential.load(JSON.stringify(credentialRecord.credential))

const revocationRegistryDefinitionId = credential.revocationRegistryId
const revocationRegistryIndex = credential.revocationRegistryIndex
const revocationRegistryDefinitionId = credentialRecord.credential.rev_reg_id
const revocationRegistryIndex = credentialRecord.credentialRevocationId

// TODO: Check if credential has a revocation registry id (check response from anoncreds-rs API, as it is
// sending back a mandatory string in Credential.revocationRegistryId)
const timestamp = attribute.timestamp

let revocationState
if (timestamp) {
if (revocationRegistryIndex) {
if (!options.revocationRegistries[revocationRegistryDefinitionId]) {
throw new AnonCredsRsError(`Revocation Registry ${revocationRegistryDefinitionId} not found`)
}
let revocationState: CredentialRevocationState | undefined
let revocationRegistryDefinition: RevocationRegistryDefinition | undefined
try {
if (timestamp) {
if (revocationRegistryIndex && revocationRegistryDefinitionId) {
if (!options.revocationRegistries[revocationRegistryDefinitionId]) {
throw new AnonCredsRsError(`Revocation Registry ${revocationRegistryDefinitionId} not found`)
}

const { definition, tailsFilePath } = options.revocationRegistries[revocationRegistryDefinitionId]
const { definition, tailsFilePath } = options.revocationRegistries[revocationRegistryDefinitionId]

const revocationRegistryDefinition = RevocationRegistryDefinition.load(JSON.stringify(definition))
revocationState = CredentialRevocationState.create({
revocationRegistryIndex,
revocationRegistryDefinition,
tailsPath: tailsFilePath,
revocationStatusList: RevocationStatusList.create({
issuanceByDefault: true,
revocationRegistryDefinition = RevocationRegistryDefinition.fromJson(definition as unknown as JsonObject)
revocationState = CredentialRevocationState.create({
revocationRegistryIndex: Number(revocationRegistryIndex),
revocationRegistryDefinition,
revocationRegistryDefinitionId,
timestamp,
}),
})
tailsPath: tailsFilePath,
revocationStatusList: RevocationStatusList.create({
issuerId: definition.issuerId,
issuanceByDefault: true,
revocationRegistryDefinition,
revocationRegistryDefinitionId,
timestamp,
}),
})
}
}
}
return {
linkSecretId: credentialRecord.linkSecretId,
credentialEntry: {
credential,
revocationState,
timestamp,
},
return {
linkSecretId: credentialRecord.linkSecretId,
credentialEntry: {
credential: credentialRecord.credential as unknown as JsonObject,
revocationState: revocationState?.toJson(),
timestamp,
},
}
} finally {
revocationState?.handle.clear()
revocationRegistryDefinition?.handle.clear()
}
}

Expand Down Expand Up @@ -163,32 +177,30 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService {
throw new AnonCredsRsError('Link Secret value not stored')
}

const presentation = Presentation.create({
presentation = Presentation.create({
credentialDefinitions: rsCredentialDefinitions,
schemas: rsSchemas,
presentationRequest: PresentationRequest.load(JSON.stringify(proofRequest)),
presentationRequest: proofRequest as unknown as JsonObject,
credentials: credentials.map((entry) => entry.credentialEntry),
credentialsProve,
selfAttest: selectedCredentials.selfAttestedAttributes,
masterSecret: MasterSecret.load(JSON.stringify({ value: { ms: linkSecretRecord.value } })),
masterSecret: { value: { ms: linkSecretRecord.value } },
})

return JSON.parse(presentation.toJson())
} catch (error) {
agentContext.config.logger.error(`Error creating AnonCreds Proof`, {
error,
proofRequest,
selectedCredentials,
})
throw new AnonCredsRsError(`Error creating proof: ${error}`, { cause: error })
return presentation.toJson() as unknown as AnonCredsProof
} finally {
presentation?.handle.clear()
}
}

public async createCredentialRequest(
agentContext: AgentContext,
options: CreateCredentialRequestOptions
): Promise<CreateCredentialRequestReturn> {
const { credentialDefinition, credentialOffer } = options
const { useLegacyProverDid, credentialDefinition, credentialOffer } = options
let createReturnObj:
| { credentialRequest: CredentialRequest; credentialRequestMetadata: CredentialRequestMetadata }
| undefined
try {
const linkSecretRepository = agentContext.dependencyManager.resolve(AnonCredsLinkSecretRepository)

Expand All @@ -204,19 +216,28 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService {
)
}

const { credentialRequest, credentialRequestMetadata } = CredentialRequest.create({
credentialDefinition: CredentialDefinition.load(JSON.stringify(credentialDefinition)),
credentialOffer: CredentialOffer.load(JSON.stringify(credentialOffer)),
masterSecret: MasterSecret.load(JSON.stringify({ value: { ms: linkSecretRecord.value } })),
const isLegacyIdentifier = credentialOffer.cred_def_id.match(legacyIndyCredentialDefinitionIdRegex)

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data

This [regular expression](1) that depends on [library input](2) may run slow on strings with many repetitions of 'a:.:a'.
if (!isLegacyIdentifier && useLegacyProverDid) {
throw new AriesFrameworkError('Cannot use legacy prover_did with non-legacy identifiers')
}

createReturnObj = CredentialRequest.create({
entropy: !useLegacyProverDid || !isLegacyIdentifier ? anoncreds.generateNonce() : undefined, // FIXME: find a better source of entropy
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option for generating entropy can be calling agentContext.wallet.generateWalletKey, which in AskarWallet creates an ephemeral key and converts its private key to a base58 string (I guess IndySdkWallet does more or less the same thing). However I'm not sure if it's good to rely on wallet implementation for that (unless we strictly specify the requirements for such generateWalletKey in API).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we're misusing the generateWalletKey in that case. I think a nonce should provide enough randomness right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO the nonce generated by anoncreds, apart from being convenient for us, is enough for the purposes of giving some entropy in credential generation. But just trying to find an alternative that provides a true 'alphanumeric' random string as stated in the spec.

proverDid: useLegacyProverDid ? generateLegacyProverDidLikeString() : undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it should be random, do wr need to make this a secure ranom generation? E.g. nonce and then encode it as base58?

We could also add a getRandomBytes method to the wallet?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied it from anoncreds/indy-sdk package implementation, but also thought that could be a good idea to rely in anoncreds native library instead of uuid as it's being done currently in generateLegacyProverDidLikeString.

Any 22-bytes long base58 string will be good for that purpose, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it's used for entropy, it may be needed for the string to be generated with actual secure random bytes instead of a random string?

credentialDefinition: credentialDefinition as unknown as JsonObject,
credentialOffer: credentialOffer as unknown as JsonObject,
masterSecret: { value: { ms: linkSecretRecord.value } },
masterSecretId: linkSecretRecord.linkSecretId,
})

return {
credentialRequest: JSON.parse(credentialRequest.toJson()),
credentialRequestMetadata: JSON.parse(credentialRequestMetadata.toJson()),
credentialRequest: createReturnObj.credentialRequest.toJson() as unknown as AnonCredsCredentialRequest,
credentialRequestMetadata:
createReturnObj.credentialRequestMetadata.toJson() as unknown as AnonCredsCredentialRequestMetadata,
}
} catch (error) {
throw new AnonCredsRsError(`Error creating credential request: ${error}`, { cause: error })
} finally {
createReturnObj?.credentialRequest.handle.clear()
createReturnObj?.credentialRequestMetadata.handle.clear()
}
}

Expand All @@ -227,35 +248,42 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService {
.resolve(AnonCredsLinkSecretRepository)
.getByLinkSecretId(agentContext, credentialRequestMetadata.master_secret_name)

const revocationRegistryDefinition = revocationRegistry?.definition
? RevocationRegistryDefinition.load(JSON.stringify(revocationRegistry.definition))
: undefined
const revocationRegistryDefinition = revocationRegistry?.definition as unknown as JsonObject

const credentialId = options.credentialId ?? utils.uuid()
const processedCredential = Credential.load(JSON.stringify(credential)).process({
credentialDefinition: CredentialDefinition.load(JSON.stringify(credentialDefinition)),
credentialRequestMetadata: CredentialRequestMetadata.load(JSON.stringify(credentialRequestMetadata)),
masterSecret: MasterSecret.load(JSON.stringify({ value: { ms: linkSecretRecord.value } })),
revocationRegistryDefinition,
})

const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository)

await credentialRepository.save(
agentContext,
new AnonCredsCredentialRecord({
credential: JSON.parse(processedCredential.toJson()) as AnonCredsCredential,
credentialId,
linkSecretId: linkSecretRecord.linkSecretId,
issuerId: options.credentialDefinition.issuerId,
schemaName: schema.name,
schemaIssuerId: schema.issuerId,
schemaVersion: schema.version,
credentialRevocationId: processedCredential.revocationRegistryIndex?.toString(),
let credentialObj: Credential | undefined
let processedCredential: Credential | undefined
try {
credentialObj = Credential.fromJson(credential as unknown as JsonObject)
processedCredential = credentialObj.process({
credentialDefinition: credentialDefinition as unknown as JsonObject,
credentialRequestMetadata: credentialRequestMetadata as unknown as JsonObject,
masterSecret: { value: { ms: linkSecretRecord.value } },
revocationRegistryDefinition,
})
)

return credentialId
const credentialRepository = agentContext.dependencyManager.resolve(AnonCredsCredentialRepository)

await credentialRepository.save(
agentContext,
new AnonCredsCredentialRecord({
credential: processedCredential.toJson() as unknown as AnonCredsCredential,
credentialId,
linkSecretId: linkSecretRecord.linkSecretId,
issuerId: options.credentialDefinition.issuerId,
schemaName: schema.name,
schemaIssuerId: schema.issuerId,
schemaVersion: schema.version,
credentialRevocationId: processedCredential.revocationRegistryIndex?.toString(),
})
)

return credentialId
} finally {
credentialObj?.handle.clear()
processedCredential?.handle.clear()
}
}

public async getCredential(
Expand Down
Loading