diff --git a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts index e5f9f1ef39..ea0c64d337 100644 --- a/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts +++ b/packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts @@ -105,20 +105,23 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService { throw new AnonCredsRsError(`Revocation Registry ${revocationRegistryDefinitionId} not found`) } - const { definition, tailsFilePath } = options.revocationRegistries[revocationRegistryDefinitionId] + const { definition, revocationStatusLists, tailsFilePath } = + options.revocationRegistries[revocationRegistryDefinitionId] + + // Extract revocation status list for the given timestamp + const revocationStatusList = revocationStatusLists[timestamp] + if (!revocationStatusList) { + throw new AriesFrameworkError( + `Revocation status list for revocation registry ${revocationRegistryDefinitionId} and timestamp ${timestamp} not found in revocation status lists. All revocation status lists must be present.` + ) + } revocationRegistryDefinition = RevocationRegistryDefinition.fromJson(definition as unknown as JsonObject) revocationState = CredentialRevocationState.create({ revocationRegistryIndex: Number(revocationRegistryIndex), revocationRegistryDefinition, tailsPath: tailsFilePath, - revocationStatusList: RevocationStatusList.create({ - issuerId: definition.issuerId, - issuanceByDefault: true, - revocationRegistryDefinition, - revocationRegistryDefinitionId, - timestamp, - }), + revocationStatusList: RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject), }) } return { diff --git a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts index b8cf7afb64..001aebb340 100644 --- a/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts +++ b/packages/anoncreds/src/formats/AnonCredsProofFormatService.ts @@ -15,13 +15,7 @@ import type { AnonCredsSelectedCredentials, AnonCredsProofRequest, } from '../models' -import type { - AnonCredsHolderService, - AnonCredsVerifierService, - CreateProofOptions, - GetCredentialsForProofRequestReturn, - VerifyProofOptions, -} from '../services' +import type { AnonCredsHolderService, AnonCredsVerifierService, GetCredentialsForProofRequestReturn } from '../services' import type { ProofFormatService, AgentContext, @@ -57,11 +51,12 @@ import { sortRequestedCredentialsMatches, createRequestFromPreview, areAnonCredsProofRequestsEqual, - assertRevocationInterval, - downloadTailsFile, + assertBestPracticeRevocationInterval, checkValidCredentialValueEncoding, encodeCredentialValue, assertNoDuplicateGroupsNamesInProofRequest, + getRevocationRegistriesForRequest, + getRevocationRegistriesForProof, } from '../utils' const ANONCREDS_PRESENTATION_PROPOSAL = 'anoncreds/proof-request@v1.0' @@ -240,7 +235,7 @@ export class AnonCredsProofFormatService implements ProofFormatService i.cred_def_id)) ) - const revocationRegistries = await this.getRevocationRegistriesForProof(agentContext, proofJson) + const revocationRegistries = await getRevocationRegistriesForProof(agentContext, proofJson) return await verifierService.verifyProof(agentContext, { proofRequest: proofRequestJson, @@ -538,7 +533,7 @@ export class AnonCredsProofFormatService implements ProofFormatService c.credentialDefinitionId)) ) - const revocationRegistries = await this.getRevocationRegistriesForRequest( + // selectedCredentials are overridden with specified timestamps of the revocation status list that + // should be used for the selected credentials. + const { revocationRegistries, updatedSelectedCredentials } = await getRevocationRegistriesForRequest( agentContext, proofRequest, selectedCredentials @@ -604,177 +601,13 @@ export class AnonCredsProofFormatService implements ProofFormatService i.cred_def_id)) ) - const revocationRegistries = await this.getRevocationRegistriesForProof(agentContext, proofJson) + const revocationRegistries = await getRevocationRegistriesForProof(agentContext, proofJson) return await verifierService.verifyProof(agentContext, { proofRequest: proofRequestJson, @@ -552,7 +547,7 @@ export class LegacyIndyProofFormatService implements ProofFormatService c.credentialDefinitionId)) ) - const revocationRegistries = await this.getRevocationRegistriesForRequest( + // selectedCredentials are overridden with specified timestamps of the revocation status list that + // should be used for the selected credentials. + const { revocationRegistries, updatedSelectedCredentials } = await getRevocationRegistriesForRequest( agentContext, proofRequest, selectedCredentials @@ -618,178 +615,13 @@ export class LegacyIndyProofFormatService implements ProofFormatService { +describe('assertBestPracticeRevocationInterval', () => { test("throws if no 'to' value is specified", () => { expect(() => - assertRevocationInterval({ + assertBestPracticeRevocationInterval({ from: 10, }) ).toThrow() @@ -11,7 +11,7 @@ describe('assertRevocationInterval', () => { test("throws if a 'from' value is specified and it is different from 'to'", () => { expect(() => - assertRevocationInterval({ + assertBestPracticeRevocationInterval({ to: 5, from: 10, }) @@ -20,7 +20,7 @@ describe('assertRevocationInterval', () => { test('does not throw if only to is provided', () => { expect(() => - assertRevocationInterval({ + assertBestPracticeRevocationInterval({ to: 5, }) ).not.toThrow() @@ -28,7 +28,7 @@ describe('assertRevocationInterval', () => { test('does not throw if from and to are equal', () => { expect(() => - assertRevocationInterval({ + assertBestPracticeRevocationInterval({ to: 10, from: 10, }) diff --git a/packages/anoncreds/src/utils/getRevocationRegistries.ts b/packages/anoncreds/src/utils/getRevocationRegistries.ts new file mode 100644 index 0000000000..ffc402d2a4 --- /dev/null +++ b/packages/anoncreds/src/utils/getRevocationRegistries.ts @@ -0,0 +1,201 @@ +import type { AnonCredsProof, AnonCredsProofRequest, AnonCredsSelectedCredentials } from '../models' +import type { CreateProofOptions, VerifyProofOptions } from '../services' +import type { AgentContext } from '@aries-framework/core' + +import { AriesFrameworkError } from '@aries-framework/core' + +import { AnonCredsRegistryService } from '../services' + +import { assertBestPracticeRevocationInterval } from './revocationInterval' +import { downloadTailsFile } from './tails' + +export async function getRevocationRegistriesForRequest( + agentContext: AgentContext, + proofRequest: AnonCredsProofRequest, + selectedCredentials: AnonCredsSelectedCredentials +) { + const revocationRegistries: CreateProofOptions['revocationRegistries'] = {} + + // NOTE: we don't want to mutate this object, when modifying we need to always deeply clone objects firsts. + let updatedSelectedCredentials = selectedCredentials + + try { + agentContext.config.logger.debug(`Retrieving revocation registries for proof request`, { + proofRequest, + selectedCredentials, + }) + + const referentCredentials = [] + + // Retrieve information for referents and push to single array + for (const [referent, selectedCredential] of Object.entries(selectedCredentials.attributes)) { + referentCredentials.push({ + type: 'attributes' as const, + referent, + selectedCredential, + nonRevoked: proofRequest.requested_attributes[referent].non_revoked ?? proofRequest.non_revoked, + }) + } + for (const [referent, selectedCredential] of Object.entries(selectedCredentials.predicates)) { + referentCredentials.push({ + type: 'predicates' as const, + referent, + selectedCredential, + nonRevoked: proofRequest.requested_predicates[referent].non_revoked ?? proofRequest.non_revoked, + }) + } + + for (const { referent, selectedCredential, nonRevoked, type } of referentCredentials) { + if (!selectedCredential.credentialInfo) { + throw new AriesFrameworkError( + `Credential for referent '${referent} does not have credential info for revocation state creation` + ) + } + + // Prefer referent-specific revocation interval over global revocation interval + const credentialRevocationId = selectedCredential.credentialInfo.credentialRevocationId + const revocationRegistryId = selectedCredential.credentialInfo.revocationRegistryId + const timestamp = selectedCredential.timestamp + + // If revocation interval is present and the credential is revocable then create revocation state + if (nonRevoked && credentialRevocationId && revocationRegistryId) { + agentContext.config.logger.trace( + `Presentation is requesting proof of non revocation for referent '${referent}', creating revocation state for credential`, + { + nonRevoked, + credentialRevocationId, + revocationRegistryId, + timestamp, + } + ) + + // Make sure the revocation interval follows best practices from Aries RFC 0441 + assertBestPracticeRevocationInterval(nonRevoked) + + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, revocationRegistryId) + + // Fetch revocation registry definition if not in revocation registries list yet + if (!revocationRegistries[revocationRegistryId]) { + const { revocationRegistryDefinition, resolutionMetadata } = await registry.getRevocationRegistryDefinition( + agentContext, + revocationRegistryId + ) + if (!revocationRegistryDefinition) { + throw new AriesFrameworkError( + `Could not retrieve revocation registry definition for revocation registry ${revocationRegistryId}: ${resolutionMetadata.message}` + ) + } + + const { tailsLocation, tailsHash } = revocationRegistryDefinition.value + const { tailsFilePath } = await downloadTailsFile(agentContext, tailsLocation, tailsHash) + + // const tails = await this.indyUtilitiesService.downloadTails(tailsHash, tailsLocation) + revocationRegistries[revocationRegistryId] = { + definition: revocationRegistryDefinition, + tailsFilePath, + revocationStatusLists: {}, + } + } + + // In most cases we will have a timestamp, but if it's not defined, we use the nonRevoked.to value + const timestampToFetch = timestamp ?? nonRevoked.to + + // Fetch revocation status list if we don't already have a revocation status list for the given timestamp + if (!revocationRegistries[revocationRegistryId].revocationStatusLists[timestampToFetch]) { + const { revocationStatusList, resolutionMetadata: statusListResolutionMetadata } = + await registry.getRevocationStatusList(agentContext, revocationRegistryId, timestampToFetch) + + if (!revocationStatusList) { + throw new AriesFrameworkError( + `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${statusListResolutionMetadata.message}` + ) + } + + revocationRegistries[revocationRegistryId].revocationStatusLists[revocationStatusList.timestamp] = + revocationStatusList + + // If we don't have a timestamp on the selected credential, we set it to the timestamp of the revocation status list + // this way we know which revocation status list to use when creating the proof. + if (!timestamp) { + updatedSelectedCredentials = { + ...updatedSelectedCredentials, + [type]: { + ...updatedSelectedCredentials[type], + [referent]: { + ...updatedSelectedCredentials[type][referent], + timestamp: revocationStatusList.timestamp, + }, + }, + } + } + } + } + } + + agentContext.config.logger.debug(`Retrieved revocation registries for proof request`, { + revocationRegistries, + }) + + return { revocationRegistries, updatedSelectedCredentials } + } catch (error) { + agentContext.config.logger.error(`Error retrieving revocation registry for proof request`, { + error, + proofRequest, + selectedCredentials, + }) + + throw error + } +} + +export async function getRevocationRegistriesForProof(agentContext: AgentContext, proof: AnonCredsProof) { + const revocationRegistries: VerifyProofOptions['revocationRegistries'] = {} + + for (const identifier of proof.identifiers) { + const revocationRegistryId = identifier.rev_reg_id + const timestamp = identifier.timestamp + + // Skip if no revocation registry id is present + if (!revocationRegistryId || !timestamp) continue + + const registry = agentContext.dependencyManager + .resolve(AnonCredsRegistryService) + .getRegistryForIdentifier(agentContext, revocationRegistryId) + + // Fetch revocation registry definition if not already fetched + if (!revocationRegistries[revocationRegistryId]) { + const { revocationRegistryDefinition, resolutionMetadata } = await registry.getRevocationRegistryDefinition( + agentContext, + revocationRegistryId + ) + if (!revocationRegistryDefinition) { + throw new AriesFrameworkError( + `Could not retrieve revocation registry definition for revocation registry ${revocationRegistryId}: ${resolutionMetadata.message}` + ) + } + + revocationRegistries[revocationRegistryId] = { + definition: revocationRegistryDefinition, + revocationStatusLists: {}, + } + } + + // Fetch revocation status list by timestamp if not already fetched + if (!revocationRegistries[revocationRegistryId].revocationStatusLists[timestamp]) { + const { revocationStatusList, resolutionMetadata: statusListResolutionMetadata } = + await registry.getRevocationStatusList(agentContext, revocationRegistryId, timestamp) + + if (!revocationStatusList) { + throw new AriesFrameworkError( + `Could not retrieve revocation status list for revocation registry ${revocationRegistryId}: ${statusListResolutionMetadata.message}` + ) + } + + revocationRegistries[revocationRegistryId].revocationStatusLists[timestamp] = revocationStatusList + } + } + + return revocationRegistries +} diff --git a/packages/anoncreds/src/utils/index.ts b/packages/anoncreds/src/utils/index.ts index 2de326adf2..7fa0da87ed 100644 --- a/packages/anoncreds/src/utils/index.ts +++ b/packages/anoncreds/src/utils/index.ts @@ -3,7 +3,8 @@ export { sortRequestedCredentialsMatches } from './sortRequestedCredentialsMatch export { assertNoDuplicateGroupsNamesInProofRequest } from './hasDuplicateGroupNames' export { areAnonCredsProofRequestsEqual } from './areRequestsEqual' export { downloadTailsFile } from './tails' -export { assertRevocationInterval } from './revocationInterval' +export { assertBestPracticeRevocationInterval } from './revocationInterval' +export { getRevocationRegistriesForRequest, getRevocationRegistriesForProof } from './getRevocationRegistries' export { encodeCredentialValue, checkValidCredentialValueEncoding } from './credential' export { IsMap } from './isMap' export { composeCredentialAutoAccept, composeProofAutoAccept } from './composeAutoAccept' diff --git a/packages/anoncreds/src/utils/revocationInterval.ts b/packages/anoncreds/src/utils/revocationInterval.ts index caf40b93c1..59f490f569 100644 --- a/packages/anoncreds/src/utils/revocationInterval.ts +++ b/packages/anoncreds/src/utils/revocationInterval.ts @@ -2,16 +2,24 @@ import type { AnonCredsNonRevokedInterval } from '../models' import { AriesFrameworkError } from '@aries-framework/core' -// TODO: Add Test +// This sets the `to` value to be required. We do this check in the `assertBestPracticeRevocationInterval` method, +// and it makes it easier to work with the object in TS +interface BestPracticeNonRevokedInterval { + from?: number + to: number +} + // Check revocation interval in accordance with https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0441-present-proof-best-practices/README.md#semantics-of-non-revocation-interval-endpoints -export function assertRevocationInterval(nonRevokedInterval: AnonCredsNonRevokedInterval) { - if (!nonRevokedInterval.to) { +export function assertBestPracticeRevocationInterval( + revocationInterval: AnonCredsNonRevokedInterval +): asserts revocationInterval is BestPracticeNonRevokedInterval { + if (!revocationInterval.to) { throw new AriesFrameworkError(`Presentation requests proof of non-revocation with no 'to' value specified`) } - if ((nonRevokedInterval.from || nonRevokedInterval.from === 0) && nonRevokedInterval.to !== nonRevokedInterval.from) { + if ((revocationInterval.from || revocationInterval.from === 0) && revocationInterval.to !== revocationInterval.from) { throw new AriesFrameworkError( - `Presentation requests proof of non-revocation with an interval from: '${nonRevokedInterval.from}' that does not match the interval to: '${nonRevokedInterval.to}', as specified in Aries RFC 0441` + `Presentation requests proof of non-revocation with an interval from: '${revocationInterval.from}' that does not match the interval to: '${revocationInterval.to}', as specified in Aries RFC 0441` ) } } diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts index ed2572dee7..6690fb6ab3 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkRevocationService.ts @@ -9,6 +9,7 @@ import type { import type { AgentContext } from '@aries-framework/core' import type { RevStates } from 'indy-sdk' +import { assertBestPracticeRevocationInterval } from '@aries-framework/anoncreds' import { AriesFrameworkError, inject, injectable } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' @@ -67,6 +68,7 @@ export class IndySdkRevocationService { referent: string credentialInfo: AnonCredsCredentialInfo referentRevocationInterval: AnonCredsNonRevokedInterval | undefined + timestamp: number | undefined }> = [] //Retrieve information for referents and push to single array @@ -76,6 +78,7 @@ export class IndySdkRevocationService { credentialInfo: selectedCredential.credentialInfo, type: RequestReferentType.Attribute, referentRevocationInterval: proofRequest.requested_attributes[referent].non_revoked, + timestamp: selectedCredential.timestamp, }) } for (const [referent, selectedCredential] of Object.entries(selectedCredentials.predicates ?? {})) { @@ -84,17 +87,18 @@ export class IndySdkRevocationService { credentialInfo: selectedCredential.credentialInfo, type: RequestReferentType.Predicate, referentRevocationInterval: proofRequest.requested_predicates[referent].non_revoked, + timestamp: selectedCredential.timestamp, }) } - for (const { referent, credentialInfo, type, referentRevocationInterval } of referentCredentials) { + for (const { referent, credentialInfo, type, referentRevocationInterval, timestamp } of referentCredentials) { // Prefer referent-specific revocation interval over global revocation interval const requestRevocationInterval = referentRevocationInterval ?? proofRequest.non_revoked const credentialRevocationId = credentialInfo.credentialRevocationId const revocationRegistryId = credentialInfo.revocationRegistryId // If revocation interval is present and the credential is revocable then create revocation state - if (requestRevocationInterval && credentialRevocationId && revocationRegistryId) { + if (requestRevocationInterval && timestamp && credentialRevocationId && revocationRegistryId) { agentContext.config.logger.trace( `Presentation is requesting proof of non revocation for ${type} referent '${referent}', creating revocation state for credential`, { @@ -104,12 +108,17 @@ export class IndySdkRevocationService { } ) - this.assertRevocationInterval(requestRevocationInterval) + assertBestPracticeRevocationInterval(requestRevocationInterval) const { definition, revocationStatusLists, tailsFilePath } = revocationRegistries[revocationRegistryId] - // NOTE: we assume that the revocationStatusLists have been added based on timestamps of the `to` query. On a higher level it means we'll find the - // most accurate revocation list for a given timestamp. It doesn't have to be that the revocationStatusList is from the `to` timestamp however. - const revocationStatusList = revocationStatusLists[requestRevocationInterval.to] + + // Extract revocation status list for the given timestamp + const revocationStatusList = revocationStatusLists[timestamp] + if (!revocationStatusList) { + throw new AriesFrameworkError( + `Revocation status list for revocation registry ${revocationRegistryId} and timestamp ${timestamp} not found in revocation status lists. All revocation status lists must be present.` + ) + } const tails = await createTailsReader(agentContext, tailsFilePath) @@ -120,7 +129,6 @@ export class IndySdkRevocationService { revocationStatusList.timestamp, credentialRevocationId ) - const timestamp = revocationState.timestamp if (!indyRevocationStates[revocationRegistryId]) { indyRevocationStates[revocationRegistryId] = {} @@ -144,31 +152,4 @@ export class IndySdkRevocationService { throw isIndyError(error) ? new IndySdkError(error) : error } } - - // TODO: Add Test - // TODO: we should do this verification on a higher level I think? - // Check revocation interval in accordance with https://github.com/hyperledger/aries-rfcs/blob/main/concepts/0441-present-proof-best-practices/README.md#semantics-of-non-revocation-interval-endpoints - private assertRevocationInterval( - revocationInterval: AnonCredsNonRevokedInterval - ): asserts revocationInterval is BestPracticeNonRevokedInterval { - if (!revocationInterval.to) { - throw new AriesFrameworkError(`Presentation requests proof of non-revocation with no 'to' value specified`) - } - - if ( - (revocationInterval.from || revocationInterval.from === 0) && - revocationInterval.to !== revocationInterval.from - ) { - throw new AriesFrameworkError( - `Presentation requests proof of non-revocation with an interval from: '${revocationInterval.from}' that does not match the interval to: '${revocationInterval.to}', as specified in Aries RFC 0441` - ) - } - } -} - -// This sets the `to` value to be required. We do this check in the `assertRevocationInterval` method, -// and it makes it easier to work with the object in TS -interface BestPracticeNonRevokedInterval { - from?: number - to: number } diff --git a/packages/indy-sdk/src/anoncreds/utils/transform.ts b/packages/indy-sdk/src/anoncreds/utils/transform.ts index 06b4b16698..e7eac06ecc 100644 --- a/packages/indy-sdk/src/anoncreds/utils/transform.ts +++ b/packages/indy-sdk/src/anoncreds/utils/transform.ts @@ -132,8 +132,9 @@ export function indySdkRevocationDeltaFromAnonCreds( accum: revocationStatusList.currentAccumulator, issued: [], revoked: revokedIndices, - // NOTE: I don't think this is used? - prevAccum: '', + // NOTE: this must be a valid accumulator but it's not actually used. So we set it to the + // currentAccumulator as that should always be a valid accumulator. + prevAccum: revocationStatusList.currentAccumulator, }, ver: '1.0', }