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): issue revocable credentials #1427

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c228f88
feat: initial revocation registry definition implementation
genaris Mar 29, 2023
cafe703
feat: register revocation status list
genaris Mar 31, 2023
1bb11d8
Merge remote-tracking branch 'upstream/main' into feat/issue-revocabl…
genaris Mar 31, 2023
b70ba0a
feat: add RevocationStatusList registration methods in API
genaris Mar 31, 2023
ea88904
feat: add revocation parameters when issuing credential
genaris Apr 1, 2023
1cba24c
test: add anoncreds revocation test
genaris Apr 1, 2023
7cb3f5f
Merge remote-tracking branch 'upstream/main' into feat/issue-revocabl…
genaris Apr 1, 2023
12c7602
Merge remote-tracking branch 'upstream/main' into feat/issue-revocabl…
genaris Apr 1, 2023
429da81
various fixes (WIP)
genaris Apr 3, 2023
e635a49
add test local tails server and some fixes to pass full flow test
genaris Apr 4, 2023
9aeb57d
Merge remote-tracking branch 'upstream/main' into feat/issue-revocabl…
genaris Apr 4, 2023
e155c8d
Merge remote-tracking branch 'upstream/main' into feat/issue-revocabl…
genaris Apr 8, 2023
0cc2cb7
add revokeCredentials method and tests
genaris Apr 10, 2023
f5fba00
Merge remote-tracking branch 'upstream/main' into feat/issue-revocabl…
genaris Apr 10, 2023
8fc9380
add revocation notification and generic tails file manager
genaris Apr 11, 2023
a8d824d
some clean up of tests and naming
genaris Apr 11, 2023
9c41913
add proofs tests with revocation + fixes in timestamp
genaris Apr 11, 2023
970a17a
remove unused interfaces and implementation for indy-vdr
genaris Apr 11, 2023
ea2e8ef
fix: types errors
genaris Apr 12, 2023
70e7651
fix: ensure tails directory is created
genaris Apr 12, 2023
7486550
Merge remote-tracking branch 'upstream/main' into feat/issue-revocabl…
genaris Apr 15, 2023
998ead9
feat: override timestamps and add revoked credential test
genaris Apr 15, 2023
0527a0b
Merge remote-tracking branch 'upstream/main' into feat/issue-revocabl…
genaris Apr 28, 2023
599d9d0
feat: update anoncreds-rs, adjust interfaces
genaris Apr 28, 2023
acf8c71
Merge branch 'main' into feat/issue-revocable-credentials
genaris May 2, 2023
87d88c8
fix: address several PR feedback
genaris Jun 1, 2023
d5c7b6d
feat: simplify TailsFileService interface
genaris Jun 1, 2023
8bf88f3
Merge branch 'main' into feat/issue-revocable-credentials
genaris Jun 1, 2023
64f6bfd
Merge remote-tracking branch 'upstream/main' into feat/issue-revocabl…
genaris Jun 1, 2023
d4685cf
fix: some adjustments after merge
genaris Jun 1, 2023
505bf08
fix: types
genaris Jun 1, 2023
64ce973
Merge branch 'main' into feat/issue-revocable-credentials
genaris Jun 7, 2023
89aca1f
Merge branch 'main' into feat/issue-revocable-credentials
genaris Jul 22, 2023
3ebb665
fix: remove revocStatusList from credential creation interface
genaris Jul 22, 2023
98e5380
fix: unused variable
genaris Jul 22, 2023
da6aa63
test: fix anoncreds module registration
genaris Jul 22, 2023
2c1a3b3
fix: remove unused import
genaris Jul 22, 2023
8f5d6bf
Merge branch 'main' into feat/issue-revocable-credentials
genaris Aug 17, 2023
e2f4964
fix: use getOutboundMessageContext
genaris Aug 17, 2023
c4c5648
Merge branch 'main' into feat/issue-revocable-credentials
genaris Aug 18, 2023
347de98
Merge branch 'main' into feat/issue-revocable-credentials
genaris Oct 30, 2023
430439e
Merge branch 'main' into feat/issue-revocable-credentials
genaris Nov 3, 2023
89d60cd
fix: adapt to latest anoncreds-rs API
genaris Nov 3, 2023
61ac750
fix: optional timestamp in updaterevstatuslist
genaris Nov 6, 2023
0096052
Merge branch 'main' into feat/issue-revocable-credentials
genaris Nov 6, 2023
0b78645
Merge branch 'main' into feat/issue-revocable-credentials
genaris Nov 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 },
genaris marked this conversation as resolved.
Show resolved Hide resolved
} 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
Loading