Skip to content

Commit

Permalink
feat(anoncreds): issue revocable credentials (openwallet-foundation#1427
Browse files Browse the repository at this point in the history
)

Signed-off-by: Ariel Gentile <gentilester@gmail.com>
  • Loading branch information
genaris committed Dec 4, 2023
1 parent 052cebe commit 40cabec
Show file tree
Hide file tree
Showing 62 changed files with 5,039 additions and 418 deletions.
1 change: 1 addition & 0 deletions demo/src/Faber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export class Faber extends BaseAgent {
issuerId: this.anonCredsIssuerId,
tag: 'latest',
},
supportRevocation: false,
options: {
endorserMode: 'internal',
endorserDid: this.anonCredsIssuerId,
Expand Down
190 changes: 183 additions & 7 deletions packages/anoncreds-rs/src/services/AnonCredsRsIssuerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import type {
AnonCredsCredentialDefinition,
CreateCredentialDefinitionReturn,
AnonCredsCredential,
CreateRevocationRegistryDefinitionOptions,
CreateRevocationRegistryDefinitionReturn,
AnonCredsRevocationRegistryDefinition,
CreateRevocationStatusListOptions,
AnonCredsRevocationStatusList,
UpdateRevocationStatusListOptions,
} from '@aries-framework/anoncreds'
import type { AgentContext } from '@aries-framework/core'
import type { CredentialDefinitionPrivate, JsonObject, KeyCorrectnessProof } from '@hyperledger/anoncreds-shared'
Expand All @@ -22,9 +28,21 @@ import {
AnonCredsKeyCorrectnessProofRepository,
AnonCredsCredentialDefinitionPrivateRepository,
AnonCredsCredentialDefinitionRepository,
AnonCredsRevocationRegistryDefinitionRepository,
AnonCredsRevocationRegistryDefinitionPrivateRepository,
AnonCredsRevocationRegistryState,
} from '@aries-framework/anoncreds'
import { injectable, AriesFrameworkError } from '@aries-framework/core'
import { Credential, CredentialDefinition, CredentialOffer, Schema } from '@hyperledger/anoncreds-shared'
import {
RevocationStatusList,
RevocationRegistryDefinitionPrivate,
RevocationRegistryDefinition,
CredentialRevocationConfig,
Credential,
CredentialDefinition,
CredentialOffer,
Schema,
} from '@hyperledger/anoncreds-shared'

import { AnonCredsRsError } from '../errors/AnonCredsRsError'

Expand Down Expand Up @@ -83,6 +101,118 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService {
}
}

public async createRevocationRegistryDefinition(
agentContext: AgentContext,
options: CreateRevocationRegistryDefinitionOptions
): Promise<CreateRevocationRegistryDefinitionReturn> {
const { tag, issuerId, credentialDefinition, credentialDefinitionId, maximumCredentialNumber, tailsDirectoryPath } =
options

let createReturnObj:
| {
revocationRegistryDefinition: RevocationRegistryDefinition
revocationRegistryDefinitionPrivate: RevocationRegistryDefinitionPrivate
}
| undefined
try {
createReturnObj = RevocationRegistryDefinition.create({
credentialDefinition: credentialDefinition as unknown as JsonObject,
credentialDefinitionId,
issuerId,
maximumCredentialNumber,
revocationRegistryType: 'CL_ACCUM',
tag,
tailsDirectoryPath,
})

return {
revocationRegistryDefinition:
createReturnObj.revocationRegistryDefinition.toJson() as unknown as AnonCredsRevocationRegistryDefinition,
revocationRegistryDefinitionPrivate: createReturnObj.revocationRegistryDefinitionPrivate.toJson(),
}
} finally {
createReturnObj?.revocationRegistryDefinition.handle.clear()
createReturnObj?.revocationRegistryDefinitionPrivate.handle.clear()
}
}

public async createRevocationStatusList(
agentContext: AgentContext,
options: CreateRevocationStatusListOptions
): Promise<AnonCredsRevocationStatusList> {
const { issuerId, revocationRegistryDefinitionId, revocationRegistryDefinition } = options

const credentialDefinitionRecord = await agentContext.dependencyManager
.resolve(AnonCredsCredentialDefinitionRepository)
.getByCredentialDefinitionId(agentContext, revocationRegistryDefinition.credDefId)

const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager
.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository)
.getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId)

let revocationStatusList: RevocationStatusList | undefined
try {
revocationStatusList = RevocationStatusList.create({
issuanceByDefault: true,
revocationRegistryDefinitionId,
credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject,
revocationRegistryDefinition: revocationRegistryDefinition as unknown as JsonObject,
revocationRegistryDefinitionPrivate: revocationRegistryDefinitionPrivateRecord.value as unknown as JsonObject,
issuerId,
})

return revocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList
} finally {
revocationStatusList?.handle.clear()
}
}

public async updateRevocationStatusList(
agentContext: AgentContext,
options: UpdateRevocationStatusListOptions
): Promise<AnonCredsRevocationStatusList> {
const { revocationStatusList, revocationRegistryDefinition, issued, revoked, timestamp, tailsFilePath } = options

let updatedRevocationStatusList: RevocationStatusList | undefined
let revocationRegistryDefinitionObj: RevocationRegistryDefinition | undefined

try {
updatedRevocationStatusList = RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject)

if (timestamp && !issued && !revoked) {
updatedRevocationStatusList.updateTimestamp({
timestamp,
})
} else {
const credentialDefinitionRecord = await agentContext.dependencyManager
.resolve(AnonCredsCredentialDefinitionRepository)
.getByCredentialDefinitionId(agentContext, revocationRegistryDefinition.credDefId)

const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager
.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository)
.getByRevocationRegistryDefinitionId(agentContext, revocationStatusList.revRegDefId)

revocationRegistryDefinitionObj = RevocationRegistryDefinition.fromJson({
...revocationRegistryDefinition,
value: { ...revocationRegistryDefinition.value, tailsLocation: tailsFilePath },
} as unknown as JsonObject)
updatedRevocationStatusList.update({
credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject,
revocationRegistryDefinition: revocationRegistryDefinitionObj,
revocationRegistryDefinitionPrivate: revocationRegistryDefinitionPrivateRecord.value,
issued: options.issued,
revoked: options.revoked,
timestamp: timestamp ?? -1, // FIXME: this should be fixed in anoncreds-rs wrapper
})
}

return updatedRevocationStatusList.toJson() as unknown as AnonCredsRevocationStatusList
} finally {
updatedRevocationStatusList?.handle.clear()
revocationRegistryDefinitionObj?.handle.clear()
}
}

public async createCredentialOffer(
agentContext: AgentContext,
options: CreateCredentialOfferOptions
Expand Down Expand Up @@ -132,14 +262,28 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService {
agentContext: AgentContext,
options: CreateCredentialOptions
): Promise<CreateCredentialReturn> {
const { tailsFilePath, credentialOffer, credentialRequest, credentialValues, revocationRegistryId } = options
const {
credentialOffer,
credentialRequest,
credentialValues,
revocationRegistryDefinitionId,
revocationStatusList,
revocationRegistryIndex,
} = options

const definedRevocationOptions = [
revocationRegistryDefinitionId,
revocationStatusList,
revocationRegistryIndex,
].filter((e) => e !== undefined)
if (definedRevocationOptions.length > 0 && definedRevocationOptions.length < 3) {
throw new AriesFrameworkError(
'Revocation requires all of revocationRegistryDefinitionId, revocationRegistryIndex and revocationStatusList'
)
}

let credential: Credential | undefined
try {
if (revocationRegistryId || tailsFilePath) {
throw new AriesFrameworkError('Revocation not supported yet')
}

const attributeRawValues: Record<string, string> = {}
const attributeEncodedValues: Record<string, string> = {}

Expand Down Expand Up @@ -172,14 +316,46 @@ export class AnonCredsRsIssuerService implements AnonCredsIssuerService {
}
}

let revocationConfiguration: CredentialRevocationConfig | undefined
if (revocationRegistryDefinitionId && revocationStatusList && revocationRegistryIndex) {
const revocationRegistryDefinitionRecord = await agentContext.dependencyManager
.resolve(AnonCredsRevocationRegistryDefinitionRepository)
.getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId)

const revocationRegistryDefinitionPrivateRecord = await agentContext.dependencyManager
.resolve(AnonCredsRevocationRegistryDefinitionPrivateRepository)
.getByRevocationRegistryDefinitionId(agentContext, revocationRegistryDefinitionId)

if (
revocationRegistryIndex >= revocationRegistryDefinitionRecord.revocationRegistryDefinition.value.maxCredNum
) {
revocationRegistryDefinitionPrivateRecord.state = AnonCredsRevocationRegistryState.Full
}

revocationConfiguration = new CredentialRevocationConfig({
registryDefinition: RevocationRegistryDefinition.fromJson(
revocationRegistryDefinitionRecord.revocationRegistryDefinition as unknown as JsonObject
),
registryDefinitionPrivate: RevocationRegistryDefinitionPrivate.fromJson(
revocationRegistryDefinitionPrivateRecord.value
),
statusList: RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject),
registryIndex: revocationRegistryIndex,
})
}
credential = Credential.create({
credentialDefinition: credentialDefinitionRecord.credentialDefinition as unknown as JsonObject,
credentialOffer: credentialOffer as unknown as JsonObject,
credentialRequest: credentialRequest as unknown as JsonObject,
revocationRegistryId,
revocationRegistryId: revocationRegistryDefinitionId,
attributeEncodedValues,
attributeRawValues,
credentialDefinitionPrivate: credentialDefinitionPrivateRecord.value,
revocationConfiguration,
// FIXME: duplicated input parameter?
revocationStatusList: revocationStatusList
? RevocationStatusList.fromJson(revocationStatusList as unknown as JsonObject)
: undefined,
})

return {
Expand Down
101 changes: 99 additions & 2 deletions packages/anoncreds-rs/src/services/AnonCredsRsVerifierService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type { AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds'
import type {
AnonCredsNonRevokedInterval,
AnonCredsProof,
AnonCredsProofRequest,
AnonCredsVerifierService,
VerifyProofOptions,
} from '@aries-framework/anoncreds'
import type { AgentContext } from '@aries-framework/core'
import type { JsonObject } from '@hyperledger/anoncreds-shared'
import type { JsonObject, NonRevokedIntervalOverride } from '@hyperledger/anoncreds-shared'

import { AnonCredsRegistryService } from '@aries-framework/anoncreds'
import { injectable } from '@aries-framework/core'
import { Presentation } from '@hyperledger/anoncreds-shared'

Expand All @@ -12,6 +19,16 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService {

let presentation: Presentation | undefined
try {
// Check that provided timestamps correspond to the active ones from the VDR. If they are and differ from the originally
// requested ones, create overrides for anoncreds-rs to consider them valid
const { verified, nonRevokedIntervalOverrides } = await this.verifyTimestamps(agentContext, proof, proofRequest)

// No need to call anoncreds-rs as we already know that the proof will not be valid
if (!verified) {
agentContext.config.logger.debug('Invalid timestamps for provided identifiers')
return false
}

presentation = Presentation.fromJson(proof as unknown as JsonObject)

const rsCredentialDefinitions: Record<string, JsonObject> = {}
Expand Down Expand Up @@ -41,9 +58,89 @@ export class AnonCredsRsVerifierService implements AnonCredsVerifierService {
schemas: rsSchemas,
revocationRegistryDefinitions,
revocationStatusLists: lists,
nonRevokedIntervalOverrides,
})
} finally {
presentation?.handle.clear()
}
}

private async verifyTimestamps(
agentContext: AgentContext,
proof: AnonCredsProof,
proofRequest: AnonCredsProofRequest
): Promise<{ verified: boolean; nonRevokedIntervalOverrides?: NonRevokedIntervalOverride[] }> {
const nonRevokedIntervalOverrides: NonRevokedIntervalOverride[] = []

// Override expected timestamps if the requested ones don't exacly match the values from VDR
const globalNonRevokedInterval = proofRequest.non_revoked

const requestedNonRevokedRestrictions: {
nonRevokedInterval: AnonCredsNonRevokedInterval
schemaId?: string
credentialDefinitionId?: string
revocationRegistryDefinitionId?: string
}[] = []

for (const value of [
...Object.values(proofRequest.requested_attributes),
...Object.values(proofRequest.requested_predicates),
]) {
const nonRevokedInterval = value.non_revoked ?? globalNonRevokedInterval
if (nonRevokedInterval) {
value.restrictions?.forEach((restriction) =>
requestedNonRevokedRestrictions.push({
nonRevokedInterval,
schemaId: restriction.schema_id,
credentialDefinitionId: restriction.cred_def_id,
revocationRegistryDefinitionId: restriction.rev_reg_id,
})
)
}
}

for (const identifier of proof.identifiers) {
if (!identifier.timestamp || !identifier.rev_reg_id) {
continue
}
const relatedNonRevokedRestrictionItem = requestedNonRevokedRestrictions.find(
(item) =>
item.revocationRegistryDefinitionId === item.revocationRegistryDefinitionId ||
item.credentialDefinitionId === identifier.cred_def_id ||
item.schemaId === item.schemaId
)

const requestedFrom = relatedNonRevokedRestrictionItem?.nonRevokedInterval.from
if (requestedFrom && requestedFrom > identifier.timestamp) {
// Check VDR if the active revocation status list at requestedFrom was the one from provided timestamp.
// If it matches, add to the override list
const registry = agentContext.dependencyManager
.resolve(AnonCredsRegistryService)
.getRegistryForIdentifier(agentContext, identifier.rev_reg_id)
const { revocationStatusList } = await registry.getRevocationStatusList(
agentContext,
identifier.rev_reg_id,
requestedFrom
)
const vdrTimestamp = revocationStatusList?.timestamp
if (vdrTimestamp && vdrTimestamp === identifier.timestamp) {
nonRevokedIntervalOverrides.push({
overrideRevocationStatusListTimestamp: identifier.timestamp,
requestedFromTimestamp: requestedFrom,
revocationRegistryDefinitionId: identifier.rev_reg_id,
})
} else {
agentContext.config.logger.debug(
`VDR timestamp for ${requestedFrom} does not correspond to the one provided in proof identifiers. Expected: ${identifier.timestamp} and received ${vdrTimestamp}`
)
return { verified: false }
}
}
}

return {
verified: true,
nonRevokedIntervalOverrides: nonRevokedIntervalOverrides.length ? nonRevokedIntervalOverrides : undefined,
}
}
}
Loading

0 comments on commit 40cabec

Please sign in to comment.