Skip to content

Commit

Permalink
feat(anoncreds): support credential attribute value and marker (#1369)
Browse files Browse the repository at this point in the history
Signed-off-by: Victor Anene <[email protected]>
  • Loading branch information
Vickysomtee authored Mar 19, 2023
1 parent 2efc009 commit 5559996
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 80 deletions.
1 change: 1 addition & 0 deletions packages/anoncreds-rs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"devDependencies": {
"@hyperledger/anoncreds-nodejs": "^0.1.0-dev.10",
"reflect-metadata": "^0.1.13",
"rimraf": "^4.0.7",
"typescript": "~4.9.4"
}
Expand Down
97 changes: 60 additions & 37 deletions packages/anoncreds-rs/src/services/AnonCredsRsHolderService.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import type {
AnonCredsCredential,
AnonCredsCredentialInfo,
AnonCredsCredentialRequest,
AnonCredsCredentialRequestMetadata,
AnonCredsHolderService,
AnonCredsProof,
AnonCredsProofRequestRestriction,
AnonCredsRequestedAttributeMatch,
AnonCredsRequestedPredicateMatch,
CreateCredentialRequestOptions,
CreateCredentialRequestReturn,
CreateLinkSecretOptions,
CreateLinkSecretReturn,
CreateProofOptions,
GetCredentialOptions,
StoreCredentialOptions,
GetCredentialsForProofRequestOptions,
GetCredentialsForProofRequestReturn,
AnonCredsCredentialInfo,
CreateLinkSecretOptions,
CreateLinkSecretReturn,
AnonCredsProofRequestRestriction,
AnonCredsCredential,
AnonCredsRequestedAttributeMatch,
AnonCredsRequestedPredicateMatch,
AnonCredsCredentialRequest,
AnonCredsCredentialRequestMetadata,
GetCredentialsOptions,
StoreCredentialOptions,
} from '@aries-framework/anoncreds'
import type { AgentContext, Query, SimpleQuery } from '@aries-framework/core'
import type {
Expand All @@ -29,20 +29,21 @@ import type {

import {
AnonCredsCredentialRecord,
AnonCredsLinkSecretRepository,
AnonCredsCredentialRepository,
AnonCredsLinkSecretRepository,
AnonCredsRestrictionWrapper,
legacyIndyCredentialDefinitionIdRegex,
} from '@aries-framework/anoncreds'
import { TypedArrayEncoder, AriesFrameworkError, utils, injectable } from '@aries-framework/core'
import { AriesFrameworkError, JsonTransformer, TypedArrayEncoder, injectable, utils } from '@aries-framework/core'
import {
anoncreds,
Credential,
CredentialRequest,
CredentialRevocationState,
MasterSecret,
Presentation,
RevocationRegistryDefinition,
RevocationStatusList,
anoncreds,
} from '@hyperledger/anoncreds-shared'

import { AnonCredsRsError } from '../errors/AnonCredsRsError'
Expand Down Expand Up @@ -353,21 +354,35 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService {
if (!requestedAttribute) {
throw new AnonCredsRsError(`Referent not found in proof request`)
}
const attributes = requestedAttribute.name ? [requestedAttribute.name] : requestedAttribute.names

const restrictionQuery = requestedAttribute.restrictions
? this.queryFromRestrictions(requestedAttribute.restrictions)
: undefined
const $and = []

const query: Query<AnonCredsCredentialRecord> = {
attributes,
...restrictionQuery,
...options.extraQuery,
// Make sure the attribute(s) that are requested are present using the marker tag
const attributes = requestedAttribute.names ?? [requestedAttribute.name]
const attributeQuery: SimpleQuery<AnonCredsCredentialRecord> = {}
for (const attribute of attributes) {
attributeQuery[`attr::${attribute}::marker`] = true
}
$and.push(attributeQuery)

// Add query for proof request restrictions
if (requestedAttribute.restrictions) {
const restrictionQuery = this.queryFromRestrictions(requestedAttribute.restrictions)
$and.push(restrictionQuery)
}

// Add extra query
// TODO: we're not really typing the extraQuery, and it will work differently based on the anoncreds implmentation
// We should make the allowed properties more strict
if (options.extraQuery) {
$and.push(options.extraQuery)
}

const credentials = await agentContext.dependencyManager
.resolve(AnonCredsCredentialRepository)
.findByQuery(agentContext, query)
.findByQuery(agentContext, {
$and,
})

return credentials.map((credentialRecord) => {
const attributes: { [key: string]: string } = {}
Expand All @@ -391,35 +406,43 @@ export class AnonCredsRsHolderService implements AnonCredsHolderService {
private queryFromRestrictions(restrictions: AnonCredsProofRequestRestriction[]) {
const query: Query<AnonCredsCredentialRecord>[] = []

for (const restriction of restrictions) {
const { restrictions: parsedRestrictions } = JsonTransformer.fromJSON({ restrictions }, AnonCredsRestrictionWrapper)

for (const restriction of parsedRestrictions) {
const queryElements: SimpleQuery<AnonCredsCredentialRecord> = {}

if (restriction.cred_def_id) {
queryElements.credentialDefinitionId = restriction.cred_def_id
if (restriction.credentialDefinitionId) {
queryElements.credentialDefinitionId = restriction.credentialDefinitionId
}

if (restriction.issuerId || restriction.issuerDid) {
queryElements.issuerId = restriction.issuerId ?? restriction.issuerDid
}

if (restriction.issuer_id || restriction.issuer_did) {
queryElements.issuerId = restriction.issuer_id ?? restriction.issuer_did
if (restriction.schemaId) {
queryElements.schemaId = restriction.schemaId
}

if (restriction.rev_reg_id) {
queryElements.revocationRegistryId = restriction.rev_reg_id
if (restriction.schemaIssuerId || restriction.schemaIssuerDid) {
queryElements.schemaIssuerId = restriction.schemaIssuerId ?? restriction.issuerDid
}

if (restriction.schema_id) {
queryElements.schemaId = restriction.schema_id
if (restriction.schemaName) {
queryElements.schemaName = restriction.schemaName
}

if (restriction.schema_issuer_id || restriction.schema_issuer_did) {
queryElements.schemaIssuerId = restriction.schema_issuer_id ?? restriction.schema_issuer_did
if (restriction.schemaVersion) {
queryElements.schemaVersion = restriction.schemaVersion
}

if (restriction.schema_name) {
queryElements.schemaName = restriction.schema_name
for (const [attributeName, attributeValue] of Object.entries(restriction.attributeValues)) {
queryElements[`attr::${attributeName}::value`] = attributeValue
}

if (restriction.schema_version) {
queryElements.schemaVersion = restriction.schema_version
for (const [attributeName, isAvailable] of Object.entries(restriction.attributeMarkers)) {
if (isAvailable) {
queryElements[`attr::${attributeName}::marker`] = isAvailable
}
}

query.push(queryElements)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ describeRunInNodeVersion([18], 'AnonCredsRsHolderService', () => {
names: ['name', 'height'],
restrictions: [{ cred_def_id: 'crededefid:uri', issuer_id: 'issuerid:uri' }],
},
attr5_referent: {
name: 'name',
restrictions: [{ 'attr::name::value': 'Alice', 'attr::name::marker': '1' }],
},
},
requested_predicates: {
predicate1_referent: { name: 'age', p_type: '>=' as const, p_value: 18 },
Expand Down Expand Up @@ -322,8 +326,14 @@ describeRunInNodeVersion([18], 'AnonCredsRsHolderService', () => {
})

expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
attributes: ['name'],
issuerId: 'issuer:uri',
$and: [
{
'attr::name::marker': true,
},
{
issuerId: 'issuer:uri',
},
],
})
})

Expand All @@ -334,7 +344,11 @@ describeRunInNodeVersion([18], 'AnonCredsRsHolderService', () => {
})

expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
attributes: ['phoneNumber'],
$and: [
{
'attr::phoneNumber::marker': true,
},
],
})
})

Expand All @@ -345,8 +359,14 @@ describeRunInNodeVersion([18], 'AnonCredsRsHolderService', () => {
})

expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
attributes: ['age'],
$or: [{ schemaId: 'schemaid:uri', schemaName: 'schemaName' }, { schemaVersion: '1.0' }],
$and: [
{
'attr::age::marker': true,
},
{
$or: [{ schemaId: 'schemaid:uri', schemaName: 'schemaName' }, { schemaVersion: '1.0' }],
},
],
})
})

Expand All @@ -357,9 +377,35 @@ describeRunInNodeVersion([18], 'AnonCredsRsHolderService', () => {
})

expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
attributes: ['name', 'height'],
credentialDefinitionId: 'crededefid:uri',
issuerId: 'issuerid:uri',
$and: [
{
'attr::name::marker': true,
'attr::height::marker': true,
},
{
credentialDefinitionId: 'crededefid:uri',
issuerId: 'issuerid:uri',
},
],
})
})

test('referent with attribute values and marker restriction', async () => {
await anonCredsHolderService.getCredentialsForProofRequest(agentContext, {
proofRequest,
attributeReferent: 'attr5_referent',
})

expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
$and: [
{
'attr::name::marker': true,
},
{
'attr::name::value': 'Alice',
'attr::name::marker': true,
},
],
})
})

Expand All @@ -370,7 +416,11 @@ describeRunInNodeVersion([18], 'AnonCredsRsHolderService', () => {
})

expect(findByQueryMock).toHaveBeenCalledWith(agentContext, {
attributes: ['age'],
$and: [
{
'attr::age::marker': true,
},
],
})
})
})
Expand Down
1 change: 1 addition & 0 deletions packages/anoncreds-rs/tests/setup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '@hyperledger/anoncreds-nodejs'
import 'reflect-metadata'

jest.setTimeout(120000)
11 changes: 11 additions & 0 deletions packages/anoncreds/src/models/AnonCredsRestrictionWrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Type } from 'class-transformer'
import { ValidateNested } from 'class-validator'

import { AnonCredsRestrictionTransformer, AnonCredsRestriction } from './AnonCredsRestriction'

export class AnonCredsRestrictionWrapper {
@ValidateNested({ each: true })
@Type(() => AnonCredsRestriction)
@AnonCredsRestrictionTransformer()
public restrictions!: AnonCredsRestriction[]
}
1 change: 1 addition & 0 deletions packages/anoncreds/src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './internal'
export * from './exchange'
export * from './registry'
export * from './AnonCredsRestrictionWrapper'
16 changes: 13 additions & 3 deletions packages/anoncreds/src/repository/AnonCredsCredentialRecord.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AnonCredsCredential } from '../models'
import type { Tags } from '@aries-framework/core'

import { BaseRecord, utils } from '@aries-framework/core'

Expand All @@ -21,7 +22,10 @@ export type DefaultAnonCredsCredentialTags = {
credentialRevocationId?: string
revocationRegistryId?: string
schemaId: string
attributes: string[]

// the following keys can be used for every `attribute name` in credential.
[key: `attr::${string}::marker`]: true | undefined
[key: `attr::${string}::value`]: string | undefined
}

export type CustomAnonCredsCredentialTags = {
Expand Down Expand Up @@ -62,15 +66,21 @@ export class AnonCredsCredentialRecord extends BaseRecord<
}

public getTags() {
return {
const tags: Tags<DefaultAnonCredsCredentialTags, CustomAnonCredsCredentialTags> = {
...this._tags,
credentialDefinitionId: this.credential.cred_def_id,
schemaId: this.credential.schema_id,
credentialId: this.credentialId,
credentialRevocationId: this.credentialRevocationId,
revocationRegistryId: this.credential.rev_reg_id,
linkSecretId: this.linkSecretId,
attributes: Object.keys(this.credential.values),
}

for (const [key, value] of Object.entries(this.credential.values)) {
tags[`attr::${key}::value`] = value.raw
tags[`attr::${key}::marker`] = true
}

return tags
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { AnonCredsCredential } from '@aries-framework/anoncreds'

import { AnonCredsCredentialRecord } from '../AnonCredsCredentialRecord'

describe('AnoncredsCredentialRecords', () => {
test('Returns the correct tags from the getTags methods based on the credential record values', () => {
const anoncredsCredentialRecords = new AnonCredsCredentialRecord({
credential: {
cred_def_id: 'credDefId',
schema_id: 'schemaId',
signature: 'signature',
signature_correctness_proof: 'signatureCorrectnessProof',
values: { attr1: { raw: 'value1', encoded: 'encvalue1' }, attr2: { raw: 'value2', encoded: 'encvalue2' } },
rev_reg_id: 'revRegId',
} as AnonCredsCredential,
credentialId: 'myCredentialId',
credentialRevocationId: 'credentialRevocationId',
linkSecretId: 'linkSecretId',
issuerId: 'issuerDid',
schemaIssuerId: 'schemaIssuerDid',
schemaName: 'schemaName',
schemaVersion: 'schemaVersion',
})

const tags = anoncredsCredentialRecords.getTags()

expect(tags).toMatchObject({
issuerId: 'issuerDid',
schemaIssuerId: 'schemaIssuerDid',
schemaName: 'schemaName',
schemaVersion: 'schemaVersion',
credentialDefinitionId: 'credDefId',
schemaId: 'schemaId',
credentialId: 'myCredentialId',
credentialRevocationId: 'credentialRevocationId',
linkSecretId: 'linkSecretId',
'attr::attr1::value': 'value1',
'attr::attr1::marker': true,
'attr::attr2::value': 'value2',
'attr::attr2::marker': true,
})
})
})
Loading

0 comments on commit 5559996

Please sign in to comment.